Classes or Prototypes in Python
What would happen if I wanted to play only with objects on Python, using other objects as prototypes? I’m going to show how we can benefit from the meta-programming tools available on Python in order to do that.
Thinking about the objects’ creation is one of the most time-consuming tasks when modeling a problem or building software. We have two ways of doing it: instantiate them from a class, or using an existing object as a prototype.
Both techniques are different, and although perhaps because of my training and experience I am a bit more inclined to choose the class-instantiation paradigm than the prototyping-instantiation paradigm. In this article I do not intend to show which one is better, but play with one to demonstrate the other – using Python.
But, what are the universities doing?
In some universities, they’ve decided to teach Object-Oriented Programming by using prototypes first (Wittgenstein way), instead of classes (Aristotelian way), given the complex relationship between classes and objects and their differences.
"Denotative Objects" is a tool built with Smalltalk that tickled my curiosity. It works inside the Cuis-University environment, and its main goal is to allow students to take their first steps towards learning objects, focusing on the interactions between them, without the need of classes. It was created by Hernán Wilkinson and Máximo Prieto, and it continues its development with the collaboration of Nahuel Garbezza. Nowadays it’s used at three universities: Buenos Aires University (UBA), Quilmes National University (UNQ) and Catholic Argentinian University (UCA).
There are other educational tools that use a similar approach, allowing us to work and learn in a similar manner, such as Wollok, developed by Fernando Dodino, or Ozono.
All of this, combined with a really interesting article about prototypes (the way of prototypes) by Lautaro García, made me wonder what would happen if I wanted to play only with objects on Python, an create them, using the prototype way.
Next I’m going to show how we can benefit from the meta-programming tools available on Python in order to use objects as prototypes.
First steps
We’ll start with a Car class. First we are going to generate an object that we’ll later use as a prototype.
class Car(object):
def __init__(self, brand, color):
self.__brand = brand
self.__color = color
def brand(self):
return self.__brand
def color(self):
return self.__color
As always, everything starts with testing, so to avoid including all the iterative and incremental steps in this post, we are going to jump ahead a little bit further so I can show you how I wish our prototype to be:
class TestPrototyping(TestCase):
def test_when_create_a_delegate_from_prototype_then_both_should_behave_same_way(self):
my_car_prototype = Car("super_movil", "blue")
creator = DelegateCreator(my_car_prototype)
my_car_delegate = creator.get()
self.assertEquals(type(my_car_prototype), type(my_car_delegate))
self.assertTrue(not my_car_prototype == my_car_delegate)
self.assertEqual(my_car_prototype.brand(), my_car_delegate.brand())
self.assertEqual(my_car_prototype.color(), my_car_delegate.color())
def test_when_delegate_change_some_behave_then_prototype_should_not(self):
my_car_prototype = Car("super_movil", "blue")
replacer = (lambda: "red")
creator = DelegateCreator(my_car_prototype, {"color": replacer})
my_car_delegate = creator.get()
self.assertTrue(type(my_car_prototype), type(my_car_delegate))
self.assertTrue(not my_car_prototype == my_car_delegate)
self.assertEqual(my_car_prototype.color(), "blue")
self.assertEqual(my_car_delegate.color(), "red")
self.assertEqual(my_car_prototype.brand(), my_car_delegate.brand())
The first test evaluates the created prototype based on an object instantiated from the Car
class. We can see both of them share the same functionality and type.
In the second case, for the same method signature, I replaced the functionality with a lambda.
The implementation that makes the tests pass is the following:
class DelegateCreator(object):
def __init__(self, prototype, methods_replace = None):
self.__prototype = prototype
self.__methods_replace = methods_replace
def get(self):
return self.__create()
def __create(self):
result = Prototypical()
result.__class__ = self.__prototype.__class__
self.__add_attributes(result)
if self.__methods_replace:
for key, value in self.__methods_replace.items():
result.__dict__[key] = value
return result
def __add_attributes(self, result):
for value in vars(self.__prototype):
result.__dict__[value] = self.__prototype.__dict__[value]
So, to make the tests pass:
- I used the original object instead of the “magic_method”
__class__
, including the__dict__
that has all the methods and functions implemented while creating the class as key-value pairs.
* if you got lost with the “magic_method”, I recommend this article and this one for further reading about this language feature. - I Added the instance attributes that were created for the original object inside the
DelegateCreator
using the__add_attributes
method.
Alternatively, if I want to change the behavior of the created objects, I can just change the value on the instance and link it to a different predefined function.
The DelegateCreator
class, once instantiated, will serve as a factory for similar objects, based on the original object, but a slightly powerful than just a clone. It’s worth to mention, Python doesn’t allow to make “message forwarding” as JS does. It has to be done manually, but I think it’s something that maybe over-passes the idea of this post, but you’ll get a surprise at the end, so you can play with that!
Erasing Methods
What would happen if we wanted to recover the original behavior? Just what’s expected from a prototype: if it does not find the implementation that we are looking for, it looks for the method in it's prototype object. Let’s test this situation.
def test_when_delegate_remove_some_behave_should_answer_with_prototype_behave(self):
my_car_prototype = Car("super_movil", "blue")
replacer = (lambda: "green")
creator = DelegateCreator(my_car_prototype, {"color": replacer})
my_car_delegate = creator.get()
self.assertEqual(my_car_delegate.color(), "green")
creator.remove_method(my_car_delegate, "color")
self.assertEqual(my_car_delegate.color(), "blue")
To make this test pass, we need the following implementation:
def remove_method(self, my_delegate, param, ):
del my_delegate.__dict__[param]
if param in my_delegate.__class__.__dict__.keys():
meth = my_delegate.__class__.__dict__[param]
my_delegate.__dict__[param] =(lambda: meth(my_delegate))
This is mandatory because the method lookup in python starts at an instance level, in the __dict__
dictionary that every python object has. But since we eliminated the reference, it looks for the method at a class level, which is the one that finally gets executed.
It’s interesting, and also ambiguous, that even if we have defined the color as an instance method, the reference to it is stored in object.__class__.__dict__
. This means that, in an implementation level, we could find something related to the instance and not to the class, even knowing that maybe its creator had referred to the definition of the class and not the class object itself.
A couple of disclaimers here:
- What would happen if the method had parameters?. It’s something I left out of the scope for this post, but I think it would be a nice thing to solve using TDD.
- If you are following the code, maybe you have seen when I replaced the method into
my_delegate
with the lambda expression, I sent that object as first param. Woow! what is going on there? We have to continue reading ;)
The tiny problem of self
The problem with prototypes is that when looking for the implementation from the created object (note that I’m not calling it child or any other reference that would make us think of inheritance) and the method doesn’t exist, the self
(or this
, depending the language) should refer to the object the message is sent to, rather than the prototype. So, remove_method
has to use my_delegate
as first parameter:
meth = my_delegate.__class__.__dict__[param]
my_delegate.__dict__[param] =(lambda: meth(my_delegate))
This time, I had to take the original method and create a lambda from which I call the original method. The instance methods in python receive the reference to the created instance as the first parameter.
Now, when the receiver of the message my_delegate
is called with the method, it will execute the action transparently for the emitter of the message, but avoiding the self
problem by using itself as the first parameter.
Here’s some output that confirms this hypothesis:
def test_when_delegate_remove_some_behave_should_answer_with_prototype_behave(self): self: test_when_delegate_remove_some_behave_should_answer_with_prototype_behave (test_my_proto.TestPrototyping)
my_car_prototype = Car("super_movil", "blue") original_car: <model.car.Car object at 0x10a3b4c10>
replacer = (lambda: "green")
creator = DelegateCreator(my_car_prototype, {"color": replacer}) creator: <apply.delegate_creator.DelegateCreator object...>
my_car_delegate = creator.get() my_car_delegate: <model.car.Car object at 0x10a3b4d50>
self.assertEqual(my_car_delegate.color(), "green")
creator.remove_method(my_car_delegate, "color")
self.assertEqual(my_car_delegate.color(), "blue")
Note that the object from which we began, simple_object
, has the reference 0x10a3b4c10
in memory, while the new prototyped object is 0x10a3b4d50
.
What happens when we call this method? Which one is self now?
class Car(object):
def __init__(self, brand, color):
self.__brand = brand
self.__color = color
def brand(self): self: <model.car.Car object at 0x10a3b4d50>
return sel.__color
We can see that it refers to the new object.
Conclusion
This has been a mental programming exercise. The goal was achieving a way of delegate into prototypes in Python, and we certainly succeeded, at least on a first approach. There are many implementation aspects that could be improved, of course, but it’s interesting to start playing with prototypes in Python.
For further reading
If you are so excited with prototypes and wants to read more, I suggest reading these ones (thanks Sergio Minutoli for the links):
- http://bibliography.selflanguage.org/_static/self-power.pdf
- https://medium.com/javascript-scene/the-heart-soul-of-prototypal-oo-concatenative-inheritance-a3b64cb27819
- The way of prototypes
If you ever saw a Marvel film, you'd know they have accustomed us there’s always something extra after the credits section.
If you are up for more, or want to go deeper, here’s the github link to fork or pull request as you wish.
Live long and prosper!