DEV Community

Cover image for Python Notes #4 - Functions
Elvin Seyidov
Elvin Seyidov

Posted on • Edited on

Python Notes #4 - Functions

Functional Utilities: map(), filter(), reduce(), any(), all()

map() – Apply a Function to Each Element

nums = [1, 2, 3] squared = list(map(lambda x: x**2, nums)) print(squared) # [1, 4, 9] 
Enter fullscreen mode Exit fullscreen mode

filter() – Extract Elements Based on Condition

nums = [-2, -1, 0, 1, 2] positives = list(filter(lambda x: x > 0, nums)) print(positives) # [1, 2] 
Enter fullscreen mode Exit fullscreen mode

reduce() – Cumulative Computation (From functools)
global local variables nested functions

from functools import reduce nums = [1, 2, 3, 4] product = reduce(lambda x, y: x * y, nums) print(product) # 24 
Enter fullscreen mode Exit fullscreen mode

✔ Use Case: Aggregation (sum, product, concatenation).

any() – Checks if At Least One Condition is True

nums = [-1, 0, 2] print(any(x > 0 for x in nums)) # True 
Enter fullscreen mode Exit fullscreen mode

all() – Checks if All Conditions Are True

nums = [1, 2, 3] print(all(x > 0 for x in nums)) # True 
Enter fullscreen mode Exit fullscreen mode

sorted() – Sort an Iterable
sorted() returns a new sorted list from an iterable without modifying the original.

nums = [3, 1, 4, 2] sorted_nums = sorted(nums) print(sorted_nums) # [1, 2, 3, 4] 
Enter fullscreen mode Exit fullscreen mode
  • Supports custom sorting with key
  • Use reverse=True for descending order
words = ["banana", "kiwi", "apple"] sorted_words = sorted(words, key=len) # Sort by length print(sorted_words) # ['kiwi', 'apple', 'banana'] 
Enter fullscreen mode Exit fullscreen mode

Scope & Lifetime in Python

✅ Local vs. Global Scope

  • Local: Variables inside a function (only accessible within that function).
  • Global: Variables declared at the top level of a module (accessible everywhere).
x = 10 # Global variable def func(): x = 5 # Local variable (does not modify global x) print(x) # 5 func() print(x) # 10 (global x remains unchanged) 
Enter fullscreen mode Exit fullscreen mode

global Keyword

x = 10 def update(): global x x += 5 # Modifies the global variable update() print(x) # 15 
Enter fullscreen mode Exit fullscreen mode

nonlocal Keyword (For Nested Functions)

def outer(): x = 10 def inner(): nonlocal x x += 5 inner() print(x) # 15 outer() 
Enter fullscreen mode Exit fullscreen mode

✅ Variable Shadowing
A local variable with the same name as a global variable "shadows" it inside a function.

x = "global" def shadow(): x = "local" # This does not change the global x print(x) # "local" shadow() print(x) # "global" 
Enter fullscreen mode Exit fullscreen mode

Function Parameters & Arguments

✅ Positional Arguments

def greet(name, age): print(f"{name} is {age} years old.") greet("Alice", 25) # Alice is 25 years old. 
Enter fullscreen mode Exit fullscreen mode

✅ Keyword Arguments

greet(age=30, name="Bob") # Bob is 30 years old. 
Enter fullscreen mode Exit fullscreen mode

✅ Default Values

def greet(name, message="Hello"): print(f"{message}, {name}!") greet("Charlie") # Hello, Charlie! greet("David", "Hi") # Hi, David! 
Enter fullscreen mode Exit fullscreen mode

*args (Variable Positional Arguments)

def add(*numbers): return sum(numbers) print(add(1, 2, 3, 4)) # 10 
Enter fullscreen mode Exit fullscreen mode

**kwargs (Variable Keyword Arguments)

def info(**details): for key, value in details.items(): print(f"{key}: {value}") info(name="Emma", age=28, city="NY") # name: Emma, age: 28, city: NY 
Enter fullscreen mode Exit fullscreen mode

First-Class Functions in Python

Python treats functions as first-class citizens, meaning they can be assigned to variables, passed as arguments, and returned from functions.

✅ Assigning Functions to Variables

def greet(name): return f"Hello, {name}!" say_hello = greet # Assign function to variable print(say_hello("Alice")) # Hello, Alice! 
Enter fullscreen mode Exit fullscreen mode

✅ Passing Functions as Arguments

def shout(text): return text.upper() def whisper(text): return text.lower() def speak(func, message): return func(message) print(speak(shout, "hello")) # HELLO print(speak(whisper, "HELLO")) # hello 
Enter fullscreen mode Exit fullscreen mode

✅ Returning Functions from Functions

def multiplier(factor): def multiply(number): return number * factor return multiply # Returning the inner function double = multiplier(2) # Create a function that doubles numbers print(double(5)) # 10 
Enter fullscreen mode Exit fullscreen mode

Lambda Expressions in Python

✅ Syntax & Limitations

add = lambda x, y: x + y print(add(3, 5)) # 8 
Enter fullscreen mode Exit fullscreen mode

✅ Use Cases

  • Sorting with lambda (Custom Key Function)
names = ["Alice", "Bob", "Charlie"] names.sort(key=lambda name: len(name)) print(names) # ['Bob', 'Alice', 'Charlie'] 
Enter fullscreen mode Exit fullscreen mode
  • Filtering with filter()
nums = [1, 2, 3, 4, 5] evens = list(filter(lambda x: x % 2 == 0, nums)) print(evens) # [2, 4] 
Enter fullscreen mode Exit fullscreen mode
  • Mapping with map()
nums = [1, 2, 3] squared = list(map(lambda x: x**2, nums)) print(squared) # [1, 4, 9] 
Enter fullscreen mode Exit fullscreen mode

Decorators – Enhancing Functions Dynamically

✅ Basic Decorator Pattern

def decorator(func): def wrapper(): print("Before function call") func() print("After function call") return wrapper @decorator # Applying the decorator def say_hello(): print("Hello!") say_hello() 
Enter fullscreen mode Exit fullscreen mode

Output

Before function call Hello! After function call 
Enter fullscreen mode Exit fullscreen mode

✅ @decorator Syntax (Shortcut for Decorating)
Instead of manually wrapping:

say_hello = decorator(say_hello) # Manual decoration 
Enter fullscreen mode Exit fullscreen mode

We use @decorator to apply it directly.

✅ Stacking Multiple Decorators
Decorators are applied from top to bottom.

def uppercase(func): def wrapper(): return func().upper() return wrapper def exclaim(func): def wrapper(): return func() + "!!!" return wrapper @uppercase @exclaim def greet(): return "hello" print(greet()) # HELLO!!! 
Enter fullscreen mode Exit fullscreen mode

✅ Using functools.wraps to Preserve Function Metadata
Without wraps, the function name and docstring are lost.

from functools import wraps def decorator(func): @wraps(func) # Preserves original function metadata def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @decorator def greet(name): """Greets a person.""" return f"Hello, {name}!" print(greet.__name__) # greet (not wrapper) print(greet.__doc__) # Greets a person. 
Enter fullscreen mode Exit fullscreen mode

✅ Decorators with Arguments
To pass arguments, nest an extra function layer.

def repeat(n): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for _ in range(n): func(*args, **kwargs) return wrapper return decorator @repeat(3) # Runs function 3 times def say_hello(): print("Hello!") say_hello() 
Enter fullscreen mode Exit fullscreen mode

Closures – Functions That Remember Their Enclosing Scope

A closure is a function defined inside another function that "remembers" variables from its enclosing scope, even after the outer function has finished executing.

✅ Functions That Remember Enclosing Scope

def outer(x): def inner(y): return x + y # `inner` remembers `x` from `outer` return inner add_five = outer(5) # Returns a function that adds 5 print(add_five(3)) # 8 
Enter fullscreen mode Exit fullscreen mode

Even after outer(5) has executed, inner() still remembers x = 5.

✅ Use Cases of Closures

  • Delayed Execution (Creating Function Templates)
def multiplier(n): def multiply(x): return x * n # `n` is remembered return multiply double = multiplier(2) triple = multiplier(3) print(double(5)) # 10 print(triple(5)) # 15 
Enter fullscreen mode Exit fullscreen mode
  • Encapsulation (Data Hiding Without Classes)
def counter(): count = 0 # Hidden variable def increment(): nonlocal count # Modify the enclosed `count` count += 1 return count return increment counter1 = counter() print(counter1()) # 1 print(counter1()) # 2 
Enter fullscreen mode Exit fullscreen mode

count is protected from external access but persists across function calls.


Recursion – Functions Calling Themselves

✅ Recursive Function Structure

def recurse(n): if n == 0: # Base case return recurse(n - 1) # Recursive call 
Enter fullscreen mode Exit fullscreen mode
  • Base case stops infinite recursion.
  • Each call adds a new stack frame, leading to stack overflow if unchecked.

✅ Factorial Using Recursion

def factorial(n): return 1 if n == 0 else n * factorial(n - 1) print(factorial(5)) # 120 
Enter fullscreen mode Exit fullscreen mode

✅ Fibonacci Using Recursion

def fibonacci(n): if n <= 1: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(6)) # 8 
Enter fullscreen mode Exit fullscreen mode

Recursive Fibonacci is inefficient; use memoization or iteration for performance.

✅ Tail Recursion (Conceptual, Not Optimized in Python)
Tail recursion eliminates extra stack frames, but Python does not optimize it.

def tail_factorial(n, acc=1): return acc if n == 0 else tail_factorial(n - 1, acc * n) print(tail_factorial(5)) # 120 
Enter fullscreen mode Exit fullscreen mode
  • Python does not optimize tail recursion, so it still consumes stack space.
  • Use recursion wisely; prefer iteration for deep recursive problems!

Introspection in Python

Introspection allows examining objects at runtime, including functions, classes, and modules.
✅ Basic Function Introspection
Python functions store metadata in special attributes.

def greet(name: str) -> str: """Returns a greeting message.""" return f"Hello, {name}!" print(greet.__name__) # greet print(greet.__doc__) # Returns a greeting message. print(greet.__annotations__) # {'name': <class 'str'>, 'return': <class 'str'>} 
Enter fullscreen mode Exit fullscreen mode
  • __name__ – Function name
  • __doc__ – Docstring
  • __annotations__ – Type hints

✅ Using the inspect Module for Advanced Inspection
The inspect module retrieves detailed function metadata.

import inspect def example(x, y=10): """An example function.""" return x + y print(inspect.signature(example)) # (x, y=10) print(inspect.getsource(example)) # Function source code print(inspect.getdoc(example)) # Docstring print(inspect.getmodule(example)) # Module where it's defined 
Enter fullscreen mode Exit fullscreen mode
  • inspect.signature(func) – Retrieves function parameters.
  • inspect.getsource(func) – Gets function source code.
  • inspect.getdoc(func) – Fetches the docstring.
  • inspect.getmodule(func) – Returns the module name.

✅ Introspection helps with debugging, metaprogramming, and documentation!

Top comments (0)