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
♦ 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]
♦ 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)
♦ ‘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
♦ 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])
♦ 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
♦ 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
♦ 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
♦ 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’)
♦ 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
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!
♦ 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']
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