forked from aosabook/500lines
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathobjmodel.tex
More file actions
981 lines (820 loc) · 37.8 KB
/
Copy pathobjmodel.tex
File metadata and controls
981 lines (820 loc) · 37.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
\begin{aosachapter}{A Simple Object Model}{s:objmodel}{Carl Friedrich Bolz}
\aosasecti{Introduction}\label{introduction}
Object-oriented programming is one of the major programming paradigms in
use today, with a lot of languages providing some form of
object-orientation. While on the surface the mechanisms that different
object-oriented programming languages provide to the programmer are very
similar, the details can vary a lot. Commonalities of most languages are
the presence of objects and some kind of inheritance mechanism. Classes,
however, are a feature that not every language supports directly. For
example, in prototype-based languages like Self or JavaScript, the
concept of class does not exist and objects instead inherit directly
from each other.
Understanding the differences between different object models can be
interesting. They often reveal the family resemblance between different
languages. It can be useful to put the model of a new language into the
context of the models of other languages, both to quickly understand the
new model, and to get a better feeling for the programming language
design space.
This chapter explores the implementation of a series of very simple
object models. It starts out with simple instances and classes, and the
ability to call methods on instances. This is the ``classical''
object-oriented approach that was established in early OO languages such
as Simula 67 and Smalltalk. This model is then extended step by step,
the next two steps exploring different language design choices, and the
last step improving the efficiency of the object model. The final model
is not that of a real language, but an idealized, simplified version of
Python's object model.
The object models presented in this chapter will be implemented in
Python. The code works on both Python 2.7 and 3.4. To understand the
behaviour and the design choices better, the chapter will also present
tests for the object model. The tests can be run with either py.test or
nose.
The choice of Python as an implementation language is quite unrealistic.
A ``real'' VM is typically implemented in a low-level language like
C/C++ and needs a lot of attention to engineering detail to make it
efficient. However, the simpler implementation language makes it easier
to focus on actual behaviour differences instead of getting bogged down
by implementation details.
\aosasecti{Method-Based Model}\label{method-based-model}
The object model we will start out with is an extremely simplified
version of that of Smalltalk. Smalltalk was an object-oriented
programming language designed by Alan Kay's group at Xerox PARC in the
1970s. It popularized object-oriented programming, and is the source of
many features found in today's programming languages. One of the core
tenets of Smalltalk's language design was ``everything is an object''.
Smalltalk's most immediate successor in use today is Ruby, which uses a
more C-like syntax but retains most of Smalltalk's object model.
The object model in this section will have classes and instances of
them, the ability to read and write attributes into objects, the ability
to call methods on objects, and the ability for a class to be a subclass
of another class. Right from the beginning, classes will be completely
ordinary objects that can themselves have attributes and methods.
A note on terminology: In this chapter I will use the word ``instance''
to mean -``an object that is not a class''.
A good approach to start with is to write a test to specify what the
to-be-implemented behaviour should be. All tests presented in this
chapter will consist of two parts. First, a bit of regular Python code
defining and using a few classes, and making use of increasingly
advanced features of the Python object model. Second, the corresponding
test using the object model we will implement in this chapter, instead
of normal Python classes.
The mapping between using normal Python classes and using our object
model will be done manually in the tests. For example, instead of
writing \texttt{obj.attribute} in Python, in the object model we would
use a method \texttt{obj.read\_attr("attribute")}. This mapping would,
in a real language implementation, be done by the interpreter of the
language, or a compiler.
A further simplification in this chapter is that we make no sharp
distinction between the code that implements the object model and the
code that is used to write the methods used in the objects. In a real
system, the two would often be implemented in different programming
languages.
Let us start with a simple test for reading and writing object fields.
\begin{verbatim}
def test_read_write_field():
# Python code
class A(object):
pass
obj = A()
obj.a = 1
assert obj.a == 1
obj.b = 5
assert obj.a == 1
assert obj.b == 5
obj.a = 2
assert obj.a == 2
assert obj.b == 5
# Object model code
A = Class(name="A", base_class=OBJECT, fields={}, metaclass=TYPE)
obj = Instance(A)
obj.write_attr("a", 1)
assert obj.read_attr("a") == 1
obj.write_attr("b", 5)
assert obj.read_attr("a") == 1
assert obj.read_attr("b") == 5
obj.write_attr("a", 2)
assert obj.read_attr("a") == 2
assert obj.read_attr("b") == 5
\end{verbatim}
The test uses three things that we have to implement. The classes
\texttt{Class} and \texttt{Instance} represent classes and instances of
our object model, respectively. There are two special instances of
class: \texttt{OBJECT} and \texttt{TYPE}. \texttt{OBJECT} corresponds to
\texttt{object} in Python and is the ultimate base class of the
inheritance hierarchy. \texttt{TYPE} corresponds to \texttt{type} in
Python and is the type of all classes.
To do anything with instances of \texttt{Class} and \texttt{Instance},
they implement a shared interface by inheriting from a shared base class
\texttt{Base} that exposes a number of methods:
\begin{verbatim}
class Base(object):
""" The base class that all of the object model classes inherit from. """
def __init__(self, cls, fields):
""" Every object has a class. """
self.cls = cls
self._fields = fields
def read_attr(self, fieldname):
""" read field 'fieldname' out of the object """
return self._read_dict(fieldname)
def write_attr(self, fieldname, value):
""" write field 'fieldname' into the object """
self._write_dict(fieldname, value)
def isinstance(self, cls):
""" return True if the object is an instance of class cls """
return self.cls.issubclass(cls)
def callmethod(self, methname, *args):
""" call method 'methname' with arguments 'args' on object """
meth = self.cls._read_from_class(methname)
return meth(self, *args)
def _read_dict(self, fieldname):
""" read an field 'fieldname' out of the object's dict """
return self._fields.get(fieldname, MISSING)
def _write_dict(self, fieldname, value):
""" write a field 'fieldname' into the object's dict """
self._fields[fieldname] = value
MISSING = object()
\end{verbatim}
The \texttt{Base} class implements storing the class of an object, and a
dictionary containing the field values of the object. Now we need to
implement \texttt{Class} and \texttt{Instance}. The constructor of
\texttt{Instance} takes the class to be instantiated and initializes the
\texttt{fields} \texttt{dict} as an empty dictionary. Otherwise
\texttt{Instance} is just a very thin subclass around \texttt{Base} that
does not add any extra functionality.
The constructor of \texttt{Class} takes the name of the class, the base
class, the dictionary of the class and the metaclass. For classes, the
fields are passed into the constructor by the user of the object model.
The class constructor also takes a base class, which the tests so far
don't need but which we will make use of in the next section.
\begin{verbatim}
class Instance(Base):
"""Instance of a user-defined class. """
def __init__(self, cls):
assert isinstance(cls, Class)
Base.__init__(self, cls, {})
class Class(Base):
""" A User-defined class. """
def __init__(self, name, base_class, fields, metaclass):
Base.__init__(self, metaclass, fields)
self.name = name
self.base_class = base_class
\end{verbatim}
Since classes are also a kind of object, they (indirectly) inherit from
\texttt{Base}. Thus, the class needs to be an instance of another class:
its metaclass.
Now our first test almost passes. The only missing bit is the definition
of the base classes \texttt{TYPE} and \texttt{OBJECT}, which are both
instances of \texttt{Class}. For these we will make a major departure
from the Smalltalk model, which has a fairly complex metaclass system.
Instead we will use the model introduced in ObjVlisp\footnote{P. Cointe,
``Metaclasses are first class: The ObjVlisp Model,'' SIGPLAN Not,
vol.~22, no. 12, pp.~156--162, 1987.}, which Python adopted.
In the ObjVlisp model, \texttt{OBJECT} and \texttt{TYPE} are
intertwined. \texttt{OBJECT} is the base class of all classes, meaning
it has no base class. \texttt{TYPE} is a subclass of \texttt{OBJECT}. By
default, every class is an instance of \texttt{TYPE}. In particular,
both \texttt{TYPE} and \texttt{OBJECT} are instances of \texttt{TYPE}.
However, the programmer can also subclass \texttt{TYPE} to make a new
metaclass:
\begin{verbatim}
# set up the base hierarchy as in Python (the ObjVLisp model)
# the ultimate base class is OBJECT
OBJECT = Class(name="object", base_class=None, fields={}, metaclass=None)
# TYPE is a subclass of OBJECT
TYPE = Class(name="type", base_class=OBJECT, fields={}, metaclass=None)
# TYPE is an instance of itself
TYPE.cls = TYPE
# OBJECT is an instance of TYPE
OBJECT.cls = TYPE
\end{verbatim}
To define new metaclasses, it is enough to subclass \texttt{TYPE}.
However, in the rest of this chapter we won't do that; we'll simply
always use \texttt{TYPE} as the metaclass of every class.
\aosafigure[240pt]{objmodel-images/inheritance.png}{Inheritance}{500l.objmodel.inheritance}
Now the first test passes. The second test checks that reading and
writing attributes works on classes as well. It's easy to write, and
passes immediately.
\begin{verbatim}
def test_read_write_field_class():
# classes are objects too
# Python code
class A(object):
pass
A.a = 1
assert A.a == 1
A.a = 6
assert A.a == 6
# Object model code
A = Class(name="A", base_class=OBJECT, fields={"a": 1}, metaclass=TYPE)
assert A.read_attr("a") == 1
A.write_attr("a", 5)
assert A.read_attr("a") == 5
\end{verbatim}
\aosasectii{\texttt{isinstance} Checking}\label{isinstance-checking}
So far we haven't taken advantage of the fact that objects have classes.
The next test implements the \texttt{isinstance} machinery:
\begin{verbatim}
def test_isinstance():
# Python code
class A(object):
pass
class B(A):
pass
b = B()
assert isinstance(b, B)
assert isinstance(b, A)
assert isinstance(b, object)
assert not isinstance(b, type)
# Object model code
A = Class(name="A", base_class=OBJECT, fields={}, metaclass=TYPE)
B = Class(name="B", base_class=A, fields={}, metaclass=TYPE)
b = Instance(B)
assert b.isinstance(B)
assert b.isinstance(A)
assert b.isinstance(OBJECT)
assert not b.isinstance(TYPE)
\end{verbatim}
To check whether an object \texttt{obj} is an instance of a certain
class \texttt{cls}, it is enough to check whether \texttt{cls} is a
superclass of the class of \texttt{obj}, or the class itself. To check
whether a class is a superclass of another class, the chain of
superclasses of that class is walked. If and only if the other class is
found in that chain, it is a superclass. The chain of superclasses of a
class, including the class itself, is called the ``method resolution
order'' of that class. It can easily be computed recursively:
\begin{verbatim}
class Class(Base):
...
def method_resolution_order(self):
""" compute the method resolution order of the class """
if self.base_class is None:
return [self]
else:
return [self] + self.base_class.method_resolution_order()
def issubclass(self, cls):
""" is self a subclass of cls? """
return cls in self.method_resolution_order()
\end{verbatim}
With that code, the test passes.
\aosasectii{Calling Methods}\label{calling-methods}
The remaining missing feature for this first version of the object model
is the ability to call methods on objects. In this chapter we will
implement a simple single inheritance model.
\begin{verbatim}
def test_callmethod_simple():
# Python code
class A(object):
def f(self):
return self.x + 1
obj = A()
obj.x = 1
assert obj.f() == 2
class B(A):
pass
obj = B()
obj.x = 1
assert obj.f() == 2 # works on subclass too
# Object model code
def f_A(self):
return self.read_attr("x") + 1
A = Class(name="A", base_class=OBJECT, fields={"f": f_A}, metaclass=TYPE)
obj = Instance(A)
obj.write_attr("x", 1)
assert obj.callmethod("f") == 2
B = Class(name="B", base_class=A, fields={}, metaclass=TYPE)
obj = Instance(B)
obj.write_attr("x", 2)
assert obj.callmethod("f") == 3
\end{verbatim}
To find the correct implementation of a method that is sent to an
object, we walk the method resolution order of the class of the object.
The first method found in the dictionary of one of the classes in the
method resolution order is called:
\begin{verbatim}
class Class(Base):
...
def _read_from_class(self, methname):
for cls in self.method_resolution_order():
if methname in cls._fields:
return cls._fields[methname]
return MISSING
\end{verbatim}
Together with the code for \texttt{callmethod} in the \texttt{Base}
implementation, this passes the test.
To make sure that methods with arguments work as well, and that
overriding of methods is implemented correctly, we can use the following
slightly more complex test, which already passes:
\begin{verbatim}
def test_callmethod_subclassing_and_arguments():
# Python code
class A(object):
def g(self, arg):
return self.x + arg
obj = A()
obj.x = 1
assert obj.g(4) == 5
class B(A):
def g(self, arg):
return self.x + arg * 2
obj = B()
obj.x = 4
assert obj.g(4) == 12
# Object model code
def g_A(self, arg):
return self.read_attr("x") + arg
A = Class(name="A", base_class=OBJECT, fields={"g": g_A}, metaclass=TYPE)
obj = Instance(A)
obj.write_attr("x", 1)
assert obj.callmethod("g", 4) == 5
def g_B(self, arg):
return self.read_attr("x") + arg * 2
B = Class(name="B", base_class=A, fields={"g": g_B}, metaclass=TYPE)
obj = Instance(B)
obj.write_attr("x", 4)
assert obj.callmethod("g", 4) == 12
\end{verbatim}
\aosasecti{Attribute-Based Model}\label{attribute-based-model}
Now that the simplest version of our object model is working, we can
think of ways to change it. This section will introduce the distinction
between a method-based model and an attribute-based model. This is one
of the core differences between Smalltalk, Ruby, and JavaScript on the
one hand and Python and Lua on the other hand.
The method-based model has the calling of methods as the primitive
operation of program execution:
\begin{verbatim}
result = obj.f(arg1, arg2)
\end{verbatim}
The attribute-based model splits up method calling into two steps:
looking up an attribute and calling the result:
\begin{verbatim}
method = obj.f
result = method(arg1, arg2)
\end{verbatim}
This difference can be shown in the following test:
\begin{verbatim}
def test_bound_method():
# Python code
class A(object):
def f(self, a):
return self.x + a + 1
obj = A()
obj.x = 2
m = obj.f
assert m(4) == 7
class B(A):
pass
obj = B()
obj.x = 1
m = obj.f
assert m(10) == 12 # works on subclass too
# Object model code
def f_A(self, a):
return self.read_attr("x") + a + 1
A = Class(name="A", base_class=OBJECT, fields={"f": f_A}, metaclass=TYPE)
obj = Instance(A)
obj.write_attr("x", 2)
m = obj.read_attr("f")
assert m(4) == 7
B = Class(name="B", base_class=A, fields={}, metaclass=TYPE)
obj = Instance(B)
obj.write_attr("x", 1)
m = obj.read_attr("f")
assert m(10) == 12
\end{verbatim}
While the setup of the classes is the same as the corresponding test for
method calls, the way that the methods are called is different. First,
the attribute with the name of the method is looked up on the object.
The result of that lookup operation is a \emph{bound method}, an object
that encapsulates both the object as well as the function found in the
class. Next, that bound method is called with a call operation\footnote{It
seems that the attribute-based model is conceptually more complex,
because it needs both method lookup and call. In practice, calling
something is defined by looking up and calling a special attribute
\texttt{\_\_call\_\_}, so conceptual simplicity is regained. This
won't be implemented in this chapter, however.)}.
To implement this behaviour, we need to change the
\texttt{Base.read\_attr} implementation. If the attribute is not found
in the dictionary, it is looked for in the class. If it is found in the
class, and the attribute is a callable, it needs to be turned into a
bound method. To emulate a bound method we simply use a closure. In
addition to changing \texttt{Base.read\_attr} we can also change
\texttt{Base.callmethod} to use the new approach to calling methods to
make sure the previous tests still pass.
\begin{verbatim}
class Base(object):
...
def read_attr(self, fieldname):
""" read field 'fieldname' out of the object """
result = self._read_dict(fieldname)
if result is not MISSING:
return result
result = self.cls._read_from_class(fieldname)
if _is_bindable(result):
return _make_boundmethod(result, self)
if result is not MISSING:
return result
raise AttributeError(fieldname)
def callmethod(self, methname, *args):
""" call method 'methname' with arguments 'args' on object """
meth = self.read_attr(methname)
return meth(*args)
def _is_bindable(meth):
return callable(meth)
def _make_boundmethod(meth, self):
def bound(*args):
return meth(self, *args)
return bound
\end{verbatim}
The rest of the code does not need to be changed at all.
\aosasecti{Meta-Object Protocols}\label{meta-object-protocols}
In addition to ``normal'' methods that are called directly by the
program, many dynamic languages support \emph{special methods}. These
are methods that aren't meant to be called directly but will be called
by the object system. In Python those special methods usually have names
that start and end with two underscores; e.g., \texttt{\_\_init\_\_}.
Special methods can be used to override primitive operations and provide
custom behaviour for them instead. Thus, they are hooks that tell the
object model machinery exactly how to do certain things. Python's object
model has
\href{https://docs.python.org/2/reference/datamodel.html\#special-method-names}{dozens
of special methods}.
Meta-object protocols were introduced by Smalltalk, but were used even
more by the object systems for Common Lisp, such as CLOS. That is also
where the name \emph{meta-object protocol}, for collections of special
methods, was coined\footnote{G. Kiczales, J. des Rivieres, and D. G.
Bobrow, The Art of the Metaobject Protocol. Cambridge, Mass: The MIT
Press, 1991.}.
In this chapter we will add three such meta-hooks to our object model.
They are used to fine-tune what exactly happens when reading and writing
attributes. The special methods we will add first are
\texttt{\_\_getattr\_\_} and \texttt{\_\_setattr\_\_}, which closely
follow the behaviour of Python's namesakes.
\aosasectii{Customizing Reading and Writing and
Attribute}\label{customizing-reading-and-writing-and-attribute}
The method \texttt{\_\_getattr\_\_} is called by the object model when
the attribute that is being looked up is not found by normal means;
i.e., neither on the instance nor on the class. It gets the name of the
attribute being looked up as an argument. An equivalent of the
\texttt{\_\_getattr\_\_} special method was part of early
Smalltalk\footnote{A. Goldberg, Smalltalk-80: The Language and its
Implementation. Addison-Wesley, 1983, page 61.} systems under the name
\texttt{doesNotUnderstand:}.
The case of \texttt{\_\_setattr\_\_} is a bit different. Since setting
an attribute always creates it, \texttt{\_\_setattr\_\_} is always
called when setting an attribute. To make sure that a
\texttt{\_\_setattr\_\_} method always exists, the \texttt{OBJECT} class
has a definition of \texttt{\_\_setattr\_\_}. This base implementation
simply does what setting an attribute did so far, which is write the
attribute into the object's dictionary. This also makes it possible for
a user-defined \texttt{\_\_setattr\_\_} to delegate to the base
\texttt{OBJECT.\_\_setattr\_\_} in some cases.
A test for these two special methods is the following:
\begin{verbatim}
def test_getattr():
# Python code
class A(object):
def __getattr__(self, name):
if name == "fahrenheit":
return self.celsius * 9. / 5. + 32
raise AttributeError(name)
def __setattr__(self, name, value):
if name == "fahrenheit":
self.celsius = (value - 32) * 5. / 9.
else:
# call the base implementation
object.__setattr__(self, name, value)
obj = A()
obj.celsius = 30
assert obj.fahrenheit == 86 # test __getattr__
obj.celsius = 40
assert obj.fahrenheit == 104
obj.fahrenheit = 86 # test __setattr__
assert obj.celsius == 30
assert obj.fahrenheit == 86
# Object model code
def __getattr__(self, name):
if name == "fahrenheit":
return self.read_attr("celsius") * 9. / 5. + 32
raise AttributeError(name)
def __setattr__(self, name, value):
if name == "fahrenheit":
self.write_attr("celsius", (value - 32) * 5. / 9.)
else:
# call the base implementation
OBJECT.read_attr("__setattr__")(self, name, value)
A = Class(name="A", base_class=OBJECT,
fields={"__getattr__": __getattr__, "__setattr__": __setattr__},
metaclass=TYPE)
obj = Instance(A)
obj.write_attr("celsius", 30)
assert obj.read_attr("fahrenheit") == 86 # test __getattr__
obj.write_attr("celsius", 40)
assert obj.read_attr("fahrenheit") == 104
obj.write_attr("fahrenheit", 86) # test __setattr__
assert obj.read_attr("celsius") == 30
assert obj.read_attr("fahrenheit") == 86
\end{verbatim}
To pass these tests, the \texttt{Base.read\_attr} and
\texttt{Base.write\_attr} methods need to be changed as follows:
\begin{verbatim}
class Base(object):
...
def read_attr(self, fieldname):
""" read field 'fieldname' out of the object """
result = self._read_dict(fieldname)
if result is not MISSING:
return result
result = self.cls._read_from_class(fieldname)
if _is_bindable(result):
return _make_boundmethod(result, self)
if result is not MISSING:
return result
meth = self.cls._read_from_class("__getattr__")
if meth is not MISSING:
return meth(self, fieldname)
raise AttributeError(fieldname)
def write_attr(self, fieldname, value):
""" write field 'fieldname' into the object """
meth = self.cls._read_from_class("__setattr__")
return meth(self, fieldname, value)
\end{verbatim}
The procedure for reading an attribute is changed to call the
\texttt{\_\_getattr\_\_} method with the fieldname as an argument, if
the method exists, instead of raising an error. Note that
\texttt{\_\_getattr\_\_} (and indeed all special methods in Python) is
looked up on the class only, instead of recursively calling
\texttt{self.read\_attr("\_\_getattr\_\_")}. That is because the latter
would lead to an infinite recursion of \texttt{read\_attr} if
\texttt{\_\_getattr\_\_} were not defined on the object.
Writing of attributes is fully deferred to the \texttt{\_\_setattr\_\_}
method. To make this work, \texttt{OBJECT} needs to have a
\texttt{\_\_setattr\_\_} method that calls the default behaviour, as
follows:
\begin{verbatim}
def OBJECT__setattr__(self, fieldname, value):
self._write_dict(fieldname, value)
OBJECT = Class("object", None, {"__setattr__": OBJECT__setattr__}, None)
\end{verbatim}
The behaviour of \texttt{OBJECT\_\_setattr\_\_} is like the previous
behaviour of \texttt{write\_attr}. With these modifications, the new
test passes.
\aosasectii{Descriptor Protocol}\label{descriptor-protocol}
The above test to provide automatic conversion between different
temperature scales worked but was annoying to write, as the attribute
name needed to be checked explicitly in the \texttt{\_\_getattr\_\_} and
\texttt{\_\_setattr\_\_} methods. To get around that clumsiness, the
\emph{descriptor protocol} was introduced in Python.
While \texttt{\_\_getattr\_\_} and \texttt{\_\_setattr\_\_} are called
on the object the attribute is being read from, the descriptor protocol
calls a special method on the \emph{result} of getting an attribute from
an object. It can be seen as the generalization of binding a method to
an object -- and indeed, binding a method to an object is done using the
descriptor protocol. In addition to bound methods, the most important
use case for the descriptor protocol in Python is the implementation of
\texttt{staticmethod}, \texttt{classmethod} and \texttt{property}.
In this subsection we will introduce the subset of the descriptor
protocol which deals with binding objects. This is done using the
special method \texttt{\_\_get\_\_}, and is best explained with an
example as a test:
\begin{verbatim}
def test_get():
# Python code
class FahrenheitGetter(object):
def __get__(self, inst, cls):
return inst.celsius * 9. / 5. + 32
class A(object):
fahrenheit = FahrenheitGetter()
obj = A()
obj.celsius = 30
assert obj.fahrenheit == 86
# Object model code
class FahrenheitGetter(object):
def __get__(self, inst, cls):
return inst.read_attr("celsius") * 9. / 5. + 32
A = Class(name="A", base_class=OBJECT,
fields={"fahrenheit": FahrenheitGetter()},
metaclass=TYPE)
obj = Instance(A)
obj.write_attr("celsius", 30)
assert obj.read_attr("fahrenheit") == 86
\end{verbatim}
The \texttt{\_\_get\_\_} method is called on the
\texttt{FahrenheitGetter} instance after that has been looked up in the
class of \texttt{obj}. The arguments to \texttt{\_\_get\_\_} are the
instance where the lookup was done{[}\^{}secondarg{]}.
{[}\^{}secondarg{]} In Python the second argument is the class where the
attribute was found, though we will ignore that here.
Implementing this behaviour is easy. We simply need to change
\texttt{\_is\_bindable} and \texttt{\_make\_boundmethod}:
\begin{verbatim}
def _is_bindable(meth):
return hasattr(meth, "__get__")
def _make_boundmethod(meth, self):
return meth.__get__(self, None)
\end{verbatim}
This makes the test pass. The previous tests about bound methods also
still pass, as Python's functions have a \texttt{\_\_get\_\_} method
that returns a bound method object.
In practice, the descriptor protocol is quite a lot more complex. It
also supports \texttt{\_\_set\_\_} to override what setting an attribute
means on a per-attribute basis. Also, the current implementation is
cutting a few corners. Note that \texttt{\_make\_boundmethod} calls the
method \texttt{\_\_get\_\_} on the implementation level, instead of
using \texttt{meth.read\_attr("\_\_get\_\_")}. This is necessary since
our object model borrows functions and thus methods from Python, instead
of having a representation for them that uses the object model. A more
complete object model would have to solve this problem.
\aosasecti{Instance Optimization}\label{instance-optimization}
While the first three variants of the object model were concerned with
behavioural variation, in this last section we will look at an
optimization without any behavioural impact. This optimization is called
\emph{maps} and was pioneered in the VM for the Self programming
language\footnote{C. Chambers, D. Ungar, and E. Lee, ``An efficient
implementation of SELF, a dynamically-typed object-oriented language
based on prototypes,'' in OOPSLA, 1989, vol.~24.}. It is still one of
the most important object model optimizations: it's used in PyPy and all
modern JavaScript VMs, such as V8 (where the optimization is called
\emph{hidden classes}).
The optimization starts from the following observation: In the object
model as implemented so far all instances use a full dictionary to store
their attributes. A dictionary is implemented using a hash map, which
takes a lot of memory. In addition, the dictionaries of instances of the
same class typically have the same keys as well. For example, given a
class \texttt{Point}, the keys of all its instances' dictionaries are
likely \texttt{"x"} and \texttt{"y"}.
The maps optimization exploits this fact. It effectively splits up the
dictionary of every instance into two parts. A part storing the keys
(the map) that can be shared between all instances with the same set of
attribute names. The instance then only stores a reference to the shared
map and the values of the attributes in a list (which is a lot more
compact in memory than a dictionary). The map stores a mapping from
attribute names to indexes into that list.
A simple test of that behaviour looks like this:
\begin{verbatim}
def test_maps():
# white box test inspecting the implementation
Point = Class(name="Point", base_class=OBJECT, fields={}, metaclass=TYPE)
p1 = Instance(Point)
p1.write_attr("x", 1)
p1.write_attr("y", 2)
assert p1.storage == [1, 2]
assert p1.map.attrs == {"x": 0, "y": 1}
p2 = Instance(Point)
p2.write_attr("x", 5)
p2.write_attr("y", 6)
assert p1.map is p2.map
assert p2.storage == [5, 6]
p1.write_attr("x", -1)
p1.write_attr("y", -2)
assert p1.map is p2.map
assert p1.storage == [-1, -2]
p3 = Instance(Point)
p3.write_attr("x", 100)
p3.write_attr("z", -343)
assert p3.map is not p1.map
assert p3.map.attrs == {"x": 0, "z": 1}
\end{verbatim}
Note that this is a different flavour of test than the ones we've
written before. All previous tests just tested the behaviour of the
classes via the exposed interfaces. This test instead checks the
implementation details of the \texttt{Instance} class by reading
internal attributes and comparing them to predefined values. Therefore
this test can be called a \emph{white-box} test.
The \texttt{attrs} attribute of the map of \texttt{p1} describes the
layout of the instance as having two attributes \texttt{"x"} and
\texttt{"y"} which are stored at position 0 and 1 of the
\texttt{storage} of \texttt{p1}. Making a second instance \texttt{p2}
and adding to it the same attributes in the same order will make it end
up with the same map. If, on the other hand, a different attribute is
added, the map can of course not be shared.
The \texttt{Map} class looks like this:
\begin{verbatim}
class Map(object):
def __init__(self, attrs):
self.attrs = attrs
self.next_maps = {}
def get_index(self, fieldname):
return self.attrs.get(fieldname, -1)
def next_map(self, fieldname):
assert fieldname not in self.attrs
if fieldname in self.next_maps:
return self.next_maps[fieldname]
attrs = self.attrs.copy()
attrs[fieldname] = len(attrs)
result = self.next_maps[fieldname] = Map(attrs)
return result
EMPTY_MAP = Map({})
\end{verbatim}
Maps have two methods, \texttt{get\_index} and \texttt{next\_map}. The
former is used to find the index of an attribute name in the object's
storage. The latter is used when a new attribute is added to an object.
In that case the object needs to use a different map, which
\texttt{next\_map} computes. The method uses the \texttt{next\_maps}
dictionary to cache already created maps. That way, objects that have
the same layout also end up using the same \texttt{Map} object.
\aosafigure[240pt]{objmodel-images/maptransition.png}{Map transitions}{500l.objmodel.maptransition}
The \texttt{Instance} implementation that uses maps looks like this:
\begin{verbatim}
class Instance(Base):
"""Instance of a user-defined class. """
def __init__(self, cls):
assert isinstance(cls, Class)
Base.__init__(self, cls, None)
self.map = EMPTY_MAP
self.storage = []
def _read_dict(self, fieldname):
index = self.map.get_index(fieldname)
if index == -1:
return MISSING
return self.storage[index]
def _write_dict(self, fieldname, value):
index = self.map.get_index(fieldname)
if index != -1:
self.storage[index] = value
else:
new_map = self.map.next_map(fieldname)
self.storage.append(value)
self.map = new_map
\end{verbatim}
The class now passes \texttt{None} as the fields dictionary to
\texttt{Base}, as \texttt{Instance} will store the content of the
dictionary in another way. Therefore it needs to override the
\texttt{\_read\_dict} and \texttt{\_write\_dict} methods. In a real
implementation, we would refactor the \texttt{Base} class so that it is
no longer responsible for storing the fields dictionary, but for now
having instances store \texttt{None} there is good enough.
A newly created instance starts out using the \texttt{EMPTY\_MAP}, which
has no attributes, and empty storage. To implement
\texttt{\_read\_dict}, the instance's map is asked for the index of the
attribute name. Then the corresponding entry of the storage list is
returned.
Writing into the fields dictionary has two cases. On the one hand the
value of an existing attribute can be changed. This is done by simply
changing the storage at the corresponding index. On the other hand, if
the attribute does not exist yet, a \emph{map transition}
(\aosafigref{500l.objmodel.maptransition}) is needed using the
\texttt{next\_map} method. The value of the new attribute is appended to
the storage list.
What does this optimization achieve? It optimizes use of memory in the
common case where there are many instances with the same layout. It is
not a universal optimization: code that creates instances with wildly
different sets of attributes will have a larger memory footprint than if
we just use dictionaries.
This is a common problem when optimizing dynamic languages. It is often
not possible to find optimizations that are faster or use less memory in
all cases. In practice, the optimizations chosen apply to how the
language is \emph{typically} used, while potentially making behaviour
worse for programs that use extremely dynamic features.
Another interesting aspect of maps is that, while here they only
optimize for memory use, in actual VMs that use a just-in-time (JIT)
compiler they also improve the performance of the program. To achieve
that, the JIT uses the maps to compile attribute lookups to a lookup in
the objects' storage at a fixed offset, getting rid of all dictionary
lookups completely\footnote{How that works is beyond the scope of this
chapter. I tried to give a reasonably readable account of it in a
paper I wrote a few years ago. It uses an object model that is
basically a variant of the one in this chapter: C. F. Bolz, A. Cuni,
M. Fijałkowski, M. Leuschel, S. Pedroni, and A. Rigo, ``Runtime
feedback in a meta-tracing JIT for efficient dynamic languages,'' in
Proceedings of the 6th Workshop on Implementation, Compilation,
Optimization of Object-Oriented Languages, Programs and Systems, New
York, NY, USA, 2011, pp.~9:1--9:8.}.
\aosasecti{Potential Extensions}\label{potential-extensions}
It is easy to extend our object model and experiment with various
language design choices. Here are some possibilities:
\begin{aosaitemize}
\item
The easiest thing to do is to add further special methods. Some easy
and interesting ones to add are \texttt{\_\_init\_\_},
\texttt{\_\_getattribute\_\_}, \texttt{\_\_set\_\_}.
\item
The model can be very easily extended to support multiple inheritance.
To do this, every class would get a list of base classes. Then the
\texttt{Class.method\_resolution\_order} method would need to be
changed to support looking up methods. A simple method resolution
order could be computed using a depth-first search with removal of
duplicates. A more complicated but better one is the
\href{https://www.python.org/download/releases/2.3/mro/}{C3
algorithm}, which adds better handling in the base of diamond-shaped
multiple inheritance hierarchies and rejects insensible inheritance
patterns.
\item
A more radical change is to switch to a prototype model, which
involves the removal of the distinction between classes and instances.
\end{aosaitemize}
\aosasecti{Conclusions}\label{conclusions}
Some of the core aspects of the design of an object-oriented programming
language are the details of its object model. Writing small object model
prototypes is an easy and fun way to understand the inner workings of
existing languages better and to get insights into the design space of
object-oriented languages. Playing with object models is a good way to
experiment with different language design ideas without having to worry
about the more boring parts of language implementation, such as parsing
and executing code.
Such object models can also be useful in practice, not just as vehicles
for experimentation. They can be embedded in and used from other
languages. Examples of this approach are common: the GObject object
model, written in C, that's used in GLib and other Gnome libraries; or
the various class system implementations in JavaScript.
\end{aosachapter}