This notebook was put together by [Jake Vanderplas](http://www.vanderplas.com) for UW's [Astro 599](http://www.astro.washington.edu/users/vanderplas/Astr599/) course. Source and license info is on [GitHub](https://github.com/jakevdp/2013_fall_ASTR599/).
We have spent much of our time so far taking a look at scientific tools in Python. But a big part of using Python is an in-depth knowledge of the language itself. The topics here may not have direct science applications, but you'd be surprised when and where they can pop up as you use and write scientific Python code.
We'll dive a bit deeper into a few of these topics here.
In [2]:
z = 1 + 2j
In [3]:
# The type function allows us to inspect the object type
type(z)
Out[3]:
In [4]:
# "calling" an object type is akin to constructing an object
z = complex(1, 2)
In [5]:
# z has real and imaginary attributes
print z.real
print z.imag
In [6]:
# z has methods to operate on these attributes
print z.conjugate()
Every data container you see in Python is an object, from integers and floats to lists to numpy arrays.
In [7]:
class MyClass(object):
# attributes and methods are defined here
pass
In [8]:
# create a MyClass "instance" named m
m = MyClass()
print type(m)
In [9]:
class MyClass(object):
def __init__(self):
print self
print "initialization called"
pass
m = MyClass()
In [10]:
print m
The first argument of __init__() points to the object itself, and is usually called self by convention.
Note above that when we print self and when we print m, we see that they point to the same thing. self is m
In [11]:
class MyClass(object):
def __init__(self, value):
self.value = value
In [12]:
m = MyClass(5.0) # note: the self argument is always implied
print m.value
In [13]:
class MyClass(object):
def __init__(self, value):
self.value = value
def squared(self):
return self.value ** 2
In [14]:
m = MyClass(5)
print m.squared()
Methods act just like functions: they can have any number of arguments or keyword arguments, they can accept *args and **kwargs arguments, and can call other methods or functions.
In [15]:
class MyClass(object):
def __init__(self, value):
self.value = value
def squared(self):
return self.value ** 2
def __repr__(self):
return "MyClass(value=" + str(self.value) + ")"
In [16]:
m = MyClass(10)
print m
print type(m)
Other special methods to be aware of:
__str__, __repr__, __hash__, etc.__add__, __sub__, __mul__, __div__, etc.__getitem__, __setitem__, etc.__getattr__, __setattr__, etc.__eq__, __lt__, __gt__, etc.__new__, __init__, __del__, etc.__int__, __long__, __float__, etc.For a nice discussion and explanation of these and many other special double-underscore methods, see http://www.rafekettler.com/magicmethods.html
Create a class MyComplex which behaves like the built-in complex numbers. You should be able to execute the following code and see these results:
>>> z = MyComplex(2, 3)
>>> print z
(2, 3j)
>>> print z.real
2
>>> print z.imag
3
>>> print z.conjugate()
(2, -3j)
>>> print type(z.conjugate())
<class '__main__.MyComplex'>
Note that the conjugate() method should return a new object of type MyComplex.
In [16]:
If you finish this quickly, search online for help on defining the __add__ method such that you can compute:
>>> z + z.conjugate()
(4, 0j)
In [16]:
In [17]:
0/0
Or you may call a function with the wrong number of arguments:
In [18]:
from math import sqrt
sqrt(2, 3)
Or you may choose an index that is out of range:
In [19]:
L = [4, 5, 6]
L[100]
Or a dictionary key that doesn't exist:
In [20]:
D = {'a':2, 'b':300}
print D['Q']
Or the wrong value for a conversion function:
In [21]:
x = int('ABC')
These are known as Exceptions, and handling them appropriately is a big part of writing usable code.
In [22]:
def try_division(value):
try:
x = value / value
return x
except ZeroDivisionError:
return 'Not A Number'
print try_division(1)
print try_division(0)
In [23]:
def get_an_int():
while True:
try:
x = int(raw_input("Enter an integer: "))
print " >> Thank you!"
break
except ValueError:
print " >> Boo. That's not an integer."
return x
get_an_int()
Out[23]:
Other things to be aware of:
except statements for different exception typeselse and finally statements can fine-tune the exception handlingMore information is available in the Python documentation and in the scipy lectures
In [24]:
def laugh(N):
if N < 0:
raise ValueError("N must be positive")
return N * "ha! "
In [25]:
laugh(10)
Out[25]:
In [26]:
laugh(-4)
In [27]:
v = ValueError("message")
print type(v)
When you raise an exception, you are creating an instance of the exception type, and passing it to the raise keyword:
In [28]:
raise ValueError("error message")
Later in the quarter we'll dive into object-oriented programming, but here's a quick preview of the principle of inheritance: new objects derived from existing objects:
In [29]:
# define a custom exception, inheriting from the base class Exception
class CustomException(Exception):
# can define custom behavior here
pass
raise CustomException("error message")
In [29]:
In [30]:
for i in range(10):
print i
One weakness here, though, is that (in Python 2.x) the range() function actually constructs a list, which is then iterated through.
So, if we were to do something like
for i in range(100000000):
print i
then before anything in the loop is executed, Python would first construct a list containing 100 million integers: that's close to a terabyte of memory!
Fortunately, Python provides iterators: objects that look and act like a list, but generate the items on-the-fly.
The iterator equivalent of range() is the function xrange() (note that in Python 3, range() itself returns an iterator rather than a list)
In [31]:
print range(10)
print xrange(10)
In [32]:
for i in range(10):
print i,
print
for i in xrange(10):
print i,
print
In [33]:
import sys
print sys.getsizeof(range(1000))
print sys.getsizeof(xrange(1000))
In [34]:
D = {'a':0, 'b':1, 'c':2}
print D.keys()
print D.iterkeys()
In [35]:
print D.values()
print D.itervalues()
In [36]:
print D.items()
print D.iteritems()
In [37]:
for key in D.iterkeys():
print key
In [38]:
for key in D:
print key
In [39]:
for key, val in D.iteritems():
print key, val
In [40]:
import itertools
dir(itertools)
Out[40]:
In [41]:
for c in itertools.combinations([1, 2, 3, 4], 2):
print c
In [42]:
for p in itertools.permutations([1, 2, 3]):
print p
In [43]:
for val in itertools.chain(range(0, 4), range(-4, 0)):
print val,
In [44]:
# zip: itertools.izip is an iterator equivalent
for val in zip([1, 2, 3], ['a', 'b', 'c']):
print val
Write a function count_pairs(N, m) which returns the number of pairs of numbers in the sequence $0 ... N-1$ whose sum is divisible by m.
For example, if N = 3 and m = 2, the pairs are
[(0, 1), (0, 2), (1, 2)]
The sum of each pair respectively is [1, 2, 3], and there is a single pair whose sum is divisible by 2, so the result is 1.
In [45]:
def select_evens(L):
for value in L:
if value % 2 == 0:
yield value
In [46]:
for val in select_evens([1,2,5,3,6,4]):
print val
The yield statement is like a return statement, but the iterator remembers where it is in the execution, and comes back to that point on the next pass.
In [47]:
def iter_primes(Nprimes):
N = 2
found_primes = []
while True:
if all([N % p != 0 for p in found_primes]):
found_primes.append(N)
yield N
if len(found_primes) >= Nprimes:
break
N += 1
In [48]:
# Find the first twenty primes
for N in iter_primes(20):
print N,
In [49]:
L = []
for i in range(20):
if i % 3 > 0:
L.append(i)
L
Out[49]:
In [50]:
# or, as a list comprehension
[i for i in range(20) if i % 3 > 0]
Out[50]:
The corresponding construction of an iterator is known as a "generator expression":
In [51]:
def genfunc():
for i in range(20):
if i % 3 > 0:
yield i
print genfunc()
print list(genfunc()) # convert iterator to list
In [52]:
# or, equivalently, as a "generator expression"
g = (i for i in range(20) if i % 3 > 0)
print g
print list(g) # convert generator expression to list
The syntax is identical to that of list comprehensions, except we surround the expression with () rather than with []. Again, this may seem a bit specialized, but it allows some extremely powerful constructions in Python, and it's one of the features of Python that some people get very excited about.