[譯] 用 Python 實現一個最簡單的對象模型

  • 原文地址:A Simple Object Model
  • 原文作者:Carl Friedrich Bolz
  • 譯文出自:掘金翻譯計劃
  • 譯者:Zheaoli
  • 校對者:Yuze Ma, Gran

一個簡單的對象模型

Carl Friedrich Bolz 是一位在倫敦國王大學任職的研究員,他沉迷於動態語言的實現及優化等領域而不可自拔。他是 PyPy/RPython 的核心開發者之一,於此同時,他也在為 Prolog, Racket, Smalltalk, PHP 和 Ruby 等語言貢獻代碼。這是他的 Twitter @cfbolz 。

開篇

面向對象編程是目前被廣泛使用的一種編程範式,這種編程範式也被大量現代編程語言所支持。雖然大部分語言給程序猿提供了相似的面向對象的機制,但是如果深究細節的話,還是能發現它們之間還是有很多不同的。大部分的語言的共同點在於都擁有對象處理和繼承機制。而對於類來說的話,並不是每種語言都完美支持它。比如對於 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 類和我們自建類之間的映射關係。比如,在我們自定類中將會使用 obj.read_attr("attribute") 來作為 Python 中的 obj.attribute 的替代品。在現實生活中,這樣的映射關係將由語言的編譯器/解釋器來進行實現。

在本文中,我們還對模型進行了進一步簡化,這樣看起來我們實現對象模型的代碼和和編寫對象中方法的代碼看起來沒什麼兩樣。在現實生活中,這同樣是基本不可能的,一般而言,這兩者都是由不同的語言實現的。

首先,讓我們來編寫一段用於測試讀取求改對象欄位的代碼:

def test_read_write_field():n # Python coden class A(object):n passn obj = A()n obj.a = 1n assert obj.a == 1n obj.b = 5n assert obj.a == 1n assert obj.b == 5n obj.a = 2n assert obj.a == 2n assert obj.b == 5nn # Object model coden A = Class(name="A", base_class=OBJECT, fields={}, metaclass=TYPE)n obj = Instance(A)n obj.write_attr("a", 1)n assert obj.read_attr("a") == 1n obj.write_attr("b", 5)n assert obj.read_attr("a") == 1n assert obj.read_attr("b") == 5n obj.write_attr("a", 2)n assert obj.read_attr("a") == 2n assert obj.read_attr("b") == 5n

在上面這個測試代碼中包含了我們必須實現的三個東西。Class 以及 Instance 類分別代表著我們對象中的類以及實例。同時這裡有兩個特殊的類的實例:OBJECT 和 TYPE。 OBJECT 對應的是作為 Python 繼承系統起點的 object 類(譯者註:在 Python 2.x 版本中,實際上是有兩套類系統,一套被統稱為 new style class , 一套被稱為 old style class ,object 是 new style class 的基類)。TYPE 對應的是 Python 類型系統中的 type 。

為了給 Class 以及 Instance 類的實例提供通用操作支持,這兩個類都會從 Base 類這樣提供了一系列方法的基礎類中進行繼承並實現:

class Base(object):n """ The base class that all of the object model classes inherit from. """n def __init__(self, cls, fields):n """ Every object has a class. """n self.cls = clsn self._fields = fieldsn def read_attr(self, fieldname):n """ read field fieldname out of the object """n return self._read_dict(fieldname)n def write_attr(self, fieldname, value):n """ write field fieldname into the object """n self._write_dict(fieldname, value)n def isinstance(self, cls):n """ return True if the object is an instance of class cls """n return self.cls.issubclass(cls)n def callmethod(self, methname, *args):n """ call method methname with arguments args on object """n meth = self.cls._read_from_class(methname)n return meth(self, *args)n def _read_dict(self, fieldname):n """ read an field fieldname out of the objects dict """n return self._fields.get(fieldname, MISSING)n def _write_dict(self, fieldname, value):n """ write a field fieldname into the objects dict """n self._fields[fieldname] = valuennMISSING = object()n

Base 實現了對象類的儲存,同時也使用了一個字典來保存對象欄位的值。現在,我們需要去實現 Class 以及 Instance 類。在Instance 的構造器中將會完成類的實例化以及 fields 和 dict 初始化的操作。換句話說,Instance 只是 Base 的子類,同時並不會為其添加額外的方法。

Class 的構造器將會接受類名、基礎類、類字典、以及元類這樣幾個操作。對於類來講,上面幾個變數都會在類初始化的時候由用戶傳遞給構造器。同時構造器也會從它的基類那裡獲取變數的默認值。不過這個點,我們將在下一章節進行講述。

class Instance(Base):n """Instance of a user-defined class. """n def __init__(self, cls):n assert isinstance(cls, Class)n Base.__init__(self, cls, {})nnclass Class(Base):n """ A User-defined class. """n def __init__(self, name, base_class, fields, metaclass):n Base.__init__(self, metaclass, fields)n self.name = namen self.base_class = base_classn

同時,你可能注意到這點,類依舊是一種特殊的對象,他們間接的從 Base 中繼承。因此,類也是一個特殊類的特殊實例,這樣的很特殊的類叫做:元類。

現在,我們可以順利通過我們第一組測試。不過這裡,我們還沒有定義 Type 以及 OBJECT 這樣兩個 Class 的實例。對於這些東西,我們將不會按照 Smalltalk 的對象模型進行構建,因為 Smalltalk 的對象模型對於我們來說太過於複雜。作為替代品,我們將採用 ObjVlisp1 的類型系統,Python 的類型系統從這裡吸收了不少東西。

在 ObjVlisp 的對象模型中,OBJECT 以及 TYPE 是交雜在一起的。OBJECT 是所有類的母類,意味著 OBJECT 沒有母類。TYPE 是 OBJECT 的子類。一般而言,每一個類都是 TYPE 的實例。在特定情況下,TYPE 和 OBJECT 都是 TYPE 的實例。不過,程序猿可以從 TYPE 派生出一個類去作為元類:

# set up the base hierarchy as in Python (the ObjVLisp model)n# the ultimate base class is OBJECTnOBJECT = Class(name="object", base_class=None, fields={}, metaclass=None)n# TYPE is a subclass of OBJECTnTYPE = Class(name="type", base_class=OBJECT, fields={}, metaclass=None)n# TYPE is an instance of itselfnTYPE.cls = TYPEn# OBJECT is an instance of TYPEnOBJECT.cls = TYPEn

為了去編寫一個新的元類,我們需要自行從 TYPE 進行派生。不過在本文中我們並不會這麼做,我們將只會使用 TYPE 作為我們每個類的元類。

好了,現在第一組測試已經完全通過了。現在讓我們來看看第二組測試,我們將會在這組測試中測試對象屬性讀寫是否正常。這段代碼還是很好寫的。

def test_read_write_field_class():n # classes are objects toon # Python coden class A(object):n passn A.a = 1n assert A.a == 1n A.a = 6n assert A.a == 6nn # Object model coden A = Class(name="A", base_class=OBJECT, fields={"a": 1}, metaclass=TYPE)n assert A.read_attr("a") == 1n A.write_attr("a", 5)n assert A.read_attr("a") == 5n

isinstance 檢查

到目前為止,我們還沒有將對象有類這點特性利用起來。接下來的測試代碼將會自動的實現 isinstance 。

def test_isinstance():n # Python coden class A(object):n passn class B(A):n passn b = B()n assert isinstance(b, B)n assert isinstance(b, A)n assert isinstance(b, object)n assert not isinstance(b, type)nn # Object model coden A = Class(name="A", base_class=OBJECT, fields={}, metaclass=TYPE)n B = Class(name="B", base_class=A, fields={}, metaclass=TYPE)n b = Instance(B)n assert b.isinstance(B)n assert b.isinstance(A)n assert b.isinstance(OBJECT)n assert not b.isinstance(TYPE)n

我們可以通過檢查 cls 是不是 obj 類或者它自己的超類來判斷 obj 對象是不是某些類 cls 的實例。通過檢查一個類是否在一個超類鏈上工作,來判斷一個類是不是另一個類的超類。如果還有其餘類存在於這個超類鏈上,那麼這些類也可以被稱為是超類。這個包含了超類和類本身的鏈條,被稱之為方法解析順序(譯者註:簡稱MRO)。它很容易以遞歸的方式進行計算:

class Class(Base):n ...nn def method_resolution_order(self):n """ compute the method resolution order of the class """n if self.base_class is None:n return [self]n else:n return [self] + self.base_class.method_resolution_order()nn def issubclass(self, cls):n """ is self a subclass of cls? """n return cls in self.method_resolution_order()n

好了,在修改代碼後,測試就完全能通過了

方法調用

前面所建立的對象模型中還缺少了方法調用這樣的重要特性。在本章我們將會建立一個簡單的繼承模型。

def test_callmethod_simple():n # Python coden class A(object):n def f(self):n return self.x + 1n obj = A()n obj.x = 1n assert obj.f() == 2nn class B(A):n passn obj = B()n obj.x = 1n assert obj.f() == 2 # works on subclass toonn # Object model coden def f_A(self):n return self.read_attr("x") + 1n A = Class(name="A", base_class=OBJECT, fields={"f": f_A}, metaclass=TYPE)n obj = Instance(A)n obj.write_attr("x", 1)n assert obj.callmethod("f") == 2nn B = Class(name="B", base_class=A, fields={}, metaclass=TYPE)n obj = Instance(B)n obj.write_attr("x", 2)n assert obj.callmethod("f") == 3n

為了找到調用對象方法的正確實現,我們現在開始討論類對象的方法解析順序。在 MRO 中我們所尋找到的類對象字典中第一個方法將會被調用:

class Class(Base):n ...nn def _read_from_class(self, methname):n for cls in self.method_resolution_order():n if methname in cls._fields:n return cls._fields[methname]n return MISSINGn

在完成 Base 類中 callmethod 實現後,可以通過上面的測試。

為了保證函數參數傳遞正確,同時也確保我們事先的代碼能完成方法重載的功能,我們可以編寫下面這段測試代碼,當然結果是完美通過測試:

def test_callmethod_subclassing_and_arguments():n # Python coden class A(object):n def g(self, arg):n return self.x + argn obj = A()n obj.x = 1n assert obj.g(4) == 5nn class B(A):n def g(self, arg):n return self.x + arg * 2n obj = B()n obj.x = 4n assert obj.g(4) == 12nn # Object model coden def g_A(self, arg):n return self.read_attr("x") + argn A = Class(name="A", base_class=OBJECT, fields={"g": g_A}, metaclass=TYPE)n obj = Instance(A)n obj.write_attr("x", 1)n assert obj.callmethod("g", 4) == 5nn def g_B(self, arg):n return self.read_attr("x") + arg * 2n B = Class(name="B", base_class=A, fields={"g": g_B}, metaclass=TYPE)n obj = Instance(B)n obj.write_attr("x", 4)n assert obj.callmethod("g", 4) == 12n

基礎屬性模型

現在最簡單版本的對象模型已經可以開始工作了,不過我們還需要去不斷的改進。這一部分將會介紹基礎方法模型和基礎屬性模型之間的差異。這也是 Smalltalk 、 Ruby 、 JavaScript 、 Python 和 Lua 之間的核心差異。

基礎方法模型將會按照最原始的方式去調用方法:

result = obj.f(arg1, arg2)n

基礎屬性模型將會將調用過程分為兩步:尋找屬性,以及返回執行結果:

method = obj.fn result = method(arg1, arg2)n

你可以在接下來的測試中體會到前文所述的差異:

def test_bound_method():n # Python coden class A(object):n def f(self, a):n return self.x + a + 1n obj = A()n obj.x = 2n m = obj.fn assert m(4) == 7nn class B(A):n passn obj = B()n obj.x = 1n m = obj.fn assert m(10) == 12 # works on subclass toonn # Object model coden def f_A(self, a):n return self.read_attr("x") + a + 1n A = Class(name="A", base_class=OBJECT, fields={"f": f_A}, metaclass=TYPE)n obj = Instance(A)n obj.write_attr("x", 2)n m = obj.read_attr("f")n assert m(4) == 7nn B = Class(name="B", base_class=A, fields={}, metaclass=TYPE)n obj = Instance(B)n obj.write_attr("x", 1)n m = obj.read_attr("f")n assert m(10) == 12n

我們可以按照之前測試代碼中對方法調用設置一樣的步驟去設置屬性調用,不過和方法調用相比,這裡面發生了一些變化。首先,我們將會在對象中尋找與函數名對應的方法名。這樣一個查找過程結果被稱之為已綁定的方法,具體來說就是,這個結果一個綁定了方法與具體對象的特殊對象。然後這個綁定方法會在接下來的操作中被調用。

為了實現這樣的操作,我們需要修改 Base.read_attr 的實現。如果在實例字典中沒有找到對應的屬性,那麼我們需要去在類字典中查找。如果在類字典中查找到了這個屬性,那麼我們將會執行方法綁定的操作。我們可以使用一個閉包來很簡單的模擬綁定方法。除了更改 Base.read_attr 實現以外,我們也可以修改 Base.callmethod 方法來確保我們代碼能通過測試。

class Base(object):n ...n def read_attr(self, fieldname):n """ read field fieldname out of the object """n result = self._read_dict(fieldname)n if result is not MISSING:n return resultn result = self.cls._read_from_class(fieldname)n if _is_bindable(result):n return _make_boundmethod(result, self)n if result is not MISSING:n return resultn raise AttributeError(fieldname)nn def callmethod(self, methname, *args):n """ call method methname with arguments args on object """n meth = self.read_attr(methname)n return meth(*args)nndef _is_bindable(meth):n return callable(meth)nndef _make_boundmethod(meth, self):n def bound(*args):n return meth(self, *args)n return boundn

其餘的代碼並不需要修改。

元對象協議

除了常規的類方法之外,很多動態語言還支持特殊方法。有這樣一些方法在調用時是由對象系統調用而不是使用常規調用。在 Python 中你可以看到這些方法的方法名用兩個下劃線作為開頭和結束的,比如 __init__ 。特殊方法可以用於重載一些常規操作,同時可以提供一些自定義的功能。因此,它們的存在可以告訴對象模型如何自動的處理不同的事情。Python 中相關特殊方法的說明可以查看這篇文檔。

元對象協議這一概念由 Smalltalk 引入,然後在諸如 CLOS 這樣的通用 Lisp 的對象模型中也廣泛的使用這個概念。這個概念包含特殊方法的集合(註:這裡沒有查到 coined3 的梗,請校者幫忙參考)。

在這一章中,我們將會為我們的對象模型添加三個元調用操作。它們將會用來對我們讀取和修改對象的操作進行更為精細的控制。我們首先要添加的兩個方法是 __getattr__ 和 __setattr__, 這兩個方法的命名看起來和我們 Python 中相同功能函數的方法名很相似。

自定義屬性讀寫操作

__getattr__ 方法將會在屬性通過常規方法無法查找到的情況下被調用,換句話說,在實例字典、類字典、父類字典等等對象中都找不到對應的屬性時,會觸發該方法的調用。我們將傳入一個被查找屬性的名字作為這個方法的參數。在早期的 Smalltalk4 中這個方法被稱為 doesNotUnderstand: 。

在 __setattr__ 這裡事情可能發生了點變化。首先我們需要明確一點的是,設置一個屬性的時候通常意味著我們需要創建它,在這個時候,在設置屬性的時候通常會觸發 __setattr__ 方法。為了確保 __setattr__ 的存在,我們需要在 OBJECT 對象中實現 __setattr__ 方法。這樣最基礎的實現完成了我們向相對應的字典里寫入屬性的操作。這可以使得用戶可以將自己定義的 __setattr__ 委託給 OBJECT.__setattr__ 方法。

針對這兩個特殊方法的測試用例如下所示:

def test_getattr():n # Python coden class A(object):n def __getattr__(self, name):n if name == "fahrenheit":n return self.celsius * 9. / 5. + 32n raise AttributeError(name)nn def __setattr__(self, name, value):n if name == "fahrenheit":n self.celsius = (value - 32) * 5. / 9.n else:n # call the base implementationn object.__setattr__(self, name, value)n obj = A()n obj.celsius = 30n assert obj.fahrenheit == 86 # test __getattr__n obj.celsius = 40n assert obj.fahrenheit == 104nn obj.fahrenheit = 86 # test __setattr__n assert obj.celsius == 30n assert obj.fahrenheit == 86nn # Object model coden def __getattr__(self, name):n if name == "fahrenheit":n return self.read_attr("celsius") * 9. / 5. + 32n raise AttributeError(name)n def __setattr__(self, name, value):n if name == "fahrenheit":n self.write_attr("celsius", (value - 32) * 5. / 9.)n else:n # call the base implementationn OBJECT.read_attr("__setattr__")(self, name, value)nn A = Class(name="A", base_class=OBJECT,n fields={"__getattr__": __getattr__, "__setattr__": __setattr__},n metaclass=TYPE)n obj = Instance(A)n obj.write_attr("celsius", 30)n assert obj.read_attr("fahrenheit") == 86 # test __getattr__n obj.write_attr("celsius", 40)n assert obj.read_attr("fahrenheit") == 104n obj.write_attr("fahrenheit", 86) # test __setattr__n assert obj.read_attr("celsius") == 30n assert obj.read_attr("fahrenheit") == 86n

為了通過測試,我們需要修改下 Base.read_attr 以及 Base.write_attr 兩個方法:

class Base(object):n ...nn def read_attr(self, fieldname):n """ read field fieldname out of the object """n result = self._read_dict(fieldname)n if result is not MISSING:n return resultn result = self.cls._read_from_class(fieldname)n if _is_bindable(result):n return _make_boundmethod(result, self)n if result is not MISSING:n return resultn meth = self.cls._read_from_class("__getattr__")n if meth is not MISSING:n return meth(self, fieldname)n raise AttributeError(fieldname)nn def write_attr(self, fieldname, value):n """ write field fieldname into the object """n meth = self.cls._read_from_class("__setattr__")n return meth(self, fieldname, value)n

獲取屬性的過程變成調用 __getattr__ 方法並傳入欄位名作為參數,如果欄位不存在,將會拋出一個異常。請注意 __getattr__ 只能在類中調用(Python 中的特殊方法也是這樣),同時需要避免這樣的 self.read_attr("__getattr__") 遞歸調用,因為如果 __getattr__ 方法沒有定義的話,上面的調用會造成無限遞歸。

對屬性的修改操作也會像讀取一樣交給 __setattr__ 方法執行。為了保證這個方法能夠正常執行,OBJECT 需要實現 __setattr__ 的默認行為,比如:

def OBJECT__setattr__(self, fieldname, value):n self._write_dict(fieldname, value)nOBJECT = Class("object", None, {"__setattr__": OBJECT__setattr__}, None)n

OBJECT.__setattr__ 的具體實現和之前 write_attr 方法的實現有著相似之處。在完成這些修改後,我們可以順利的通過我們的測試。

描述符協議

在上面的測試中,我們頻繁的在不同的溫標之間切換,不得不說,在執行修改屬性操作的時候這樣真的很蛋疼,所以我們需要在 __getattr__ 和 __setattr__ 中檢查所使用的的屬性的名稱為了解決這個問題,在 Python 中引入了描述符協議的概念。

我們將從 __getattr__ 和 __setattr__ 方法中獲取具體的屬性,而描述符協議則是在屬性調用過程結束返回結果時觸發一個特殊的方法。描述符協議可以視為一種可以綁定類與方法的特殊手段,我們可以使用描述符協議來完成將方法綁定到對象的具體操作。除了綁定方法,在 Python 中描述符最重要的幾個使用場景之一就是 staticmethod、 classmethod 和 property。

在接下來一點文字中,我們將介紹怎麼樣來使用描述符進行對象綁定。我們可以通過使用 __get__ 方法來達成這一目標,具體請看下面的測試代碼:

def test_get():n # Python coden class FahrenheitGetter(object):n def __get__(self, inst, cls):n return inst.celsius * 9. / 5. + 32nn class A(object):n fahrenheit = FahrenheitGetter()n obj = A()n obj.celsius = 30n assert obj.fahrenheit == 86nn # Object model coden class FahrenheitGetter(object):n def __get__(self, inst, cls):n return inst.read_attr("celsius") * 9. / 5. + 32nn A = Class(name="A", base_class=OBJECT,n fields={"fahrenheit": FahrenheitGetter()},n metaclass=TYPE)n obj = Instance(A)n obj.write_attr("celsius", 30)n assert obj.read_attr("fahrenheit") == 86n

__get__ 方法將會在屬性查找完後被 FahrenheitGetter 實例所調用。傳遞給 __get__ 的參數是查找過程結束時所處的那個實例。

實現這樣的功能倒是很簡單,我們可以很簡單的修改 _is_bindable 和 _make_boundmethod 方法:

def _is_bindable(meth):n return hasattr(meth, "__get__")nndef _make_boundmethod(meth, self):n return meth.__get__(self, None)n

好了,這樣簡單的修改能保證我們通過測試了。之前關於方法綁定的測試也能通過了,在 Python 中 __get__ 方法執行完了將會返回一個已綁定方法對象。

在實踐中,描述符協議的確看起來比較複雜。它同時還包含用於設置屬性的 __set__ 方法。此外,你現在所看到我們實現的版本是經過一些簡化的。請注意,前面 _make_boundmethod 方法調用 __get__ 是實現級的操作,而不是使用 meth.read_attr(__get__) 。這是很有必要的,因為我們的對象模型只是從 Python 中借用函數和方法,而不是展示 Python 的對象模型。進一步完善模型的話可以有效解決這個問題。

實例優化

這個對象模型前面三個部分的建立過程中伴隨著很多的行為變化,而最後一部分的優化工作並不會伴隨著行為變化。這種優化方式被稱為 map ,廣泛存在在可以自舉的語言虛擬機中。這是一種最為重要對象模型優化手段:在 PyPy ,諸如 V8 現代 JavaScript 虛擬機中得到應用(在 V8 中這種方法被稱為 hidden classes)。

這種優化手段基於如下的觀察:到目前所實現的對象模型中,所有實例都使用一個完整的字典來儲存他們的屬性。字典是基於哈希表進行實現的,這將會耗費大量的內存。在很多時候,同一個類的實例將會擁有同樣的屬性,比如,有一個類 Point ,它所有的實例都包含同樣的屬性 x y。

Map 優化利用了這樣一個事實。它將會將每個實例的字典分割為兩個部分。一部分存放可以在所有實例中共享的屬性名。然後另一部分只存放對第一部分產生的 Map 的引用和存放具體的值。存放屬性名的 map 將會作為值的索引。

我們將為上面所述的需求編寫一些測試用例,如下所示:

def test_maps():n # white box test inspecting the implementationn Point = Class(name="Point", base_class=OBJECT, fields={}, metaclass=TYPE)n p1 = Instance(Point)n p1.write_attr("x", 1)n p1.write_attr("y", 2)n assert p1.storage == [1, 2]n assert p1.map.attrs == {"x": 0, "y": 1}nn p2 = Instance(Point)n p2.write_attr("x", 5)n p2.write_attr("y", 6)n assert p1.map is p2.mapn assert p2.storage == [5, 6]nn p1.write_attr("x", -1)n p1.write_attr("y", -2)n assert p1.map is p2.mapn assert p1.storage == [-1, -2]nn p3 = Instance(Point)n p3.write_attr("x", 100)n p3.write_attr("z", -343)n assert p3.map is not p1.mapn assert p3.map.attrs == {"x": 0, "z": 1}n

注意,這裡測試代碼的風格和我們之前的才是代碼看起不太一樣。之前所有的測試只是通過已實現的介面來測試類的功能。這裡的測試通過讀取類的內部屬性來獲取實現的詳細信息,並將其與預設的值進行比較。這種測試方法又被稱之為白盒測試。

p1 的包含 attrs 的 map 存放了 x 和 y 兩個屬性,其在 p1 中存放的值分別為 0 和 1。然後創建第二個實例 p2 ,並通過同樣的方法網同樣的 map 中添加同樣的屬性。 換句話說,如果不同的屬性被添加了,那麼其中的 map 是不通用的。

Map 類長下面這樣:

class Map(object):n def __init__(self, attrs):n self.attrs = attrsn self.next_maps = {}nn def get_index(self, fieldname):n return self.attrs.get(fieldname, -1)nn def next_map(self, fieldname):n assert fieldname not in self.attrsn if fieldname in self.next_maps:n return self.next_maps[fieldname]n attrs = self.attrs.copy()n attrs[fieldname] = len(attrs)n result = self.next_maps[fieldname] = Map(attrs)n return resultnnEMPTY_MAP = Map({})n

Map 類擁有兩個方法,分別是 get_index 和 next_map 。前者用於查找對象儲存空間中的索引中查找對應的屬性名稱。而在新的屬性添加到對象中時應該使用後者。在這種情況下,不同的實例需要用 next_map 計算不同的映射關係。這個方法將會使用 next_maps 來查找已經存在的映射。這樣,相似的實例將會使用相似的 Map 對象。

Figure 14.2 - Map transitions

使用 map 的 Instance 實現如下:

class Instance(Base):n """Instance of a user-defined class. """nn def __init__(self, cls):n assert isinstance(cls, Class)n Base.__init__(self, cls, None)n self.map = EMPTY_MAPn self.storage = [] nn def _read_dict(self, fieldname):n index = self.map.get_index(fieldname)n if index == -1:n return MISSINGn return self.storage[index]nn def _write_dict(self, fieldname, value):n index = self.map.get_index(fieldname)n if index != -1:n self.storage[index] = valuen else:n new_map = self.map.next_map(fieldname)n self.storage.append(value)n self.map = new_mapn

現在這個類將給 Base 類傳遞 None 作為欄位字典,那是因為 Instance 將會以另一種方式構建存儲字典。因此它需要重載 _read_dict 和 _write_dict 。在實際操作中,我們將重構 Base 類,使其不在負責存放欄位字典。不過眼下,我們傳遞一個 None 作為參數就足夠了。

在一個新的實例創建之初使用的是 EMPTY_MAP ,這裡面沒有任何的對象存放著。在實現 _read_dict 後,我們將從實例的 map 中查找屬性名的索引,然後映射相對應的儲存表。

向欄位字典寫入數據分為兩種情況。第一種是現有屬性值的修改,那麼就簡單的在映射的列表中修改對應的值就好。而如果對應屬性不存在,那麼需要進行 map 變換(如上面的圖所示一樣),將會調用 next_map 方法,然後將新的值存放入儲存列表中。

你肯定想問,這種優化方式到底優化了什麼?一般而言,在具有很多相似結構實例的情況下能較好的優化內存。但是請記住,這不是一個通用的優化手段。有些時候代碼中充斥著結構不同的實例之時,這種手段可能會耗費更大的空間。

這是動態語言優化中的常見問題。一般而言,不太可能找到一種萬能的方法去優化代碼,使其更快,更節省空間。因此,具體情況具體分析,我們需要根據不同的情況去選擇優化方式。

在 Map 優化中很有意思的一點就是,雖然這裡只有花了內存佔用,但是在 VM 使用 JIT 技術的情況下,也能較好的提高程序的性能。為了實現這一點,JIT 技術使用映射來查找屬性在存儲空間中的偏移量。然後完全除去字典查找的方式。

潛在擴展

擴展我們的對象模型和引入不同語言的設計選擇是一件非常容易的事兒。這裡給出一些可能的方向:

  • 最簡單的是添加更多的特殊方法方法,比如一些 __init__, __getattribute__, __set__ 這樣非常容易實現和有趣的方法。

  • 擴展模型支持多重繼承。為了實現這一點,每一個類都需要一個父類列表。然後 Class.method_resolution_order 需要進行修改,以便支持方法查找。一個簡單的 MRO 計算規則可以使用深度優先原則。然後更為複雜的可以採用C3 演算法, 這種演算法能更好的處理菱形繼承結構所帶來的一些問題。

  • 一個更為瘋狂的想法是切換到原型模式,這需要消除類和實例之間的差別。

總結

面向對象編程語言設計的核心是其對象模型的細節。編寫一些簡單的對象模型是一件非常簡單而且有趣的事情。你可以通過這種方式來了解現有語言的工作機制,並且深入了解面向對象語言的設計原則。編寫不同的對象模型驗證不同對象的設計思路是一個非常棒的方法。你也不在需要將注意力放在其餘一些瑣碎的事情上,比如解析和執行代碼。

這樣編寫對象模型的工作在實踐中也是非常有用的。除了作為實驗品以外,它們還可以被其餘語言所使用。這種例子有很多:比如 GObject 模型,用 C 語言編寫,在 GLib 和 其餘 Gonme 中得到使用,還有就是用 JavaScript 實現的各類對象模型。

參考文獻

  1. P. Cointe, 「Metaclasses are first class: The ObjVlisp Model,」 SIGPLAN Not, vol. 22, no. 12, pp. 156–162, 1987.?

  2. 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 __call__, so conceptual simplicity is regained. This won』t be implemented in this chapter, however.)?

  3. G. Kiczales, J. des Rivieres, and D. G. Bobrow, The Art of the Metaobject Protocol. Cambridge, Mass: The MIT Press, 1991.?

  4. A. Goldberg, Smalltalk-80: The Language and its Implementation. Addison-Wesley, 1983, page 61.?

  5. In Python the second argument is the class where the attribute was found, though we will ignore that here.?

  6. 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.?

  7. 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.?

推薦閱讀:

TAG:Python | 后端技术 | 稀土掘金 |