A08 Functions
Funcion Definition
# Anatomy of a Python function
# ------------------------------------------------------------------------------
# A function definition begins with the ``def`` keyword followed by its name and
# parameters. The body can perform operations using those parameters and return
# a value. Well-documented functions include a docstring that briefly states
# their purpose.
def function_name(parameter1, parameter2):
""" Docstring: description of the function """
# Code to be executed when the function is called
result = parameter1 + parameter2
print(result)
# Return statement (optional)
return result
Positional Arguments
# Using positional arguments
# ------------------------------------------------------------------------------
# Each value is matched to a parameter based on where it appears, so the order
# of the provided arguments matters. Positional parameters correspond directly
# to the order defined in the function signature. Mixing up the order can lead
# to incorrect results or errors.
def greet(name, age):
print("Hello, {0}! You are {1} years old.".format(name, age))
# Calling the greet() function with positional arguments in the correct order
greet("Alice", 25)
# Output: Hello, Alice! You are 25 years old.
# Calling the greet() function with positional arguments incorrectly
greet(30, "Bob")
# Output: Hello, 30! You are Bob years old.
Named Arguments
# Calling functions with keyword arguments
# ------------------------------------------------------------------------------
# When a function call includes parameter names, the order of those arguments no
# longer matters. Keyword arguments make the call site clearer and allow some
# parameters to be skipped if they have defaults. They also pair well with
# functions that accept many optional settings.
def greet(name, age):
print("Hello, {0}! You are {1} years old.".format(name, age))
# Calling the greet() function with named arguments
greet(name="Alice", age=25)
Default Arguments
# Default arguments in functions
# ------------------------------------------------------------------------------
# Functions may define default values for parameters so callers can omit those
# arguments. This simplifies the call site and allows optional behavior.
# Remember that default expressions are evaluated when the function is defined.
def greet(name='Nemo', age=42):
print("Hello, {0}! You are {1} years old.".format(name, age))
greet()
# Output: Hello, Nemo! You are 42 years old.
Dynamic Arguments Python3
# Mixing positional arguments with keyword-only arguments
# ------------------------------------------------------------------------------
# Python 3 lets you combine regular positional parameters with ``*args`` and
# keyword-only parameters that have default values. The `*` separator defines
# that the positional parameters until a key-value pair is encountered.
def variable_number_of_arguments(a, *args, b=1, **kwargs):
print(f"a: {a}")
print(f"b: {b}")
print(f"args: {args}")
print(f"kwargs: {kwargs}")
variable_number_of_arguments(1, 2, 3, c=4)
Dynamic Arguments Python2
# Handling a variable number of arguments in Python 2
# ------------------------------------------------------------------------------
# This code captures extra positional arguments with ``*args`` and extra
# keyword arguments with ``**kwargs`` when keyword-only parameters are
# unavailable. In Python 3 you can declare keyword-only parameters using the
# ``*`` separator instead of relying on ``**kwargs``. See
# ``func_variable_arguments_python3.py`` for comparison.
def variable_number_of_arguments(a, b, *args, **kwargs):
print("a: {a}".format(a=a))
print("b: {b}".format(b=b))
print("args: {args}".format(args=args))
print("kwargs: {kwargs}".format(kwargs=kwargs))
variable_number_of_arguments(1, 2, 3, c=4)
Positional Only Arguments
# Positional-only arguments
# ------------------------------------------------------------------------------
# Some parameters can be declared positional-only so they cannot be passed by
# name. This keeps the API minimal and prevents accidental clashes with keyword
# arguments. The syntax uses a ``/`` in the parameter list to mark the end of
# positional-only parameters.
# The arguments a and b are positional-only
def positional_only_arguments(a, b, /):
return a + b
# The argument a is positional-only, b is positional or keyword
def one_positional_only_argument(a, /, b):
return a + b
Keyword Only Arguments
# Keyword-only arguments
# ------------------------------------------------------------------------------
# Keyword-only parameters must be specified by name in the call. This avoids
# ambiguity and makes the purpose of each argument clear. It is particularly
# helpful when a function accepts many optional parameters.
# The arguments a and b are keyword-only
def keyword_only_arguments(*, a, b):
return a + b
# The argument a is positional or keyword, b is keyword-only
def one_keyword_only_argument(a, *, b):
return a + b
# The argument a is positional only, b is keyword-only
def separate_arguments(a, /, *, b):
return a + b
Unpacking Arguments
# Argument unpacking with `*args`
# ------------------------------------------------------------------------------
# When calling a function, the star operator can expand an iterable into
# positional arguments. This allows you to store the arguments in a list or
# other iterable and pass them all at once.
def my_function(a, b, c):
print(a, b, c)
args = [1, 2, 3]
my_function(*args)
Variable Scope
# Understanding variable scope in Python
# ------------------------------------------------------------------------------
# There are two types of variable scope in Python: global and local. If a
# local variable has the same name as a global variable, the local variable
# will take precedence within the function.
#
# If the function needs to use the global variable, it must declare it as
# global using the `global` keyword.
#
# !!! WARNING !!!
# Modifying a global variable inside a function can lead to unexpected behavior
# and should be done with caution. A good practice is to avoid the use of
# global variables altogether, unless absolutely necessary.
var = 1
print(var)
# Output: 1
# Local variable with the same name
def func_local_var():
# Redefine the variable within the function scope
var = 2
print(var)
func_local_var()
print(var)
# Output: 1
def func_using_global_var():
# Declare that we want to use the global variable
global var
var = 3
print(var)
func_using_global_var()
Nested Functions
# Nested functions and their access to enclosing variables
# ------------------------------------------------------------------------------
# Inner functions can access variables from the outer function that defines
# them. This ability creates a closure which preserves the environment even
# after the outer function has finished executing. It allows the inner function
# to operate using data that would otherwise be out of scope.
def absolute_value(x):
# Emulate the built-in abs() function, e.g. abs(-1) == 1 and abs(1) == 1
def negative_value():
# An inner function can access the variables of the outer function
return -x
def positive_value():
# An inner function can also access the variables of the outer function
return x
# Use the inner functions to return the correct value
return negative_value() if x < 0 else positive_value()
print(absolute_value(-1)) # 1
print(absolute_value(1)) # 1
Closure Functions
# Closures in Python
# ------------------------------------------------------------------------------
# A closure in Python is a function object that “remembers” values from its
# enclosing scope even when that scope has finished execution. In other
# words, a closure lets you bind variables from an outer function into an
# inner function, and keep using them later.
def greet(message):
def inner_function(name):
return "{} {}".format(message, name)
return inner_function
welcome = greet("Welcome")
print(welcome('Branko'))
# Output: Welcome Branko
print(greet('Hello')('Branko'))
# Output: Hello Branko
Function Factory
# Function factories to create specialized functions
# ------------------------------------------------------------------------------
# A function factory returns a new function tailored to the argument it
# receives. It enables creation of many small functions without repeating code.
# Each generated function captures the parameters provided to the factory.
def power_of(n):
def power(x):
return x ** n
return power
# Square root
sqrt = power_of(0.5)
print(sqrt(100.0))
# Square
sqr = power_of(2)
print(sqr(10.0))
Recursive Function
# Recursive functions in Python
# ------------------------------------------------------------------------------
# A recursive function repeatedly calls itself with a simpler version of the
# original problem. Each call works toward a base case that stops the recursion.
# This technique is often used for tasks that can be defined in terms of similar
# subproblems.
def factorial(n):
# Base case
if n == 0:
return 1
# Recursive case
else:
return n * factorial(n - 1)
test_function = factorial(5)
print(test_function)
Callback Funciton
# Using callback functions to handle events
# ------------------------------------------------------------------------------
# A callback function is passed as an argument to another function and executed
# when a particular event occurs. This technique lets the caller customize
# behavior without changing the callee. Callbacks are common in event-driven
# architectures and asynchronous code.
_listeners = {}
def on(event_name, callback):
_listeners.setdefault(event_name, []).append(callback)
def emit(event_name, *args, **kwargs):
for callback in _listeners.get(event_name, []):
callback(*args, **kwargs)
def handle_data(data):
print(f"[DATA] Received: {data!r}")
def handle_error(msg):
print(f"[ERROR] {msg}")
on("data", handle_data)
on("error", handle_error)
emit("data", {"id": 1, "value": 42})
emit("data", {"id": 2, "value": 99})
emit("error", "Timeout occurred")
Functions Attributes
# Adding attributes to functions
# ------------------------------------------------------------------------------
# Functions in Python are can have attributes. They are accessed using the dot
# notation (e.g. `foo.name`), and can be used to store metadata about the
# function, such as its name, description, or author.
def foo():
pass
foo.name = "MyFunc"
foo.description = "This is my function"
foo.author = "Me"
print(foo.author)
# Output: Me
print(foo.name)
# Output: MyFunc
print(foo.description)
# This is my function