5. Functions

5.1. def

Format:

def name(arg1, arg2, ..., argN):
  ...
  [return value]

Executes at runtime:

if test:
  def func():
    print 'Define func this way'
else:
  def func():
    print 'Or else this way'
...
func()    # Call the func defined

5.2. Scopes

  • If a variable is assigned inside a def, it is local to that function.
  • If a variable is assigned in an enclosing def, it is nonlocal to nested functions.
  • If a variable is assigned outside all defs, it is global to the entire file.
x = 99          # Global(module)

def func():
  x = 88        # Local(func): a different variable

  def inner():
    print(x)    # Nonlocal(inner)

Name Resolution: The LEGB Rule

  • Name assignments create or change local names by default.
  • Name references search at most four scopes: local(L), then enclosing(E) functions (if any), then global(G), then built-in(B).
  • Names declared in global and nonlocal statements map assigned names to enclosing module and function scopes, respectively.

The built-in scope:

>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
>>> zip
<class 'zip'>
>>> zip is builtins.zip
True

Use global and nonlocal for changes:

>>> x = 99
>>> def func():
...   global x
...   x = 88
...
>>> print(x)
99
>>> func()
>>> print(x)
88

>>> def func():
...   x = 88
...   def inner():
...     nonlocal x
...     x = 77
...   print(x)
...   inner()
...   print(x)
...
>>> func()
88
77

>>> x = 99
>>> def func():
...   nonlocal x
...   x = 88
...
  File "<stdin>", line 2
SyntaxError: no binding for nonlocal 'x' found

See PEP 3104: nonlocal statement. Using nonlocal x you can now assign directly to a variable in an outer (but non-global) scope. nonlocal is a new reserved word

5.3. Arguments

Argument Matching Basics

  • Positionals: matched from left to right
  • Keywords: matched by argument name
  • Defaults: specify values for optional arguments that aren’t passed
  • Varargs collecting: collect arbitrarily many positional or keyword arguments
  • Varargs unpacking: pass arbitrarily many positional or keyword arguments
  • Keyword-only arguments: arguments that must be passed by name
Syntax Interpretation
func(value) Normal argument: matched by position
func(name=value) Keyword argument: matched by name
func(*iterable) Pass all objects in iterable as individual positional arguments
func(**dict) Pass all key/value pairs in dict as individual keyword arguments
def func(name) Normal argument: matches any passed value by position or name
def func(name=value) Default argument value, if not passed in the call
def func(*name) Matches and collects remaining positional arguments in a tuple
def func(**name) Matches and collects remaining keyword arguments in a dictionary
def func(*other, name) Arguments that must be passed by keyword only in calls (3.X)
def func(*, name=value) Arguments that must be passed by keyword only in calls (3.X)
>>> def f(a, *pargs, **kargs): print(a, pargs, kargs)
>>> f(1, 2, 3, x=1, y=2)
1 (2, 3) {'y': 2, 'x': 1}

>>> def func(a, b, c, d): print(a, b, c, d)
>>> args = (1, 2)
>>> args += (3, 4)
>>> func(*args)     # Same as func(1, 2, 3, 4)
1 2 3 4

>>> args = {'a': 1, 'b': 2, 'c': 3}
>>> args['d'] = 4
>>> func(**args)    # Same as func(a=1, b=2, c=3, d=4)
1 2 3 4

>>> func(*(1, 2), **{'d': 4, 'c': 3})   # Same as func(1, 2, d=4, c=3)
1 2 3 4
>>> func(1, *(2, 3), **{'d': 4})        # Same as func(1, 2, 3, d=4)
1 2 3 4
>>> func(1, c=3, *(2,), **{'d': 4})     # Same as func(1, 2, c=3, d=4)
1 2 3 4
>>> func(1, *(2, 3), d=4)               # Same as func(1, 2, 3, d=4)
1 2 3 4
>>> func(1, *(2,), c=3, **{'d':4})      # Same as func(1, 2, c=3, d=4)
1 2 3 4

Quiz: Write a function max accepts any number of arguments and returns the bigest of them.

3.x keyword-only arguments:

>>> def kwonly(a, *b, c, **d): print(a, b, c, d)
>>> kwonly(1, 2, c=3)
1 (2,) 3 {}
>>> kwonly(a=1, c=3)
1 () 3 {}
>>> kwonly(1, 2, 3)
TypeError: kwonly() missing 1 required keyword-only argument: 'c'
>>> kwonly(1, 2, c=3, d=4, e=5)
1 (2,) 3 {'d':4, 'e': 5}
Keyword-only arguments must be specified after a single star, not two.
Named arguments cannot appear after the **args arbitrary keywords form, and a ** can’t appear by itself in the arguments list.
>>> def kwonly(a, **pargs, b, c):
SyntaxError: invalid syntax
>>> def kwonly(a, **, b, c):
SyntaxError: invalid syntax

Why keyword-only arguments ?

def process(*args, notify=False): ...

process(X, Y, Z)            # Use flag's default
process(X, Y, notify=True)  # Override flag default

Without keyword-only arguments we have to use both *args and **args and manually inspect the keywords, but with keyword-only arguments less code is required.

Quiz: try to implement the same feature above without using keyword-only arguments.

5.4. Function design principles

  • use arguments for inputs and return for outputs.
  • use global variables only when truly necessary.
  • don’t change mutable arguments unless the caller expects it.
  • each function should have a single, unified purpose.
  • each function should be relatively small.
  • avoid changing variables in another module file directly.

5.5. “First Class” Objects

Python functions are full-blown objects:

>>> schedule = [ (echo, 'Spam!'), (echo, 'Ham!') ]
>>> for (func, arg) in schedule:
func(arg)

5.6. Function Introspection

>>> def mul(a, b):
...   """Multiple a by b times"""
...   return a * b
...
>>> mul('spam', 8)
'spamspamspamspamspamspamspamspam'

>>> mul.__name__
'mul'
>>> mul.__doc__
'Multiple a by b times'

>>> mul.__code__
<code object func at 0x104f24c90, file "<stdin>", line 1>
>>> func.__code__.co_varnames
('a', 'b')
>>> func.__code__.co_argcount
2

5.7. Function Annotations in 3.x

Annotations are completely optional, and when present are simply attached to the function object’s __annotations__ attribute for use by other tools.

>>> def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
...     ...
...
>>> foo.__annotations__
{'a': 'x', 'return': 9, 'c': <class 'list'>, 'b': 11}

See PEP 3107: Function argument and return value annotations.

5.8. Anonymous Functions: lambda

lambda argument1, argument2,… argumentN : expression using arguments
  • lambda is an expression, not a statement.
  • lambda’s body is a single expression, not a block of statements.
  • annotations are not supported in lambda

5.9. Functional programming tools

map, filter, functools.reduce

5.10. Generator functions

yield vs. return:

>>> def gensquares(N):
...   for i in range(N):
...     yield i ** 2
...
>>> for i in gensquares(5): # Resume the function
...   print(i, end=' : ')
...
0 : 1 : 4 : 9 : 16 :

>>> x = gensquares(2)
>>> x
<generator object gensquares at 0x000000000292CA68>
>>> next(x)
0
>>> next(x)
1
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module> StopIteration

>>> y = gensquares(5)
>>> iter(y) is y
True

Why using generators ?

send vs. next:

>>> def gen():
...   for i in range(10):
...     x = yield i
...     print('x=', x)
...
>>> g = gen()
>>> next(g)
0
>>> g.send(77)
x= 77
1
>>> g.send(88)
x= 88
2
>>> next(g)
x= None
3

See PEP 342 – Coroutines via Enhanced Generators

yield from
allows a generator to delegate part of its operations to another generator.

For simple iterators, yield from iterable is essentially just a shortened form of for item in iterable: yield item:

>>> def g(x):
...     yield from range(x, 0, -1)
...     yield from range(x)
...
>>> list(g(5))
[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]

However, unlike an ordinary loop, yield from allows subgenerators to receive sent and thrown values directly from the calling scope, and return a final value to the outer generator:

>>> def accumulate():
...     tally = 0
...     while 1:
...         next = yield
...         if next is None:
...             return tally
...         tally += next
...
>>> def gather_tallies(tallies):
...     while 1:
...         tally = yield from accumulate()
...         tallies.append(tally)
...
>>> tallies = []
>>> acc = gather_tallies(tallies)
>>> next(acc) # Ensure the accumulator is ready to accept values
>>> for i in range(4):
...     acc.send(i)
...
>>> acc.send(None) # Finish the first tally
>>> for i in range(5):
...     acc.send(i)
...
>>> acc.send(None) # Finish the second tally
>>> tallies
[6, 10]

See PEP 380: Syntax for Delegating to a Subgenerator

itertools: Functions creating iterators for efficient looping

>>> from itertools import *
>>> def take(n, iterable):
...     "Return first n items of the iterable as a list"
...     return list(islice(iterable, n))
...
>>>

>>> take(10, count(2))
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
>>> take(10, cycle('abcd'))
['a', 'b', 'c', 'd', 'a', 'b', 'c', 'd', 'a', 'b']
>>> take(5, repeat(6))
[6, 6, 6, 6, 6]

>>> list(accumulate([1,2,3,4,5]))
[1, 3, 6, 10, 15]
>>> list(chain('abc', 'ABC'))
['a', 'b', 'c', 'A', 'B', 'C']
>>> list(takewhile(lambda x: x<5, [1,4,6,4,1]))
[1, 4]

>>> list(permutations('ABCD', 2))
[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'A'), ('B', 'C'), ('B', 'D'), ('C', 'A'), ('C', 'B'), ('C', 'D'), ('D', 'A'), ('D', 'B'), ('D', 'C')]
>>> list(combinations('ABCD', 2))
[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]

5.11. Function Decorators

Decorator is just a function returning another function. It is merely syntactic sugar, the following two function definitions are semantically equivalent:

@f1(arg)
@f2
def func(): pass

def func(): pass
func = f1(arg)(f2(func))

Common examples for decorators are classmethod() and staticmethod():

def f(...):
    ...
f = staticmethod(f)

@staticmethod
def f(...):
    ...

functools

>>> def bar(func):
...   def inner():
...     print('New function')
...     return func()
...   return inner
...
>>> @bar
... def foo():
...   print('I am foo')
...
>>> foo()
New function
I am foo
>>> foo.__name__    # It's bad!
'inner'

>>> from functools import wraps
>>> def my_decorator(f):
...     @wraps(f)
...     def wrapper(*args, **kwds):
...         print('Calling decorated function')
...         return f(*args, **kwds)
...     return wrapper
...
>>> @my_decorator
... def example():
...     """Docstring"""
...     print('Called example function')
...
>>> example()
Calling decorated function
Called example function
>>> example.__name__
'example'
>>> example.__doc__
'Docstring'