1. Declaring Functions

Define a function with no arguments and no return values:


In [2]:
def print_text():
    print('this is text')

In [32]:
# call the function
print_text()


this is text

Use pass as a placeholder if you haven't written the function body:


In [16]:
def stub():
    pass

2. Return Values


In [31]:
def say_hello():
    return 'hello'
    
say_hello()


Out[31]:
'hello'

Return two values from a single function:


In [17]:
def min_max(nums):
    return min(nums), max(nums)

In [18]:
# return values can be assigned into multiple variables using tuple unpacking
nums = [3, 6, 5, 8, 2, 19, 7]

min_num, max_num = min_max(nums)
print(min_num)
print(max_num)


2
19

3. Parameters


In [4]:
def print_this(x):
    print (x)
    
print_this(3)


3

Define a function with a default value:


In [34]:
def calc(a, b, op='add'):
    if op == 'add':
        return a+b
    elif op == 'sub':
        return a-b
    else:
        print('valid operations are add and sub')

calc(10, 4)


Out[34]:
14

In [35]:
calc(10,4, op='add')


Out[35]:
14

In [15]:
# unnamed arguments are inferred by position
calc(10, 4, 'add')


Out[15]:
14

In [16]:
x = 42
def spam(a, b=x):
    print(a, b)

spam(1)


1 42

In [18]:
x = 23 # Has no effect
spam(1)


1 42

Default values should always be const values, or you can get in trouble


In [23]:
def spam(a, b=[]): # b escapes the function as a return variable, which can be altered!
    return b

x = spam(1)
x


Out[23]:
[]

In [22]:
x = spam(1)
x.append(99)
x.append('Yow!')

spam(1) # Modified list gets returned!


Out[22]:
[99, 'Yow!']

Function taking an arbitrary number of arguments


In [1]:
# arbitrary positional arguments 
def print_all(seperator, *args):
    print(seperator.join(args))
    
print_all(',', 'first','second','third')


first,second,third

In [4]:
# arbitrary positional AND keyword arguments

def anyargs(*args, **kwargs):
    print(args) # A tuple
    print(kwargs) # A dict
    
anyargs(3, 'ddddd', 5.666, foo='bar', blah='zed')


(3, 'ddddd', 5.666)
{'foo': 'bar', 'blah': 'zed'}

In [6]:
# keyword arguments have access to attribute name
import html

def make_element(name, value, **attrs):
    keyvals = [' %s="%s"' % item for item in attrs.items()]
    attr_str = ''.join(keyvals)
    element = '<{name}{attrs}>{value}</{name}>'.format(
        name=name,
        attrs=attr_str,
        value=html.escape(value))
    return element
# Example
# Creates '<item size="large" quantity="6">Albatross</item>'
make_element('item', 'Albatross', size='large', quantity=6)


Out[6]:
'<item size="large" quantity="6">Albatross</item>'

Define a function that only accepts keyword arguments


In [7]:
def recv(maxsize, *, block):
    'Receives a message'
    pass

recv(1024, block=True) # Ok

# the following will fail if uncommented
#recv(1024, True) # TypeError

Define a function that take a callable (function) as a parameter):


In [37]:
def dedupe(items):
    seen = set()
    for item in items:
        if item not in seen:
            yield item
            seen.add(item)

a = [1, 5, 2, 1, 9, 1, 5, 10]
list(dedupe(a))


Out[37]:
[1, 5, 2, 9, 10]

4. DocStrings


In [13]:
def calc(a, b, op='add'):
    """
    calculates the result of a simple math operation.

    :param a: the first parameter in the math operation
    :param b: the first parameter in the math operation
    :param op: which type of math operation (valid values are 'add', 'sub')
    :returns: this result of applying the math argument to the two parameters
    :raises keyError: raises an exception
    """
    if op == 'add':
        return a+b
    elif op == 'sub':
        return a-b
    else:
        print('valid operations are add and sub')

In [14]:
help(calc)


Help on function calc in module __main__:

calc(a, b, op='add')
    calculates the result of a simple math operation.
    
    :param a: the first parameter in the math operation
    :param b: the first parameter in the math operation
    :param op: which type of math operation (valid values are 'add', 'sub')
    :returns: this result of applying the math argument to the two parameters
    :raises keyError: raises an exception

Attaching additional metadata to a function definition


In [11]:
# the compiler does not check any of this, it is just documentation!
def add(x:int, y:int) -> int:
    return x + y

add('hello', 'world')


Out[11]:
'helloworld'

In [12]:
help(add)


Help on function add in module __main__:

add(x:int, y:int) -> int
    # the compiler does not check any of this, it is just documentation!

5. Unpacking Parameters

Unpacking iterables into positional function arguments (star operator)


In [21]:
# range takes start and stop parameters
list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]


Out[21]:
[3, 4, 5]

In [22]:
# can also pass start and stop arguments using argument unpacking
args = [3,6]  #can be used on tuple too
list(range(*args))


Out[22]:
[3, 4, 5]

Unpacking dictionaries into named arguments (double-star operator)


In [39]:
# a dictionary can be unpacked into names arguments with the ** / double star operator

from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time'])

# Create a prototype instance
stock_prototype = Stock('', 0, 0.0, None, None)

# Function to convert a dictionary to a Stock
def dict_to_stock(s):
    return stock_prototype._replace(**s)

a = {'name': 'ACME', 'shares': 100, 'price': 123.45}
dict_to_stock(a)


Out[39]:
Stock(name='ACME', shares=100, price=123.45, date=None, time=None)

6. Generator Functions

Generator functions with yield


In [25]:
def myrange(n):
    for i in range(n):
        yield i

max(myrange(5))


Out[25]:
4

Generator function that uses an internal iterator


In [26]:
#using yield from

def myrange(n):
    yield from range(n)

print(max(myrange(5)))


4

7. Lambas Anonymous Functions


In [42]:
squared = lambda x: x**2

squared(3)


Out[42]:
9

In [47]:
simpsons = ['bart', 'maggie', 'homer', 'lisa', 'marge']
sorted(simpsons, key = lambda word: word[-1])


Out[47]:
['lisa', 'maggie', 'marge', 'homer', 'bart']

In [51]:
# no parameter lambda
say_hello = lambda : 'hello'

say_hello()


Out[51]:
'hello'

Default Parameters in Lambdas


In [49]:
talkback = lambda message='hello' : message

talkback()


Out[49]:
'hello'

In [50]:
talkback('hello world')


Out[50]:
'hello world'

Capturing local variables in lambdas


In [52]:
test = 'hello world'
talkback = lambda : test

talkback()


Out[52]:
'hello world'

In [53]:
# parameters are resolved when the code runs, not when lambda is declared
test = 'what???'

talkback()


Out[53]:
'what???'

In [54]:
# to prevent this, use a default parameter set to the local variable
test = 'hello world'
talkback = lambda message = test: message

test='nope'
talkback()


Out[54]:
'hello world'

8. Partial

Partial allows you convert an n-parameter function into a function with less arguments


In [24]:
def spam(a, b, c, d):
    print(a, b, c, d)
    
from functools import partial
s1 = partial(spam, 1) # a = 1
s1(2, 3, 4)


1 2 3 4

In [28]:
s1(4, 5, 6)


1 4 5 6

In [29]:
s2 = partial(spam, d=42) # d = 42
s2(1, 2, 3)


1 2 3 42

In [30]:
s2(4, 5, 5)


4 5 5 42

In [31]:
s3 = partial(spam, 1, 2, d=42) # a = 1, b = 2, d = 42
s3(3)


1 2 3 42

In [32]:
s3(4)


1 2 4 42

In [33]:
s3(5)


1 2 5 42

9. Closures Nested Functions


In [36]:
# this inner closure is used to carry state around
def name_func_from_family(last_name):
    def print_name(first_name):
        print('{} {}'.format(first_name, last_name))
    return print_name #the key here is that the outer function RETURNS the inner function / closure

print_saltwell = name_func_from_family('saltwell')
print_saltwell('erik')
print_saltwell('kasia')
print_saltwell('jacob')


erik saltwell
kasia saltwell
jacob saltwell

10. Nonlocal and global

nonlocal allows you to modify a variable outside your scope (but not global scope)


In [37]:
def outside():
    msg = "Outside!"
    def inside():
        msg = "Inside!"
        print(msg)
    inside()
    print(msg) # this prints 'Outside!' even though Inside() mosifies a variable called msg (its a local copy)

outside()


Inside!
Outside!

In [41]:
# to have a variable refer to something outside local scope use nonlocal
def outside():
    msg = "Outside!"
    def inside():
        nonlocal msg
        msg = "Inside!"
        print(msg)
    inside()
    print(msg) 

outside()


Inside!
Inside!

In [42]:
# the global keyword makes a variable reference a global variable rather then a copy
msg = 'Global!!'

def outside():
    msg = "Outside!"
    def inside():
        global msg
        msg = "Inside!"
        print(msg)
    inside()
    print(msg) # this prints 'Outside!' because the copy in Inside() references the global variable

outside()
msg


Inside!
Outside!
Out[42]:
'Inside!'