Manjusaka

Manjusaka

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

一個簡單的物件模型#

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():
    # 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

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

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

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()

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

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

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

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

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

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

# 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

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

Figure 14.1 - Inheritance

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

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

isinstance 檢查#

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

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)

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

 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()

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

方法調用#

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

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

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

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

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

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

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

基礎屬性模型#

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

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

result = obj.f(arg1, arg2)

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

    method = obj.f
    result = method(arg1, arg2)

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

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

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

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

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

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

元物件協議#

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

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

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

自定義屬性讀寫操作#

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

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

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

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

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

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)

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

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

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

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

描述符協議#

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

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

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

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

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

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

def _is_bindable(meth):
    return hasattr(meth, "__get__")

def _make_boundmethod(meth, self):
    return meth.__get__(self, None)

好了,這樣簡單的修改能保證我們通過測試了。之前關於方法綁定的測試也能通過了,在 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():
    # 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}

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

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

Map 類長下面這樣:

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({})

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

image

Figure 14.2 - Map transitions

使用 mapInstance 實現如下:

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

現在這個類將給 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.↩

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。