Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download

All published worksheets from http://sagenb.org

Views: 168746
Image: ubuntu2004

581d - 2010-10-14:

A Crash course in Python, for people who (think they) know Python.

Today: Functions and Classes

NOTE: This worksheet should be run in Python mode.  Adjust the dropdown at the top of the worksheet to make sure it is in that mode.

Functions

A big gotcha (explain):

def f(a, L=[]): L.append(a) print L f(1) f(2) f(3)
[1] [1, 2] [1, 2, 3]

Right thing to do is this:

def f(a, L=None): if L is None: L = [] L.append(a) print L f(1) f(2) f(3)
[1] [2] [3]

How to do variable number of arguments and keywords:

def f(a, b, *args, **kwds): print a, b print "args = ", args print "kwds = ", kwds
f(2,3, 10, x=5, yz=20)
2 3 args = (10,) kwds = {'x': 5, 'yz': 20}

You can magically turn a tuple or dictionary into the inputs to a function using * and **:

def f(a, b, xyz): print "a = ", a print "b = ", b print "xyz = ", xyz
f(3,4, 'hello')
a = 3 b = 4 xyz = hello
v = (3,4) d = {'xyz':'hello'} f(*v, **d)
a = 3 b = 4 xyz = hello

When you explicitly specify the input variable names, you can give them in any order:

def f(a, b): print "a =", a print "b =", b
f(b=20, a=7)
a = 7 b = 20
f(7, 20)
a = 7 b = 20
f(a=7, b=20)
a = 7 b = 20

A function can sometimes tell whether it was called with the input variable explicitly specified via a keyword or not.  So there is a difference between f(7,20) and f(a=7,b=20).

def f(*args, **kwds): if len(kwds) > 0: print "a keyword was used in calling me" else: print "no keywords used"
f(7,20)
no keywords used
f(a=7,b=20)
a keyword was used in calling me

How to return multiple values.

def f(n): return n^2, n^3
a, b = f(10) print "a =", a print "b =", b
a = 8 b = 9

This actually looks exactly like what you do in Magma, but it is much different (and better) than Magma, since really a tuple is returned, and Python does tuple unpacking:

t = f(10); t
(8, 9)
type(t)
<type 'tuple'>
a, b = t print a, b
8 9

Python functions can be easily passed around, and you can figure out some things about them in some cases.

def f(op, a, b): return op(a,b)
def my_op(a, b): return a*b
f(my_op, 4, 5)
20
my_op.func_name
'my_op'

You can attach extra attributes to a function itself.  (This only works with functions defined in Python.)

def f(n, m): return f.op(n,m)
f.op = lambda a,b: a*b +1
f(3,8)
25
f.op
<function <lambda> at 0x10ca42398>

You can't attach attributes to functions that are "builtin".

print type(f) import math print type(math.sin)
<type 'function'> <type 'builtin_function_or_method'>
math.sin.op = 5
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_56.py", line 10, in <module> exec compile(u"print _support_.syseval(python, u'math.sin.op = 5', __SAGE_TMP_DIR__)" + '\n', '', 'single') File "", line 1, in <module> File "/Users/wstein/sage/install/sage-4.5.3/local/lib/python2.6/site-packages/sagenb-0.8.2-py2.6.egg/sagenb/misc/support.py", line 473, in syseval return system.eval(cmd, sage_globals, locals = sage_globals) File "/Users/wstein/sage/install/sage-4.5.3/local/lib/python2.6/site-packages/sage/misc/python.py", line 56, in eval eval(z, globals) File "", line 1, in <module> AttributeError: 'builtin_function_or_method' object has no attribute 'op'

Another gotcha is recursion, which does not scale in Python. 

def myfactorial(n): if n == 1: return n assert n > 0 return n * myfactorial(n-1)
myfactorial(69)
171122452428141311372468338881272839092270544893520369393648040923257279754140647424000000000000000L
a = myfactorial(994) len(str(a))
2550
myfactorial(995)
WARNING: Output truncated!
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_72.py", line 10, in <module> exec compile(u"print _support_.syseval(python, u'myfactorial(995)', __SAGE_TMP_DIR__)" + '\n', '', 'single') File "", line 1, in <module> File "/Users/wstein/sage/install/sage-4.5.3/local/lib/python2.6/site-packages/sagenb-0.8.2-py2.6.egg/sagenb/misc/support.py", line 473, in syseval return system.eval(cmd, sage_globals, locals = sage_globals) File "/Users/wstein/sage/install/sage-4.5.3/local/lib/python2.6/site-packages/sage/misc/python.py", line 56, in eval eval(z, globals) File "", line 1, in <module> File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial ... File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial File "", line 5, in myfactorial RuntimeError: maximum recursion depth exceeded

So watch out and be careful when writing recursive functions that you only use them when the depth of the recursion is bounded by 1000.  Or, simply increase the recursionlimit as illustrarted below. 

import sys sys.getrecursionlimit()
1000
sys.setrecursionlimit(10000)
a = myfactorial(2000)

Style: Go through http://docs.python.org/tutorial/controlflow.html#intermezzo-coding-style

The following function illustrates all the points.

def good_function(a, b = 10): """ This is a good function. This function has a docstring and is named using lower_case_with_underscores. It takes as input integers a and b and outputs something computed using them. (Notice that the above line is <= 79 characters.) """ c = 0 for i in range(a): # add i-th power of b to a and # put spaces around operators (comment on line of its own). c = b**i + a # Another block, and a correctly named class (CamelCase). class UselessWrapper(int): pass return UselessWrapper(c)
n = good_function(2, 5); n
7
type(n)
<class '__main__.UselessWrapper'>

Aliasing:

From the Python documentation: "Objects have individuality, and multiple names (in multiple scopes) can be bound to the same object. This is known as aliasing in other languages. This is usually not appreciated on a first glance at Python, and can be safely ignored when dealing with immutable basic types (numbers, strings, tuples). However, aliasing has a possibly surprising effect on the semantics of Python code involving mutable objects such as lists, dictionaries, and most other types. This is usually used to the benefit of the program, since aliases behave like pointers in some respects. For example, passing an object is cheap since only a pointer is passed by the implementation; and if a function modifies an object passed as an argument, the caller will see the change — this eliminates the need for two different argument passing mechanisms as in Pascal."

This is surprising and different than every other language I know.  You must understand this point completely, or you'll get seriously tripped up.

Example

v = [1,2,3,10]
def sum_up(w): while len(w) > 1: w[-1] += w[0] del w[0] return w[0]
sum_up(v)
16
v
[16]

Hey, what just happened to v?!

def sum_up(w): # make a copy of w: w = list(w) while len(w) > 1: w[-1] += w[0] del w[0] return w[0]
v = [1,2,3,10] sum_up(v)
16
v
[1, 2, 3, 10]

Classes

class MyClass: """ A simple example class. """ # a Python object attribute; this is basically a default # piece of data that is available to each instance of the # class, but can be changed in the instance without changing # it in the class. (See example below.) i = 12345 # A function attribute. Again, this is available to each # instance, and can be changed in the instance without # changing the class object itself. def f(self): return 'hello world'
# A class is an object: MyClass
<class __main__.MyClass at 0x10c917530>
MyClass.i
12345
MyClass.f
<unbound method MyClass.f>
MyClass.__doc__
'A simple example class'
x = MyClass() type(x)
<type 'instance'>
x.f()
'hello world'
x.i
12345
x.i = 100
y = MyClass() y.i
12345
MyClass.i = 100
z = MyClass() z.i
100
z.f = lambda : 'goodbye world'
z.f()
'goodbye world'
x.f()
'hello world'
w = MyClass() w.f()
'hello world'

Inheritence

class DerivedClass(MyClass): def g(self): return "derived class" def f(self): return "hello *new* world"
d = DerivedClass() d.g()
'derived class'
d.f()
'hello *new* world'
d.i
12345

Question: How do we call the f method defined in the superclass?

One way: explicitly get the superclass's f method, and call it with d as input.

class DerivedClass(MyClass): def g(self): return "derived class" def f(self): return "hello *new* world" def s(self): return MyClass.f(d)
d = DerivedClass() d.s()
'hello world'

Approach 2: use the super function.  This makes no explicit reference to MyClass, which is nice.

class MyClass: i = 12345 def f(self): return 'hello world' class DerivedClass(MyClass): def g(self): return "derived class" def f(self): return "hello *new* world" def s(self): return super(DerivedClass, self).f()
d = DerivedClass() d.s()
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_92.py", line 10, in <module> exec compile(u"print _support_.syseval(python, u'd = DerivedClass()\\nd.s()', __SAGE_TMP_DIR__)" + '\n', '', 'single') File "", line 1, in <module> File "/Users/wstein/sage/install/sage-4.5.3/local/lib/python2.6/site-packages/sagenb-0.8.2-py2.6.egg/sagenb/misc/support.py", line 473, in syseval return system.eval(cmd, sage_globals, locals = sage_globals) File "/Users/wstein/sage/install/sage-4.5.3/local/lib/python2.6/site-packages/sage/misc/python.py", line 56, in eval eval(z, globals) File "", line 1, in <module> File "", line 13, in s TypeError: super() argument 1 must be type, not classobj

Huh!?  See http://stackoverflow.com/questions/489269/python-super-raises-typeerror-why.  The problem is that super only operators on new style classes, i.e., classes that derive from object.   (Note: In Python 3.x, all classes are new style classes.)

class MyClass(object): i = 12345 def f(self): return 'hello world' class DerivedClass(MyClass): def g(self): return "derived class" def f(self): return "hello *new* world" def s(self): # no explicit reference to MyClass here. return super(DerivedClass, self).f()
d = DerivedClass() d.s()
'hello world'

Multiple Inheritence: new versus old style classes

 -----------
|           |
|    O      |
|  /   \    |
 - X    Y  /
   |  / | /
   | /  |/
   A    B
   \   /
     C

Old style classes -- allow ambiguous class hierarchies:

class O: pass class X(O): pass class Y(O): pass class A(X,Y): pass class B(Y,X): pass class C(A,B): pass

New style classes don't allow this:

O = object class X(O): pass class Y(O): pass class A(X,Y): pass class B(Y,X): pass class C(A,B): pass
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_126.py", line 10, in <module> exec compile(u"print _support_.syseval(python, u'O = object\\nclass X(O): pass\\nclass Y(O): pass\\nclass A(X,Y): pass\\nclass B(Y,X): pass\\nclass C(A,B): pass', __SAGE_TMP_DIR__)" + '\n', '', 'single') File "", line 1, in <module> File "/Users/wstein/sage/install/sage-4.5.3/local/lib/python2.6/site-packages/sagenb-0.8.2-py2.6.egg/sagenb/misc/support.py", line 473, in syseval return system.eval(cmd, sage_globals, locals = sage_globals) File "/Users/wstein/sage/install/sage-4.5.3/local/lib/python2.6/site-packages/sage/misc/python.py", line 56, in eval eval(z, globals) File "", line 1, in <module> TypeError: Error when calling the metaclass bases Cannot create a consistent method resolution order (MRO) for bases X, Y

Method Resolution Order: 

Given any Python class C, there is a linearly ordered list of the superclasses of C that Python computes, called the MRO "Method Resolution Order".  When you try to call a function of (an instance of) C, Python just checks for that function in each of the methods of the classes in the MRO, in order.

  1. Different for old and new style classes.  Old style classes have a very easy MRO,  but it has quite bad theoretical properties.  "For old-style classes, the only rule is depth-first, left-to-right."
  2. For new style classes see http://www.python.org/download/releases/2.3/mro/ for a detailed discussion on the new style MRO.
  3. Fortunately, you can simply ask a new style class for its MRO.  When in doubt, just do this.  
O = object class X(O): pass class Y(O): pass class A(X,Y): pass class B(Y,X): pass A.mro()
[<class '__main__.A'>, <class '__main__.X'>, <class '__main__.Y'>, <type 'object'>]
B.mro()
[<class '__main__.B'>, <class '__main__.Y'>, <class '__main__.X'>, <type 'object'>]

Another:

                           6
                          ---
Level 3                  | O |
                       /  ---  \
                      /    |    \
                     /     |     \
                    /      |      \
                  ---     ---    ---
Level 2        2 | E | 4 | D |  | F | 5
                  ---     ---    ---
                   \      / \     /
                    \    /   \   /
                     \  /     \ /
                      ---     ---
Level 1            1 | B |   | C | 3
                      ---     ---
                       \       /
                        \     /
                          ---
Level 0                0 | A |
                          ---
O = object class F(O): pass class E(O): pass class D(O): pass class C(D,F): pass class B(E,D): pass class A(B,C): pass
A.mro()
[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.F'>, <type 'object'>]

Private Variables.

This is done in Python via "mangling".

class MyOwnClass(object): def __init__(self): self.__secret_number = 10 def __repr__(self): return str(self.__secret_number)
x = MyOwnClass() x
10
x.__secret_number = 20
x
10
x.__dict__
{'__secret_number': 20, '_MyOwnClass__secret_number': 10}

Easily thwarted:

x._MyOwnClass__secret_number = 20
x
20

From experience I can tell you most people really hate the use of these mangled private variables.  It is much better to just use a single underscore, which is meant as a convention "However, there is a convention that is followed by most Python code: a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API (whether it is a function, a method or a data member). It should be considered an implementation detail and subject to change without notice."

class MyOwnClass(object): def __init__(self): self._confidential_number = 10 def __repr__(self): return str(self._confidential_number)
x = MyOwnClass(); x
10
x._confidential_number = 20 x
20