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

¨  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”

 

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

¨   New in 2.2: enclosing function locals (if any) searched before global

 

 

 

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__)

·        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

 

 

 

 

More on “global”

 

 

¨   ‘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

 

 

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

 

 

 

 

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

 

 

¨   Passed by assigning shared object to local name

¨   Assigning to argument name doesn’t effect caller

¨   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

 

 

 

 

 

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

 


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

¨   Generator expressions (2.4+)

¨   Generator functions and iterators (new in 2.2, 2.3)

¨   apply function calls functions with arguments tuple

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

 

 

 

¨  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]        # like  if A: B  else: C

 

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

 

 

 

 

 

¨  List comprehensions (added in 2.0)

 

>>> 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\Misc\timerseqs.py)

 

 

 

 

 

¨  Generator expressions (2.4+)

 

# list comrehensions 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

 

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

>>> squares

<generator object at 0x00B2EC88>

>>> squares.next()

0

>>> squares.next()

1

>>> squares.next()

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 iterators

Generators implement iterator protocol: .next()

Retains local scope when suspended

Distributes work over time (see also: threads)

Related: generator expressions, enumerate function, file iterators

In 2.5, caller can pass values to generators: gen.send(X) method, yield is an expression with a value

 

 

# functions compiled specially when 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

>>> x.next()

0

>>> x.next()

1

>>> x.next()

4

>>> x.next()

…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 :

 

 

 

 

 

¨  Apply built-in and syntax

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

9

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

9

 

 

See also Python 2.0+ apply-like call syntax:

 

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

 

>>> 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

 

 

 

 

 

¨  Default return values

>>> def proc(x):

...     print x

...

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

testing 123...

>>> print x

None

 

 


 

 

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:

    apply(func, (arg,))         # func(arg) works too

 

 

 

 

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 same name as local+global unless use 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 and set to [] in function body

 

>>> 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

 

# no longer necessary as of 2.2: enclosing function scopes

 

>>> 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

 

 


 

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