Variable Scope and Nested Functions
Function Basics
Functions are defined using the def keyword, followed by a name and parentheses. The body is indented and may include an optional return statement.
def calculate_total(price, tax_rate):
"""Compute total cost including tax."""
return price * (1 + tax_rate)
final_cost = calculate_total(100, 0.08)
print(final_cost) # Output: 108.0
Variable Scope
Variables defined inside a function are local. Those defined outside are global and accessible throughout the program.
global_data = "Accessible everywhere"
def show_data():
local_data = "Only inside function"
print(global_data) # Works
print(local_data) # Works
show_data()
# print(local_data) # Error: NameError
To modify a global variable within a function, use the global keyword.
counter = 0
def increment():
global counter
counter += 1
increment()
print(counter) # Output: 1
Nested Functions and nonlocal
Functions can be defined inside other functions. The nonlocal keyword allows an inner function to modify a variable from an enclosing (non-global) scope.
def outer_container():
value = 10
def inner_modifier():
nonlocal value
value *= 2
return value
return inner_modifier
mod_func = outer_container()
print(mod_func()) # Output: 20
print(mod_func()) # Output: 40
Closures
A closure is a nested function that captures and remembers values from its enclosing scope, even after the outer function has finished executing.
Core Components of a Closure
- A nested function.
- The nested function references a variable from its enclosing scope.
- The outer function returns the nested function.
def create_greeter(greeting):
def greet(name):
print(f"{greeting}, {name}!")
return greet
say_hi = create_greeter("Hi")
say_hi("Alice") # Output: Hi, Alice!
Here, the inner function greet retains access to the greeting variable from create_greeter.
Multi-Level Closure Example
def level_one(a):
def level_two(b):
def level_three(c):
return a + b + c
return level_three
return level_two
func_a = level_one(5)
func_b = func_a(10)
print(func_b(15)) # Output: 30
The final function func_b (which is level_three) remembers the values a=5 and b=10 from its outer scopes.
Decorators
Functions as First-Class Objects
In Python, functions are objects that can be assigned, passed as arguments, returnde, and stored in data structures.
def shout(text):
return text.upper()
# Assign function to a variable
yell = shout
print(yell("hello")) # Output: HELLO
# Store functions in a list
operations = [str.lower, str.upper]
for op in operations:
print(op("TeSt")) # Output: test, TEST
Higher-Order Functions
Higher-order functions either take a function as an argument or return a function.
def apply_operation(func, data):
return [func(item) for item in data]
def cube(x):
return x ** 3
numbers = [1, 2, 3]
cubed = apply_operation(cube, numbers)
print(cubed) # Output: [1, 8, 27]
Basic Decorator
A decorator is a function that wraps another function to extend its behavior without modifying its source code.
def simple_decorator(original_func):
def wrapper():
print("Action before function.")
original_func()
print("Action after function.")
return wrapper
@simple_decorator
def display_message():
print("Core function executing.")
display_message()
# Output:
# Action before function.
# Core function executing.
# Action after function.
The @simple_decorator syntax is equivalent to display_message = simple_decorator(display_message).
Decorators with Arguments
Decorators can accept arguments themselves, requiring an extra level of nesting.
def tag_decorator(tag_name):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"<{tag_name}>")
result = func(*args, **kwargs)
print(f"</{tag_name}>")
return result
return wrapper
return decorator
@tag_decorator("div")
def render_content(text):
print(text)
render_content("Page Content")
# Output:
# <div>
# Page Content
# </div>
Class-Based Decorators
A class can act as a decorator if it implements the __call__ method.
class CounterDecorator:
def __init__(self, func):
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"Function '{self.func.__name__}' called {self.call_count} time(s).")
return self.func(*args, **kwargs)
@CounterDecorator
def compute_sum(a, b):
return a + b
print(compute_sum(3, 4)) # Output: Function 'compute_sum' called 1 time(s).\n7
print(compute_sum(5, 6)) # Output: Function 'compute_sum' called 2 time(s).\n11
The __call__ method enables an instance to be called like a function.
class Multiplier:
def __call__(self, x, y):
return x * y
double = Multiplier()
print(double(7, 3)) # Output: 21
The @property Decorator
The @property decorator allows class methods to be accessed like attributes, enabling getter, setter, and deleter functionality.
class Product:
def __init__(self, cost):
self._cost = cost
self._markup = 1.5 # 50% markup
@property
def price(self):
"""Getter for the calculated price."""
return self._cost * self._markup
@price.setter
def price(self, new_price):
"""Setter adjusts the base cost."""
self._cost = new_price / self._markup
@price.deleter
def price(self):
"""Deleter resets the markup."""
self._markup = 1.0
item = Product(100)
print(item.price) # Output: 150.0
item.price = 180 # Uses setter
print(item._cost) # Output: 120.0
del item.price # Uses deleter
print(item.price) # Output: 120.0