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]

filter() – Extract Elements Based on Condition

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

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

✔ 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

all() – Checks if All Conditions Are True

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

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]
  • 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']

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)

global Keyword

x = 10  

def update():
    global x  
    x += 5  # Modifies the global variable

update()
print(x)  # 15

nonlocal Keyword (For Nested Functions)

def outer():
    x = 10  
    def inner():
        nonlocal x  
        x += 5  
    inner()
    print(x)  # 15

outer()

✅ 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"

Function Parameters & Arguments

✅ Positional Arguments

def greet(name, age):
    print(f"{name} is {age} years old.")

greet("Alice", 25)  # Alice is 25 years old.

✅ Keyword Arguments

greet(age=30, name="Bob")  # Bob is 30 years old.

✅ Default Values

def greet(name, message="Hello"):
    print(f"{message}, {name}!")

greet("Charlie")   # Hello, Charlie!
greet("David", "Hi")  # Hi, David!

*args (Variable Positional Arguments)

def add(*numbers):
    return sum(numbers)

print(add(1, 2, 3, 4))  # 10

**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

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!

✅ 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

✅ 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

Lambda Expressions in Python

✅ Syntax & Limitations

add = lambda x, y: x + y
print(add(3, 5))  # 8

✅ Use Cases

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

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()

Output

Before function call
Hello!
After function call

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

say_hello = decorator(say_hello)  # Manual decoration

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!!!

✅ 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.

✅ 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()

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

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
  • 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

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
  • 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

✅ Fibonacci Using Recursion

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(6))  # 8

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
  • 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': , 'return': }
  • __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
  • 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!