Object-Oriented Programming with Python 3

David MacQuigg

See http://ece.arizona.edu/~edatools/Python/Prototypes.doc for figures and change bars.

 

This is a re-write of pp. 295-390 in Learning Python, 2nd ed., by Mark Lutz and David Ascher.  I expect it grow to about 30 pages with the inclusion of examples and exercises.

The language assumed in this chapter is a hypothetical future version of Python.  My intent is to put the proposed language into a teaching context, so we can get a feel for how it "plays" to this audience.  Many of the changes will seem inconsequential to experts.  By looking in detail at how the language might be taught, we can gain some insight into how these changes might have a big impact on the usability of the language for new and part-time users.

One restraint that we must accept in dreaming up new features is that we must keep the language consistent with Python 2 to the extent that existing programs may be automatically translated to Python 3.  The value of ten years of development in Python cannot be abandoned for any imaginable improvement in current syntax.  See Appendix 1 for more discussion of this vital requirement.  See also Planned Revisions for changes not yet done.

These pages are written with a particular audience in mind.  I am assuming that readers are familiar with Python data structures, functions, and modules, as described in Learning Python up to page 295, but are new to object-oriented programming.  They have used objects, so they know how to get and set attributes ( sys.path.append(pathX) ), but they know nothing of how classes are defined.  Almost all of the chapters before page 295 is directly applicable.  The major change is in the simplification of built-in objects figure ( see figure 7-3 below.)

Please send comments or suggestions to dmq@gain.com   There are also related discussions on comp.lang.python and the Prothon-user mailing list, and a wiki page at PrototypesForPython

 

Fig 7-3  Built-in type hierarchies

( part of figure on p.124  relating to logic packages )

Eliminate Methods

Change Class to Prototype

 

 

 


Chapter 19

Object Oriented Programming

We have seen how to "modularize" a large program by packaging its functions and data in modules – each having its own namespace, and each having its own file on disk.  Now we are going to look at a new way to package data and functions – a "prototype".

Prototypes are more versatile than modules, mainly because they can "inherit" data and functions from other prototypes, and they can serve as a template or "prototype" for multiple "instances", each with small variations in the data and functions provided by the prototype, and each having its own namespace.  This allows us to build large hierarchies of objects with complex behavior.  Each object acts as a "component" in the larger system, with all the internal details "encapsulated" so the user of the component has to learn only a simple interface.

Here are some examples of prototype objects and instances:

# Animals_1.py

proto Animal(Proto):  # Inherit from the primitive Proto.

    home = "Earth"

    numAnimals = 0

 

proto Cat(Animal):     # Inherit from Animal.

    numCats = 0

    genus = "feline"

    set_vars( n, s ):

        .name  = n     # Set instance variables.

        .sound = s

    talk():

        print "My name is ...", .name

        print "I say ...", .sound

        print "I am a %s from %s" % ( .genus, .home )

       

cat1 = Cat()   # Create an instance of Cat.

cat2 = Cat():                                   ( see Planned Revisions )

    home = "Tucson"

As you can see, a prototype is a collection of data and functions, very similar to a module.  Like a module, each prototype has its own namespace, so you don't have to worry about conflicts with names in other prototypes.  By convention, proto names are capitalized, and instance names are lower case.

The first line of the Cat prototype says -- make me a new prototype called "Cat".  Start with the prototype named "Animal", and inherit all of its data and functions.  Then add or replace the items that follow.   After defining our prototypes, we create two cat instances by "calling" the prototype Cat.  ( No, prototypes are not functions, but the "calling" syntax is the same.)  That's the essence of prototyping in Python.

Now for some details.  Animal inherits from object, which is the primitive ancestor of all prototypes.  It provides default behavior for all prototypes, and must be at the top of all hierarchies.  When we use Animal as a prototype for Cat, we are providing Cat with the two variables defined in Animal.  At each level, a prototype inherits all the variables provided by its ancestors, so Dogs, Cats and any other creatures descended from Animal will all have a default home = "Earth", and access to the variable numAnimals, which we can guess keeps the total of all animals on the planet.

Inheritance is not the same as copying.  An inherited variable remains attached to its original prototype.  The examples below will show the difference in behavior.

There is one other difference ( besides the first line ) between a prototype and the code you might find in a simple module.  Some of the variables inside the functions in a prototype have a leading dot.  This is to distinguish local variables in the function from "instance variables".  When a function is called from an instance ( cat1.talk() ) a special global variable __self__ is automatically assigned to that instance ( __self__ = cat1 )  Then when the function needs an instance variable ( .sound ) it uses __self__ just as if you had typed it in front of the dot ( __self__.sound )  The leading dot is just an abbreviation to avoid typing __self__ everywhere.

Local variables are lost when a function returns.  Instance variables survive as long as the instance to which they are attached has some reference in your program.  Attachment of instance variables occurs via assignment statements, like the ones in Cat.set_vars above, or immediately after an instantiation statement ending with the optional colon ( cat2 = Cat(): ), or from someplace else with a fully-qualified name, like cat1.name = "Garfield"

Let's play with our cats:

1>> Cat.numCats = 2

>>> cat1.name, cat1.sound = ("Garfield", "Meow")

3>> cat2.set_vars("Fluffy", "Purr")

>>> cat1.home, cat1.genus, cat1.name, cat1.sound

('Earth', 'feline', 'Garfield', 'Meow')

5>> cat2.talk()

My name is ... Fluffy

I say ... Purr

I am a feline from Tucson

>>> Cat.numCats

2

We had to set the number of cats manually, because we don't yet have "initiators" to do it automatically.  We'll show that in the next example.  Instance variables can be set directly, as in line 2 above, or via a function call, as in line 3.

There are two pieces of "magic" that make line 3 work.  First, the interpreter has to find the function set_vars.  It's not in cat2.  Then, the __self__ variable has to be set to cat2 for proper resolution of the instance variables.  The function is found by a search starting at cat2 ( the instance on which the function is called ).  The search proceeds to the parent prototype, then to the grandparent, and so on up to object, the ancestor of all prototypes.  In this case, we find set_vars in Cat.

The __self__ object was set to cat2 immediately on execution of line 3.  This happens whenever you call a function as an attribute of an instance.  It does not happen if we call the same function some other way.  Had we said Cat.set_vars("Fluffy", "Purr") we would have left __self__ set to None, and the call would have failed.  The interpreter knows the difference between an instance cat2 and a prototype Cat even if we fail to follow the convention of capitalizing prototypes.

Line 4 is another example of searching a hierarchy for needed attributes.  Only name and sound are attached to cat1, but they all appear to be attributes of cat1 because of inheritance.

Line 5 shows how having a custom function to display parameters of an object can be a lot more convenient and produce a better display.  Let's follow this call step-by-step, just to make sure we understand inheritance and instance variables.  The initial call cat2.talk() sets __self__ to cat2 and resolves to Cat.talk().  Cat.talk() finds .name, .sound, and .home attached to cat2, and .genus attached to Cat. 

There is one more bit of magic with __self__.home  We now have two places where home is defined.  cat2.home = "Tucson" and Animal.home = "Earth"   The interpreter follows the same search procedure as used in searching for a function.  It uses the variable lowest in the hierarchy. cat2.home "over-rides" Animal.home  Fluffy knows she is from Tucson, not just Earth.

So what's the difference between making a new prototype and making an instance?  Both inherit all the data and functions of their "ancestors".  Both can be modified by adding extra statements in an indented block following an end-of-line colon.  There are two differences.  Prototypes can inherit from multiple parents.  ( We'll say more about multiple inheritance later.)  Instances can be automatically "initialized" when they are created.

Say you need to create a bunch of cats and dogs, and you don't want to laboriously modify the data and functions like we did above.  You can't do this with inheritance, because that will give only identical data and functions to each instance.  But you can set up an "initiator" function that will customize each instance.  Initiator functions are just normal functions with a special name.  They can do anything you want, including customize each new instance, keep a total of all instances, modify variables in ancestor prototypes, whatever.

The automatic initializing works by looking for a function named __init__ in the newly created instance, and running that function in the namespace of the new instance.  Let's modify our prototype Cat             ( see Planned Revisions )

>>> with Cat:        

        __init__( n, s ):

            .name  = n  # Set instance variables.

            .sound = s

            Cat.numCats += 1

        talk():

            print "My name is %s ... %s" % ( .name, .sound )

            print "I am one of %s cats." % Cat.numCats

The with statement "re-opens" the prototype Cat, and allows us to add or replace data and functions.  Here we add the function __init__  and replace the function talk.

Creating a Cat instance will now automatically run  __init__ creating new instance variables name and sound, adding them to the dictionary of the new instance, and assigning the values we provide in arguments to the instantiation call.  The function __init__ will also increment the variable Cat.numCats, so we don't have to remember this each time we create a new cat.

>>> cat3 = Cat("Tubby", "Burp")

>>> cat3.talk()

My name is Tubby ... Burp

I am one of 3 cats.

>>> Cat.numCats

3

Notice that the original numCats variable kept its value even while we did surgery on the Cat prototype.  The original talk function ( the one that said "I am a feline from Tucson" ) was over-ridden ( replaced ) by the definition in the with statement above, and is on its way to the landfill :>)  If you want to save the old function from the garbage collector, you must create an independent reference to it.  ( See the section on Bound Functions below.)

Variables in a prototype definition outside of a function are shared by all instances of that prototype.  If you re-assign one of those instance variables, however, that variable will point to its own unique value in memory.  The reference to the inherited value is over-ridden for that one instance.  This is no different than variables in a module.

>>> cat1.numCats, cat2.numCats, cat3.numCats

(3, 3, 3)

>>> Cat.numCats = 2

>>> cat1.numCats, cat2.numCats, cat3.numCats

(2, 2, 2)

>>> cat1.numCats = 0

>>> cat1.numCats, cat2.numCats, cat3.numCats

(0, 2, 2)

>>> Cat.numCats = 1

>>> cat1.numCats, cat2.numCats, cat3.numCats

(0, 1, 1)

Attachment of Variables to Prototypes and Instances

The example above may be a little confusing if you don't have a clear picture of what variables are attached to each object.  The dir(cat1) function shows all variables that are available to the cat1 instance, including inherited variables and system variables.

>>> dir(cat1)

['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'genus', 'home', 'name', 'numAnimals', 'numCats', 'set_vars', 'sound', 'talk']

You can see which of these variables is attached to each prototype or instance by looking at their namespace dictionaries:

>>> cat1.__dict__.keys()

['sound', 'numCats', 'name']

>>> cat2.__dict__.keys()

['sound', 'name']

>>> Cat.__dict__.keys()

['numCats', '__module__', 'talk', 'genus', 'set_vars', '__init__', '__doc__']

>>> Animal.__dict__.keys()

['__module__', 'numAnimals', '__dict__', 'home', '__weakref__', '__doc__']

Bound and Unbound Functions

As we saw in Chapter 14, functions are objects which can be assigned to a variable, saved in a list item, passed via a function argument, or anything else you can do with a variable.  Prototypes add one little twist.  Functions can be bound or unbound, meaning they can have a binding to a particular instance, or not.  Normally, binding happens automatically when you call a function from an instance, and you don't have to worry about the details.  There are times, however, when it is handy to bind a function with an instance and save the bundle for later use.  Had we saved cat1.talk this way, before replacing Cat.talk, we could now call that old function and it would run as it did in our first example, preserving both the function and the state of the cat1 instance as it was when we saved cat1.talk.

Let's assume we had done this sometime before the with surgery.  You can add these lines and re-run the example to check it out.

bf = cat1.talk

uf = Cat.talk

bf is now a bound function, and uf is the equivalent unbound function.  Remember, if you call a function from an instance, you get __self__ automatically set to that instance.  In this case, we aren't calling the function ( yet ), but it works the same in "binding" the instance to the function.

Let's make a new cat1 and see how this works.

>>> cat1 = Cat("Fritz", "Hisss")

>>> Animal.home = "Mars"

>>> cat1.talk()

My name is Fritz ... Hisss

I am one of 4 cats.

>>> bf()

My name is ... Garfield

I say ... Meow

I am a feline from Mars

Not only is the old talk function preserved, but cat1's local instance variables ( .name and .sound ) are captured in the bound function as well.  Only the planet has changed.  !!!

This is just like reloading a module.  Remember the example in Chapter 16.  If we change a variable home in a module Animal, any direct references to items in the old module ( x = Animal.home ) will *not* be changed when we reload the module. ( x will remain the same.)  However, fully-qualified references to Animal.home *do* get changed when Animal is reloaded. 

The same thing happens with bound functions.  When the Cat.talk function bound in bf tries to find __self__.sound or __self__.name, it finds these variables already attached to cat1.  __self__.noise resolves to cat1.noise, and that is the old value "Meow", which was saved in bf.

When the Cat.talk function tries to find __self__.home or __self__.genus it has to do the usual search of the inheritance tree.  __self__.home resolves to Animal.home, and that is the new value "Mars", which we just now set.  Bound functions preserve the value of instance variables, only if those variables are in the namespace dictionary of the bound instance.  Inherited values can change.

Bound functions are immutable.  They don't get re-bound to another instance, even if you pass them to another function, or change the __self__ object.  If you need a different binding, make a fresh one from the new object.

>>> bf = cat2.talk

The __self__ object is set when you first call a function from an instance.  It remains set until that call returns, unless you make another call from a different instance within the function you just called ( not recommended ), or you explicitly re-assign the __self__ object to something else ( a rare situation ).  Normally, the __self__ is set once and returned to None when the call is complete.

Why Use Prototypes

A prototype is much "lighter" than a module.  You can put as many prototypes as you want into a single module.  This makes it easy to define large collections of prototypes.

Making new instances from a prototype is easier than copying a module and changing its contents.  A prototype can be used as a "factory", stamping out many instances, each with a different initialization, and each with its own namespace for variables unique to that instance.

Prototypes "inherit" variables from all their ancestors.  Changing an inherited variable changes all prototypes and instances descended from the prototype where the change is made.  If all variables were copied, rather than inherited, changing a large hierarchy would be difficult and error prone.

A Larger Prototype Hierarchy

This section shows a larger example of using prototypes to define a hierarchy of objects.  With a full hierarchy, we can put each data item or function at exactly the level it belongs.  In the example above genus is not a good item to associate with Cat.  It really ought to be one level up.

It is helpful in building large hierarchies to make a sketch showing the inheritance relationship of the prototypes and the variables defined or set in each prototype.

 

Animal     -->    Mammal     -->    Feline     -->    Cat

-------           -------           -------           -------

_numAnimals       _numMammals       _numFelines       _numCats

home                                genus

__init__()        __init__()        __init__()        __init__()

                      .sound                              .sound

                                                          .name

show()            show()            show()            show()

                  talk()                              talk()

 

 

Line Callout 2: Variables with a leading underscore are _private [1]## Animals_2.py -- a complete OO zoo !!

proto Animal(object):

    _numAnimals = 0

    home = "Earth"

    __init__():

        Animal._numAnimals += 1

    show():

        print "Inventory:"

        print " Animals:", Animal._numAnimals

 

proto Reptile(Animal)

proto Mammal(Animal):

    _numMammals = 0

    basal_metabolic_rate = 7.2

Line Callout 2: Calling an ancestor's initiator is a common pattern.  Use this when you want to add additional steps to a default initialization. [2]    __init__(s = "Maa... Maa..."):

        Animal.__init__()

        Mammal._numMammals += 1

        .sound = s

    show():

        Animal.show()

        print " Mammals:", Mammal._numMammals,

        print "        BMR =", Mammal.basal_metabolic_rate

    talk():

        print "Mammal sound: ", .sound

 

proto Bovine(Mammal)

proto Canine(Mammal)

Line Callout 2: Use a fully-qualified name to avoid creating a local _numFelines variable.proto Feline(Mammal):

    _numFelines = 0

    genus = "feline"

    __init__():

        Mammal.__init__()

        Feline._numFelines += 1

    show():

        Mammal.show()

        print " Felines:", Feline._numFelines

 

proto Cat(Feline):

    _numCats = 0

    __init__( n = "unknown", s = "Meow" ):

        Feline.__init__()

        Cat._numCats += 1

        .name = n

        .sound = s

    show():

        Feline.show()

        print "    Cats:", Cat._numCats

    talk():

        print "My name is ...", .name

        print "I am a %s from %s" % (.genus, .home)

        Mammal.talk()

Line Callout 2: Calling an unbound function does *not* change the current __self__   [3] 


a = Animal()

m = Mammal(); print "m:",; m.talk()

f = Feline(); print "f:",; f.talk()

c = Cat();    print "c:",; c.talk()

c.show()

 

cat1 = Cat("Garfield", "Purr")

cat1.talk()

 

>>> import Animals

m: Mammal sound:  Maa... Maa...

f: Mammal sound:  Maa... Maa...

c: My name is ... unknown

I am a feline from Earth

Mammal sound:  Meow

Inventory:

 Animals: 4

 Mammals: 3         BMR = 7.2

 Felines: 2

    Cats: 1

My name is ... Garfield

I am a feline from Earth

Mammal sound:  Purr

>>>

[Note 1]  The leading underscore to mark the privacy of a variable is just a convention, not something enforced by the language.  It means "Don't change this variable, other than by using the functions provided."  In this case, the counts in the _num... variables should be changed only by the __init__ functions, never some direct manipulation by the user.

[Note 2]  Instead of calling a function from a specific prototype, you could generalize this to call the function from whatever prototype is the parent of Mammal.

        parent = Mammal.__bases__[0]

        parent.__init__()

Like other "robust programming" techniques described in the next section, "super calls" gain some maintainability, for some increase in the initial complexity of the program.  In this case, if someone were to add a prototype between Animal and Mammal, you could avoid a possible error if they forget to change the Animal.__init__() call in Mammal.

 [Note 3]  In the Cat.talk function definition above, we call a function Mammal.talk, which then prints .sound  How does Mammal.talk know which sound to use ( the one from Cat or the one from Mammal )?   It depends on the __self__ instance at the time the value of .sound is needed.  In the example above, that instance is 'c', an instance of Cat.  So .sound is equivalent to c.sound, and this has been set to "Meow".  See the Gotchas section for further discussion.

Additional Topics

Robust Programming

The Animals_2 example in the previous section has some features which may be a problem in a large program that needs to be maintained by programmers who may not be expert in every facet of the program.  There is no absolute rule that all programs should be maximally robust.  You've got to anticipate how your program will grow and be modified in the future, then decide how much effort to make up-front in "bullet-proofing" your program.  You may want to be safe and make the additional effort early, even though you don't anticipate the need.  Many programs which were never intended to become widespread, have become enormous maintenance headaches, because the original programmers didn't have even a few hours to make the initial design more robust.

The examples in this section illustrate some techniques to make the Animals_2 example more robust.  We have already seen how to use "super calls" to avoid direct references to specific prototypes outside the current prototype.  This is an example of "encapsulation", making each prototype self-contained, so that it can survive changes in external code.

Another example of a lack of encapsulation in Animals_2 is the "non-locality" of the _num variables.  Keeping a total in each prototype which is dependent on totals in other prototypes, can lead to data which is "out-of-sync" from one prototype to another.  If someone adds a prototype in the hierarchy somewhere below Mammal, and forgets to call the __init__ function from its parent, then all _num variables in prototypes above the new one will fail to update when instances of the new prototype ( or any prototype below the new one ) are created. 

You can imagine problems like this might occur also, if updating data in the hierarchy were to depend on the continuity of an online session, or the absence of program crashes.  These kinds of interruptions are inevitable in a large system, and can cause very subtle errors which are difficult to track down.  Data in prototype Animal is wrong.  It happens about once a day, usually when Karen in accounting updates the Cat inventory.  But there have been no changes in either Animal or Cat since the problem started.  Oops, the problem is in Feline, which was "fixed" three weeks ago.

The general solution to "out-of-sync" problems is to avoid redundancy in the data, and each time an item is needed which is dependent on other data items, re-calculate it from the original independent data.  So instead of _numAnimals holding a potentially corrupt total of all instances in the hierarchy, we should probably use a numAnimals() function, which re-calculates a fresh total each time it is called.  _numAnimals and other _num... variables can then hold just the count of their respective instances.

-- example here --    Animals_2c.py at http://ece.arizona.edu/~edatools/Python/Exercises

The techniques in Animals_2c will go a long way to making the program more robust, but it is still not "bullet-proof".  At this point, we need to think of all possible threats, and how much effort we want to make to avoid those threats.  If this is a program to track incoming missiles, we might want absolute assurance that no program crash or interruption will corrupt the count of missiles.  To avoid this we could implement a "transaction" system in which each of several steps in a sequence has to be completed successfully before the transaction can be "committed".  If there is any failure, the sequence is "rolled back", and we start over.

If this is a program in which we expect a large number of prototypes to be added at various places in the hierarchy, we may want to automate the generation of prototypes so that the programmer can specify a few variables, and the location in the hierarchy where the new prototype is to be inserted, then have the generator produce a prototype which is automatically correct and properly connected in the hierarchy.

-- example here – Animals_JM.py at http://ece.arizona.edu/~edatools/Python/Exercises

This example does make insertion of new prototypes foolproof, but it is also rather complex.  It has the disadvantage that it is more difficult to make unique changes in each prototype.  The generator depends on regularity in all the prototypes.

To summarize, robust prototypes should use _private variables to encapsulate data that is not needed outside the prototype, and should strive to minimize any dependence on functions or data outside the prototype.  "Super-calls" can avoid most direct references to external prototypes.  If the cost of re-calculation is not too high, you should store only independent data and avoid redundant data which can get out-of-sync.  Ultimately, there is always a tradeoff of robustness vs simplicity, efficiency, initial programming cost, and generality of the final program.

Multiple Inheritance

In most programs, prototype hierarchies are arranged so that each level inherits from only one prototype above it.  Multiple inheritance is an advanced feature that allows you to mix data and functions from multiple objects into one prototype.  Say you want a mutt that is basically a dog, but has characteristics of both a spaniel and a labrador.

proto Mutt( Spaniel, Labrador )

Larger example and discussion. ...

Method resolution order ...

Shortcuts for simple programs  *** syntax needs revision ***

These shortcuts seemed like a key feature when I first looked at Prothon.  Now, I am not convinced that there is an advantage in using instances as prototypes for other instances.  Creating an instance from a class is only one line.  In the kind of programming I do, keeping classes distinct from instances is an advantage in organizing the program. I have invited the "pure classless" advocates to show us a use case.  I will wait for that before spending any more time on this section.  See 1)  Elimination of "prototype" behavior.

In most programs, it is convenient to define a small set of prototypes, then make many instances from that small set.  Prototypes and instances are basically the same kind of object, however.  You can use prototypes as your working objects, or you can build object hierarchies using instances only.  It all depends on how you wish to organize your program.

Think carefully before using these shortcuts.  Often saving a few lines will make your program harder for someone to understand.  That someone may be you, six months later!

Shortcut 1:  If you have no need for initializing, you don't need to run the .instance() function.

>>> dog1 = Dog.proto()

>>> dog2 = Dog.proto()

>>> dog1.talk(); dog2.talk()

Bow Wow!!

Bow Wow!!

Shortcut 2:  If you have no need for multiple inheritance, you don't need the .proto() function.  You can "clone" one object from another using the .instance() function, which normally inherits from the single object on which it is run.

cat = Mammal.instance( name = "unknown" )

cat1 = cat.instance( name = "fluffy" )

cat2 = cat.instance( name = "tabby" )

In the example above, it looks like 'cat' is actually serving as a prototype, and it doesn't look like much is being done with the default name.  So we could probably have written:

cat = Mammal.proto():

    name = "unknown"

cat1 = cat.instance( name = "fluffy" )

cat2 = cat.instance( name = "tabby" )

This would preserve the distinction between prototypes and instances, which in most programs is a helpful clarification.

Operator Overloading

Looks the same as in Python. { pp. 327 – 336 in Learning Python, 2nd ed. }  ???

Odds and Ends

Changing prototypes "on the fly".  This is a questionable feature.  We need a good use case.  ???

Gotchas

Changing a prototype may not affect existing instances

It depends on whether the variables are attached to the instance or the prototype.

Instance variables must have a __self__ instance

If a function definition contains instance variables, you cannot call that function directly.  It must be called from an instance.

proto Mammal(object):

    talk():

        print "I am a mammal."

        .make_noise()

    make_noise():

        print "I make mammal noise."

 

>>> Mammal.talk()

Traceback (most recent call last):

  ...

  File ".../Animals_gotcha.py", line 6, in talk

    .make_noise()

MissingInstanceError: instance variable '.make_noise' has no __self__ object.

 

>>> mam1 = Mammal()

>>> mam1.talk()

I am a mammal.

I make mammal noise.

>>>

Calling a bound function within another function changes the __self__

This error is a little more subtle.  There is no error message, just a wrong result.  If you can follow this step-by-step, you will have a good understanding of how instance variables work with __self__.

proto Mammal(object):

    talk():

        print "I am a mammal."

        .make_noise()

    make_noise():

        print "I make mammal noise."

proto Cat(Mammal):

    talk():

        print "I am a cat."

        mam1.talk()

    make_noise():

        print "meow !!"

mam1 = Mammal(); cat1 = Cat()

>>> cat1.talk()

I am a cat.

I am a mammal.

I make mammal noise.

>>>

You might have been expecting the cat to say "meow !!" instead of just the generic mammal noise.  Here is what happened:  The first function call cat1.talk() resolves to Cat.talk(), which calls mam1.talk(), which resolves to Mammal.talk(), which calls .make_noise from the current __self__, which happens to be 'mam1'.  'mam1.make_noise()' resolves to Mammal.make_noise, which is not the function we want.

The problem arose when we called mam1.talk().  This changed the __self__ from 'cat1' to 'mam1'.  What we should have done is called Mammal.talk().  Because Mammal is a prototype, not an instance, we will get the desired function without changing the __self__ instance.

with Cat:

    talk():

        print "I am a cat."

        Mammal.talk()  <== No change in __self__

>>> cat1.talk()

I am a cat.

I am a mammal.

meow !!                    <== Now it works.

>>>

In general, you should not, within a prototype definition, be calling functions from specific instances.  That can make your definition weirdly dependent on whatever the calling program sets up as instances.  If you need a function from another prototype, call that function directly from the prototype, not from one of its instances.  If you really must change the __self__ instance in the middle of a call, do it explicitly with

__self__ = mam1

Mammal.talk()

The __self__ variable is one of those "system" variables that you are not supposed to mess with, available for the rare case where you must change default behavior.  In most programs, you will just call .talk() and let the system resolve which function you want.  In some programs, you will need to call a specific function from another module or prototype.  In that case, use a fully-qualified name, like Mammal.talk(). It is only in rare cases that you will need to over-ride the __self__ variable.

Appendix 1: Translating Python Classes to Prototypes

section moved to PrototypeSyntax.doc at http://ece.arizona.edu/~edatools/Python

Planned Revisions

These are major changes, which will be done in a future revision of this document, when we can go through it carefully, and keep the chapter self-consistent.

1)  Elimination of "prototype" behavior.

After much discussion on comp.lang.python and the Prothon mailing list, I am persuaded that the benefits of prototype behavior are not worth changing the core language of Python.  In the next revision, I will take out "cloning", the "with" statement, and the ability to change attributes in an indented block after an instantiation.  Cloning ( the ability make one prototype a copy of another ) is easily provided in Python by other means ( see "Prototypes in Python",  Michele Simionato, comp.lang.python, 4/28/04 ).   The "with" statement, like GOTO, may cause more bad programming for very little benefit. 

>>> with Cat:

        __init__( n, s ):

            .name  = n  # Set instance variables.

            .sound = s

            Cat._numCats += 1

        talk():

            print "My name is %s ... %s" % ( .name, .sound )

            print "I am one of %s cats." % Cat._numCats

This example will be replaced by:

>>> proto Cat2(Cat):

        __init__( n, s ):

            .name  = n  # Set instance variables.

            .sound = s

            Cat._numCats += 1

        talk():

            print "My name is %s ... %s" % ( .name, .sound )

            print "I am one of %s cats." % Cat._numCats

Instead of changing a class in the middle of a program, the Python way is to either go back and edit the class, or derive a new class which over-rides items in the old class.

The remaining question is:  Should we change the 'proto' keyword back to 'class'.  Do we want to signal that classes in Python 3 are different or that they are basically the same as Python 2 ?  These matters of personal preference should be left to the BDFL. :>)