7. The Function
The function is another organizational unit of code in Python. Python functions are either named or anonymous set of statements or expressions. In Python, functions are first class objects. This means that there is no restriction on function use as values; introspection on functions can be carried out, functions can be assigned to variables, functions can be used as arguments to other function and functions can be returned from method or function calls just like any other python value such as strings and numbers.
7.1 Function Definitions
The def keyword is the usual way of creating user-defined functions. Function definitions are executable statements.
def square(x):
return x**2
When a function definition such as the square function defined above is encountered, only the function definition statement, that is def square(x), is executed; this implies that all arguments are evaluated. The evaluation of arguments has some implications for function default arguments that have mutable data structure as values; this will be covered later on in this chapter. The execution of a function definition binds the function name in the current name-space to a function object which is a wrapper around the executable code for the function. This function object contains a reference to the current global name-space which is the global name-space that is used when the function is called. The function definition does not execute the function body; this gets executed only when the function is called.
Python also has support for anonymous functions. These functions are created using the lambda keyword. Lambda expressions in python are of the form:
lambda_expr ::= "lambda" [parameter_list]: expression
Lambda expressions return function objects after evaluation and have same attributes as named functions. Lambda expressions are normally only used for very simple functions in python due to the fact that a lambda definition can contain only one expression. A lambda definition for the square function defined above is given in the following snippet.
>>> square = lambda x: x**2
>>> for i in range(10):
square(i)
0
1
4
9
16
25
36
49
64
81
>>>
7.2 Functions are Objects
Functions just like other values are objects. Functions have the type function.
>>> def square(x):
... return x*x
>>> type(square)
<class 'function'>
Like every other object, introspection on functions using the dir() function provides a list of function attributes.
def square(x):
return x**2
>>> square
<function square at 0x031AA230>
>>> dir(square)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__',\
'__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__glo\
bals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__',\
'__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof_\
_', '__str__', '__subclasshook__']
>>>
Some important function attributes include:
-
__annotations__this attribute contains optional meta-data information about arguments and return types of a function definition. Python 3 introduced the optional annotation functionality primarily to help tools used in developing python software. An example of a function annotation is shown in the following example.>>>defsquare(x:int)->int:...returnx*x...square.__annotations__{'x':<class'int'>, 'return': <class 'int'>}Parameters are annotated by a colon after the parameter name, followed by an expression evaluating to the value of the annotation. Return values are annotated by a literal
->, followed by an expression, between the parameter list and the colon denoting the end of thedefstatement. In the case of default values for functions, the annotation is of the following form.>>>defdef_annotation(x:int,y:str="obi"):...pass -
__doc__returns the documentation string for the given function.defsquare(x):"""return square of given number"""returnx**2>>>square.__doc__'return square of given number' -
__name__returns function name.>>>square.func_name'square' -
__module__returns the name of module function is defined in.>>>square.__module__'__main__' -
__defaults__returns a tuple of the default argument values. Default arguments are discussed later on. -
__kwdefaults__returns adictcontaining default keyword argument values. -
__globals__returns a reference to the dictionary that holds the function’s global variables (see the chapter 5 for a word on global variables).>>>square.func_globals{'__builtins__':<module'__builtin__'(built-in)>,'__name__':'__main__','square':<functionsqua\reat0x10f099c08>,'__doc__':None,'__package__':None} -
__dict__returns the name-space supporting arbitrary function attributes.>>>square.func_dict{} -
__closure__returns tuple of cells that contain bindings for the function’s free variables. Closures are discussed later on in this chapter.
Functions can be passed as arguments to other functions. These functions that take other functions as argument are commonly referred to as higher order functions and these form a very important part of functional programming. A very good example of a higher order function is the map function that takes a function and an iterable and applies the function to each item in the iterable returning a new list. In the following example, we illustrate the use of the map() higher order function by passing the square function previously defined and an iterable of numbers to the map function.
>>> map(square, range(10))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
A function can be defined inside another function as well as returned from a function call.
>>> def make_counter():
... count = 0
... def counter():
... nonlocal count # nonlocal captures the count binding from enclosing scope not global s\
cope
... count += 1
... return count
... return counter
In the previous example, a function, counter is defined within another function, make_counter, and the counter function is returned whenever the make_counter function is executed. Functions can also be assigned to variables just like any other python object as shown below:
>>> def make_counter():
... count = 0
... def counter():
... nonlocal count # nonlocal captures the count binding from enclosing scope not global scope
... count += 1
... return count
... return counter
>>> func = make_counter()
>>> func
<function inner at 0x031AA270>
>>>
In the above example, the make_counter function returns a function when called and this is assigned to the variable func. This variable refers to a function object and can be called just like any other function as shown in the following example:
>>> func()
1
7.3 Functions are descriptors
As mentioned in the previous chapter, functions are also descriptors. An inspection of the attributes of a function as shown in the following example shows that a function has the __get__ method attribute thus making them non-data descriptors.
>>> def square(x):
... return x**2
...
>>> dir(square) # see the __get__ attribute
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__',\
'__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__glo\
bals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__',\
'__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof_\
_', '__str__', '__subclasshook__']
>>>
This __get__ method is called whenever a function is referenced and provides the mechanism for handling method calls from objects and ordinary function calls. This descriptor characteristic of a function enables functions to return either itself or a bound method/ when referenced depending on where and how it is referenced.
7.4 Calling Functions
In addition to calling functions in the conventional way with normal arguments, Python also supports functions with variable number of arguments. These variable number of arguments come in three flavours that are described below:
-
Default Argument Values: This allows a user to define default values for some or all function arguments. In this case, such a function can be called
with fewer arguments and the interpreter will use default values for arguments that are not supplied during function call. This following example is illustrative.
defshow_args(arg,def_arg=1,def_arg2=2):return"arg={}, def_arg={}, def_arg2={}".format(arg,def_arg,def_arg2)The above function has been defined with a single normal positional argument,
argand two default arguments,def_arganddef_arg2. The function can be called in any of the following ways below:- Supplying non-default positional argument values only; in this case the other arguments take on the supplied default values:
defshow_args(arg,def_arg=1,def_arg2=2):return"arg={}, def_arg={}, def_arg2={}".format(arg,def_arg,def_arg2)>>>show_args("tranquility")'arg=tranquility, def_arg=1, def_arg2=2' - Supplying values to override some default arguments in addition to the non-default positional arguments:
defshow_args(arg,def_arg=1,def_arg2=2):return"arg={}, def_arg={}, def_arg2={}".format(arg,def_arg,def_arg2)>>>show_args("tranquility","to Houston")'arg=tranquility, def_arg=to Houston, def_arg2=2' - Supplying values for all arguments overriding even arguments with default values.
defshow_args(arg,def_arg=1,def_arg2=2):return"arg={}, def_arg={}, def_arg2={}".format(arg,def_arg,def_arg2)>>>show_args("tranquility","to Houston","the eagle has landed")'arg=tranquility, def_arg=to Houston, def_arg2=the eagle has landed'
It is also very important to be careful when using mutable data structures as default arguments. Function definitions get executed only once so these mutable data structures are created once at definition time. This means that the same mutable data structure is used for all function calls as shown in the following example:
defshow_args_using_mutable_defaults(arg,def_arg=[]):def_arg.append("Hello World")return"arg={}, def_arg={}".format(arg,def_arg)>>>show_args_using_mutable_defaults("test")"arg=test, def_arg=['Hello World']">>>show_args_using_mutable_defaults("test 2")"arg=test 2, def_arg=['Hello World', 'Hello World']"On every function call,
Hello Worldis added to thedef_arglist and after two function calls the default argument has two hello world strings. It is important to take note of this when using mutable default arguments as default values. - Supplying non-default positional argument values only; in this case the other arguments take on the supplied default values:
-
Keyword Argument: Functions can be called using keyword arguments of the form
kwarg=value. Akwargrefers to the name of arguments used in a function definition. Take the function defined below with positional non-default and default arguments.defshow_args(arg,def_arg=1):return"arg={}, def_arg={}".format(arg,def_arg)To illustrate function calls with key word arguments, the following function can be called in any of the following ways:
show_args(arg="test",def_arg=3)show_args(test)show_args(arg="test")show_args("test",3)In a function call, keyword arguments must not come before non-keyword arguments thus the following will fail:
show_args(def_arg=4)A function cannot supply duplicate values for an argument so the following is illegal:
show_args("test",arg="testing")In the above the argument
argis a positional argument so the value
testis assigned to it. Trying to assign to the keywordargagain is an attempt at multiple assignment and this is illegal.All the keyword arguments passed must match one of the arguments accepted by the function and the order of keywords including non-optional arguments is not important so the following in which the order of argument is switched is legal:
show_args(def_arg="testing",arg="test") -
Arbitrary Argument List: Python also supports defining functions that
take arbitrary number of arguments that are passed to the function in a
tuple. An example of this from the python tutorial is given below:defwrite_multiple_items(file,separator,*args):file.write(separator.join(args))The arbitrary number of arguments must come after normal arguments; in this case, after the
fileandseparatorarguments. The following is an example of function calls to the above defined function:f=open("test.txt","wb")write_multiple_items(f," ","one","two","three","four","five")The arguments
one two three four fiveare all bunched together into a tuple that can be accessed via theargsargument.
Unpacking Function Argument
Sometimes, arguments for a function call are either in a tuple, a list or a dict. These arguments can be unpacked into functions for function calls using * or ** operators. Consider the following function that takes two positional arguments and prints out the values
def print_args(a, b):
print a
print b
If the values for a function call are in a list then these values can be unpacked directly into the function as shown below:
>>> args = [1, 2]
>>> print_args(*args)
1
2
Similarly, dictionaries can be used to store keyword to value mapping and the ** operator is used to unpack the keyword arguments to the functions as shown below:
>>> def parrot(voltage, state=’a stiff’, action=’voom’):
print "-- This parrot wouldn’t", action,
print "if you put", voltage, "volts through it.",
print "E’s", state, "!"
>>> d = {"voltage": "four million", "state": "bleedin’ demised", "action": "VOOM"}
>>> parrot(**d)
>>> This parrot wouldn’t VOOM if you put four million volts through it. E’s bleedin’ demised
* and ** Function Parameters
Sometimes, when defining a function, it is not known before hand the number of arguments to expect. This leads to function definitions of the following signature:
show_args(arg, *args, **kwargs)
The *args argument represents an unknown length of sequence of positional arguments while **kwargs represents a dict of keyword name value mappings which may contain any amount of keyword name value mapping. The *args must come before **kwargs in the function definition. The following illustrates this:
def show_args(arg, *args, **kwargs):
print arg
for item in args:
print args
for key, value in kwargs:
print key, value
>>> args = [1, 2, 3, 4]
>>> kwargs = dict(name='testing', age=24, year=2014)
>>> show_args("hey", *args, **kwargs)
hey
1
2
3
4
age 24
name testing
year 2014
The normal argument must be supplied to the function but the *args and **kwargs are optional as shown below:
>>> show_args("hey", *args, **kwargs)
hey
At function call the normal argument(s) is/are supplied normally while the optional arguments are unpacked. This kind of function definition comes in handy when dealing with function decorators as will be seen in the chapter on decorators.
7.5 Nested functions and Closures
Function definitions within another function creates nested functions as shown in the following snippet.
>>> def make_counter():
... count = 0
... def counter():
... nonlocal count # nonlocal captures the count binding from enclosing scope not global scope
... count += 1
... return count
... return counter
...
In the nested function definition, the function counter is in scope only inside the function make_counter, so it is often useful when the counter function is returned from the make_counter function.
In nested functions such as in the above example, a new instance of the nested function is created on each call to outer function. This is because during each execution of the make_counter function, the definition of the counter function is executed but the body is not executed.
A nested function has access to the environment in which it was created. A result is that a variable defined in the outer function can be referenced in the inner function even after the outer functions has finished execution.
>>> x = make_counter()
>>> x
<function counter at 0x0273BCF0>
>>> x()
1
When nested functions reference variables from the outer function in which they are defined, the nested function is said to be closed over the referenced variable.
The __closure__ special attribute of a function object is used to access the closed variables as shown in the next example.
>>> cl = x.__closure__
>>> cl
(<cell at 0x029E4470: str object at 0x02A0FD90>,)
>>> cl[0].cell_contents
0
Closures in previous versions of Python have a quirky behaviour. In Python 2.x and below, variables that reference immutable types such as string and numbers cannot be rebound within a closure. The following example illustrates this.
def counter():
count = 0
def c():
count += 1
return count
return c
>>> c = counter()
>>> c()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in c
UnboundLocalError: local variable 'count' referenced before assignment
A rather wonky solution to this is to make use of a mutable type to capture the closure as shown below:
def counter():
count = [0]
def c():
count[0] += 1
return count[0]
return c
>>> c = counter()
>>> c()
1
>>> c()
2
>>> c()
3
Python 3 introduced the nonlocal key word that fixed this closure scoping issue as shown in the following snippet.
def counter():
count = 0
def c():
nonlocal count
count += 1
return count
return c
Closures can be used for maintaining states (isn’t that what classes are for) and for some simple cases provide a more succinct and readable solution than classes. A class version of a logging API tech_pro is shown in the following example.
class Log:
def __init__(self, level):
self._level = level
def __call__(self, message):
print("{}: {}".format(self._level, message))
log_info = Log("info")
log_warning = Log("warning")
log_error = Log("error")
The same functionality that the class based version possesses can be implemented with functions closures as shown in the following snippet:
def make_log(level):
def _(message):
print("{}: {}".format(level, message))
return _
log_info = make_log("info")
log_warning = make_log("warning")
log_error = make_log("error")
The closure based version as can be seen is way more succinct and readable even though both versions implement exactly the same functionality. Closures also play a major role in a major function decorators. This is a widely used functionality that is explained in the chapter on meta-programming.
Closures also form the basis for the partial function, a function that is described in detail in the next section.
With a firm understanding of functions, a tour of some techniques and modules for functional programming in Python is given in the following section.
7.6 A Byte of Functional Programming
The Basics
The hallmark of functional programming is the absence of side effects in written code. This essentially means that in the functional style of programming object values do not change once they are created and to reflect a change in an object value, a new object with the changed value is created. An example of a function with side effects is the following snippet in which the original argument is modified and then returned.
def squares(numbers):
for i, v in enumerate(numbers):
numbers[i] = v**2
return numbers
A functional version of the above would avoid any modification to arguments and create new values that are then returned as shown in the following example.
def squares(numbers):
return map(lambda x:x*x, numbers)
Language features such as first class functions make functional programming possible while programming techniques such as mapping, reducing, filtering, currying and recursion are examples of techniques for implementing a functional style of programming. In the above example, the map function applies the function lambda x:x*x to each element in the supplied sequence of numbers.
Python provides built-in functions such as map, filter and reduce that aid in functional programming. A description of these functions follows.
-
map(func, iterable): This is a classic functional programming construct that takes a function and an iterable as argument and returns an iterator that applies the function to each item in the iterable. Thesquaresfunction from above is an illustration ofmapin use. The ideas behind themapandreduceconstructs have seen application in large scale data processing with the popularMapReduceprogramming model that is used to fan out (map) operation on large data streams to a cluster of distributed machines for computation and then gather the result of these computations together (reduce). -
filter(func, iterable): This also takes a function and an iterable as argument. It returns an iterator that appliesfuncto each element of the iterable and returns elements of the iterable for which the result of the application isTrue. The following trivial example selects all even numbers from a list.>>>even=lambdax:x%2==0>>>even(10)True>>>filter(even,range(10))<filterobjectat0x101c7b208>>>>list(filter(even,range(10)))[0,2,4,6,8] -
reduce(func, iterable[, initializer]): This is no longer a built-in and was moved into thefunctoolsmodules in Python 3 but is discussed here for completeness. Thereducefunction appliesfunccumulatively to the items initerablein order to get a single value that is returned.funcis a function that takes two positional arguments. For example,reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])calculates((((1+2)+3)+4)+5); it starts out reducing the first two arguments then reduces the third with the result of the first two and so on. If the optional initializer is provided, then it serves as the base case. An illustration of this is flattening a nested list which we illustrate below.>>>importfunctools>>>defflatten_list(nested_list):...returnfunctools.reduce(lambdax,y:x+y,nested_list,[])...>>>flatten_list([[1,3,4],[5,6,7],[8,9,10]])[1,3,4,5,6,7,8,9,10]
The above listed functions are examples of built-in higher order functions in Python. Some of the functionality they provide can be replicated using more common constructs. Comprehensions are one of the most popular alternatives to these higher order functions.
Comprehensions
Python comprehensions are syntactic constructs that enable sequences to be built from other sequences in a clear and concise manner. Python comprehensions are of three types namely:
- List Comprehensions.
- Set Comprehensions.
- Dictionary Comprehensions.
List Comprehensions
List comprehensions are by far the most popular Python comprehension construct. List comprehensions provide a concise way to create new list of elements that satisfy a given condition from an iterable. A list of squares for a sequence of numbers can be computed using the following squaresfunction that makes use of the map function.
def squares(numbers):
return map(lambda x:x*x, numbers)
>>> sq = squares(range(10))
The same list can be created in a more concise manner by using list comprehensions rather than the map function as in the following example.
>>> squares = [x**2 for x in range(10)]
The comprehension version is clearer and more concise than the conventional map method for one without any experience in higher order functions.
According to the python documentation,
a list comprehension consists of square brackets containing an expression followed by a for clause and zero or more for or if clauses.
[expression for item1 in iterable1 if condition1
for item2 in iterable2 if condition2
...
for itemN in iterableN if conditionN ]
The result of a list comprehension expression is a new list that results from evaluating the expression in the context of the for and if clauses that follow it. For example, to create a list of the squares of even numbers between 0 and 10, the following comprehension is used.
>>> even_squares = [i**2 for i in range(10) if i % 2 == 0]
>>> even_squares
[0, 4, 16, 36, 64]
The expression i**2 is computed in the context of the for clause that iterates over the numbers from 0 to 10 and the if clause that filters out non-even numbers.
Nested for loops and List Comprehensions
List comprehensions can also be used with multiple or nested for loops. Consider for example, the simple code fragment shown below that creates a tuple from pair of numbers drawn from the two sequences given.
>>> combs = []
>>> for x in [1,2,3]:
... for y in [3,1,4]:
... if x != y:
... combs.append((x, y))
...
>>> combs
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
The above can be rewritten in a more concise and simple manner as shown below using list comprehensions
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
It is important to take into consideration the order of the for loops as used in the list comprehension. Careful observation of the code snippets using comprehension and that without comprehension shows that the order of the for loops in the comprehension follows the same order if it had been written without comprehensions. The same applies to nested for loops with nesting depth greater than two.
Nested List Comprehensions
List comprehensions can also be nested. Consider the following example drawn from the Python documentation of a 3x4 matrix implemented as a list of 3 lists each of length 4:
>>> matrix = [
... [1, 2, 3, 4],
... [5, 6, 7, 8],
... [9, 10, 11, 12],
... ]
Transposition is a matrix operation that creates a new matrix from an old one using the rows of the old matrix as the columns of the new matrix and the columns of the old matrix as the rows of the new matrix. The rows and columns of the matrix can be transposed using the following nested list comprehension:
>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
The above is equivalent to the following snippet.
>>> transposed = []
>>> for i in range(4):
... transposed.append([row[i] for row in matrix])
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
Set Comprehensions
In set comprehensions, braces rather than square brackets are used to create new sets. For example, to create the set of the squares of all numbers between 0 and 10, the following set comprehensions is used.
>>> x = {i**2 for i in range(10)}
>>> x
set([0, 1, 4, 81, 64, 9, 16, 49, 25, 36])
>>>
Dict Comprehensions
Braces are also used to create new dictionaries in dict comprehensions. In the following example, a mapping of a number to its square is created using
dict comprehensions.
>>> x = {i:i**2 for i in range(10)}
>>> x
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
Functools
The functools module in Python contains a few higher order functions that act on and return other functions. A few of the interesting higher order functions that are included in this module are described.
-
partial(func, *args, **keywords)This is a function that when called returns an object that can be called like the originalfuncargument with*argsand**keywordsas arguments. If the returned object is called with additional*argsor**keywordarguments then these are added to the original*argsand**keywordsand the updated set of arguments are used in the function call. This is illustrated with the following trivial example.>>>fromfunctoolsimportpartial>>>basetwo=partial(int,base=2)>>>basetwo.__doc__='Convert base 2 string to an int.'>>>basetwo('10010')18In the above example, a new callable,
basetwo, that takes a number in binary and converts it a number in decimal is created. What has happened is that theint()functions that takes two arguments has been wrapped by a callable,basetwothat takes only one argument. To understand how this may work, take your mind back to the discussion about closures and how variable captures work. Once this is understood, it is easy to imagine how to implement this partial function. The partial function has functionality that is equivalent to the following closure as defined in the Python documentation.defpartial(func,*args,**keywords):defnewfunc(*fargs,**fkeywords):newkeywords=keywords.copy()newkeywords.update(fkeywords)returnfunc(*(args+fargs),**newkeywords)newfunc.func=funcnewfunc.args=argsnewfunc.keywords=keywordsreturnnewfuncPartial objects provide elegant solutions to some practical problems that are encountered during development. For example, suppose one has a list of points represented as tuples of
(x,y)coordinates and there is a requirement to sort all the points according to their distance from some other central point. The following function computes the distance between two points in the xy plane:>>>points=[(1,2),(3,4),(5,6),(7,8)]>>>importmath>>>defdistance(p1,p2):...x1,y1=p1...x2,y2=p2...returnmath.hypot(x2-x1,y2-y1)The built-in
sort()method of lists is handy here and accepts a key argument that can be used to customize sorting, but it only works with functions that take a single argument thusdistance()is unsuitable. Thepartialmethod provides an elegant method of dealing with this as shown in the following snippet.>>>pt=(4,3)>>>points.sort(key=partial(distance,pt))>>>points[(3,4),(1,2),(5,6),(7,8)]>>>The
partialfunction creates and returns a callable that takes a single argument, a point. Now note that the partial object has captured the reference point,ptalready so when the key is called with the point argument, the distance function passed to the partial function is used to compute the distance between the supplied point and the reference point. -
@functools.lru_cache(maxsize=128, typed=False): This decorator is used to wrap a function with a memoizing callable that saves up to themaxsizenumber of most recent calls. Whenmaxsizeis reached, oldest cached values are ejected. Caching can save time when an expensive or I/O bound function is periodically called with the same arguments. This decorator makes use of adictionaryfor storing results so is limited to caching only arguments that are hashable. Thelru_cachedecorator provides a function, thecache_infofor stats on cache useage. -
@functools.singledispatch: This is a decorator that changes a function into a single dispatch generic function. The functionality aims to handle dynamic overloading in which a single function can handle multiple types. The mechanics of this is illustrated with the following code snippet.@singledispatchdeffun(arg,verbose=False):ifverbose:print("Let me just say,",end=" ")print(arg)@fun.register(int)def_(arg,verbose=False):ifverbose:print("Strength in numbers, eh?",end=" ")print(arg)@fun.register(list)def_(arg,verbose=False):ifverbose:print("Enumerate this:")fori,eleminenumerate(arg):print(i,elem)fun("Hello, world.")fun(1,verbose=True)fun([1,2,3],verbose=True)fun((1,2,3),verbose=True)Hello,world.Strengthinnumbers,eh?1Enumeratethis:011223Letmejustsay,(1,2,3)A generic function is defined with the
@singledispatchfunction, theregisterdecorator is then used to define functions for each type that is handled. Dispatch to the correct function is carried out based on the type of the first argument to the function call hence the name,single generic dispatch. In the event that no function is defined for the type of the first argument then the base generic function,funin this case is called.
Sequences and Functional Programming
Sequences such as lists and tuples play a central role in functional programming. The Structure and Interpretation of Computer Programs, one of the greatest computer science books ever written devotes almost a whole chapter to discussing sequences and their processing. The importance of sequences can be seen from their pervasiveness in the language. Built-ins such as map and filter consume and produce sequences. Other built-ins such as min, max, reduce etc. consume sequence and return values. Functions such range, dict.items() produce sequences.
The ubiquity of sequences requires that they are represented efficiently. One could come up with multiple ways of representing sequences. For example, a naive way of implementing sequences would be to store all the members of a sequence in memory. This however has a significant drawback that sequences are limited in size to the RAM available on the machine. A more clever solution is to use a single object to represent sequences. This object knows how to compute the next required elements of the sequence on the fly just as it is needed. Python has a built-in protocol exactly for doing this, the __iter__ protocol. This is strongly related to generators, a brilliant feature of the language and these are both dived into in the next chapter.