Variable-Length Arguments
*args Parameter
When defining a function, you can use *args to collect any number of positional arguments into a tuple:
def process_values(*values):
# Converts excess positional arguments into a tuple
for value in values:
print(value)
**kwargs Parameter
Similarly, **kwargs collects any number of keyword arguments into a dictionary:
def process_data(**data):
# Converts excess keyword arguments into a dictionary
for key, value in data.items():
print(f"{key}: {value}")
Unpacking Arguments
* Operator with Arguments
The * operator can unpack iterables into positional arguments:
numbers = [1, 2, 3, 4]
def add(a, b, c, d):
return a + b + c + d
result = add(*numbers) # Unpacks the list into individual arguments
** Operator with Arguments
The ** operator unpacks dictionaries into keyword arguments:
person = {"name": "Alice", "age": 30}
def introduce(name, age):
print(f"{name} is {age} years old")
introduce(**person) # Unpacks the dictionary into keyword arguments
Combining *args and **kwargs
You can use both to accept any combination of arguments:
def flexible_function(*args, **kwargs):
print("Positional arguments:", args)
print("Keyword arguments:", kwargs)
# Example usage
sample_list = [1, 2, 3]
sample_dict = {"a": 100, "b": 200}
flexible_function(*sample_list, **sample_dict)
Function Objects
Everything in Python is an Object
In Python, functions are first-class objects, meaning they can be treated like any other object.
Using Functions as Objects
1. Referencing Functions
def greet():
print("Hello, World!")
# Assigning function to another variable
say_hello = greet
say_hello() # Calls the same function as greet()
2. Functions as Container Elements
def add(a, b):
return a + b
def subtract(a, b):
return a - b
# Storing functions in a list
operations = [add, subtract]
result = operations[0](5, 3) # Calls add(5, 3)
print(result) # Output: 8
3. Functions as Parameters
def apply_operation(func, x, y):
return func(x, y)
def multiply(a, b):
return a * b
# Passing function as argument
product = apply_operation(multiply, 4, 5)
print(product) # Output: 20
4. Functions as Return Values
def get_multiplier(factor):
def multiplier(number):
return number * factor
return multiplier
# Returning a function
double = get_multiplier(2)
print(double(10)) # Output: 20
Function Nesting
Introduction to Nesting
Just as we can nest loops, we can also nest functions within each other.
# Example of nested loops (multiplication table)
for i in range(1, 10):
for j in range(1, i + 1):
print(f"{j}×{i}={j*i}", end=" ")
print()
Nested Functions
Functions can be defined inside other functions:
def outer_function():
print("This is the outer function")
def inner_function():
print("This is the inner function")
inner_function() # This works
return inner_function
# Calling the outer function
inner_func = outer_function()
# inner_function() # This would cause an error - inner function is not accessible here
Namespaces and Scope
Understanding Namespaces
A namespace is a mapping from names to objects. Python has several types of namespaces:
Built-in Namespace
Contains built-in functions and types like len(), str(), int(), etc. This namespace is created when the Python interpreter starts and is destroyed when it closes.
Global Namespace
Contains names defined at the module level. It's created when the module is loaded and lasts until the module is unloaded.
Local Namespace
Contains names defined within a function. It's created when the function is called and destroyed when the function returns.
Namespace Execution Order
- Built-in namespace: Created when the Python interpreter starts
- Global namespace: Created when the module is loaded
- Local namespace: Created when a function is called
Namespace Search Order
When looking up a name, Python searches in this order:
- Local namespace
- Global namespace
- Built-in namespace
If a name isn't found in any namespace, a NameError is raised.
x = 10 # Global variable
def display_x():
x = 20 # Local variable
print(x) # Prints local x
print(x) # Prints global x
display_x()
Scope
Global Scope
Names in the global and built-in namespaces have global scope. They are accessible throughout the module.
Local Scope
Names in a local namespace have local scope. They are only accessible within the function where they are defined.
def outer():
x = "outer"
def middle():
x = "middle"
def inner():
x = "inner"
print(x) # Prints "inner"
inner()
middle()
outer()
Important Note on Scope
The scope of a variable is determined at function definition time, not at call time:
x = 10
def func1():
x = 20
print(x) # Prints 20
def func2():
x = 30
func1() # This still prints 20, not 30
print("func2's x:", x) # Prints 30
func2()
Modifying Scope with Keywords
global Keyword
The global keyword allows you to modify a global variable from within a function:
counter = 0
def increment():
global counter
counter += 1
print(counter) # Output: 0
increment()
print(counter) # Output: 1
nonlocal Keyword
The nonlocal keyword allows you to modify a variable from an enclosing (but non-global) scope:
def outer():
x = "outer"
def middle():
x = "middle"
def inner():
nonlocal x
x = "modified"
print("Inner function:", x)
print("Before inner:", x)
inner()
print("After inner:", x)
middle()
outer()
Important Notes on Modifying Scope
- You can modify mutable global objects (like lists) without using the
globalkeyword. - You must use the
globalkeyword to modify immutable global objects (like numbers, strings, tuples).
# Example with mutable object
items = [1, 2, 3]
def modify_list():
items.append(4) # No need for global keyword
modify_list()
print(items) # Output: [1, 2, 3, 4]
# Example with immutable object
number = 10
def modify_number():
global number # Required for immutable objects
number = 20
modify_number()
print(number) # Output: 20
Additional Example: Default Mutable Arguments
# Common pitfall with mutable default arguments
def append_to_list(item, target_list=[]):
target_list.append(item)
return target_list
# This accumulates items across calls
print(append_to_list(1)) # Output: [1]
print(append_to_list(2)) # Output: [1, 2]
# Better approach
def append_to_list_fixed(item, target_list=None):
if target_list is None:
target_list = []
target_list.append(item)
return target_list
# This works as expected
print(append_to_list_fixed(1)) # Output: [1]
print(append_to_list_fixed(2)) # Output: [2]