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!

Neon image

Serverless Postgres in 300ms (❗️)

10 free databases with autoscaling, scale-to-zero, and read replicas. Start building without infrastructure headaches. No credit card needed.

Try for Free →

Top comments (0)

Retry later
👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, cherished by the supportive DEV Community. Coders of every background are encouraged to bring their perspectives and bolster our collective wisdom.

A sincere “thank you” often brightens someone’s day—share yours in the comments below!

On DEV, the act of sharing knowledge eases our journey and forges stronger community ties. Found value in this? A quick thank-you to the author can make a world of difference.

Okay