Python Classes

Based partly on the Tutorial, section 9. Other references: see the list at http://www.python.org/doc/newstyle/

No Secrets?

We often hear this:

There is no real "privacy" in Python, in the sense of C++ or Java "private" fields or methods. All instance variables and methods are public.

On the other hand, starting a variable name with two underscores (__myvar) creates a sort-of private variable: the name is mangled so that it only makes sense within that class.

Virtuality

All Python methods (and Java) are what C++ calls "virtual" methods. That is, the actual type of the object whose method is called, rather than its declared type, determines which method is run.

In Python, it really couldn't be otherwise, since variables don't have type declarations.

Methods and Functions

Suppose we have x = an instance of MyClass. Then,

Although methods are usually defined using "def" inside a class definition, externally defined functions can also be assigned and become methods, and so can the value of a lambda expression:

def foo():
  ...

class Gummy:
  f = foo # without arguments
  g = lambda self, x, y: ...

→ Example: circle.py

Inheritance

A superclass is named in parentheses after the class name in its declaration:

class MyClass (HigherClass):
  ...

If no superlcass is named, then in Python 2 there is no superclass, but in Python 3, the superclass defaults to object.

class Tops:
  ...

Exception is a class, and programmers can define subclasses of Exception.

→ Example: old_style_accounts.py; session I/O

Old-Style (Python 2) Classes

  1. Calling superclass methods in a method that overrides them uses the superclass name followed by the method name. The "self" object is given as the first argument:
    HigherClass.foo(self, x, y)
    instead of
    self.foo(x, y)
    This is ugly. What if the superclass is changed, or changes its name?
  2. Searching for methods: the "method resolution order" (class precedence list) is the order in which classes will be searched to find a method that is called. In old-style classes, the MRO is "left to right, depth first."

    So if we have this class hierarchy:

        A
       / \
      /   \
     B     C
      \   /
       \ /
        D
    The MRO of D is D, B, A, C.

    The problem here is that if A defines a method and C overrides it, shouldn't we use C's method instead of A's, since C is a more specific class?

New-Style (Python 3) Classes

These two "warts" — the method resolution order and the syntax for calling a method of the superclass — are now fixed in what are called "new-style classes." Old-style classes only exist in Python 2; Python 3 uses new-style classes.

A new-style class is distinguished by the fact that it is a subclass of a type. For example, dict, list, and float are types. An old-style class cannot be derived from a type. The highest (most general) type is named object. Consequently, we can define a new-style class by making it a subclass of object or some other type:

class X (object):
  ...

class Y (dict):
  ...
  1. Calling superclass methods: new-style classes support the "cooperative super call". Here's an example:
    class Z (X, Y):
    
      def foo (self, a, b):
        r = super(Z, self).foo(a, b)
        ...
    The meaning of "super(Z, self)" is "call the next method in the method resolution order after the method for the class Z" (which is the method we are currently in). Consequently, there's no problem if the names of the superclasses change.
  2. Method resolution order: The MRO is also different in new-style classes.

    Suppose we have

      object
        |
        |
        A
       / \
      /   \
     B     C
      \   /
       \ /
        D
    The MRO of class D is D, B, C, A, object. Consequently, if C defines a more specific method than A, the method for C will be found first.

    The exact algorithm for the new-style MRO seems to be complex, so perhaps it's best to call the help function to be sure, or to look at the attribute __mro__ which is defined for new-style classes:

    help(D)
    D.__mro__

→ Example: accounts.py

→ Example: old_new.py

There is much more in new-style classes that I do not yet understand:

One feature seems to be lacking: the "generic function, multi-method" approach of Common Lisp. Multi-method calls are written foo(a, b, c) instead of a.foo(b, c), and the method dispatch can depend not only on the class of a, but also the classes of b and c. (Actually, in Lisp, the multi-method call would be written (foo a b c), like any other Lisp function call.)

9.9 Iterators

If mylist = [1, 2, 3], then the iteration

for x in mylist: ...

is supported, behind the scenes, by objects known as iterators.

What actually happens in the for statement is that it obtains an iterator for mylist by calling mylist.__iter__().

The iterator has a next() method which returns the next method in the sequence if there is one; otherwise, it raises a StopIterator exception. The for statement catches this exception and terminates itself.

You can define an iterator like this:

class X:
  def __iter__ (self):
    return ...

The value returned must be an object — perhaps of the same class X, perhaps a different, related class — which has a next() method which behaves as described above.

→ Example: iter_generator.py

9.10 Generators

Generators are functions which use the yield statement to return values. When called again, they resume at the same point and deliver the next value. They can serve as iterators. Their __iter__ and next methods are created automatically.

→ Example: iter_generator.py

→ Example: generators.py

9.11 Generator Expressions

Generator expressions are written like list comprehensions, but using ()'s instead of []'s. They generate a sequence one element at a time, as needed, without storing all of them in memory. They are less flexible than generator definitions, but more convenient to write when they can do the job.

→ Example: iter_generator.py

Suggested Exercises

Use new-style (Python 3) classes for all of these. But note that in Python 3, you don't have to declare the superclasses as `(object)`, because that is the default.

  1. Define a Chunk class, representing a chunk of cheese. Each cheese chunk has a cheese type (e.g., Swiss), a weight (in ounces), and a price. Also give the class a method that calculates its price per ounce. Create a list of three Chunks, and use a loop to print the type and price per ounce of each chunk in the list.
  2. Define a Student class, with methods to add a course, drop a course, and list the courses the student is enrolled in (initially none).
  3. Define GraduateStudent, a subclass of Student, with additional methods to set and retrieve a thesis topic. Convince yourself that GraduateStudent inherits the methods and data of the Student class.

→ Solutions