In [2]:
# 1. Functions
def foo():
return 1
foo()
Out[2]:
In [14]:
# 2. Scope
a_string = "This is global"
def foo_scope():
a_string = "This is local"
print locals()
foo_scope()
print globals()['a_string']
In [23]:
# 3. Variable resolution rules
def foo_vrr():
# variable scope is resolved by first checking the local scope
# and if it isn't found, it is considered a new var, even if
# it uses the same namespace
a_string = "test"
print locals()
foo_vrr()
a_string
Out[23]:
In [30]:
# 4. Variable Lifetime
def foo_vl():
x = 1
foo_vl()
# print x gives an error
In [33]:
# 5. Function arguments and parameters
# functions are first-class objects, so they can be passed as params to a func
def foo_fap(x):
print locals()
foo_fap(1)
In [42]:
# 6. Nested functions
def outer():
x = 1
def inner():
print x
inner()
# outer()
thing = outer
# thing()
In [53]:
# 7. Funcs are 1st Class
print issubclass(int, object)
print isinstance("hello", object)
# all objects in python are subclass of object
def foo_fcc():
print 'hello'
print foo_fcc.__class__
print type(foo_fcc)
print issubclass(foo_fcc.__class__, object)
In [57]:
# functions that receive functions as parameters
def add(x, y):
return x + y
def sub(x, y):
return x - y
def apply_operation(func, x, y):
return func(x, y)
print apply_operation(add, 2, 1)
print apply_operation(sub, 2, 5)
In [67]:
# functions can also return functions
def outer():
def inner():
print "Inside inner"
# now, we just pass back a function label
return inner
# but by doing this we can assign the inner function to another variable
foo = outer()
print foo
foo()
def outer_one():
def inner(x):
print "Inside with {0}".format(x)
return inner
bar = outer_one()
print bar
bar("mittens")
In [70]:
# 8. Closures
def outer():
x = 1
def inner():
print x
return inner
foo = outer()
print foo
print foo.func_closure
In [72]:
# closures: functions remember their enclosing scope at the time of definition
def outer_param(x):
def inner():
print x
return inner
print1 = outer_param(1)
print2 = outer_param(2)
print1()
print2()
In [82]:
# 9. Decorators!
# a decorator is just a callable that takes a function as an argument
# and returns a replacement function
def outer(some_func):
def innert():
print "before some_func"
ret = some_func() # get the value of some_func, store it
return ret + 1 # return the value + 1
return innert # return the innert func label
def return_one():
return 1
decorated = outer(return_one)
print decorated()
# you could say: decorated is a decorated version of return_one, because
# it does return_one plus something else
# we could also replace the original version of return_one with the
# decorated form so that we would always get our "plus something else"
# version of return_one
return_one = outer(return_one)
print return_one()
# now, any future calls to return_one will return the decorated version
# instead of the original
print return_one()
In [83]:
class Coordinate(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return "Coord: " + str(self.__dict__)
def add(a, b):
return Coordinate(a.x + b.x, a.y + b.y)
def sub(a, b):
return Coordinate(a.x - b.x, a.y - b.y)
one = Coordinate(100,200)
two = Coordinate(300,200)
add(one, two)
Out[83]:
In [85]:
# the above works well, but what if we had this:
three = Coordinate(-100, -100)
print sub(one, two)
print add(one, three)
# we'd rather have the difference of one and two be {0, 0} and the sum of
# one and three be {100, 200} without having to modify one, two or three
In [87]:
# one solution would be to add a bounds checking decorator
def wrapper(func):
def checker(a, b):
if a.x < 0 or a.y < 0:
a = Coordinate(a.x if a.x>0 else 0, a.y if a.y>0 else 0)
if b.x < 0 or b.y < 0:
b = Coordinate(b.x if b.x>0 else 0, b.y if b.y>0 else 0)
ret = func(a,b)
if ret.x < 0 or ret.y < 0:
ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y>0 else 0)
return ret
return checker
add = wrapper(add)
sub = wrapper(sub)
# now, the functions add and sub work as before, but with a little
# something extra -- hence they are 'decorated'
In [88]:
sub(one, two)
Out[88]:
In [89]:
add(one, three)
Out[89]:
In [97]:
# 10. The @ symbol applies a decorator to a function
# prior to python 2.4, you'd have to rename the function label to have the
# decorated effect. but now, you can explicitly declare it
# this:
add = wrapper(add)
# is the same as:
@wrapper
def add(a,b):
print 'much decorated'
return Coordinate(a.x+b.x, a.y+b.y)
add(one, three)
Out[97]:
In [96]:
@wrapper
def sub(a,b):
print 'param decorated'
return Coordinate(a.x-b.x, a.y-b.y)
sub(one, two)
Out[96]:
In [101]:
# 11. *args and **kwargs
# the * operator when used in defining a function means that
# any extra positional arguments passed to the function end up
# with the variable prefaced with a *. So:
def one(*args):
print args
one()
one(1, 2, 3)
def two(x,y,*args):
print x, y, args
two('a','b','c')
two(1,2,3,4,5,6,7,8,9)
In [106]:
'''
The * operator, when prefacing a variable that is passed into a function
means that the variable contents should be extracted and used a positional
arguments
'''
def add(x, y):
return x + y
lst = [1, 2]
# the below two function the same
print add(lst[0], lst[1])
print add(*lst)
In [108]:
# ** does for dicts and k/v pairs what * does for positional parameters
def foobar(**kwargs):
print kwargs
foobar()
foobar(x=1, y=2)
In [112]:
# neither args nor kwargs are part of the python language, it is just
# convention to use those words
def barfoo(x, y):
print x+y
dct = {'x':1, 'y':2} # these must match the expected param labels, x/y
barfoo(**dct)
In [118]:
# 12. More generic decorators
'''
We can now write a decorator that "logs" the arguments to a functions:
'''
def logger(func):
def inner(*args, **kwargs):
print 'Arguments were: %s, %s' % (args, kwargs)
return func(*args, **kwargs)
return inner
'''
The inner function takes any arbitrary number of type of parameters and
passes them along as arguments to the wrapped function, inner. This allows
you to wrap or decorate any function, no matter the signature
'''
# mitten = logger(mitten)
@logger
def mitten(x, y=5):
return x*y
# smitten = logger(smitten)
@logger
def smitten():
return 2
# calling these functions results in a "logging" output line, as well
# as the expected return value of each function
print mitten(5, 4)
print mitten(10)
print smitten()
In [ ]: