Python Functions

In [1]:
import numpy as np

Custom functions

Anatomy

name, arguments, docstring, body, return statement

In [2]:
def func_name(arg1, arg2):
    """Docstring starts wtih a short description.

    May have more information here.

    arg1 = something
    arg2 = somehting

    Returns something

    Example usage:

    func_name(1, 2)
    """
    result = arg1 + arg2

    return result
In [3]:
help(func_name)
Help on function func_name in module __main__:

func_name(arg1, arg2)
    Docstring starts wtih a short description.

    May have more information here.

    arg1 = something
    arg2 = somehting

    Returns something

    Example usage:

    func_name(1, 2)

Function arguments

place, keyword, keyword-only, defaults, mutatble an immutable arguments

In [4]:
def f(a, b, c, *args, **kwargs):
    return a, b, c, args, kwargs
In [5]:
f(1, 2, 3, 4, 5, 6, x=7, y=8, z=9)
Out[5]:
(1, 2, 3, (4, 5, 6), {'x': 7, 'y': 8, 'z': 9})
In [6]:
def g(a, b, c, *, x, y, z):
    return a, b, c, x, y, z
In [7]:
try:
    g(1,2,3,4,5,6)
except TypeError as e:
    print(e)
g() takes 3 positional arguments but 6 were given
In [8]:
g(1,2,3,x=4,y=5,z=6)
Out[8]:
(1, 2, 3, 4, 5, 6)
In [9]:
def h(a=1, b=2, c=3):
    return a, b, c
In [10]:
h()
Out[10]:
(1, 2, 3)
In [11]:
h(b=9)
Out[11]:
(1, 9, 3)
In [12]:
h(7,8,9)
Out[12]:
(7, 8, 9)

Default mutable argumnet

binding is fixed at function definition, the default=None idiom

In [13]:
def f(a, x=[]):
    x.append(a)
    return x
In [14]:
f(1)
Out[14]:
[1]
In [15]:
f(2)
Out[15]:
[1, 2]
In [16]:
def f(a, x=None):
    if x is None:
        x = []
    x.append(a)
    return x
In [17]:
f(1)
Out[17]:
[1]
In [18]:
f(2)
Out[18]:
[2]

Pure functions

deterministic, no side effects

In [19]:
def f1(x):
    """Pure."""
    return x**2
In [20]:
def f2(x):
    """Pure if we ignore local state change.

    The x in the function baheaves like a copy.
    """
    x = x**2
    return x
In [21]:
def f3(x):
    """Impure if x is mutable.

    Augmented assignemnt is an in-place operation for mutable structures."""
    x **= 2
    return x
In [22]:
a = 2
b = np.array([1,2,3])
In [23]:
f1(a), a
Out[23]:
(4, 2)
In [24]:
f1(b), b
Out[24]:
(array([1, 4, 9]), array([1, 2, 3]))
In [25]:
f2(a), a
Out[25]:
(4, 2)
In [26]:
f2(b), b
Out[26]:
(array([1, 4, 9]), array([1, 2, 3]))
In [27]:
f3(a), a
Out[27]:
(4, 2)
In [28]:
f3(b), b
Out[28]:
(array([1, 4, 9]), array([1, 4, 9]))
In [29]:
def f4():
    """Stochastic functions are tehcnically impure
    since a global seed is changed between function calls."""

    import random
    return random.randint(0,10)
In [30]:
f4(), f4(), f4()
Out[30]:
(1, 0, 8)

Recursive functions

Euclidean GCD algorithm

gcd(a, 0) = a
gcd(a, b) = gcd(b, a mod b)
In [31]:
def factorial(n):
    """Simple recursive funciton."""
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)
In [32]:
factorial(4)
Out[32]:
24
In [33]:
def factorial1(n):
    """Non-recursive version."""
    s = 1
    for i in range(1, n+1):
        s *= i
    return s
In [34]:
factorial1(4)
Out[34]:
24
In [35]:
def gcd(a, b):
    if b == 0:
        return a
    else:
        return gcd(b, a % b)
In [36]:
gcd(16, 24)
Out[36]:
8

Generators

yield and laziness, infinite streams

In [37]:
def count(n=0):
    while True:
        yield n
        n += 1
In [38]:
for i in count(10):
    print(i)
    if i >= 15:
        break
10
11
12
13
14
15
In [39]:
from itertools import islice
In [40]:
list(islice(count(), 10, 15))
Out[40]:
[10, 11, 12, 13, 14]
In [41]:
def updown(n):
    yield from range(n)
    yield from range(n, 0, -1)
In [42]:
updown(5)
Out[42]:
<generator object updown at 0x109e54728>
In [43]:
list(updown(5))
Out[43]:
[0, 1, 2, 3, 4, 5, 4, 3, 2, 1]

First class functions

functions as arguments, functions as return values

In [44]:
def double(x):
    return x*2

def twice(x, func):
    return func(func(x))
In [45]:
twice(3, double)
Out[45]:
12

Example from standard library

In [46]:
xs = 'banana apple guava'.split()
In [47]:
xs
Out[47]:
['banana', 'apple', 'guava']
In [48]:
sorted(xs)
Out[48]:
['apple', 'banana', 'guava']
In [49]:
sorted(xs, key=lambda s: s.count('a'))
Out[49]:
['apple', 'guava', 'banana']
In [50]:
def f(n):
    def g():
        print("hello")
    def h():
        print("goodbye")
    if n == 0:
        return g
    else:
        return h
In [51]:
g = f(0)
g()
hello
In [52]:
h = f(1)
h()
goodbye

Function dispatch

Poor man’s switch statement

In [53]:
def add(x, y):
    return x + y

def mul(x, y):
    return x * y
In [54]:
ops = {
    'a': add,
    'm': mul
}
In [55]:
items = zip('aammaammam', range(10), range(10))
In [56]:
for item in items:
    key, x, y = item
    op = ops[key]
    print(key, x, y, op(x, y))
a 0 0 0
a 1 1 2
m 2 2 4
m 3 3 9
a 4 4 8
a 5 5 10
m 6 6 36
m 7 7 49
a 8 8 16
m 9 9 81

Closure

Capture of argument in enclosing scope

In [57]:
def f(x):
    def g(y):
        return x + y
    return g
In [58]:
f1 = f(0)
f2 = f(10)
In [59]:
f1(5), f2(5)
Out[59]:
(5, 15)

Decorators

A timing decorator

In [60]:
def timer(f):
    import time
    def g(*args, **kwargs):
        tic = time.time()
        res = f(*args, **kwargs)
        toc = time.time()
        return res, toc-tic
    return g
In [61]:
def f(n):
    s = 0
    for i in range(n):
        s += i
    return s
In [62]:
timed_f = timer(f)
In [63]:
timed_f(100000)
Out[63]:
(4999950000, 0.008235931396484375)

Decorator syntax

In [64]:
@timer
def g(n):
    s = 0
    for i in range(n):
        s += i
    return s
In [65]:
g(100000)
Out[65]:
(4999950000, 0.008067846298217773)

Anonymous functions

Short, one-use lambdas

In [66]:
f = lambda x: x**2
In [67]:
f(3)
Out[67]:
9
In [68]:
g = lambda x, y: x+y
In [69]:
g(3,4)
Out[69]:
7

Map, filter and reduce

Funcitonal building blocks

In [70]:
xs = range(10)
list(map(lambda x: x**2, xs))
Out[70]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [71]:
list(filter(lambda x: x%2 == 0, xs))
Out[71]:
[0, 2, 4, 6, 8]
In [72]:
from functools import reduce
In [73]:
reduce(lambda x, y: x+y, xs)
Out[73]:
45
In [74]:
reduce(lambda x, y: x+y, xs, 100)
Out[74]:
145

Functional modules in the standard library

itertools, functional and operator

In [75]:
import operator as op
In [76]:
reduce(op.add, range(10))
Out[76]:
45
In [77]:
import itertools as it
In [78]:
list(it.islice(it.cycle([1,2,3]), 1, 10))
Out[78]:
[2, 3, 1, 2, 3, 1, 2, 3, 1]
In [79]:
list(it.permutations('abc', 2))
Out[79]:
[('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')]
In [80]:
list(it.combinations('abc', 2))
Out[80]:
[('a', 'b'), ('a', 'c'), ('b', 'c')]
In [81]:
from functools import partial, lru_cache
In [82]:
def f(a, b, c):
    return a + b + c
In [83]:
g = partial(f, b = 2, c=3)
In [84]:
g(1)
Out[84]:
6
In [85]:
def fib(n, trace=False):
    if trace:
        print("fib(%d)" % n, end=',')
    if n <= 2:
        return 1
    else:
        return fib(n-1, trace) + fib(n-2, trace)
In [86]:
fib(10, True)
fib(10),fib(9),fib(8),fib(7),fib(6),fib(5),fib(4),fib(3),fib(2),fib(1),fib(2),fib(3),fib(2),fib(1),fib(4),fib(3),fib(2),fib(1),fib(2),fib(5),fib(4),fib(3),fib(2),fib(1),fib(2),fib(3),fib(2),fib(1),fib(6),fib(5),fib(4),fib(3),fib(2),fib(1),fib(2),fib(3),fib(2),fib(1),fib(4),fib(3),fib(2),fib(1),fib(2),fib(7),fib(6),fib(5),fib(4),fib(3),fib(2),fib(1),fib(2),fib(3),fib(2),fib(1),fib(4),fib(3),fib(2),fib(1),fib(2),fib(5),fib(4),fib(3),fib(2),fib(1),fib(2),fib(3),fib(2),fib(1),fib(8),fib(7),fib(6),fib(5),fib(4),fib(3),fib(2),fib(1),fib(2),fib(3),fib(2),fib(1),fib(4),fib(3),fib(2),fib(1),fib(2),fib(5),fib(4),fib(3),fib(2),fib(1),fib(2),fib(3),fib(2),fib(1),fib(6),fib(5),fib(4),fib(3),fib(2),fib(1),fib(2),fib(3),fib(2),fib(1),fib(4),fib(3),fib(2),fib(1),fib(2),
Out[86]:
55
In [87]:
%timeit -r1 -n100 fib(20)
2.93 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 100 loops each)
In [88]:
@lru_cache(3)
def fib1(n, trace=False):
    if trace:
        print("fib(%d)" % n, end=',')
    if n <= 2:
        return 1
    else:
        return fib1(n-1, trace) + fib1(n-2, trace)
In [89]:
fib1(10, True)
fib(10),fib(9),fib(8),fib(7),fib(6),fib(5),fib(4),fib(3),fib(2),fib(1),
Out[89]:
55
In [90]:
%timeit -r1 -n100 fib1(20)
348 ns ± 0 ns per loop (mean ± std. dev. of 1 run, 100 loops each)

Using toolz

funcitonal power tools

In [91]:
import toolz as tz
import toolz.curried as c

Find the 5 most common sequences of length 3 in the dna variable.

In [92]:
dna = np.random.choice(list('ACTG'), (10,80), p=[.1,.2,.3,.4])
In [93]:
dna
Out[93]:
array([['T', 'C', 'G', 'G', 'T', 'G', 'C', 'C', 'G', 'T', 'G', 'G', 'C',
        'G', 'C', 'C', 'C', 'C', 'T', 'G', 'A', 'T', 'G', 'T', 'G', 'G',
        'T', 'G', 'C', 'C', 'C', 'T', 'G', 'G', 'T', 'C', 'T', 'T', 'G',
        'C', 'G', 'T', 'C', 'G', 'T', 'T', 'T', 'G', 'G', 'T', 'G', 'T',
        'G', 'T', 'G', 'G', 'G', 'G', 'C', 'G', 'A', 'G', 'G', 'G', 'G',
        'T', 'G', 'T', 'T', 'T', 'G', 'T', 'G', 'G', 'C', 'A', 'T', 'C',
        'T', 'G'],
       ['G', 'A', 'T', 'G', 'C', 'C', 'G', 'A', 'G', 'T', 'G', 'G', 'A',
        'G', 'G', 'C', 'G', 'C', 'G', 'G', 'G', 'G', 'C', 'G', 'T', 'G',
        'G', 'T', 'G', 'G', 'G', 'G', 'C', 'G', 'G', 'G', 'C', 'C', 'G',
        'T', 'C', 'A', 'A', 'C', 'T', 'T', 'T', 'G', 'G', 'G', 'G', 'G',
        'G', 'G', 'G', 'G', 'A', 'G', 'G', 'C', 'C', 'G', 'G', 'T', 'T',
        'A', 'T', 'T', 'C', 'G', 'G', 'G', 'A', 'C', 'T', 'G', 'T', 'G',
        'C', 'C'],
       ['C', 'C', 'G', 'C', 'T', 'G', 'G', 'T', 'C', 'G', 'C', 'G', 'G',
        'G', 'C', 'G', 'G', 'T', 'T', 'T', 'C', 'T', 'C', 'G', 'T', 'G',
        'A', 'G', 'G', 'C', 'G', 'T', 'G', 'G', 'G', 'T', 'T', 'G', 'G',
        'C', 'C', 'G', 'T', 'T', 'G', 'T', 'C', 'T', 'C', 'C', 'G', 'C',
        'T', 'T', 'C', 'G', 'G', 'T', 'A', 'G', 'C', 'T', 'T', 'T', 'G',
        'G', 'G', 'A', 'T', 'T', 'T', 'A', 'G', 'G', 'G', 'C', 'T', 'G',
        'C', 'G'],
       ['T', 'T', 'C', 'G', 'T', 'T', 'T', 'C', 'C', 'C', 'G', 'G', 'T',
        'C', 'T', 'T', 'G', 'T', 'G', 'T', 'G', 'C', 'G', 'G', 'C', 'G',
        'G', 'C', 'T', 'C', 'G', 'T', 'G', 'T', 'T', 'C', 'G', 'C', 'T',
        'T', 'T', 'T', 'G', 'T', 'C', 'G', 'C', 'T', 'A', 'T', 'G', 'G',
        'A', 'T', 'G', 'C', 'G', 'G', 'G', 'G', 'C', 'T', 'T', 'C', 'T',
        'G', 'G', 'G', 'G', 'G', 'A', 'T', 'A', 'C', 'T', 'G', 'C', 'C',
        'G', 'T'],
       ['G', 'C', 'G', 'G', 'G', 'T', 'T', 'T', 'G', 'C', 'G', 'G', 'G',
        'C', 'G', 'T', 'A', 'T', 'T', 'T', 'C', 'G', 'G', 'G', 'G', 'G',
        'T', 'T', 'T', 'T', 'T', 'G', 'T', 'A', 'C', 'C', 'G', 'G', 'T',
        'C', 'G', 'T', 'T', 'G', 'G', 'T', 'G', 'G', 'G', 'T', 'T', 'G',
        'C', 'T', 'C', 'G', 'G', 'C', 'C', 'T', 'G', 'T', 'G', 'G', 'T',
        'C', 'A', 'G', 'T', 'G', 'C', 'G', 'G', 'A', 'G', 'C', 'C', 'G',
        'T', 'G'],
       ['G', 'A', 'T', 'G', 'C', 'T', 'T', 'T', 'C', 'T', 'T', 'G', 'G',
        'C', 'C', 'A', 'G', 'G', 'T', 'T', 'T', 'G', 'C', 'C', 'T', 'G',
        'T', 'C', 'C', 'C', 'G', 'G', 'T', 'T', 'G', 'C', 'C', 'G', 'A',
        'T', 'G', 'T', 'T', 'C', 'G', 'G', 'C', 'T', 'T', 'T', 'G', 'T',
        'G', 'G', 'G', 'C', 'T', 'G', 'C', 'G', 'C', 'G', 'T', 'A', 'C',
        'T', 'G', 'G', 'G', 'A', 'G', 'C', 'C', 'T', 'G', 'G', 'T', 'G',
        'C', 'A'],
       ['C', 'G', 'T', 'T', 'G', 'A', 'G', 'G', 'T', 'T', 'A', 'T', 'C',
        'G', 'A', 'G', 'C', 'T', 'C', 'C', 'G', 'T', 'G', 'G', 'G', 'T',
        'T', 'G', 'G', 'G', 'G', 'G', 'A', 'G', 'A', 'G', 'G', 'G', 'G',
        'C', 'T', 'A', 'T', 'T', 'T', 'G', 'G', 'T', 'G', 'T', 'G', 'G',
        'T', 'G', 'A', 'G', 'G', 'G', 'G', 'G', 'A', 'G', 'T', 'C', 'T',
        'G', 'C', 'T', 'G', 'G', 'C', 'G', 'C', 'T', 'A', 'G', 'T', 'G',
        'T', 'G'],
       ['T', 'A', 'T', 'T', 'C', 'G', 'A', 'G', 'A', 'T', 'A', 'T', 'C',
        'T', 'G', 'C', 'C', 'C', 'C', 'C', 'C', 'G', 'C', 'T', 'G', 'G',
        'G', 'A', 'G', 'T', 'G', 'G', 'T', 'T', 'A', 'C', 'G', 'G', 'C',
        'T', 'G', 'C', 'T', 'C', 'T', 'C', 'G', 'T', 'C', 'C', 'A', 'G',
        'T', 'G', 'T', 'T', 'T', 'A', 'G', 'C', 'T', 'T', 'G', 'G', 'C',
        'G', 'T', 'G', 'C', 'C', 'A', 'T', 'T', 'G', 'T', 'G', 'C', 'G',
        'G', 'T'],
       ['G', 'T', 'T', 'A', 'G', 'G', 'G', 'C', 'C', 'A', 'G', 'T', 'G',
        'C', 'C', 'T', 'G', 'G', 'G', 'G', 'G', 'C', 'G', 'T', 'A', 'T',
        'C', 'G', 'G', 'T', 'G', 'G', 'G', 'A', 'T', 'C', 'T', 'C', 'G',
        'T', 'A', 'C', 'C', 'G', 'T', 'G', 'G', 'C', 'T', 'T', 'G', 'G',
        'G', 'T', 'T', 'T', 'C', 'G', 'T', 'C', 'G', 'C', 'C', 'C', 'G',
        'G', 'G', 'C', 'C', 'T', 'C', 'G', 'T', 'T', 'C', 'G', 'T', 'C',
        'C', 'G'],
       ['G', 'C', 'T', 'G', 'C', 'G', 'T', 'G', 'T', 'G', 'G', 'C', 'T',
        'C', 'G', 'G', 'T', 'G', 'G', 'G', 'G', 'A', 'T', 'A', 'G', 'T',
        'T', 'T', 'T', 'T', 'G', 'G', 'G', 'G', 'G', 'C', 'A', 'G', 'A',
        'G', 'A', 'T', 'A', 'C', 'G', 'T', 'G', 'T', 'C', 'T', 'G', 'G',
        'G', 'A', 'G', 'T', 'G', 'G', 'G', 'T', 'G', 'G', 'T', 'G', 'T',
        'C', 'T', 'T', 'T', 'T', 'C', 'A', 'G', 'G', 'T', 'C', 'G', 'T',
        'G', 'G']],
      dtype='<U1')
In [94]:
tz.pipe(
    dna,
    c.map(lambda s: ''.join(s)),
    list
)
Out[94]:
['TCGGTGCCGTGGCGCCCCTGATGTGGTGCCCTGGTCTTGCGTCGTTTGGTGTGTGGGGCGAGGGGTGTTTGTGGCATCTG',
 'GATGCCGAGTGGAGGCGCGGGGCGTGGTGGGGCGGGCCGTCAACTTTGGGGGGGGGAGGCCGGTTATTCGGGACTGTGCC',
 'CCGCTGGTCGCGGGCGGTTTCTCGTGAGGCGTGGGTTGGCCGTTGTCTCCGCTTCGGTAGCTTTGGGATTTAGGGCTGCG',
 'TTCGTTTCCCGGTCTTGTGTGCGGCGGCTCGTGTTCGCTTTTGTCGCTATGGATGCGGGGCTTCTGGGGGATACTGCCGT',
 'GCGGGTTTGCGGGCGTATTTCGGGGGTTTTTGTACCGGTCGTTGGTGGGTTGCTCGGCCTGTGGTCAGTGCGGAGCCGTG',
 'GATGCTTTCTTGGCCAGGTTTGCCTGTCCCGGTTGCCGATGTTCGGCTTTGTGGGCTGCGCGTACTGGGAGCCTGGTGCA',
 'CGTTGAGGTTATCGAGCTCCGTGGGTTGGGGGAGAGGGGCTATTTGGTGTGGTGAGGGGGAGTCTGCTGGCGCTAGTGTG',
 'TATTCGAGATATCTGCCCCCCGCTGGGAGTGGTTACGGCTGCTCTCGTCCAGTGTTTAGCTTGGCGTGCCATTGTGCGGT',
 'GTTAGGGCCAGTGCCTGGGGGCGTATCGGTGGGATCTCGTACCGTGGCTTGGGTTTCGTCGCCCGGGCCTCGTTCGTCCG',
 'GCTGCGTGTGGCTCGGTGGGGATAGTTTTTGGGGGCAGAGATACGTGTCTGGGAGTGGGTGGTGTCTTTTCAGGTCGTGG']
In [95]:
res = tz.pipe(
    dna,
    c.map(lambda s: ''.join(s)),
    lambda s: ''.join(s),
    c.sliding_window(3),
    c.map(lambda s: ''.join(s)),
    tz.frequencies
)
In [96]:
[(k,v) for i, (k, v) in enumerate(sorted(res.items(), key=lambda x: -x[1])) if i < 5]
Out[96]:
[('GGG', 58), ('GTG', 47), ('TGG', 44), ('GGT', 33), ('GGC', 31)]

Function annotations and type hints

Function annotations and type hints are optional and meant for 3rd party libraries (e.g. a static type checker or JIT compiler). They are NOT enforced at runtime.

Notice the type annotation, default value and return type.

In [97]:
def f(a: str = "hello") -> bool:
    return a.islower()
In [98]:
f()
Out[98]:
True
In [99]:
f("hello")
Out[99]:
True
In [100]:
f("Hello")
Out[100]:
False

Function annotations can be accessed through a special attribute.

In [101]:
f.__annotations__
Out[101]:
{'a': str, 'return': bool}

Type and function annotations are NOT enforced. In fact, the Python interpreter essentially ignores them.

In [102]:
def f(x: int) -> int:
    return x + x
In [103]:
f("hello")
Out[103]:
'hellohello'

For more types, import from the typing module

In [104]:
from typing import Sequence, TypeVar
In [105]:
from functools import reduce
import operator as op
In [106]:
T = TypeVar('T')

def f(xs: Sequence[T]) -> T:
    return reduce(op.add, xs)
In [107]:
f([1,2,3])
Out[107]:
6
In [108]:
f({1., 2., 3.})
Out[108]:
6.0
In [109]:
f(('a', 'b', 'c'))
Out[109]:
'abc'