6.0
Prototype-based Programming
6.1
Making Your Own Object
So
far you have created many objects using special creation methods built
into the Prothon language such as integer and string and list constants.
It's time to learn how to build objects from scratch. The most fundamental
way to build an object is to use the form "obj = Object()".
This creates an "empty" object. It has no binary data and
no attributes. Let's create an empty object called "Rectangle":
>>> Rectangle = Object()
<Object:939f40>
>>>
Prothon
displays the empty object by just showing it's address in memory in
hexadecimal. This address is sometimes known as the object's ID. This
can be useful at times because the address of an object never changes
for the life of the object. When you compare two objects using the "a
is b" operator, Prothon just compares the addresses.
If
we want the new Rectangle object to represent a real rectangle, we should
give it a width and height. We can use attributes to do that. Let's
also add a method (function that uses "self", also in an attribute)
that calculates it's area:
>>> Rectangle.width = 10
10
>>> Rectangle.height = 10
10
>>> def Rectangle.area():
... return self.width * self.height
...
>>>
We
promised you earlier that the with statement would be useful. Here is
our chance to use it. Let's redo that last example using the "with"
statement. Let's also show the area method in action:
>>> with Rectangle:
... width = 10
... height = 10
... def area():
... return self.width * self.height
...
>>> print Rectangle.area()
100
>>>
So now we have an object called "Rectangle" that represents
a Rectangle by storing its width and height in attributes called "width"
and "height" and can calculate its own area by it's "area"
method that is also stored in an attribute called "area".
Those
of you familiar with object-oriented programming might notice that this
is very similar behaviour to an object instance from a class called
"Rectangle", yet we have not defined any class at all, we
just created the object directly. This is much simpler and more direct.
This is called "object-centric" programming instead of "object-oriented"
programming even though it is obviously "oriented" towards
objects. It is just that all the books and courses have already been
written that say that object-oriented programming has classes and it's
a bit too late too argue with them.
At
this point we might want a lot of Rectangles. We can always make a second
one by copying it. This is how some languages, and the Self language
in particular, makes more of a "type" of object:
>>> rect2 = Rectangle.copy()
>>> print rect2.area()
100
>>> rect2.width = 50
50
>>> print rect2.area()
500
>>> print Rectangle.area()
100
>>>
So you can see that we now have two objects each with different width
attributes and different areas that represent different rectangles.
At this point there is nothing stopping us from using a for loop to
make a thousand rectangles.
So
we can make many instances of rectangles that are just like the first
Rectangle. That is why we call the first one the prototype. It is not
like the class in an object-oriented language. A class is just a blueprint
for making objects. A prototype is a real, working instance of an object.
You can make the prototype and test it before you use it to make other
instances. Using prototypes instead of classes is what makes Prothon
a prototype-based language instead of a class-based language.
So
did I trick you and let you think you were just making an object when
you were really making a prototype? No, in reality every object in Prothon
can also be a prototype. You could copy "rect2" to make a
"rect3" and use it just like you copied "Rectangle".
Later you will find out things that make certain objects better at being
prototypes than other objects, but Prothon is an equal opportunity employer.
Any object may apply to be a Prototype.
6.2
Parent Prototype Links
You
may have noticed that the instances created above by the copy commands
above have some disadvantages compared to object-oriented intances made
by classes. The biggest problem is storage efficiency. Each one of the
rectangle instance objects has its very own copy of the area method.
Also, what if these objects are stored away somewhere and we decide
we want to make an improvement to the Prothon code for the area method?
It would be nice to change the code in one place and have it change
for all rectangles. It would not be fun to change the code in a thousand
different area methods.
To
solve that problem and to add many new features, Prothon has added a
third feature to objects, after binary data and attributes. This is
an ordered list of links to prototypes. Each object has at least one
reference (link) to an object that is its "parent prototype".
There can be more than one but let's discuss the case of one for right
now.
The
parent prototype is just an ordinary object. The parent prototype is
used whenever the object is having an attribute looked up and the attribute
is not found. In this case, the search for that attribute continues
in the parent's attributes. If the attribute isn't found there, then
it continues in the parent's parent (grandparent's) attributes. In other
words, search for attributes goes up the chain of prototype objects
linked together by the prototype links.
You
might wonder what ends this prototype chain. There is a special object,
cleverly named "Object", that is the end of all prototype
chains. It is the ultimate parent of all objects. The common way to
create an empty object is to give it one prototype link to Object. That
is what we did earlier when we said "Rectangle = Object()".
(We will explain this statement in the next paragraph). To show this,
will use the function protoList() which returns the prototype chain
for an object as a list. It includes Rectangle as the head of the list
and Object as the end:
>>> print Rectangle.protoList()
[<Object:93b100>, <Object:923020:Object: prototype base for all objects>]
>>>
So
what good is attribute lookup in prototypes? Let's redo rect2 using
a prototype link instead of doing a copy. The easiest way to create
an object that has a prototype link to another object is to use the
form "obj = Prototype()". This looks exactly like a function
call, but we know it isn't, because we know "Prototype" is
not a function object. This will create a new object that has no binary
data or attributes but it will have one prototype link to the object
"Prototype". Now you can see what our original statement "Rectangle
= Object()" was doing. It created an empty object with only a prototype
link to Object.
>>> rect2 = Rectangle()
>>> print rect2.attrs() # attrs() returns attrs as dictionary
{}
>>> print rect2.width, rect2.height, rect2.area()
10 10 100
>>>
We created a new object called rect2 with no attributes as before,
but this time we gave it a prototype link to Rectangle instead of copying
it from Rectangle. We showed that it had no attributes of it's own by
printing them out using the .attrs() function. Yet when we accessed
width, height, and area we got the same results as before. This is because
each of the attribute lookups, both for data and functions, failed on
rect2 so the lookup went to Rectangle where the lookup succeeded.
In
the case of rect2.area() resolving to Rectangle.area(), this kind of
method lookup reminds us a lot of the method inheritance in object-oriented
programming. So we sometimes say in Prothon that rect2 "inherited"
the method area from Rectangle. In Prothon, we also "inherit"
data since we lookup all attributes in the prototype parents. This is
not done in object-oriented languages and it is a powerful advantage
of prototype-based languages. Let's continue the example:
>>> rect2 = Rectangle()
>>> rect2.width = 33
>>> rect2.height = 12
>>> print rect2.attrs()
{'width':33, 'height':12}
>>> print rect2.area()
396
>>>
Now we have a rectangle object with it's own width and height attributes
only, and it behaves as if it had an area method, because it has the
prototype "Rectangle". Also, if we were to change the code
of the method "area" in the "Rectangle" prototype,
then the behaviour would change in rect2 also.
You
can see that we could make a thousand objects that each have a link
to Rectangle and each object would only need to have the data that is
unique to that object. Also, any of the objects could have a different
area method for different behaviour if you wished. Prototype-based programming
is very flexible compared to class-based programming in addition to
being simpler.
6.3
Object Statement
There
is a convenient statement, the "Object" statement, that combines
the creation of the object and the with statement. The following replaces
the first three code examples in this section 6:
>>> object Rectangle:
... width = 10
... height = 10
... def area():
... return self.width * self*height
...
>>>
The
general form of the Object statement is "object name(prototypes):
body". "Name" is the new object being created. "Prototypes"
is the list of prototypes for the object. If the list is missing, as
in this example, then just Object is assumed. "body" is the
exact same body that is in the "with" statement that is executed
exactly once immediately.
|