卡尔·弗里德里希·博尔茨是伦敦国王学院的研究员,他对各种动态语言的实现和优化有着广泛的兴趣。他是 PyPy/RPython 的核心作者之一,并参与了 Prolog、Racket、Smalltalk、PHP 和 Ruby 的实现工作。他在 Twitter 上的用户名是 @cfbolz。
面向对象编程是当今使用的主要编程范式之一,许多语言都提供某种形式的面向对象。虽然从表面上看,不同的面向对象编程语言为程序员提供的机制非常相似,但细节却可能大相径庭。大多数语言的共同点是存在对象以及某种继承机制。然而,类并非每种语言都直接支持。例如,在基于原型的语言(如 Self 或 JavaScript)中,类的概念并不存在,对象直接从彼此继承。
了解不同对象模型之间的差异可能会很有趣。它们通常揭示了不同语言之间的家族相似性。将新语言的模型置于其他语言的模型的背景下,既有助于快速理解新模型,也有助于更好地了解编程语言设计空间。
本章探讨了一系列非常简单的对象模型的实现。它从简单的实例和类开始,以及在实例上调用方法的能力。这是在 Simula 67 和 Smalltalk 等早期面向对象语言中建立的“经典”面向对象方法。然后逐步扩展该模型,接下来的两步探索不同的语言设计选择,最后一步提高对象模型的效率。最终的模型不是真实语言的模型,而是 Python 对象模型的理想化、简化版本。
本章介绍的对象模型将在 Python 中实现。该代码适用于 Python 2.7 和 3.4。为了更好地理解行为和设计选择,本章还将介绍该对象模型的测试。这些测试可以使用 py.test 或 nose 运行。
选择 Python 作为实现语言相当不切实际。一个“真正的”VM 通常是用 C/C++ 等低级语言实现的,需要对工程细节给予高度关注才能使其高效。然而,更简单的实现语言使得更容易专注于实际的行为差异,而不是陷入实现细节的泥潭。
我们将从一个极其简化的 Smalltalk 对象模型开始。Smalltalk 是一种面向对象编程语言,由 Alan Kay 的团队在 20 世纪 70 年代的 Xerox PARC 设计。它普及了面向对象编程,是当今编程语言中许多功能的来源。Smalltalk 语言设计的一个核心原则就是“一切皆对象”。Smalltalk 当今最直接的继承者是 Ruby,它使用更类似于 C 的语法,但保留了 Smalltalk 的大多数对象模型。
本节中的对象模型将拥有类及其实例、读取和写入对象属性的能力、在对象上调用方法的能力以及一个类成为另一个类的子类的能力。从一开始,类就将是完全普通的对象,它们本身可以具有属性和方法。
术语说明:在本章中,我将使用“实例”一词来表示“不是类的对象”。
一个好的起点是编写一个测试来指定要实现的行为。本章介绍的所有测试都将包含两个部分。首先,一些定义和使用几个类的普通 Python 代码,并使用 Python 对象模型的越来越高级的功能。其次,使用我们将要在本节中实现的对象模型(而不是普通的 Python 类)进行相应的测试。
在使用普通 Python 类和使用我们的对象模型之间进行映射将在测试中手动完成。例如,在 Python 中,我们不会写 obj.attribute
,而在对象模型中,我们将使用方法 obj.read_attr("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
类分别代表我们对象模型的类和实例。Class
类有两个特殊的实例:OBJECT
和 TYPE
。OBJECT
对应于 Python 中的 object
,是继承层次结构的最终基类。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
的构造函数接收要实例化的类,并初始化 fields
字典为空字典。除此之外,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
是所有类的基类,这意味着它没有基类。TYPE
是 OBJECT
的子类。默认情况下,每个类都是 TYPE
的实例。特别是,TYPE
和 OBJECT
都是 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
作为每个类的元类。
图 14.1 - 继承
现在第一个测试通过了。第二个测试检查在类上读取和写入属性是否有效。它很容易编写,并且立即通过。
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)
要检查一个对象 obj
是否是某个类 cls
的实例,只需检查 cls
是否是 obj
类或类本身的超类。要检查一个类是否另一个类的超类,需要遍历该类的超类链。当且仅当在该链中找到另一个类时,它才是超类。一个类的超类链(包括该类本身)称为该类的“方法解析顺序”。它可以很容易地递归计算
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
为了找到发送到对象的正确方法实现,我们遍历对象的类的“方法解析顺序”。在方法解析顺序中的某个类的字典中找到的第一个方法将被调用
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
虽然设置与方法调用的相应测试相同,但调用方法的方式不同。首先,在对象上查找具有方法名称的属性。该查找操作的结果是一个绑定方法,它封装了对象以及在类中找到的函数。接下来,使用调用操作2 调用该绑定方法。
要实现这种行为,我们需要更改 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 引入的,但 Common Lisp 的对象系统(如 CLOS)更多地使用它。这也是 *元对象协议*(用于特殊方法的集合)一词的由来3。
在本章中,我们将为我们的对象模型添加三个这样的元钩子。它们用于微调读取和写入属性时究竟发生了什么。我们将首先添加的特殊方法是 __getattr__
和 __setattr__
,它们与 Python 同名方法的行为非常相似。
当以常规方式找不到要查找的属性时,对象模型会调用 __getattr__
方法;即,既不在实例上也不在类上。它将要查找的属性的名称作为参数。__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__
没有在对象上定义,后者会导致 read_attr
的无限递归。
属性的写入完全委托给 __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 中描述符协议最重要的用例是 staticmethod
、classmethod
和 property
的实现。
在本小节中,我们将介绍描述符协议中处理对象绑定的子集。这是使用特殊方法 __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
在 FahrenheitGetter
实例在 obj
的类中被查找后,__get__
方法会在该实例上被调用。__get__
的参数是进行查找的实例5。
实现这种行为很容易。我们只需要更改 _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 中借用函数和方法,而不是使用对象模型来表示它们。一个更完整对象模型必须解决这个问题。
虽然对象模型的前三个变体关注的是行为变化,但在本节中,我们将研究一个没有任何行为影响的优化。这种优化称为 *映射*,它是由 Self 编程语言的 VM 首次提出的6。它仍然是最重要的对象模型优化之一:它被用于 PyPy 和所有现代 JavaScript VM,如 V8(该优化被称为 *隐藏类*)。
这种优化从以下观察结果开始:在目前实现的对象模型中,所有实例都使用完整的字典来存储它们的属性。字典是使用哈希映射实现的,占用大量的内存。此外,同一类的实例的字典通常具有相同的键。例如,给定一个类 Point
,其所有实例的字典的键可能都是 "x"
和 "y"
。
映射优化利用了这一事实。它有效地将每个实例的字典分成两部分。一部分存储键(映射),可以由所有具有相同属性名称集的实例共享。然后,实例只存储对共享映射的引用以及属性值列表(在内存中比字典紧凑得多)。映射存储属性名称到该列表中索引的映射。
这种行为的简单测试如下所示
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}
请注意,这与我们之前编写的测试不同。所有先前的测试只是通过暴露的接口测试了类的行为。相反,此测试通过读取内部属性并将其与预定义的值进行比较来检查 Instance
类的实现细节。因此,这个测试可以被称为 *白盒* 测试。
p1
的映射的 attrs
属性描述了实例的布局,其中包含两个属性 "x"
和 "y"
,它们存储在 p1
的 storage
的位置 0 和 1。创建第二个实例 p2
并按相同的顺序向其中添加相同的属性,将使其最终具有相同的映射。另一方面,如果添加了不同的属性,则映射当然不能共享。
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({})
映射有两个方法:get_index
和 next_map
。前者用于在对象的存储中查找属性名称的索引。后者在向对象添加新属性时使用。在这种情况下,对象需要使用不同的映射,这是由 next_map
计算的。该方法使用 next_maps
字典来缓存已创建的映射。这样,具有相同布局的对象最终也会使用相同的 Map
对象。
图 14.2 - 映射转换
使用映射的 Instance
实现如下所示
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
该类现在将 None
作为字段字典传递给 Base
,因为 Instance
将以另一种方式存储字典的内容。因此,它需要覆盖 _read_dict
和 _write_dict
方法。在实际实现中,我们将重构 Base
类,使其不再负责存储字段字典,但目前让实例存储 None
已经足够了。
新创建的实例从使用 EMPTY_MAP
开始,该映射没有属性,并且存储为空。为了实现 _read_dict
,实例的映射会查询属性名称的索引。然后返回存储列表中相应的条目。
写入字段字典有两种情况。一方面,可以更改现有属性的值。这是通过简单地更改存储中相应的索引来完成的。另一方面,如果属性还不存在,则需要进行 *映射转换* (图 14.2),方法是使用 next_map
方法。新属性的值被追加到存储列表中。
这种优化能实现什么?它优化了在许多具有相同布局的实例的常见情况下内存的使用。它不是一种通用的优化:创建具有截然不同属性集的实例的代码将比我们只使用字典具有更大的内存占用空间。
这是优化动态语言的常见问题。通常不可能找到在所有情况下都更快或使用更少内存的优化。在实践中,选择的优化适用于语言的 *典型* 使用方式,而可能使使用极其动态功能的程序的行为变差。
映射的另一个有趣方面是,虽然它们在这里只优化了内存使用,但在实际使用即时 (JIT) 编译器的 VM 中,它们也提高了程序的性能。为了实现这一点,JIT 使用映射将属性查找编译为对象存储中固定偏移量的查找,完全消除了所有字典查找7。
很容易扩展我们的对象模型并尝试各种语言设计选择。以下是一些可能性
最容易做到的是添加更多的特殊方法。一些简单而有趣的要添加的方法是 __init__
、__getattribute__
、__set__
。
该模型可以非常容易地扩展以支持多重继承。为此,每个类都将获得一个基类列表。然后,Class.method_resolution_order
方法需要进行更改以支持查找方法。可以使用深度优先搜索并删除重复项来计算一个简单的求解方法顺序。一个更复杂但更好的方法是 C3 算法,它在菱形多重继承层次结构的基部添加了更好的处理方式,并拒绝了不合逻辑的继承模式。
一个更激进的改变是切换到原型模型,这涉及消除类和实例之间的区别。
面向对象编程语言设计中的一些核心方面是其对象模型的细节。编写小型对象模型原型是一种简单有趣的方式,可以更好地理解现有语言的内部机制,并获得对面向对象语言设计空间的洞察。玩弄对象模型是试验不同语言设计理念的好方法,而无需担心语言实现中更枯燥的部分,例如解析和执行代码。
这些对象模型在实践中也可能有用,不仅仅是作为实验的工具。它们可以嵌入到其他语言中并从其他语言中使用。这种方法的例子很常见:用 C 语言编写的 GObject 对象模型,在 GLib 和其他 Gnome 库中使用;或者 JavaScript 中的各种类系统实现。
P. Cointe,“元类是一等公民:ObjVlisp 模型”,SIGPLAN Not,第 22 卷,第 12 期,第 156-162 页,1987。 ↩
似乎基于属性的模型在概念上更复杂,因为它需要方法查找和调用。在实践中,调用某个东西是通过查找和调用一个特殊的属性 __call__
来定义的,因此概念上的简单性就恢复了。不过,本章不会实现这一点。) ↩
G. Kiczales,J. des Rivieres 和 D. G. Bobrow,元对象协议的艺术。马萨诸塞州剑桥:麻省理工学院出版社,1991 年。 ↩
A. Goldberg,Smalltalk-80:语言及其实现。Addison-Wesley,1983 年,第 61 页。 ↩
在 Python 中,第二个参数是找到属性的类,但这里我们将忽略它。 ↩
C. Chambers,D. Ungar 和 E. Lee,“SELF 的高效实现,一种基于原型的动态类型面向对象语言”,OOPSLA,1989 年,第 24 卷。 ↩
它的工作原理超出了本章的范围。我尝试在我几年前写的一篇论文中对其进行了合理可读的描述。它使用一个对象模型,基本上是本章中模型的一个变体:C. F. Bolz,A. Cuni,M. Fijałkowski,M. Leuschel,S. Pedroni 和 A. Rigo,“元跟踪 JIT 中的运行时反馈,用于高效的动态语言”,面向对象语言、程序和系统实施、编译、优化的第六届研讨会论文集,纽约,纽约,美国,2011 年,第 9:1-9:8 页。 ↩