In [1]:
# A function can accept a function as an argument, and can return a function
# This is useful if you want to reuse code that calls a function,
# and don't want to specify in advance which function it calls

In [2]:
# first, a little syntax
# if you want a function to take any number of arguments,
# use "*" before the parameter name and the parameter's value
# will be a tuple of arguments passed to the function

def multiple(*args):
    return args

multiple('blah', 1)


Out[2]:
('blah', 1)

In [3]:
multiple('one', 2, 'tres', 4.0)


Out[3]:
('one', 2, 'tres', 4.0)

In [4]:
# you can also use "*" before a parameter name you're passing to a function,
# and it will convert the value of the parameter (which must be iterable)
# to the arguments of the function being called, in order

def print_msg(text, number):
    print '%s %g' % (text, number)
    
args = ('blah', 1)

print_msg(*args)


blah 1

In [5]:
args = ('one', 2, 'tres', 4.0)

print_msg(*args)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-fd04e664eaf1> in <module>()
      1 args = ('one', 2, 'tres', 4.0)
      2 
----> 3 print_msg(*args)

TypeError: print_msg() takes exactly 2 arguments (4 given)

In [1]:
# more syntax: you can define a function inside another function. its name is only usable
# in the function that it is defined in. it can reference variables in the enclosing function

def hello(who='world'):
    def make_greeting():
        return 'Hello, %s.' % who
    print make_greeting()

hello()


Hello, world.

In [5]:
# but be very very careful in the inner function not to try to assign a value to a variable
# from the enclosing function, as the assignment will create a new variable with the same name
# only visible within the inner function.

# this example does not work:

def avg(numbers):
    sm = 0
    ct = 0
    def add(n):
        sm = sm + n
        ct = ct + 1
    for n in numbers:
        add(n)
    return sm / ct

avg(range(10))


---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-5-c60ae4e18382> in <module>()
     13     return sm / ct
     14 
---> 15 avg(range(10))

<ipython-input-5-c60ae4e18382> in avg(numbers)
     10         ct = ct + 1
     11     for n in numbers:
---> 12         add(n)
     13     return sm / ct
     14 

<ipython-input-5-c60ae4e18382> in add(n)
      7     ct = 0
      8     def add(n):
----> 9         sm = sm + n
     10         ct = ct + 1
     11     for n in numbers:

UnboundLocalError: local variable 'sm' referenced before assignment

In [4]:
# because local variables are usually desirable in a function,
# this behavior is correct and by design, but not useful in this case.
# there is no clean way to solve this problem in Python 2.7.
# here is a workaround:

def avg(numbers):
    state = {
        'sum': 0.,
        'count': 0.
    }
    def add(n):
        state['sum'] += n
        state['count'] += 1
    for n in numbers:
        add(n)
    return state['sum'] / state['count']

# this works because "state" is never assigned in the inner function. modifying its
# contents doesn't assign a new value to the variable called "state"; its value is
# still the same mutable dict object that was assigned to it in the outer function.

avg(range(10))


Out[4]:
4.5

In [55]:
# back to decorators.

# lets define a function that does something. this converts its argument
# to a string and returns a list of the characters in the string

str(3.1415926535)


Out[55]:
'3.1415926535'

In [60]:
list(str(3.1415926353))


Out[60]:
['3', '.', '1', '4', '1', '5', '9', '2', '6', '3', '5', '3']

In [61]:
def charlist(value):
    return list(str(value))

charlist(78)


Out[61]:
['7', '8']

In [62]:
d = {
    'x': 3
}

d


Out[62]:
{'x': 3}

In [63]:
charlist(d)


Out[63]:
['{', "'", 'x', "'", ':', ' ', '3', '}']

In [64]:
# here's another simple function that just computes the sum of all
# integers up to n non-inclusive

def sum_to(n):
    return sum(range(n))

sum_to(10)


Out[64]:
45

In [54]:
# as_charlist, given a function fn, returns a funtion inner
# that calls fn with its arguments and passes the result to
# charlist before returning it

def as_charlist(fn):
    def inner(*args):
        return charlist(fn(*args))
    return inner

s = as_charlist(sum_to)
s


Out[54]:
<function __main__.inner>

In [50]:
s(10)


Out[50]:
['4', '5']

In [52]:
# what if we made this the new definition of the function
# we want to treat this way?

def sum_to(n):
    return sum(range(n))

sum_to = as_charlist(sum_to)

sum_to(13)


Out[52]:
['7', '8']

In [53]:
# that's precisely what the decorator notation does

@as_charlist
def sum_to(n):
    return sum(range(n))

sum_to(20)


Out[53]:
['1', '9', '0']

In [68]:
# decorators can be stacked

def backwards(fn):
    def inner(*args):
        return list(reversed(fn(*args)))
    return inner

@backwards
@as_charlist
def sum_to(n):
    return sum(range(n))

sum_to(20)


Out[68]:
['0', '9', '1']

In [86]:
# decorators can take arguments, but that requires an additional level of "wrapping"

def as_charlist(upcase=False):
    def outer(fn):
        def inner(*args):
            s = str(fn(*args))
            if upcase:
                s = s.upper()
            return list(s)
        return inner
    return outer

In [90]:
@as_charlist(True)
def greeting(who='world'):
    return 'Hi, %s.' % who

greeting('everyone')


Out[90]:
['H', 'I', ',', ' ', 'E', 'V', 'E', 'R', 'Y', 'O', 'N', 'E', '.']