What's so great about Python? Well, I know a couple of programming languages and have a fairly good idea how a much larger number work, and it's clear to me that Python simply has a much higher power/ease of use ratio than anything else out there. Professional programmers use it for all sorts of things. You can go here for a description of some of the many things I've used it for - on this page I'm going to say a little bit about doing mathematical programming in Python. (No, it's not a substitute for things like Maple and Mathematica. Note as well that this page is not a tutorial in Python programming, the point is just to pique your interest. You can download Python for free at www.python.org; it comes with a very nice tutorial. Or you can ask me (Dr. Ullrich) about Python programming issues, or find lots of help on the internet and in many books on the topic (you can find links to many of these resources at www.python.org).)
There's no reason anyone should take my word for what's a good programming language. But Eric Raymond is a huge name in open-source programming - go here for excerpts from an article by him on what's so great about Python (also a link to the original article). Whether I'm qualified to have an opinion or not, here's a few comments on what I personally find so keen about the language:
x = 2 print x x = [1, 2, 3] print xwithout needing to "declare" the variable x as an integer or a "list"; the first "print x" above prints "2", and the second prints "[1, 2, 3]", no problem. We're going to use Python list objects for vectors.
Well, Python list objects are not vectors, they're lists. So for example if you say
x = [1, 2, 3] print x + xwhat gets printed is the list [1, 2, 3, 1, 2, 3], which is a reasonable way to "add" two lists, useful for the things we use lists for, but it's not the way we want to add two vectors. We're going to "wrap" Python list objects in a Vector class, and then teach Vectors the right way to add themselves.
We create a Python class using the keyword "class" (hmm). The first thing we need to do is give the Vector class a __init__ "method", which will allow us to insert some data into a Vector object when we construct it:
class Vector:
def __init__(self, data)
self.data = data
That's a valid and totally useless Vector class. We create a Vector
object by calling "Vector" as a function, passing a list of data:
class Vector:
def __init__(self, data):
self.data = data
x = Vector([1, 2, 3])
(there are tricks you could use to make "Vector(1, 2, 3)" do the
same thing as "Vector([1,2,3])", but never mind that for now...)
If you execute the code above then x is a Vector object, which has a field "data" equal to the list [1, 2, 3]; if you say
print x.dataat this point you'll see a printout of the list [1, 2, 3]. But saying "print x" will not give what you want, instead it prints something like "<__main__.Vector instance at f1ae10>", not very helpful. Well, so far we haven't said what we want to happen when we print a Vector object, so Python just tells us it's a Vector. We can fix that by adding a __repr__ method:
class Vector:
def __init__(self, data):
self.data = data
def __repr__(self):
return repr(self.data)
x = Vector([1, 2, 3])
print x
That __repr__ method tells Python that when we print a Vector
object we actually want to print the data contained in the Vector,
and now the "print x" prints "[1, 2, 3]", which is more like it.
(Technical note that you should ignore at this point...)
Of course we want to be able to add vectors; right now if we say
class Vector:
def __init__(self, data):
self.data = data
def __repr__(self):
return repr(self.data)
x = Vector([1, 2, 3])
print x + x
we don't get [2, 4, 6] or [1, 2, 3, 1, 2, 3], instead we get an error
message, because we haven't told Python how Vectors should be added.
This is where the keen part starts. We say how Vectors should be added by giving the class a __add__ method:
class Vector:
def __init__(self, data):
self.data = data
def __repr__(self):
return repr(self.data)
def __add__(self, other):
#note that there are much better ways to write this
#code, here we're trying to write self-explanatory code
#instead of "good" code
data = [] #start with an empty list
for j in range(len(self.data)):
#that's Python for "for j = 0 to len(self.data) - 1 do:
data.append(self.data[j] + other.data[j])
#so far data is the list the the resulting Vector
#should contain - now we wrap it into a Vector:
return Vector(data)
x = Vector([1, 2, 3])
print x + x
Now the "print x+x" prints [2, 4, 6]. Hooray, we can add Vectors.
Hmm, all those comments make the code look a lot more complicated than it really is - of course comments are a Good Thing, but these comments are just intended for readers who have no idea at all how the language works, not the sort of comment you'd include in actual code. If you leave out the comments it's just
class Vector:
def __init__(self, data):
self.data = data
def __repr__(self):
return repr(self.data)
def __add__(self, other):
data = []
for j in range(len(self.data)):
data.append(self.data[j] + other.data[j])
return Vector(data)
x = Vector([1, 2, 3])
print x + x
That's all there is to making Vectors that know how to
add themselves.
Of course there's a lot more you'd want vectors to be able to do, like you should be able to subtract them, multiply them by scalars, etc. You can do those things by adding various methods to the Vector class; here we're just trying to illustrate a few things, not develop actual working code, so let's leave Vector for now and go on to Matrix, where we illustrate another extremely keen and powerful aspect of Python programming.
So far the entries in our vectors have been numbers. But they don't have to be! Look at the __add__ method of the Vector class again:
def __add__(self, other):
data = []
for j in range(len(self.data)):
data.append(self.data[j] + other.data[j])
return Vector(data)
The part that actually does the addition is the "self.data[j] + other.data[j]",
which adds one entry of one Vector (self) to an entry of another Vector
(other). You might think that self.data[j] has to be a number, but it
doesn't have to be, this is going to work as long as self.data[j] and
other.data[j] are any sort of value, as long as Python knows what
their sum should be.
First a silly but useless example for illustration, then a useful example. Python adds strings by concatenating them: 'Hello ' + 'World' evaluates to 'Hello World'. In particular, Python does have a notion of the sum of two strings, and that means we should be able to add Vectors whose components are strings. And it turns out we can: if we say
x = Vector(['Hello ', 'silly ']) y = Vector(['World', 'example']) print x + ythen the elements of x and y get added componentwise, and x + y gets printed as ['Hello World', 'silly example']. (The components of a Vector don't have to be the same type, for example
x = Vector(['Hello ', 1]) y = Vector(['World', 2]) print x + yprints ['Hello World', 3].)
I can't really imagine why anyone would want to do that, add Vectors whose components were strings. But hmm, the components of a Vector can be anything that Python knows how to add, and Python knows how to add Vectors. So the components of a Vector can themselves be Vectors, which gives us a way to represent matrices! If you add two Vectors that have Vectors for components the components should get added correctly, giving us the sum of two matrices (regarding a matrix, or a 2x2 array, as a sequence of sequences of numbers.) Let's see if this works:
x = Vector([Vector([1, 2]), Vector([3, 4])]) print x print x + xYup. The "print x" prints [[1, 2], [3, 4]], and then the "print x + x" prints [[2, 4], [6, 8]], just as we'd want.
This is "polymorphism": Python code doesn't care what things actually are, as long as they're things that can perform the operations that it wants to perform on them.
It's also where things get so keen I just can't stand it: we wrote a loop to add Vectors, now you'd think we'd need to write a double loop to add matrices, but no, Vector addition automatically works for adding matrices, as long as we represent the matrices as Vectors whose components are also Vectors.
Of course having to write "Vector([Vector([1, 2]), Vector([3, 4])])" to define a matrix is a pain. We could try just "Vector([[1, 2], [3, 4]])" instead, but that doesn't work because then we get a Vector whose components are bare Python list objects instead of Vectors. (Quiz: If we say
x = Vector([[1, 2], [3, 4]]) print x + xwhat will the result look like? There's earlier in this document...)
Fixing the "Vector([Vector([1, 2]), Vector([3, 4])])" problem leads to our final topic: inheritance (also known as "subclassing".) We're going to say
class Matrix(Vector)That means Matrix is a "subclass" of Vector: a Matrix object will act exactly like a Vector object except for any behavior we explicitly change by writing a new method. Here Vectors act the way we want Matrices to act, except for the initialization - we want to be able to say Matrix([[1, 2], [3, 4]]) to construct the matrix in the previous example, so we override (rewrite) the __init__ method.
The new __init__ method assumes that data is a list of lists, and it uses calls to Vector() to convert it to a list of Vectors (As with the __add__ method above, note that there are much "better" ways the new __init__ could be written:)
class Matrix(Vector):
def __init__(self, data):
newdata = []
for v in data:
newdata.append(Vector(v))
self.data = newdata
x = Matrix([[1, 2], [3, 4]])
print x + x
Sure enough that works, prints [[1, 2], [3, 4]].
If you're not impressed I suspect that you've never tried to write code that does the same thing in languages like C, Pascal or Fortran (if you have done that and you're still not impressed let me know.)
For example, here's the "better" way to write Matrix.__init__() mentioned above:
class Matrix(Vector):
def __init__(self, data):
self.data = map(Vector, data)
x = Matrix([[1, 2], [3, 4]])
print x + x
Again, for those "advanced" readers: map() takes two
(or more) arguments, a function and a list to apply
the function to. Vector is not a function, but
Vector (I mean the class Vector itself, not instances of the
class) is a "callable object", which means you can use
it anywhere a function is expected. Another illustration
of one of the things that makes Python so powerful:
It doesn't care what things are, as long as those things
know how to do what they're asked to do! (Ie here map()
doesn't care whether the function is really a function
or not, as long as it's callable. Yes, you can make your
own objects callable, by giving them a __call__ method.)
Hmm, does this mean that the second argument to map() could be a Vector() instead of a list? Not with the present Vector class, but yes if we make instances of Vector into "list-like" objects by adding a __getitem__ method and a __len__ method:
class Vector:
def __init__(self, data):
self.data = data
def __repr__(self):
return repr(self.data)
def __add__(self, other):
return Vector(map(lambda x, y: x+y, self, other))
def __getitem__(self, index):
return self.data[index]
def __len__(self):
return len(self.data)
x = Vector([1,2,3])
y = map(lambda x: x**2, x)
print y
That prints [1, 4, 9]:
Having a __getitem__ means that a Vector can be indexed;
the way we defined it implies that if x is a Vector then
x[0] is the same as x.data[0]. The __len__ method specifies
what the "len" of a Vector should be, and the class having
both methods makes it sufficiently list-like that we
can pass it to map. (Note that the fact that Vector
has become "list-like" is why I could say
map(lambda x, y: x+y, self, other) in the revised
__add__ method, instead of map(lambda x, y: x+y, self.data, other.data))
Amazing.