CSX91: Python Tutorial

1. Functions

  • Fucntions in Python are created using the keyword def
  • It can return values with return
  • Let's create a simple function:

In [ ]:
def foo():
    return 1
foo()

Q. What happens if there is no return?

2. Scope

  • In python functions have their own scope (namespace).
  • Python first looks at the function's namespace first before looking at the global namespace.
  • Let's use locals() and globals() to see what happens:

In [ ]:
aString = 'Global var'
def foo():
    a = 'Local var'
    print locals()

foo()
print globals()

2.1 Variable lifetime

Variables within functions exist only withing their namespaces. Once the function stops, all the variables inside it gets destroyed. For instance, the following won't work.


In [ ]:
def foo():
    x = 10

foo()
print x

3. Variable Resolution

  • Python first looks at the function's namespace first before looking at the global namespace.

In [ ]:
aString = 'Global var'
def foo():
    print aString

foo()
  • If you try and reassign a global variable inside a function, like so:

In [ ]:
aString = 'Global var'
def foo():
    aString = 'Local var'
    print aString

foo()

Q. What would be the value of aString if I print it?

  • As we can see, global variables can be accessed (even changed if they are mutable data types) but not (by default) assigned to.
  • Global variables are very dangerous. So, python wants you to be sure of what you're doing.
  • If you MUST reassign it. Declare it as global. Like so:

In [ ]:
aString = 'Global var'
def foo():
    global aString # <------ Declared here
    aString = 'Local var'
    print aString

def bar():
    print aString

foo()
bar()

4. Function Arguments: args and kwargs

  • Python allows us to pass function arguments (duh..)
  • The arguments are local to the function. For instance:

In [ ]:
def foo(x):
    print locals()

foo(1)
  • Arguments in functions can be classified as:
    • Args
    • kwargs (keyword args)
  • When calling a function, args are mandatory. kwargs are optional.

In [ ]:
"Args"
def foo(x,y):
    print x+y

"kwargs"
def bar(x=5, y=8):
    print x-y

"Both"
def foobar(x,y=100):
    print x*y

"Calling with args"
foo(5,12)

"Calling with kwargs"
bar()

"Calling both"
foobar(10)

Other ways of calling:

  • All the following are legit:

In [ ]:
"Args"
def foo(x,y):
    print x+y

"kwargs"
def bar(x=5, y=8):
    print x-y

"Both"
def foobar(x,y=100):
    print x*y

"kwargs"
bar(5,8) # kwargs as args (default: x=5, y=8)
bar(5,y=8) # x=5, y=8
"Change the order of kwargs if you want"
bar(y=8, x=5)

"args as kwargs will also work"
foo(x=5, y=12)

Q. will these two work?


In [ ]:
"Args"
def foo(x,y):
    print x+y

"kwargs"
def bar(x=5, y=8):
    print x-y

"Both"
def foobar(x,y=100):
    print x*y

bar(x=9, 7) #1
foo(x=5, 6) #2
  • Never call args after kwargs

5. Nesting functions

  • You can nest functions.
  • Class nesting is somewhat uncommon, but can be done.

In [ ]:
def outer():
    x=1
    def inner():
        print x
    inner()

outer()
  • All the namespace conventions apply here.

What would happen if I changed x inside inner()?


In [ ]:
def outer():
    x = 1
    def inner(): 
        x = 2
        print 'Inner x=%d'%(x)
    inner()
    return x

print 'Outer x=%d'%outer()

What about global variables?


In [ ]:
x = 4
def outer(): 
    global x
    x = 1
    def inner(): 
        global x
        x = 2
        print 'Inner x=%d'%(x)
    inner()
    return x

print 'Outer x=%d'%outer()
print 'Global x=%d'%x
  • Declare global every time the global x needs changing

6. Classes

  • Define classes with the class keyword
  • Here's a simple class

In [ ]:
class foo():
    def __init__(i, arg1): # self can br replaced by anything.
        i.arg1 = arg1
    def bar(i, arg2): # Always use self as the first argument
        print i.arg1, arg2

FOO = foo(7)

FOO.bar(5)

print FOO.arg1
  • All arg and kwarg conventions apply here

6.1 Overriding class methods

Lets try:


In [ ]:
class foo():
    def __init__(i, num):
        i.num = num

d = foo(2)
d()
  • We know the __call__ raises an exception. Python lets you redefine it:

In [ ]:
class foo():
    def __init__(i, num):
        i.num = num
    def __call__(i):
        return i.num
d = foo(2)
d()
  • There are many such redefinitions permitted by python. See Python Docs

6.2 Emulating numeric types

  • A very useful feature in python is the ability to emulate numeric types.

Would this work?


In [ ]:
class foo():
    def __init__(i, num):
        i.num = num
FOO = foo(5)
FOO += 1

Let's rewrite this:


In [ ]:
class foo():
    def __init__(i, num):
        i.num = num
    def __add__(i, new):
        i.num += new
        return i
    def __sub__(i, new):
        i.num -= new
        return i

FOO = foo(5)
FOO += 1
print FOO.num
FOO -= 4
print FOO.num
  • Aside: __repr__, __call__,__getitem__,... are all awesome.

In [ ]:
class foo():
    "Me is foo"
    def __init__(i, num):
        i.num = num
    def __add__(i, new):
        i.num += new
        return i
    def __sub__(i, new):
        i.num -= new
        return i
    def __repr__(i):
        return i.__doc__
    def __getitem__(i, num):
        print "Nothing @ %d"%(num)

FOO = foo(4)
FOO[2]

7. Functions and Classes are Objects

  • Functions and objects are like anything else in python.
  • All objects inherit from a base class in python.
  • For instance,

In [ ]:
issubclass(int, object)
  • It follows that the variable a here is a class.

In [ ]:
a = 9
dir(a)
  • This means:
    • Functions and Classes can be passed as arguments.
    • Functions can return other functions/classes.

In [ ]:
from pdb import set_trace # pdb is quite useful

def add(x,y): return x+y
def sub(x,y): return x-y
def foo(x,y,func=add): 
    set_trace()
    return func(x,y)

foo(7,4,sub)

8. Closures

Remember this example?


In [ ]:
def foo():
    x=1
    
foo()
print x

Obviously, this fails. Why? As per variable lifetime rules (see 2.1), foo() has ceased execution, x is destroyed.

So how about this?


In [ ]:
def foo():
    x='Outer String'
    def bar():
        print x
    return bar

test = foo()

test()

This works. But it shouldn't, because x is local to foo(), when foo() has ceased execution, x must be destroyed. Right? Turns out, Python supports a feature called function closure. This enables nested inner functions to keep track of their namespaces.

8.1 Aside: lambda functions and sorted

  • Anonymous functions in python can be defined using the lambda keyword.
  • The following two are the same:

In [ ]:
def foo(x,y): return x**y

bar = lambda x,y: x**y # <--- Notice no return statements

print foo(4,2)
print bar(4,2)
  • Nested lambda is permitted (idk why you'd use them, still, worth a mention)

In [ ]:
foo = lambda x: lambda y: x+y
print foo(3)(5)

8.1.1 Sorted

  • Python's sorted function can sort based on a key argument, key is a lambda function that deterimes how the data is sorted.

In [ ]:
student_tuples = [ #(Name, height(cms), weight(kg))
        ('john', 180, 85),
        ('doe', 177, 99),
        ('jane', 169, 69),
]

# Sort based on height
print 'Weight: ', sorted(student_tuples,  key=lambda stud: stud[1])

# Sort based on Name
print 'Name: ', sorted(student_tuples, key=lambda stud: stud[0])

# Sort based on BMI
print 'BMI: ', sorted(student_tuples, key=lambda stud: stud[2]*100/stud[1])

9. Decorators!

  • Decorators are callables that take a function as argument, and return a replacement function (with additional functionalities)

In [ ]:
def outer(func):
    def inner(*args):
        "Inner"
        print 'Decorating...'
        ret = func()
        ret += 1
        return ret
    return inner

def foo():
    "I'm foo"
    return 1

print foo()

decorated_foo = outer(foo)

print decorated_foo()
  • Lets look at memory locations of the functions.

In [ ]:
def outer(func):
    def inner(*args):
        "Inner"
        print 'Decorating...'
        ret = func()
        ret += 1
        return ret
    print inner.__doc__, inner
    return inner

def foo():
    "I'm foo"
    return 1

print foo.__name__, foo

decorated_foo = outer(foo)

print decorated_foo.__name__, decorated_foo
  • A common practice is to replace the original function with the decorated function

In [ ]:
def outer(func):
    def inner():
        "Inner"
        print 'Decorating...'
        ret = func()
        ret += 1
        return ret
    return inner

def foo():
    "I'm foo"
    return 1

print foo()
foo = outer(foo)

print foo()
  • Python uses @ to represent foo = outer(foo). The above code can be retwritten as follows:

In [ ]:
def outer(func):
    def inner():
        "Inner"
        print 'Decorating...'
        ret = func()
        ret += 1
        return ret
    return inner

@outer
def foo():
    "I'm foo"
    return 1

print foo()

9.1 Logging and timing a function

  • Decorators can be classes, they can take input arguments/keyword args.
  • Lets build a decorator that logs and times another function

In [ ]:
import time
from pdb import set_trace

def logger(func):
    def inner(*args, **kwargs):
        print "Arguments were: %s, %s"%(args, kwargs)
        return func(*args, **kwargs)
    return inner

def timer(func):
    def inner(*args, **kwargs):
        tb=time.time()
        result = func(*args, **kwargs)
        ta=time.time()
        print "Time taken: %f sec"%(ta-tb)
        return result
    return inner

@logger
@timer
def foo(a=5, b=2):
    return a+b

@logger
@timer
def bar(a=10, b=1):
    time.sleep(0.1)
    return a-b

if __name__=='__main__': ## <----- Note
    foo(2,3)
    bar(5,7)

References