5. Functions

 

 

 

 

 

Why use functions?

 

 

     Code reuse

     Procedural decomposition

     Alternative to cut-and-paste: redundancy

 

 

 

 

 

Function topics

 

 

     The basics

     Scope rules

     Argument matching modes

     Odds and ends

     Generator expressions and functions

     Design concepts

     Functions are objects

     Function gotchas

 


 

 

Function basics

 

 

     def is an executable statement; usually run during import

     def creates a function object and assigns to a name

     return sends a result object back to the caller

     Arguments are passed by object reference (assignment)

     Arguments, return types, and variables are not declared

     Polymorphism: code to object interfaces, not datatypes

 

 

General form

 

 

def <name>(arg1, arg2,… argN):

    <statements>

    return <value>

 

 

 

Definition

 

>>> def times(x, y):      # create and assign function

...     return x * y      # body executed when called

...

 

 

Calls

 

>>> times(2, 4)           # arguments in parenthesis

8

>>> times('Ni', 4)        # functions are 'typeless'

'NiNiNiNi'

 

 

 

 

 

“Polymorphism”

 

The meaning of an operation depends on its subject.

By not caring about types, code becomes more flexible.

Any object with a compatible interface will work.

Most errors are best caught by Python, not your code.


 

 

 

 

Example: intersecting sequences

 

 

 

    Definition

 

 

def intersect(seq1, seq2):

    res = []                     # start empty

    for x in seq1:               # scan seq1

        if x in seq2:            # common item?

            res.append(x)        # add to end

    return res

 

 

 

 

    Calls

 

>>> s1 = "SPAM"

>>> s2 = "SCAM"

 

>>> intersect(s1, s2)               # strings

['S', 'A', 'M']

 

>>> intersect([1, 2, 3], (1, 4))    # mixed types

[1]

 

 


 

Scope rules in functions

 

 

     Enclosing module is a ‘global’ scope

     Each call to a function is a new ‘local’ scope

     Assigned names are local, unless declared “global”

     All other names are global or builtin

     Added in 2.2: ‘nonlocal’ enclosing function locals (if any) searched before global

     Added in 3.X: ‘nonlocal’ variables can be changed if declared, just like ‘globals’

 

 

 

Name resolution: the “LEGB” rule

 

      References search up to 4 scopes:

1.    Local                                   (function)

2.    Enclosing functions           (if any)

3.    Global                                 (module)

4.    Builtin                                 (__builtin__ (2.X), builtins (3.X))

      Assignments create or change local names by default

      “global” declarations map assigned names to module

 

 

 

Example

 

     Global names: ‘X’, ‘func

     Local names: ‘Y’, ‘Z’

     Interactive prompt: module ‘__main__’



X = 99            # X and func assigned in module

         

def func(Y):      # Y and Z assigned in function

    Z = X + Y     # X not assigned: global

    return Z

 

func(1)           # func in module: result=100

 

 

 

 

Enclosing Function Scopes (2.2+)

 

 

def f1():

    x = 88

    def f2():

        print x         # 2.2: x found in enclosing function

    f2()

f1()                    # prints 88

 

 

def f1():

    x = 88

    def f2(x=x):        # before 2.2: pass in values with defaults (ahead)

        print x

    f2()

f1()

 

 

 

# More useful with lambda (ahead)

def func( ):

    x = 42

    action = (lambda n: x ** n)          # 2.2

 

def func( ):

    x = 42

    action = (lambda n, x=x: x ** n)     # before 2.2

 

 

 

 

# Most useful for closures: state retention (non-OOP)

 

>>> def maker(N):

...     def action(X):            # make, don’t call

...         return X ** N

...     return action             # return new func

...

>>> f = maker(3)                  # “remembers” 3 (N)

>>> f(2), f(3)                    # arg to X, not N

(8, 27)

 

>>> g = maker(4)                  # “remembers” 4 (N)

>>> g(2), g(3)

(16, 81)

 

>>> f(2), f(3)                    # f still has 3

(8, 27)

 

 

 

 

More on “global”, and 3.X “nonlocal”

 

 

     ‘global’ means assigned at top-level of a module file

     Global names must be declared only if assigned

     Global names may be referenced without being declared

     3.X: “nonlocal X” means X in enclosing def changeable

 

 

 

All Pythons

 

y, z = 1, 2         # global variables in module

 

def all_global():

    global x        # declare globals assigned

    x = y + z       # no need to declare y,z: 3-scope rule

 

 

 

Python 3.X Only

 

# enclosing function vars are changeable (state retention)

 

>>> def outer():

...     x = 1

...     def inner():

...         nonlocal x

...         x += 1

...         print(x)

...     return inner

...

>>> f = outer()      # f is really an inner

>>> f()            

2

>>> f()              # each outer() would have own “x”

3                    # see “closures” above

 

 

 

 

More on “return”

 

 

     Return sends back an object as value of call

     Can return multiple arguments in a tuple

     Can return modified argument name values

 

 

>>> def multiple(x, y):

...     x = 2

...     y = [3, 4]

...     return x, y

...

>>> X = 1

>>> L = [1, 2]

>>> X, L = multiple(X, L)

>>> X, L

(2, [3, 4])


 

 

 

More on argument passing

 

 

     Pass by object reference: assign shared object to local name

     In def, assigning to argument name doesn’t effect caller

     In def, changing mutable object argument may impact caller

     Not pass ‘by reference’ (C++), but:

      Immutables act like ‘by value’ (C)

      Mutables act like ‘by pointer’ (C)

 

 

 

>>> def changer(a, b):

...    a = 2             # changes local name's value only

...    b[0] = 'spam'     # changes shared object in-place

...

>>> X = 1

>>> L = [1, 2]

>>> changer(X, L)

>>> X, L

(1, ['spam', 2])

 

 

 

 

Equivalent to these assignments:

 

>>> X = 1

>>> a = X           # they share the same object

>>> a = 2           # resets a only, X is still 1

 

>>> L = [1, 2]

>>> b = L           # they share the same object

>>> b[0] = 'spam'   # in-place change: L sees the change too

 

 

 

 

 

 


 

 

 

Special argument matching modes

 

 

 

     Positional      matched left-to-right in header (normal)

     Keywords      matched by name in header

     Varargs        catch unmatched positional or keyword args

     Defaults        header can provide default argument values

     3.X: Keyword Only, after “*” in def,  must pass by name

 

 

 

 

 

Operation

Location

Interpretation

func(value)

caller

normal argument: matched by position

func(name=value)

caller

keyword argument: matched by name

def func(name)

function

normal argument: matches any by name or position

def func(name=value)

function

default argument value, if not passed in call

def func(*name)

function

matches remaining positional args (tuple)

def func(**name)

function

matches remaining keyword args (dictionary)

func(*args, **kargs)

caller

subsumes old apply(): unpack tuple/dict of args

def func(a, *b, c)

def func(a, *, c)

function

3.X keyword-only (c must be passed by name only)

 

 

 

 


Examples

 

 

Positionals and keywords

 

>>> def f(a, b, c): print a, b, c

 

>>> f(1, 2, 3)

1 2 3

 

>>> f(c=3, b=2, a=1)

1 2 3

 

>>> f(1, c=3, b=2)

1 2 3

 

 

Defaults

 

>>> def f(a, b=2, c=3): print a, b, c

 

>>> f(1)

1 2 3

 

>>> f(1, 4, 5)

1 4 5

 

>>> f(1, c=6)

1 2 6

 

 

Arbitrary positionals

 

>>> def f(*args): print args

 

>>> f(1)

(1,)

 

>>> f(1,2,3,4)

(1, 2, 3, 4)

 

 

Arbitrary keywords

 

>>> def f(**args): print args

 

>>> f()

{}

 

>>> f(a=1, b=2)

{'a': 1, 'b': 2}

 

>>> def f(a, *pargs, **kargs): print a, pargs, kargs

>>> f(1, 2, 3, x=1, y=2)

1 (2, 3) {'y': 2, 'x': 1}

 

 

 

 

Example: min value functions

 

 

 

 

Example

 

      Only deals with matching: still passed by assignment

      Defaults retain an object: may change if mutable

 

 

def func(spam, eggs, toast=0, ham=0):   # first 2 required

    print (spam, eggs, toast, ham)

 

func(1, 2)                      # output: (1, 2, 0, 0)

func(1, ham=1, eggs=0)          # output: (1, 0, 0, 1)

func(spam=1, eggs=0)            # output: (1, 0, 0, 0)

func(toast=1, eggs=2, spam=3)   # output: (3, 2, 1, 0)

func(1, 2, 3, 4)                # output: (1, 2, 3, 4)

 

 

 

 

 

Ordering rules

 

     Call: keyword arguments after non-keyword arguments

     Header: normals, then defaults, then *name, then **name

 

 

 

 

Matching algorithm (see exercise)

 

1.   Assign non-keyword arguments by position

2.   Assign keyword arguments by matching names

3.   Assign extra non-keyword arguments to *name tuple

4.   Assign extra keyword arguments to **name dictionary

5.   Unassigned arguments in header assigned default values


 

 

 

 

Odds and ends

 

 

     lambda expression creates anonymous functions

     list comprehensions, map, filter apply expressions to sequences (see also prior unit)

     Generator expressions (2.4+)

     Generator functions and yield (new in 2.2, 2.3)

     apply function calls functions with arguments tuple

     Functions return ‘None’ if they don’t use a real ‘return

     Python 3.X function annotations and keyword arguments

 

 

 

 

    Lambda expressions

>>> def func(x, y, z): return x + y + z

...

>>> func(2, 3, 4)

9

>>> f = lambda x, y, z: x + y + z

>>> f(2, 3, 4)

9

 

 

 

hint: embedding logic in a lambda body

 

(A and B) or C

 

((A and [B]) or [C])[0]        # or newer: B if A else C

 

hint: embedding loops in a lambda body...next topic

 

 

 

 

 

 

    List comprehensions (added in 2.0)

 

(See also prior unit)

 

 

>>> ord('s')

115

 

>>> res = []

>>> for x in 'spam': res.append(ord(x))

...

>>> res

[115, 112, 97, 109]

 

>>> map(ord, 'spam')              # apply func to sequence

[115, 112, 97, 109]

 

>>> [ord(x) for x in 'spam']      # apply expr to sequence

[115, 112, 97, 109]

 

 

# adding arbitrary expressions

 

>>> [x ** 2 for x in range(10)]

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

 

>>> map((lambda x: x**2), range(10))

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

 

>>> lines = [line[:-1] for line in open('README.txt')]

>>> lines[:2]

['This is Python version 2.4 alpha 3', '==================================']

 

 

# adding if tests

 

>>> [x for x in range(10) if x % 2 == 0]

[0, 2, 4, 6, 8]

 

>>> filter((lambda x: x % 2 == 0), range(10))

[0, 2, 4, 6, 8]

 

 

# advanced usage

 

>>> [x**2 for x in range(10) if x % 2 == 0]

[0, 4, 16, 36, 64]

 

>>> [x+y for x in 'abc' for y in 'lmn']

['al', 'am', 'an', 'bl', 'bm', 'bn', 'cl', 'cm', 'cn']

 

>>> res = []

>>> for x in 'abc':

...    for y in 'lmn':

...       res.append(x+y)

...

>>> res

['al', 'am', 'an', 'bl', 'bm', 'bn', 'cl', 'cm', 'cn']

 

 

# nice for matrixes

 

>>> M = [[1, 2, 3],

         [4, 5, 6],

         [7, 8, 9]]

>>> M[1]

[4, 5, 6]

>>> col2 = [row[1] for row in M]

>>> col2

[2, 5, 8]

>>> quad = [M[i][j] for i in (0,1) for j in (0, 1)]

>>> quad

[1, 2, 4, 5]

 

 

List comprehensions can become incomprehensible when nested, but map and list comprehensions may be faster than simple for loops

 

(In 2.4, comprehensions are twice as fast as for loops, and for loops are now quicker than map: see middle of this page, and CD’s Extras\Code\Misc\timerseqs.py)

 

 

 

 

 

 

    Generator expressions (2.4+)

 

# list comprehensions generate entire list in memory

 

>>> squares = [x**2 for x in range(5)]

>>> squares

[0, 1, 4, 9, 16]

 

 

# generator expressions yield 1 result at a time: saves memory, distributes work

 

>>> squares = (x**2 for x in range(5))

>>> squares

<generator object at 0x00B2EC88>

>>> next(squares)

0

>>> next(squares)

1

>>> next(squares)

4

>>> list(squares)

[9, 16]

 

 

# iteration contexts automatically call next()

 

>>> for x in (x**2 for x in range(5)):

        print x,

    

0 1 4 9 16

 

>>> sum(x**2 for x in range(5))

30

 

 

 

 

 

 

    Generator functions and yield

Generators implement iteration protocol: .next() (3.X .__next__())

Retains local scope when suspended

Distributes work over time, may save memory (see also: threads)

Related: generator expressions, enumerate function, file iterators

 

 

# functions compiled specially when contain yield

 

>>> def gensquares(N):

...     for i in range(N):         # suspends and resumes itself

...         yield i ** 2           # <- return value and resume here later

 

 

 

# generator objects support iteration protocol: next()

 

>>> x = gensquares(10)

>>> x                              # also retain all local variables between calls

<generator object at 0x0086C378>   # classes define __iter__ to return iter object

>>> next(x)

0

>>> next(x)

1

>>> next(x)

4

>>> next(x)

StopIteration exception raised at end…

 

 

 

# for loops (and others) automatically call next()

 

>>> for i in gensquares(5):        # resume the function each time

...     print i, ':',              # print last yielded value

...

0 : 1 : 4 : 9 : 16 :

 

 

 

# (advanced) application to coroutines via task switchers

 

 

See Extras\Code\Misc\asynch1.py

 

 

# (advanced) generators, through the years…

 

 

See Extras\Code\Misc\gendemo.py

 

Original

def with yield statement supports iteration protocol, returning yielded value for next()

In 2.5+

caller can resume generator and pass it value via G.send(X): result of “yield” expression

In 3.3+

return” value allowed in generators: attached to exception, result of “yield from” expression

In 3.5+

async” and “await” distinguish some coroutine roles, similar to “yield from”

 

 

 

 

 

 

 

 

 

    Apply built-in (2.X only) and syntax (all Pys)

>>> def func(a, b, c):

        return a + b + c

 

>>> apply(func, (2, 3, 4))         # 2.X only

9

>>> apply(func, (2, 3), {'c': 4})

9

 

 

Versus Python 2.0+ apply-like call syntax

 

func(*a)        …like…   apply(func, a)

func(*a, **b)   …like…   apply(func, a, b)

 

>>> func(*(2, 3, 4))

9

>>> func(*(2, 3), **{'c': 4})     # 2.X and 3.X

9

 

# call syntax is more flexible:

 

>>> def func(a, b, c, d): return a + b + c + d

 

>>> args1 = (1, 2)

>>> args2 = {'c': 3, 'd': 4}

 

>>> func(*args1, **args2)

10

 

>>> func(1, *(2,), **args2)

10

 

 

See also: “About the stars” sidebar above

 

 

 

 

 

 

    Default return values

>>> def proc(x):

...     print x

...

>>> x = proc('testing 123...')

testing 123...

>>> print x

None

 

 

 

 

 

 

    Python 3.X function annotations

>>> def func(a: int, b: 'spam', c: 88 = 99) -> float:

...     print(a, b, c)

...

>>> func(1, 2)

1 2 99

>>> func.__annotations__

{'b': 'spam', 'return': <class 'float'>, 'a': <class 'int'>, 'c': 88}

 

 

 

 

 

 

    Python 3.X keyword-only arguments

>>> def f(a, b, *, c=3, d): print(a, b, c, d)

...

>>> f(1, 2)

TypeError: f() missing 1 required keyword-only argument: 'd'

>>> f(1, 2, 3)

TypeError: f() takes 2 positional arguments but 3 were given

>>> f(1, 2, d=4)

1 2 3 4

>>> f(1, 2, c=3, d=4)

1 2 3 4

 

 

 

 


 

 

Function design concepts

 

 

     Use global variables only when absolutely necessary

     Use arguments for input, ‘return’ for outputs

     Don’t change mutable arguments unless expected

     But globals are only state-retention tool without classes

     But classes depend on mutable arguments (‘self’)

 

 

 

 

 


 

 

 

Functions are objects: indirect calls

 

 

     Function objects can be assigned, passed, etc.

     Can call objects generically: function, bound method, ...

 

def echo(message): print message

x = echo

x('Hello world!')

 

def indirect(func, arg):

    func(arg)

indirect(echo, 'Hello world!')

 

schedule = [ (echo, 'Hello!'), (echo, 'Ni!') ]

for (func, arg) in schedule:

    func(arg)

 

 

 

 

File scanners

 

 

file: scanfile.py

def scanner(name, function):

    file = open(name, 'r')          # create file

    for line in file.readlines():

        function(line)              # call function

    file.close()

 

 

file: commands.py

import string

from scanfile import scanner

 

def processLine(line):

    print string.upper(line)

 

scanner("data.txt", processLine)    # start scanner

 

 


 

Function gotchas

 

 

 

Local names are detected statically

 

# cant use both local+global version of name, unless reference through module

 

>>> X = 99

>>> def selector():        # X used but not assigned

...     print X            # X found in global scope

...

>>> selector()

99

 

 

 

 

>>> def selector():

...     print X            # does not yet exist!

...     X = 88             # X classified as a local name

...

>>> selector()

Traceback (innermost last):

  File "<stdin>", line 1, in ?

  File "<stdin>", line 2, in selector

NameError: X

 

 

 

 

>>> def selector():

...     global X           # force X to be global

...     print X

...     X = 88

...

>>> selector()

99

 

 

 

 

Mutable defaults created just once

 

# to avoid: check for None inside def and set to [] if so

 

>>> def grow(A, B=[]):

        B.append(A)

        return B

 

>>> grow(1)

[1]

>>> grow(1)

[1, 1]

>>> grow(1)

[1, 1, 1]

 

 


 

Nested functions weren’t nested scopes

 

# this works as of 2.2 – enclosing function scopes!

 

>>> def outer(x):

...     def inner(i):          # assign in outer’s local

...         print i,           # i is in inner’s local

...         if i: inner(i-1)   # not in my local or global!

...     inner(x)

...

>>> outer(3)

3

Traceback (innermost last):

  File "<stdin>", line 1, in ?

  File "<stdin>", line 6, in outer

  File "<stdin>", line 5, in inner

NameError: inner

 

 

>>> def outer(x):

...     global inner

...     def inner(i):          # assign in enclosing module

...         print i,

...         if i: inner(i-1)   # found in my global scope

...     inner(x)

...

>>> outer(3)

3 2 1 0

 

 

 

 

Use defaults to save references

 

# normally no longer necessary as of 2.2: enclosing function scopes

# BUT still required to retain current value of loop variables!

 

>>> def outer(x, y):

        def inner():

            return x ** y

        return inner

 

>>> x = outer(2, 4)

>>> x()

16

 

 

# code before 2.2

 

>>> def outer(x, y):

...     def inner(a=x, b=y):     # save x,y bindings/objects

...         return a**b          # from the enclosing scope

...     return inner

...

>>> x = outer(2, 4)

>>> x()

16

 

 

>>> def outer(x, y):

...     return lambda a=x, b=y: a**b

...

>>> y = outer(2, 5)

>>> y()

32

 

 

for I in someiterable:

    actions.append(lambda I=I: …)  # retain current I, not last!

 

 


 

Optional reading: set functions

 

 

     Functions process passed-in sequence objects

     Work on any type of sequence objects

     Supports mixed types: list and tuple, etc.

 

 

file: inter.py

 

def intersect(seq1, seq2):

    res = []                     # start with an empty list

    for x in seq1:               # scan the first sequence

        if x in seq2:

            res.append(x)        # add common items to end

    return res

 

def union(seq1, seq2):

    res = map(None, seq1)        # copy of seq1 (see ch13)

    for x in seq2:               # add new items in seq2

        if not x in res:

            res.append(x)

    return res

 

 

 

% python

>>> from inter import intersect, union

>>> s1 = "SPAM"

>>> s2 = "SCAM"

 

>>> intersect(s1, s2), union(s1, s2)           # strings

(['S', 'A', 'M'], ['S', 'P', 'A', 'M', 'C'])

 

>>> intersect([1,2,3], (1,4))                  # mixed types

[1]

 

>>> union([1,2,3], (1,4))

[1, 2, 3, 4]

 


 

Supporting multiple operands: *varargs

 

 

  

file: inter2.py

 

def intersect(*args):

    res = []

    for x in args[0]:                  # scan first sequence

        for other in args[1:]:         # for all other args

            if x not in other: break   # this in each one?

        else:

            res.append(x)              # add items to end

    return res

 

def union(*args):

    res = []

    for seq in args:                   # for all args

        for x in seq:                  # for all nodes

            if not x in res:

                res.append(x)          # add items to result

    return res

 

 

 

 

% python

>>> from inter2 import intersect, union

>>> s1, s2, s3 = "SPAM", "SCAM", "SLAM"

 

>>> intersect(s1, s2), union(s1, s2)           # 2 operands

(['S', 'A', 'M'], ['S', 'P', 'A', 'M', 'C'])

 

>>> intersect([1,2,3], (1,4))

[1]

 

>>> intersect(s1, s2, s3)                      # 3 operands

['S', 'A', 'M']

 

>>> union(s1, s2, s3)

['S', 'P', 'A', 'M', 'C', 'L']

 

 

 

 

 

Lab Session 4

 

Click here to go to lab exercises

Click here to go to exercise solutions

Click here to go to solution source files

Click here to go to lecture example files