Advanced Function Concepts
Python treats functions as first-class objects, meaning they can be assigned to variables, passed as arguments, returned from functions, and stored in data structures. This section explores these powerful concepts.
Functions as First-Class Objects
In Python, functions are objects just like integers, strings, or lists. This means you can:
Assign Functions to Variables
def greet(name): return f"Hello, {name}!"
say_hello = greet # Assign function to variableprint(say_hello("Alice")) # Hello, Alice!Pass Functions as Arguments
def apply_operation(x, y, operation): return operation(x, y)
def add(a, b): return a + b
def multiply(a, b): return a * b
print(apply_operation(5, 3, add)) # 8print(apply_operation(5, 3, multiply)) # 15Return Functions from Functions
def get_operation(operation_type): def add(x, y): return x + y
def subtract(x, y): return x - y
if operation_type == "add": return add else: return subtract
operation = get_operation("add")print(operation(5, 3)) # 8Store Functions in Data Structures
Functions can be stored in lists and dictionaries:
def add(x, y): return x + y
def sub(x, y): return x - y
def mul(x, y): return x * y
ops = [add, sub, mul]
print("Choose any operation", "0. Add", "1. Sub", "2. Mul", sep="\n")operation = int(input())num1 = int(input())num2 = int(input())res = ops[operation](num1, num2)print(res)Nested Functions
Functions can be defined inside other functions. These are called nested or inner functions:
def outer_function(x): def inner_function(y): return y * 2
return inner_function(x)
result = outer_function(5)print(result) # 10Why Use Nested Functions?
- Encapsulation: Hide implementation details
- Helper functions: Create utility functions specific to the outer function
- Closures: Create functions that remember their environment
def process_data(data, operation): def validate(item): return type(item) in (type(int), type(float))
def transform(item): return operation(item)
validated = [] for item in data: if validate(item): validated.append(item) transformed = [] for item in validated: transformed.append(transform(item)) return transformed
numbers = [1, 2, "three", 4, 5.5]result = process_data(numbers, lambda x: x * 2)print(result) # [2, 4, 8, 11.0]Closures
A closure is a nested function that remembers and has access to variables from its enclosing scope, even after the outer function has finished executing:
def outer_function(x): def inner_function(y): return x + y # x is from enclosing scope return inner_function
add_five = outer_function(5)print(add_five(3)) # 8print(add_five(10)) # 15How Closures Work
The inner function “closes over” the variables from the outer function’s scope:
def create_multiplier(factor): def multiplier(number): return number * factor # factor is "captured" from outer scope return multiplier
double = create_multiplier(2)triple = create_multiplier(3)
print(double(5)) # 10print(triple(5)) # 15Some examples:
def create_counter(): count = 0
def counter(): nonlocal count count += 1 return count
return counter
counter1 = create_counter()counter2 = create_counter()
print(counter1()) # 1print(counter1()) # 2print(counter2()) # 1 (independent counter)print(counter1()) # 3def create_validator(min_value, max_value): def validate(value): if min_value <= value <= max_value: return True return False return validate
validate_age = create_validator(0, 120)validate_percentage = create_validator(0, 100)
print(validate_age(25)) # Trueprint(validate_age(150)) # Falseprint(validate_percentage(75)) # Trueprint(validate_percentage(150)) # FalseLambda Functions
A lambda is a small anonymous function which can take any number of arguments but can only have one expression as the body.
Syntax: lambda var1, var2, …: expression
operations = { 'add': lambda x, y: x + y, 'subtract': lambda x, y: x - y, 'multiply': lambda x, y: x * y}
result = operations['add'](3, 4)print(result) # 7Higher-Order Functions
Higher-order functions are functions that take other functions as arguments or return functions:
def apply_twice(func, value): """Apply a function twice to a value""" return func(func(value))
def square(x): return x ** 2
result = apply_twice(square, 3)print(result) # 81 (square(square(3)) = square(9) = 81)Function Factories
Function factories create and return new functions:
def create_power_function(exponent): def power(base): return base ** exponent return power
square = create_power_function(2)cube = create_power_function(3)
print(square(4)) # 16print(cube(4)) # 64Function Documentation
Docstrings
A docstring or documentation string is a multi-line string inserted on the first line of a function. It serves as documentation for the function. It’s what gets printed when we write help(fun). We can also access it using fun.__doc__:
def custom_max(x, y): """This function returns the maximum between 2 numbers""" return x if x > y else y
print(custom_max(1, 2)) # 2help(custom_max)Docstrings are string literals that Python stores and can access at runtime, making them different from regular comments. They provide a way to document what a function does, how to use it, and what it returns.
Exercises
Exercise 1: Functions as Objects
Create a list of three functions: add, subtract, and multiply. Then call the function at index 1 with arguments 10 and 5.
def add(x, y): return x + y
def subtract(x, y):return x - y
def multiply(x, y):return x \* y
ops = [add, subtract, multiply]result = ops[1](10, 5) # subtract(10, 5)print(result) # 5Exercise 2: Nested Functions
Write a function called outer that defines an inner function inner which prints a message. Call inner from within outer.
def outer(): def inner(): print("This is the inner function") inner()
outer() # This is the inner functionExercise 3: Closures
Write a function called create_multiplier that takes a factor and returns a function that multiplies any number by that factor. Use it to create a function that doubles numbers. Read the factor and the number to multiply.
Closures
def create_multiplier(factor): def multiplier(number): return number * factor return multiplier
factor = int(input())number = int(input())double = create_multiplier(factor)print(double(number))Exercise 4: Lambda Functions
Create a dictionary called operations with keys ‘add’, ‘subtract’, and ‘multiply’, where each value is a lambda function that performs that operation on two numbers. Then use it to calculate 10 + 5.
operations = { 'add': lambda x, y: x + y, 'subtract': lambda x, y: x - y, 'multiply': lambda x, y: x * y}
result = operations['add'](10, 5)print(result) # 15Exercise 5: Function Factory
Write a function called create_power_function that takes an exponent and returns a function that raises any number to that exponent. Read the exponent and base from input.
Function Factory
def create_power_function(exponent): def power(base): return base ** exponent return power
exponent = int(input())base = int(input())power_func = create_power_function(exponent)print(power_func(base))Exercise 6: Writing a Docstring
Write a function called calculate_area that takes length and width as parameters, returns the area, and includes a docstring explaining what the function does.
def calculate_area(length, width): """This function calculates and returns the area of a rectangle""" return length * width
print(calculate_area(5, 3)) # 15Exercise 7: Accessing Docstring
Given the following function, write code to access its docstring using both __doc__ and help().
def greet(name): """This function greets a person by name""" print(f"Hello, {name}!")def greet(name): """This function greets a person by name""" print(f"Hello, {name}!")
print(greet.__doc__) # This function greets a person by namehelp(greet) # Shows help informationExercise 8: Function Without Docstring
What will be the value of __doc__ for a function that doesn’t have a docstring?
def no_docstring(): pass
print(no_docstring.__doc__) # NoneFunctions without docstrings have __doc__ set to None.
