<b>本文講的是用 Python 實作一個最簡單的對象模型,</b>
<a></a>
面向對象程式設計是目前被廣泛使用的一種程式設計範式,這種程式設計範式也被大量現代程式設計語言所支援。雖然大部分語言給程式猿提供了相似的面向對象的機制,但是如果深究細節的話,還是能發現它們之間還是有很多不同的。大部分的語言的共同點在于都擁有對象處理和繼承機制。而對于類來說的話,并不是每種語言都完美支援它。比如對于 Self 或者 JavaScript 這樣的原型繼承的語言來說,是沒有類這個概念的,他們的繼承行為都是在對象之間所産生的。
深入了解不同語言的對象模型是一件非常有意思的事兒。這樣我們可以去欣賞不同的程式設計語言的相似性。不得不說,這樣的經曆可以在我們學習新的語言的時候,利用上我們已有的經驗,以便于我們快速的掌握它。
這篇文章将會帶領你實作一套簡單的對象模型。首先我們将實作一個簡單的類與其執行個體,并能夠通過這個執行個體去通路一些方法。這是被諸如 Simula 67 、Smalltalk 等早期面向對象語言所采用的面向對象模型。然後我們會一步步的擴充這個模型,你可以看到接下來兩步會為你展現不同語言的模型設計思路,然後最後一步是來優化我們的對象模型的性能。最終我們所得到的模型并不是哪一門真實存在的語言所采用的模型,不過,硬是要說的話,你可以把我們得到的最終模型視為一個低配版的 Python 對象模型。
這篇文章裡所展現的對象模型都是基于 Python 實作的。代碼在 Python 2.7 以及 Python 3.4 上都可以完美運作。為了讓大家更好的了解模型裡的設計哲學,本文也為我們所設計的對象模型準備了單元測試,這些測試代碼可以利用 py.test 或者 nose 來運作。
講真,用 Python 來作為對象模型的實作語言并不是一個好的選擇。一般而言,語言的虛拟機都是基于 C/C++ 這樣更為貼近底層的語言來實作的,同時在實作中需要非常注意很多的細節,以保證其執行效率。不過,Python 這樣非常簡單的語言能讓我們将主要精力都放在不同的行為表現上,而不是糾結于實作細節不可自拔。
我們将以 Smalltalk 中的實作的非常簡單的對象模型來開始講解我們的對象模型。Smalltalk 是一門由施樂帕克研究中心下屬的 Alan Kay 所帶領的小組在 70 年代所開發出的一門面向對象語言。它普及了面向對象程式設計,同時在今天的程式設計語言中依然能看到當時它所包含的很多特性。在 Smalltalk 核心設計原則之一便是:“萬物皆對象”。Smalltalk 最廣為人知的繼承者是 Ruby,一門使用類似 C 語言文法的同時保留了 Smalltalk 對象模型的語言。
在這一部分中,我們所實作的對象模型将包含類,執行個體,屬性的調用及修改,方法的調用,同時允許子類的存在。開始前,先聲明一下,這裡的類都是有他們自己的屬性和方法的普通的類
友情提示:在這篇文章中,“執行個體”代表着“不是類的對象”的含義。
一個非常好的習慣就是優先編寫測試代碼,以此來限制具體實作的行為。本文所編寫的測試代碼由兩個部分組成。第一部分由正常的 Python 代碼組成,可能會使用到 Python 中的類及其餘一些更進階的特性。第二部分将會用我們自己建立的對象模型來替代 Python 的類。
在編寫測試代碼時,我們需要手動維護正常的 Python 類和我們自建類之間的映射關系。比如,在我們自定類中将會使用 <code>obj.read_attr("attribute")</code> 來作為 Python 中的 <code>obj.attribute</code> 的替代品。在現實生活中,這樣的映射關系将由語言的編譯器/解釋器來進行實作。
在本文中,我們還對模型進行了進一步簡化,這樣看起來我們實作對象模型的代碼和和編寫對象中方法的代碼看起來沒什麼兩樣。在現實生活中,這同樣是基本不可能的,一般而言,這兩者都是由不同的語言實作的。
首先,讓我們來編寫一段用于測試讀取求改對象字段的代碼:
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
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.b == 5
obj.a = 2
assert obj.a == 2
# 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("b") == 5
obj.write_attr("a", 2)
assert obj.read_attr("a") == 2
在上面這個測試代碼中包含了我們必須實作的三個東西。<code>Class</code> 以及 <code>Instance</code> 類分别代表着我們對象中的類以及執行個體。同時這裡有兩個特殊的類的執行個體:<code>OBJECT</code> 和 <code>TYPE</code>。 <code>OBJECT</code> 對應的是作為 Python 繼承系統起點的 <code>object</code> 類(譯者注:在 Python 2.x 版本中,實際上是有兩套類系統,一套被統稱為 new style class , 一套被稱為 old style class ,<code>object</code> 是 new style class 的基類)。<code>TYPE</code>對應的是 Python 類型系統中的 <code>type</code> 。
為了給 <code>Class</code> 以及 <code>Instance</code> 類的執行個體提供通用操作支援,這兩個類都會從 <code>Base</code> 類這樣提供了一系列方法的基礎類中進行繼承并實作:
27
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()
<code>Base</code> 實作了對象類的儲存,同時也使用了一個字典來儲存對象字段的值。現在,我們需要去實作<code>Class</code> 以及 <code>Instance</code> 類。在<code>Instance</code> 的構造器中将會完成類的執行個體化以及 <code>fields</code> 和 <code>dict</code> 初始化的操作。換句話說,<code>Instance</code> 隻是 <code>Base</code> 的子類,同時并不會為其添加額外的方法。
<code>Class</code> 的構造器将會接受類名、基礎類、類字典、以及元類這樣幾個操作。對于類來講,上面幾個變量都會在類初始化的時候由使用者傳遞給構造器。同時構造器也會從它的基類那裡擷取變量的預設值。不過這個點,我們将在下一章節進行講述。
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
同時,你可能注意到這點,類依舊是一種特殊的對象,他們間接的從 <code>Base</code> 中繼承。是以,類也是一個特殊類的特殊執行個體,這樣的很特殊的類叫做:元類。
現在,我們可以順利通過我們第一組測試。不過這裡,我們還沒有定義 <code>Type</code> 以及 <code>OBJECT</code> 這樣兩個<code>Class</code> 的執行個體。對于這些東西,我們将不會按照 Smalltalk 的對象模型進行建構,因為 Smalltalk 的對象模型對于我們來說太過于複雜。作為替代品,我們将采用 ObjVlisp1 的類型系統,Python 的類型系統從這裡吸收了不少東西。
在 ObjVlisp 的對象模型中,<code>OBJECT</code> 以及 <code>TYPE</code> 是交雜在一起的。<code>OBJECT</code> 是所有類的母類,意味着<code>OBJECT</code> 沒有母類。<code>TYPE</code> 是 <code>OBJECT</code> 的子類。一般而言,每一個類都是 <code>TYPE</code> 的執行個體。在特定情況下,<code>TYPE</code> 和 <code>OBJECT</code> 都是 <code>TYPE</code> 的執行個體。不過,程式猿可以從 <code>TYPE</code> 派生出一個類去作為元類:
# 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
為了去編寫一個新的元類,我們需要自行從 <code>TYPE</code> 進行派生。不過在本文中我們并不會這麼做,我們将隻會使用 <code>TYPE</code> 作為我們每個類的元類。
好了,現在第一組測試已經完全通過了。現在讓我們來看看第二組測試,我們将會在這組測試中測試對象屬性讀寫是否正常。這段代碼還是很好寫的。
def test_read_write_field_class():
# classes are objects too
A.a = 1
assert A.a == 1
A.a = 6
assert A.a == 6
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
到目前為止,我們還沒有将對象有類這點特性利用起來。接下來的測試代碼将會自動的實作 <code>isinstance</code>。
def test_isinstance():
class B(A):
b = B()
assert isinstance(b, B)
assert isinstance(b, A)
assert isinstance(b, object)
assert not isinstance(b, 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)
我們可以通過檢查 <code>cls</code> 是不是 <code>obj</code> 類或者它自己的超類來判斷 <code>obj</code> 對象是不是某些類 <code>cls</code> 的執行個體。通過檢查一個類是否在一個超類鍊上工作,來判斷一個類是不是另一個類的超類。如果還有其餘類存在于這個超類鍊上,那麼這些類也可以被稱為是超類。這個包含了超類和類本身的鍊條,被稱之為方法解析順序(譯者注:簡稱MRO)。它很容易以遞歸的方式進行計算:
...
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()
好了,在修改代碼後,測試就完全能通過了
前面所建立的對象模型中還缺少了方法調用這樣的重要特性。在本章我們将會建立一個簡單的繼承模型。
def test_callmethod_simple():
def f(self):
return self.x + 1
obj.x = 1
assert obj.f() == 2
obj = B()
assert obj.f() == 2 # works on subclass too
def f_A(self):
return self.read_attr("x") + 1
A = Class(name="A", base_class=OBJECT, fields={"f": f_A}, metaclass=TYPE)
obj.write_attr("x", 1)
assert obj.callmethod("f") == 2
obj = Instance(B)
obj.write_attr("x", 2)
assert obj.callmethod("f") == 3
為了找到調用對象方法的正确實作,我們現在開始讨論類對象的方法解析順序。在 MRO 中我們所尋找到的類對象字典中第一個方法将會被調用:
def _read_from_class(self, methname):
for cls in self.method_resolution_order():
if methname in cls._fields:
return cls._fields[methname]
return MISSING
在完成 <code>Base</code> 類中 <code>callmethod</code> 實作後,可以通過上面的測試。
為了保證函數參數傳遞正确,同時也確定我們事先的代碼能完成方法重載的功能,我們可以編寫下面這段測試代碼,當然結果是完美通過測試:
28
29
30
def test_callmethod_subclassing_and_arguments():
def g(self, arg):
return self.x + arg
assert obj.g(4) == 5
return self.x + arg * 2
obj.x = 4
assert obj.g(4) == 12
def g_A(self, arg):
return self.read_attr("x") + arg
A = Class(name="A", base_class=OBJECT, fields={"g": g_A}, metaclass=TYPE)
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.write_attr("x", 4)
assert obj.callmethod("g", 4) == 12
現在最簡單版本的對象模型已經可以開始工作了,不過我們還需要去不斷的改進。這一部分将會介紹基礎方法模型和基礎屬性模型之間的差異。這也是 Smalltalk 、 Ruby 、 JavaScript 、 Python 和 Lua 之間的核心差異。
基礎方法模型将會按照最原始的方式去調用方法:
result = obj.f(arg1, arg2)
基礎屬性模型将會将調用過程分為兩步:尋找屬性,以及傳回執行結果:
method = obj.f
result = method(arg1, arg2)
你可以在接下來的測試中體會到前文所述的差異:
31
def test_bound_method():
def f(self, a):
return self.x + a + 1
obj.x = 2
m = obj.f
assert m(4) == 7
assert m(10) == 12 # works on subclass too
def f_A(self, a):
return self.read_attr("x") + a + 1
m = obj.read_attr("f")
assert m(10) == 12
我們可以按照之前測試代碼中對方法調用設定一樣的步驟去設定屬性調用,不過和方法調用相比,這裡面發生了一些變化。首先,我們将會在對象中尋找與函數名對應的方法名。這樣一個查找過程結果被稱之為已綁定的方法,具體來說就是,這個結果一個綁定了方法與具體對象的特殊對象。然後這個綁定方法會在接下來的操作中被調用。
為了實作這樣的操作,我們需要修改 <code>Base.read_attr</code> 的實作。如果在執行個體字典中沒有找到對應的屬性,那麼我們需要去在類字典中查找。如果在類字典中查找到了這個屬性,那麼我們将會執行方法綁定的操作。我們可以使用一個閉包來很簡單的模拟綁定方法。除了更改 <code>Base.read_attr</code> 實作以外,我們也可以修改 <code>Base.callmethod</code> 方法來確定我們代碼能通過測試。
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)
raise AttributeError(fieldname)
meth = self.read_attr(methname)
return meth(*args)
def _is_bindable(meth):
return callable(meth)
def _make_boundmethod(meth, self):
def bound(*args):
return bound
其餘的代碼并不需要修改。
元對象協定這一概念由 Smalltalk 引入,然後在諸如 CLOS 這樣的通用 Lisp 的對象模型中也廣泛的使用這個概念。這個概念包含特殊方法的集合(注:這裡沒有查到 coined3 的梗,請校者幫忙參考)。
在這一章中,我們将會為我們的對象模型添加三個元調用操作。它們将會用來對我們讀取和修改對象的操作進行更為精細的控制。我們首先要添加的兩個方法是 <code>__getattr__</code> 和 <code>__setattr__</code>, 這兩個方法的命名看起來和我們 Python 中相同功能函數的方法名很相似。
<code>__getattr__</code> 方法将會在屬性通過正常方法無法查找到的情況下被調用,換句話說,在執行個體字典、類字典、父類字典等等對象中都找不到對應的屬性時,會觸發該方法的調用。我們将傳入一個被查找屬性的名字作為這個方法的參數。在早期的 Smalltalk4 中這個方法被稱為 <code>doesNotUnderstand:</code> 。
在 <code>__setattr__</code> 這裡事情可能發生了點變化。首先我們需要明确一點的是,設定一個屬性的時候通常意味着我們需要建立它,在這個時候,在設定屬性的時候通常會觸發 <code>__setattr__</code> 方法。為了確定<code>__setattr__</code> 的存在,我們需要在 <code>OBJECT</code> 對象中實作 <code>__setattr__</code> 方法。這樣最基礎的實作完成了我們向相對應的字典裡寫入屬性的操作。這可以使得使用者可以将自己定義的 <code>__setattr__</code> 委托給<code>OBJECT.__setattr__</code> 方法。
針對這兩個特殊方法的測試用例如下所示:
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def test_getattr():
def __getattr__(self, name):
if name == "fahrenheit":
return self.celsius * 9\. / 5\. + 32
raise AttributeError(name)
def __setattr__(self, name, value):
self.celsius = (value - 32) * 5\. / 9.
# call the base implementation
object.__setattr__(self, name, value)
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
return self.read_attr("celsius") * 9\. / 5\. + 32
self.write_attr("celsius", (value - 32) * 5\. / 9.)
OBJECT.read_attr("__setattr__")(self, name, value)
A = Class(name="A", base_class=OBJECT,
fields={"__getattr__": __getattr__, "__setattr__": __setattr__},
metaclass=TYPE)
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
為了通過測試,我們需要修改下 <code>Base.read_attr</code> 以及 <code>Base.write_attr</code> 兩個方法:
meth = self.cls._read_from_class("__getattr__")
if meth is not MISSING:
return meth(self, fieldname)
meth = self.cls._read_from_class("__setattr__")
return meth(self, fieldname, value)
擷取屬性的過程變成調用 <code>__getattr__</code> 方法并傳入字段名作為參數,如果字段不存在,将會抛出一個異常。請注意 <code>__getattr__</code> 隻能在類中調用(Python 中的特殊方法也是這樣),同時需要避免這樣的<code>self.read_attr("__getattr__")</code> 遞歸調用,因為如果 <code>__getattr__</code> 方法沒有定義的話,上面的調用會造成無限遞歸。
對屬性的修改操作也會像讀取一樣交給 <code>__setattr__</code> 方法執行。為了保證這個方法能夠正常執行,<code>OBJECT</code> 需要實作 <code>__setattr__</code> 的預設行為,比如:
def OBJECT__setattr__(self, fieldname, value):
OBJECT = Class("object", None, {"__setattr__": OBJECT__setattr__}, None)
<code>OBJECT.__setattr__</code> 的具體實作和之前 <code>write_attr</code> 方法的實作有着相似之處。在完成這些修改後,我們可以順利的通過我們的測試。
在上面的測試中,我們頻繁的在不同的溫标之間切換,不得不說,在執行修改屬性操作的時候這樣真的很蛋疼,是以我們需要在 <code>__getattr__</code> 和 <code>__setattr__</code> 中檢查所使用的的屬性的名稱為了解決這個問題,在 Python 中引入了描述符協定的概念。
我們将從 <code>__getattr__</code> 和 <code>__setattr__</code> 方法中擷取具體的屬性,而描述符協定則是在屬性調用過程結束傳回結果時觸發一個特殊的方法。描述符協定可以視為一種可以綁定類與方法的特殊手段,我們可以使用描述符協定來完成将方法綁定到對象的具體操作。除了綁定方法,在 Python 中描述符最重要的幾個使用場景之一就是 <code>staticmethod</code>、 <code>classmethod</code> 和 <code>property</code>。
在接下來一點文字中,我們将介紹怎麼樣來使用描述符進行對象綁定。我們可以通過使用 <code>__get__</code> 方法來達成這一目标,具體請看下面的測試代碼:
def test_get():
class FahrenheitGetter(object):
def __get__(self, inst, cls):
return inst.celsius * 9\. / 5\. + 32
fahrenheit = FahrenheitGetter()
return inst.read_attr("celsius") * 9\. / 5\. + 32
fields={"fahrenheit": FahrenheitGetter()},
<code>__get__</code> 方法将會在屬性查找完後被 <code>FahrenheitGetter</code> 執行個體所調用。傳遞給 <code>__get__</code> 的參數是查找過程結束時所處的那個執行個體。
實作這樣的功能倒是很簡單,我們可以很簡單的修改 <code>_is_bindable</code> 和 <code>_make_boundmethod</code> 方法:
return hasattr(meth, "__get__")
return meth.__get__(self, None)
好了,這樣簡單的修改能保證我們通過測試了。之前關于方法綁定的測試也能通過了,在 Python 中<code>__get__</code> 方法執行完了将會傳回一個已綁定方法對象。
在實踐中,描述符協定的确看起來比較複雜。它同時還包含用于設定屬性的 <code>__set__</code> 方法。此外,你現在所看到我們實作的版本是經過一些簡化的。請注意,前面 <code>_make_boundmethod</code> 方法調用 <code>__get__</code> 是實作級的操作,而不是使用 <code>meth.read_attr('__get__')</code> 。這是很有必要的,因為我們的對象模型隻是從 Python 中借用函數和方法,而不是展示 Python 的對象模型。進一步完善模型的話可以有效解決這個問題。
這個對象模型前面三個部分的建立過程中伴随着很多的行為變化,而最後一部分的優化工作并不會伴随着行為變化。這種優化方式被稱為 map ,廣泛存在在可以自舉的語言虛拟機中。這是一種最為重要對象模型優化手段:在 PyPy ,諸如 V8 現代 JavaScript 虛拟機中得到應用(在 V8 中這種方法被稱為 hidden classes)。
這種優化手段基于如下的觀察:到目前所實作的對象模型中,所有執行個體都使用一個完整的字典來儲存他們的屬性。字典是基于哈希表進行實作的,這将會耗費大量的記憶體。在很多時候,同一個類的執行個體将會擁有同樣的屬性,比如,有一個類 <code>Point</code> ,它所有的執行個體都包含同樣的屬性 <code>x</code> <code>y</code>。
<code>Map</code> 優化利用了這樣一個事實。它将會将每個執行個體的字典分割為兩個部分。一部分存放可以在所有執行個體中共享的屬性名。然後另一部分隻存放對第一部分産生的 <code>Map</code> 的引用和存放具體的值。存放屬性名的map 将會作為值的索引。
我們将為上面所述的需求編寫一些測試用例,如下所示:
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.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}
注意,這裡測試代碼的風格和我們之前的才是代碼看起不太一樣。之前所有的測試隻是通過已實作的接口來測試類的功能。這裡的測試通過讀取類的内部屬性來擷取實作的詳細資訊,并将其與預設的值進行比較。這種測試方法又被稱之為白盒測試。
<code>p1</code> 的包含 <code>attrs</code> 的 <code>map</code> 存放了 <code>x</code> 和 <code>y</code> 兩個屬性,其在 <code>p1</code> 中存放的值分别為 0 和 1。然後建立第二個執行個體 <code>p2</code> ,并通過同樣的方法網同樣的 <code>map</code> 中添加同樣的屬性。 換句話說,如果不同的屬性被添加了,那麼其中的 <code>map</code> 是不通用的。
<code>Map</code> 類長下面這樣:
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)
EMPTY_MAP = Map({})
Map 類擁有兩個方法,分别是 <code>get_index</code> 和 <code>next_map</code> 。前者用于查找對象儲存空間中的索引中查找對應的屬性名稱。而在新的屬性添加到對象中時應該使用後者。在這種情況下,不同的執行個體需要用<code>next_map</code> 計算不同的映射關系。這個方法将會使用 <code>next_maps</code> 來查找已經存在的映射。這樣,相似的執行個體将會使用相似的 <code>Map</code> 對象。
<a href="http://ww4.sinaimg.cn/large/65e4f1e6jw1fa3aoxjr2vj20b7077q37.jpg"></a>
Figure 14.2 - Map transitions
使用 <code>map</code> 的 <code>Instance</code> 實作如下:
Base.__init__(self, cls, None)
self.map = EMPTY_MAP
self.storage = []
index = self.map.get_index(fieldname)
if index == -1:
return self.storage[index]
if index != -1:
self.storage[index] = value
new_map = self.map.next_map(fieldname)
self.storage.append(value)
self.map = new_map
現在這個類将給 <code>Base</code> 類傳遞 <code>None</code> 作為字段字典,那是因為 <code>Instance</code> 将會以另一種方式建構存儲字典。是以它需要重載 <code>_read_dict</code> 和 <code>_write_dict</code> 。在實際操作中,我們将重構 <code>Base</code> 類,使其不在負責存放字段字典。不過眼下,我們傳遞一個 <code>None</code> 作為參數就足夠了。
在一個新的執行個體建立之初使用的是 <code>EMPTY_MAP</code> ,這裡面沒有任何的對象存放着。在實作 <code>_read_dict</code>後,我們将從執行個體的 <code>map</code> 中查找屬性名的索引,然後映射相對應的儲存表。
向字段字典寫入資料分為兩種情況。第一種是現有屬性值的修改,那麼就簡單的在映射的清單中修改對應的值就好。而如果對應屬性不存在,那麼需要進行 <code>map</code> 變換(如上面的圖所示一樣),将會調用<code>next_map</code> 方法,然後将新的值存放入儲存清單中。
你肯定想問,這種優化方式到底優化了什麼?一般而言,在具有很多相似結構執行個體的情況下能較好的優化記憶體。但是請記住,這不是一個通用的優化手段。有些時候代碼中充斥着結構不同的執行個體之時,這種手段可能會耗費更大的空間。
這是動态語言優化中的常見問題。一般而言,不太可能找到一種萬能的方法去優化代碼,使其更快,更節省空間。是以,具體情況具體分析,我們需要根據不同的情況去選擇優化方式。
在 <code>Map</code> 優化中很有意思的一點就是,雖然這裡隻有花了記憶體占用,但是在 VM 使用 JIT 技術的情況下,也能較好的提高程式的性能。為了實作這一點,JIT 技術使用映射來查找屬性在存儲空間中的偏移量。然後完全除去字典查找的方式。
擴充我們的對象模型和引入不同語言的設計選擇是一件非常容易的事兒。這裡給出一些可能的方向:
最簡單的是添加更多的特殊方法方法,比如一些 <code>__init__</code>, <code>__getattribute__</code>, <code>__set__</code> 這樣非常容易實作和有趣的方法。
一個更為瘋狂的想法是切換到原型模式,這需要消除類和執行個體之間的差别。
面向對象程式設計語言設計的核心是其對象模型的細節。編寫一些簡單的對象模型是一件非常簡單而且有趣的事情。你可以通過這種方式來了解現有語言的工作機制,并且深入了解面向對象語言的設計原則。編寫不同的對象模型驗證不同對象的設計思路是一個非常棒的方法。你也不在需要将注意力放在其餘一些瑣碎的事情上,比如解析和執行代碼。
這樣編寫對象模型的工作在實踐中也是非常有用的。除了作為實驗品以外,它們還可以被其餘語言所使用。這種例子有很多:比如 GObject 模型,用 C 語言編寫,在 GLib 和 其餘 Gonme 中得到使用,還有就是用 JavaScript 實作的各類對象模型。
P. Cointe, “Metaclasses are first class: The ObjVlisp Model,” SIGPLAN Not, vol. 22, no. 12, pp. 156–162, 1987.↩
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 <code>__call__</code>, so conceptual simplicity is regained. This won’t be implemented in this chapter, however.)↩
G. Kiczales, J. des Rivieres, and D. G. Bobrow, The Art of the Metaobject Protocol. Cambridge, Mass: The MIT Press, 1991.↩
A. Goldberg, Smalltalk-80: The Language and its Implementation. Addison-Wesley, 1983, page 61.↩
In Python the second argument is the class where the attribute was found, though we will ignore that here.↩
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.↩
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.↩
<b></b>
<b>原文釋出時間為:2016年12月15日</b>
<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>