Functions#
Providing repeating tasks or calculations in functions is an effective way of code reuse ("write oncy, use many times"). This is a brief introduction of Python functions, not covering all possible variations of function definitions. More on this can be found in the official Python docs on functions.
Functions - as basically everything in Python - are themselves first class Python objects. Once defined, they can of course get executed (by "calling" them). But like any other object they can just as well be assigned to a variable, passed as arguments to another callable or used as a return value.
Function Definition#
In Python user-defined functions are created using the def
statement. A
function definition is made up of a function header (defining the function name
and the call signature) and a function body (the implementation of the
task/calculation as a sequence of statements with an optional return
statement).
Note: The terms "parameter" and "argument" are often used interchangeably, but this is a little bit diffuse. The terms "formal parameters" for their use in function definitions and "actual parameter" or "argument" for the use in function calls may be a bit more precise. Here we'll use "parameter" for the parameter variable names in function definitions and "argument" for the actual values provided by the function caller.
Function without Parameters#
Function definition:
>>> def get_greeting(): # function header
... """Return a friendly greeting.""" # optional doc string
... # function body
... greet_text = "Hello!"
... return greet_text
...
>>>
Such a function is called simply by using the function name followed by parentheses:
Function call:
Function with Parameters#
Function definition:
>>> def increment(number, stride): # function header
... """Return number incremented with stride.""" # optional doc string
... # function body
... result = number + stride
... return result
...
>>>
To call the function we now add the arguments in parentheses.
Function call:
Note: The number of call arguments equals the number of defined parameters. The order of the arguments must match the order of function parameters: During the function call the 1.st call argument is mapped to the 1st function parameter, the 2.nd call argument is mapped to the 2nd function parameter, etc. I.e. a positional mapping from call arguments to parameters takes place - the arguments are positional arguments here.
Let's give it a try!
Lesson: Check palindromes
Create a function that
- takes a single string argument and
- returns
True
if string is a palindrome,False
otherwise.
A palindrome is a text that reads the same forward and backwards, e.g. "abba".
- Use a loop for implementation.
- Try an alternative implementation using "extended slicing": Check
word == word[::-1]
- Test the function by calling it with a palindromes and other texts.
Optional: Create a Python script check_palindromes.py
that asks the
user to enter a text to be checked if it qualifies as a palindrome.
E.g.
python3 check_palindromes.py
Please enter a word: abba
entered word: abba # optional output
reverse word: abba
is_palindrome: abba ==> True
python3 check_palindromes.py
Please enter a word: foo
entered word: foo # optional output
reverse word: oof
is_palindrome: foo ==> False
Optional: Instead of interactive user input, accept a command line
argument to your Python script so that it can be invoked like python
check_palindromes.py "racecar"
.
Use the input()
built-in function to read user input interactively.
The most basic form to read command line arguments is by accessing
them through sys.argv
. For anything more serious the
argparse standard
library module can be used.
Really take a peek now?
"""check_palindromes.py
"""
import sys
def is_palindrome(text):
"""Check if text is a palindrome using the built-in reversed() function.
"""
reversed_text = ''.join(reversed(text))
return text == reversed_text
def is_palindrome_ext_slicing(text):
"""Check if text is a palindrome using extended slicing.
"""
reversed_text = text[::-1]
return text == reversed_text
def is_palindrome_loop(text):
"""Check if text is a palindrome by looping through the string.
"""
reversed_order = []
for idx, character in enumerate(text):
idx_back = -(idx + 1)
if text[idx] != text[idx_back]:
return False
return True
def parse_args(args=None):
"""Parse arguments from sys.argv if args is None (the default) or from args
sequence otherwise.
"""
# https://docs.python.org/3/howto/argparse.html
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
'texts', nargs='*',
help='One or more palindrome candidate texts')
args = parser.parse_args(args)
return args
def main(args=None):
"""Main module function.
Exposes this module's executable functionality for use as a module
function.
Parses arguments from sys.argv if args is None (the default) or from args
sequence otherwise.
"""
args = parse_args(args)
if not args.texts:
# optional-argument
text = input("text: ")
texts = [text]
else:
texts = args.texts
for text in texts:
print(f'\nentered text: {text}')
print(f'reverse text: {text[::-1]}')
for method in [
is_palindrome,
is_palindrome_ext_slicing,
is_palindrome_loop
]:
print(f'{method.__name__}("{text}") --> {method(text)}')
if __name__ == "__main__":
sys.exit(main())
Function with Optional Parameters#
Optional parameters are parameters with default values in the function definition. Such optional parameters may be omitted during the function call.
Function definition with default paramater value:
>>> def increment(number, stride=1): # function header with default argument
... """Return number incremented with stride.""" # optional doc string
... # function-body
... result = number + stride
... return result
...
>>>
Function call omitting the optional parameter:
Function call overwriting the default value of the optional parameter:
Notes:
- any parameters without default values must be provided by the caller when calling a function
- optional parameters must be defined at the end of the parameter list, otherwise a SyntaxError is raised
E.g.
>>> def add(a, b=1, c):
... return a + b + c
...
File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
Function with Variable Parameter List (Variadic Parameter)#
A function can be defined having a variable args parameter. This is specified
by a last positional parameter with the asterisk character *
as a prefix, in
the function definition.
Function definition:
>>> # 2 normal parameters & variable args parameter
>>> def print_info(header, footer, *args):
... print(header)
... for elem in args: # args is a tuple
... print(elem)
... print(footer)
...
>>>
Varargs function call:
>>> # last 2 arguments are mapped as a tuple into the *-parameter
>>> print_info('-->', '<--', 'Hello', 'World')
-->
Hello
World
<--
>>>
Varargs function call with more args:
# last 3 arguments are mapped as a tuple into the *-parameter
>>> print_info('-->', '<--', 'Tic', 'Tac', 'Toe')
-->
Tic
Tac
Toe
<--
>>>
By convention, the variable args parameter is usually called *args
.
Keyword Arguments#
In the above sections the functions are called with positional arguments. In addition functions can also be called using named arguments (keyword arguments).
To demonstrate this, we use the increment
-function definition from above.
Function definition:
>>> def increment(number, stride=1): # function header
... """Return number incremented with stride.""" # optional doc string
... # function-body
... result = number + stride
... return result
...
>>>
Function call using keyword parameter:
Function call using multiple keyword parameters:
Note: When (only) using keyword arguments the positions of the call arguments don't matter.
Functions with Variable Keyword Args#
Functions can also accept variable additional keyword parameters. This is
specified by prefixing the last parameter with double asterisk characters **
.
When calling the function, additional keyword arguments (that are not defined as explicit parameters) are captured in the "variable keyword args" parameter as a dictionary:
>>> def print_info(header, footer, **kwargs):
... print(header)
... for (arg_name, arg_value) in kwargs.items():
... print(f'{arg_name}: {arg_value}')
... print(footer)
...
>>>
Function call:
>>> print_info('-->', '<--', city1='Madrid', city2='Berlin', city3='Paris')
-->
city1: Madrid
city2: Berlin
city3: Paris
<--
>>>
Similarly to *args
the variable keyword args parameter is usually called
**kwargs
.
Functions with Variable Args and Variable Keyword Args#
Python allows function definitions with arbitray additional keyword parameters.
This is specified in preceeding the last parameter with double-asterisk
characters **
. Additional keyword arguments are mapped into the keyword parameter as a dictionary during the function call.
Function definition with variable keyword parameters:
>>> def print_info(header, footer, *args, **kwargs):
... print(header)
... for elem in args:
... print(elem)
... for (arg_name, arg_value) in kwargs.items():
... print(f'{arg_name}: {arg_value}')
... print(footer)
...
>>>
Function call:
>>> print_info('-->', '<--', 'Madrid', 'Berlin', 'Paris', capitals_of='European Countries', belonging_to='EU')
-->
Madrid
Berlin
Paris
capitals_of: European Countries
belonging_to: EU
<--
>>>
A note on naming: As mentioned, the variable positional and keyword arg
parameters are usually called *args
and **kwargs
by convention. This is
not mandatory. You can (and should) name them differently - but only when it's
more appropriate, to best communicate/document your function's behaviour.
Function Return Value#
Functions always return a single value. If a function body doesn't contain any
explicit return statement the function implicitly returns None
. I.e. the
return statement is optional, still the function returns a value (the None
object).
>>> def say_hello():
... print("Hello!") # Look Ma, no explicit return statement!
...
>>> function_result = say_hello()
Hello!
>>> print(function_result) # We still get a None return value
None
>>>
Functions can contain return
at multiple places in the function body. Every
return statement immediately exits the function, with the return value being
the function's result.
While Python functions always return a single object you can easily return
multiple values by packing them into a collection (tuple
, list
, ...).
>>> def divide(number, divisor):
... """Divmod implementation, returns a (quotient, remainder) tuple. """
... quotient = number // divisor
... remainder = number % divisor
... return (quotient, remainder)
...
>>> result = divide(9, 4)
>>> print(result)
(2, 1)
You can "destructure" a tuple to its elements very conveniently:
>>> # divide(...) returns a 2-element tuple, which we destructure
>>> res_quotient, res_remainder = divide(9, 4)
>>> print(res_quotient)
2
>>> print(res_remainder)
1
>>>
Inner functions#
Functions can be defined at every place, at module level, inside classes (as methods), but also inside functions.
function definition
>>> def outer_func(inner_func_name):
... """Function that defines inner functions and returns the requested
... inner function by name.
... """
... def x():
... print(x.__name__)
... def y():
... print(y.__name__)
... # return an inner function, simply by its name
... if inner_func_name == "x":
... return x
... elif inner_func_name == "y":
... return y
... else:
... return None
...
>>>
function call
>>> x_func = outer_func("x") # assign a function
>>> x_func() # call the function
x
>>> y_func = outer_func("y") # assign a function
>>> y_func() # call the function
y
>>>
Note: As can be seen, functions are like ordinary Python objects that can be returned and assigned. Of course, they can also be used as arguments for other functions.
Function Annotations#
Function Annotations allow programmers to associate meta information to a function header. One kind of interesting meta information are so called 'type hints', which can provide type information about the function parameters and return value.
Function annotations are stored in the __annotations__
attribute of a
function object.
Function Annotation example
>>> def concatenate(string_1: str, string_2: str) -> str:
... return string_1 + string_2
...
>>> concatenate.__annotations__
{'string_1': <class 'str'>, 'string_2': <class 'str'>, 'return': <class 'str'>}
>>> concatenate('foo', 'bar')
'foobar'
>>>
For a more detailed inforamtions please refer to PEP 3107 -- Function Annotations and PEP 484 -- Type Hints.
Notes:
- Function annotations are optional, they are just informations. They are neither evaluated nor enforced by the interpreter itself. The language feature exists to help other tools, e.g. to do type checking as static code analysis.
- The Python standard library provides "type hinting support" in the
typing
module.
Python Function Call Semantics#
Traditional function call semantics (as e.g. known from C/C++) are:
-
Call-by-value:
-
the value of the argument variable is copied to the call parameter of the function
-
changing the value inside the function doesn't effect the caller
-
Call-by-reference:
-
a reference of the caller's variable is passed to the call parameter of the function
- as a consequence, changes to the variable inside the function will affect the callers variable (side effect from callee back to the caller)
- alongside the function return value, this provides additional communication channels between caller and callee (since the changes made inside the function can be seen on/provided to the outside)
Python does not have such a distinction. Every variable is a name for an object and constitutes a reference to the object. In that sense everything is passed around by reference, which is also the case for function calls.
Instead, the Python function call behaviour with regard to modification of parameters is solely influenced by the mutability or immutability of the caller's function call arguments:
- Argument variables referring to an immutable object will not produce side effects on the caller side, since the function (the callee) can not change the immutable object.
- Argument variables referring to a mutable object may have side effects on the caller side. If the mutable object is changed inside the function (callee), these changes will be visible to whatever reference/name for it on the outside.
Function call with immutable argument:
>>> a = 1
>>> id(1) # object id of the 1 integer object
139752035048832
>>> def increment(number):
... # number refers to the same object as the callers variable (1)
... print(id(number))
... number += 1 # assignment (number = number + 1) creates new object
... print(id(number)) # id of the new object
... return a
...
>>> b = increment(a)
139752035048832
139752035048864
>>> a
1
>>> id(a) # a still refers to the unchanged same object 1
139752035048832
>>>
Note: Immutable objects of the caller are not affected by changes made by the callee (the called function).
Function call with mutable call argument:
>>> caller_dict = {'a': 1, 'b': 2}
>>> id(caller_dict) # id of the object referred to by name caller_dict
139752035393824
>>> def change_mutable_arg(dct):
... print(id(dct)) # dct refers to the same object as the callers variable (1)
... for elem in dct.keys(): dct[elem] += 1 # changes the mutable object
... print(id(dct)) # dct still refers to (changed) origin object
... return dct
...
>>> result_dict = change_mutable_arg(caller_dict)
139752035393824
139752035393824
>>> caller_dict # reflects the changes, side effect of the function call
{'a': 2, 'b': 3}
>>> id(result_dict) # same object, same id
139752035393824
>>> result_dict
{'a': 2, 'b': 3}
>>>
Note: Function calls with mutable object arguments may have side effects to the caller - if the called function modifies a mutable object.
Anonymous Functions#
Instead of creating a named function using def
the lambda
keyword allows
for the creation of (simple) anonymous functions:
>>> # lambda creates an anonymous function.
>>> lambda x: x**2
<function <lambda> at 0x7fd54160c670>
>>> # You can call a lambda function right away.
>>> (lambda x: x**2)(4)
16
>>> # Of course everything's an object so you can assign to a variable name.
>>> square = lambda x: x**2
>>> square(4)
16
>>>
These function definitions are equivalent, apart from func.__name__
:
>>> square = lambda x: x**2
>>> square(3)
9
>>> square.__name__
'<lambda>'
>>> def square(x):
... return x**2
...
>>> square(3)
9
>>> square.__name__
'square'
>>>
An anonymous function of lambda function can not contain multiple statements like a normal function, just a single result expression.
So everything that you can do with a lambda function (and more) can be done with a regular function.
Still, lambda functions can be handy for on-the-fly generation of very short functions "inline" or as arguments, e.g.:
>>> calculate = {
... 'plus': lambda x, y: x + y,
... 'minus': lambda x, y: x - y,
... 'multiply': lambda x, y: x * y,
... 'divide': lambda x, y: x / y,
... }
>>> calculate['plus'](8, 2)
10
>>> calculate['divide'](8, 2)
4.0
>>>
Functional Programming#
Functional programming usually builds upon some of these characteristics:
- (pure) functions do not hold internal state (nor are they able to modify external state and produce side effects)
- usage of recursion
- higher order functions (basically: functions taking other functions as arguments to apply/combine/compose them and/or produce new functions)
Python has several features in support of this:
- functions are 1st class objects
- lambda functions
- the
map
andfilter
built-ins andfunctools.reduce
- the
functools
anditertools
modules - iterators
Find more information on Python's approach to functional programming in the Functional Programming HOWTO.
Assorted Built-in Functions#
Python features a number of essential built-in functions. Here's an assorted selection of frequently used ones:
----------------------------------------------------------------------
abs(x)
# Return the absolute value of number `x`
----------------------------------------------------------------------
all(iterable)
# Return True if all elements in `iterable` are true or `iterable` is empty.
----------------------------------------------------------------------
any(iterable)
# Return True if any element in `iterable` is true. Returns False ff the iterable is empty.
----------------------------------------------------------------------
callable(object)
# Return True if `object` is callable.
----------------------------------------------------------------------
chr(i)
# Return the character for integer unicode code point i.
----------------------------------------------------------------------
delattr(object, name)
# Delete attribute `name` from `object`.
----------------------------------------------------------------------
dir()
dir(object)
# Return the list of names in the local scope (without arguments) or the list of `object` attributes.
----------------------------------------------------------------------
enumerate(iterable, start=0)
# Return an enumerate iterator object that yields (index, value) tuples when iterated over.
# Warning: evaluates arbitrary code.
----------------------------------------------------------------------
eval(source, /, globals=None, locals=None)
# Evaluate expression and return the result. Source can be a string or code object.
----------------------------------------------------------------------
exec(source, /, globals=None, locals=None, *, closure=None)
# Execute `source` (string or code object).
# Warning: execture arbitrary code.
----------------------------------------------------------------------
getattr(object, name)
getattr(object, name, default)
# Return the value of object's name attribute or `default`. Raises if
----------------------------------------------------------------------
hasattr(object, name)
# Return True if `object` has attribute `name`, False otherwise.
----------------------------------------------------------------------
input()
input(prompt)
# Read and return input from stdin.
----------------------------------------------------------------------
len(s)
# Return the length of an object (i.e. the number of items in `object`).
----------------------------------------------------------------------
map(function, iterable, *iterables)
# Apply `function` to each element in `iterable`. If n iterables are given, `function` must take n arguments
# and is applied to all iterables in parallel.
----------------------------------------------------------------------
max(iterable, *, key=None)
max(iterable, *, default, key=None)
max(arg1, arg2, *args, key=None)
# Return the maximum value in `iterable` (or of args).
----------------------------------------------------------------------
min(iterable, *, key=None)
min(iterable, *, default, key=None)
min(arg1, arg2, *args, key=None)
# Return the minimun value in `iterable` (or args args).
----------------------------------------------------------------------
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
# Open a file and return the file object.
----------------------------------------------------------------------
ord(c)
# Return the unicode codepoint integer value of character `c`.
----------------------------------------------------------------------
pow(base, exp, mod=None)
# Return base**exp (optionally modulo `mod`).
----------------------------------------------------------------------
print(*objects, sep=' ', end='\n', file=None, flush=False)
# Print objects. Output defaults to stdout.
----------------------------------------------------------------------
reversed(seq)
# Return an iterator over `seq` in reverse order.
----------------------------------------------------------------------
round(number, ndigits=None)
# Return `number` rounded to `ndigits` precision after the decimal point.
----------------------------------------------------------------------
sum(iterable, /, start=0)
# Return the sum of elements in `iterable`.
----------------------------------------------------------------------
zip(*iterables, strict=False)
# Return an iterator of (l1[i], l2[i], ..., ln[i]) tuples of `iterables` l1, ..., ln.