1. Branko Vukelic
  2. PyProto

Overview

PyProto

PyProto introduces JavaScript-like prototypal object model to Python. While there are still some limitations compared to JavaScript syntax (e.g., not possible to directly assign function definitions to attributes, or not possible to use dictionary-like syntax for defining new instances), it does reproduce the core of the prototypal style.

Creating objects

To create a new object, simply instantiate an Object instance:

>>> my_object = Object()

The newly creted object has a __proto__ attribute, which points to the Object class. Object class itself has no __proto__ attribute:

>>> my_object = Object()
>>> my_object.__proto__
<class 'pyproto.Object'>

The object pointed to by __proto__ is called the prototype. Object class itself is called the root prototype.

Object attributes

Each object has attributes. These attributes can be written and read using either dot notation or subscript notation:

>>> my_object = Object()
>>> my_object.foo = 1
>>> my_object['foo']
1
>>> my_object['bar'] = 2
>>> my_object.bar
2

Prototype chain

Attributes are resolved first by looking at any attributes defined for the instance, and are then looked up in the instance's prototype (__proto__). If the object's prototype is also a prototype, a chain of lookups is started which ends with the root prototype (Object). If an attribute is not defined on any of the prototypes or the root prototype, None is returned:

>>> my_object.foo == 1
True
>>> my_object.bogus == None
True

See more details in the Simple inheritance and Multiple inheritance sections.

Overloading root prototype attributes

Having Object as root prototype actually has one very important side effect. In essence, we are able to Patch the base object by accessing the instance's prototype (provided the instance is directly linked to Object class). Consider this code:

>>> my_object = Object()
>>> my_other_object = Object()
>>> my_object.__proto__.foo = 'bar'
>>> my_other_object.foo
'bar'
>>> my_object.foo
'bar'

As you can see, we have added the foo attribute directly to root prototype.

Because of the way Python resolves attributes, any attribute defined on the root prototype takes precedence over any inhertied attributes. (See notes in the Simple inheritance section for more information.)

Methods

To add methods to an object, you have to first define a function, and then assign it to the appropriate attribute:

>>> def my_method(self):
...     return 'foo'
>>> my_object = Object()
>>> my_object.my_method = my_method
>>> my_object.my_method()
'foo'

Iterating attribute names

Objects can be iterated just like normal Python dictionaries:

>>> my_object = Object()
>>> my_object.foo = 1
>>> my_object.bar = 2
>>> atrrs = []
>>> for key in my_object:
...    print('%s: %s' % (key, my_object[key]))
foo: 1
bar: 2

Deleting attributes

Attributes can be deleted using either dot notation or subscript notation, and the del statement:

>> del my_object.foo
>> del my_object['bar']
>> my_object.attrs()
[]

Utility methods

Objects also have a few utility methods that makes working with its attibutes easier:

>>> my_object = Object()
>>> my_object.foo = 1
>>> my_object.bar = 2

hasattr

Returns True if object has its own attribute:

>>> my_object.hasattr('foo')
True
>>> my_object.hasattr('bogus')
False

attrs

Returns a list of attributes defined on the object (attributes of the prototypes are not included):

>>> my_object.attrs()
['foo', 'bar']

items

Returns a list of two-tuples containing attribute name and value pairs:

>>> my_object.items()
[('foo', 1), ('bar', 2)]
>>> for attr, val in my_object.items():
...     print('%s: %s' % (attr, val))
foo: 1
bar: 2

dict

Return object attributes as a dict (does not include the prototype attributes):

>>> my_object.dict()
{'foo': 1, 'bar': 2}

Simple inheritance

Inheritance is simple. Instantiate an Object instance with an existing object as it's first argument:

>>> my_object = Object()
>>> my_other_object = Object(my_object)
>>> my_object.bar = 2
>>> my_other_object.bar
2
>>> my_other_object.__proto__ == my_object
True

We should keep in mind that inheritance is not the same as cloning. While the child object has access to all the parent's attributes, it can also have its own. Conversely, none of the parent's attributes are child's own, so they cannot be iterated over. Here's an example:

>>> my_object = Object()
>>> my_object.foo = 'bar'
>>> my_other_object = Object()
>>> my_other_object.foo
'bar'
>>> my_other_object.attrs()
[]
>>> my_object.attrs()
['foo']

Notes about __proto__ and __prototypes__

Please do not directly manipulate these two properties. They represent internal implementation details, and aren't meant to be used directly.

Notes about root prototype attributes

Because of the way attribute resolution works in Python classes, any attribute defined on the root prototype (Object class) takes precedence over any inhertied attributes. Here is an illustration of this behavior:

>>> my_object = Object()
>>> my_object.foo = 'bar'
>>> my_object.__proto__.foo = 'fam'
>>> my_other_object = Object(my_object)
>>> my_other_object.foo
'fam'
>>> my_other_object.foo = 1
>>> my_other_object.foo
1

Extending objects

Extending objects is done using the addition operator:

>>> my_object = Object()
>>> my_object.foo = 1
>>> my_mixin = Object()
>>> my_mixin.bar = 2
>>> my_object + my_mixin
>>> my_object.bar
2
>>> my_mixin.fam = 3
>>> my_object.attrs()
['foo', 'bar']

As we can see, the bar property has been copied to my_object. This is distinct from inheritance in that my_object has no ties to my_mixin. The fam attribute that has been added to my_mixin after extending my_object is not present in my_object.

Multiple inheritance

Unlike extending or simple inheritance, multiple inheritance is true inheritance with multiple prototypes.

To have an object inherit from multiple object, simply call the Object class with multiple objects as positional arguments:

>>> first = Object()
>>> second = Object()
>>> my_object = Object(first, second)

The __proto__ attribute on the child object points to the first object passed. The first object is called the master prototype. Let's take a look at a few examples of how the prototype chain works. First, let's set up the attributes:

>>> del Object.foo

# Defined on root, master, and secondary prototypes
>>> Object.bee = 'bee bee'
>>> first.bee = 3
>>> second.bee = 'd'

# Defined on master and secondary but not root prototype
>>> first.foo = 1
>>> second.foo = 'a'

# Defined only on secondary prototype
>>> second.bar = 'b'

# Defined on secondary prototype and object itself
>>> second.fam = 'c'
>>> my_object.fam = 'mine'

Resolving an attribute that is defined on both the master and secondary prototypes will give the master prototype's version:

>>> my_object.foo
1

Resolving an attribute that is defined on secondary prototype but not on the master prototype will give us the version on the secondary prototype:

>>> my_object.bar
'b'

If the attribute is defined on the object itself, it takes precendence:

>>> my_object.fam
'mine'

Finally, any attributes on the root prototype will take precendence over attributes on any other prototype:

>>> my_object.bee
'bee bee'

In short, the attributes are resolved in this order:

  • The attribute on the object first
  • Attribute on the root prototype next
  • Attribute on the master prototype next
  • Finally attributes on all other prototypes in the order they have been passed