- 原文作者:Carl Friedrich Bolz
- 译文出自:掘金翻译计划
- 译者:Zheaoli
- 校对者:Yuze Ma, Gran
シンプルなオブジェクトモデル#
Carl Friedrich Bolz はロンドン大学キングスカレッジの研究者で、動的言語の実装や最適化などの分野に没頭しています。彼は PyPy/RPython のコア開発者の一人であり、同時に Prolog、Racket、Smalltalk、PHP、Ruby などの言語にもコードを提供しています。彼の Twitter は@cfbolzです。
はじめに#
オブジェクト指向プログラミングは現在広く使用されているプログラミングパラダイムであり、多くの現代プログラミング言語によってサポートされています。ほとんどの言語はプログラマに似たオブジェクト指向のメカニズムを提供していますが、詳細を掘り下げると、それぞれの言語には多くの違いがあることがわかります。ほとんどの言語の共通点は、オブジェクト処理と継承メカニズムを持っていることです。しかし、クラスに関しては、すべての言語が完璧にサポートしているわけではありません。たとえば、Self や JavaScript のようなプロトタイプ継承の言語にはクラスの概念がなく、継承の振る舞いはオブジェクト間で発生します。
異なる言語のオブジェクトモデルを深く理解することは非常に興味深いことです。これにより、異なるプログラミング言語の類似性を楽しむことができます。このような経験は、新しい言語を学ぶ際に既存の経験を活用し、迅速に習得するのに役立ちます。
この記事では、シンプルなオブジェクトモデルを実装する方法を紹介します。まず、シンプルなクラスとそのインスタンスを実装し、そのインスタンスを通じていくつかのメソッドにアクセスできるようにします。これは、Simula 67 や Smalltalk などの初期のオブジェクト指向言語で採用されているオブジェクトモデルです。その後、このモデルを段階的に拡張していきます。次の 2 つのステップでは、異なる言語のモデル設計の考え方を示し、最後のステップではオブジェクトモデルのパフォーマンスを最適化します。最終的に得られるモデルは、実際に存在する言語のモデルではありませんが、強いて言えば、得られた最終モデルを低スペック版の Python オブジェクトモデルと見なすことができます。
この記事で示されるオブジェクトモデルはすべて Python で実装されています。コードは Python 2.7 および Python 3.4 で完璧に動作します。モデルの設計哲学をよりよく理解するために、この記事では設計したオブジェクトモデルの単体テストも用意しています。これらのテストコードは、py.test または nose を使用して実行できます。
正直なところ、オブジェクトモデルの実装言語として Python を選ぶのは良い選択ではありません。一般的に、言語の仮想マシンは C/C++ のようなより低レベルの言語に基づいて実装されており、実装中には多くの詳細に注意を払う必要があります。とはいえ、Python のような非常にシンプルな言語は、実装の詳細に悩まされることなく、異なる振る舞いに主な焦点を当てることを可能にします。
基本メソッドモデル#
私たちは Smalltalk で実装された非常にシンプルなオブジェクトモデルから始めて、私たちのオブジェクトモデルを説明します。Smalltalk は、Xerox PARC の Alan Kay が率いるグループによって 1970 年代に開発されたオブジェクト指向言語です。Smalltalk はオブジェクト指向プログラミングを普及させ、今日のプログラミング言語にも当時の多くの特徴が見られます。Smalltalk の核心的な設計原則の一つは「すべてはオブジェクトである」ということです。Smalltalk の最も広く知られている後継者は Ruby であり、C 言語の構文を使用しながら Smalltalk のオブジェクトモデルを保持している言語です。
この部分では、私たちが実装するオブジェクトモデルにはクラス、インスタンス、属性の呼び出しと変更、メソッドの呼び出しが含まれ、サブクラスの存在も許可されます。始める前に、ここでのクラスはそれぞれ独自の属性とメソッドを持つ通常のクラスであることを宣言します。
良い習慣は、具体的な実装の振る舞いを制約するために、最初にテストコードを書くことです。この記事で書かれたテストコードは 2 つの部分から構成されています。第一部は通常の Python コードで構成され、Python のクラスやその他の高度な機能を使用する可能性があります。第二部は、私たちが自分で作成したオブジェクトモデルを使用して Python のクラスを置き換えます。
テストコードを書く際には、通常の Python クラスと自作クラスの間のマッピング関係を手動で維持する必要があります。たとえば、私たちの自作クラスではobj.read_attr("attribute")
を Python のobj.attribute
の代わりに使用します。現実の生活では、このようなマッピング関係は言語のコンパイラ / インタプリタによって実現されます。
この記事では、モデルをさらに簡素化しており、オブジェクトモデルを実装するコードとオブジェクト内のメソッドを書くコードがほとんど同じに見えるようにしています。現実の生活では、これは基本的に不可能であり、一般的にはこれらは異なる言語によって実装されます。
まず、オブジェクトフィールドの読み取りと変更をテストするためのコードを書いてみましょう:
def test_read_write_field():
# Pythonコード
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
# オブジェクトモデルコード
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
上記のテストコードには、私たちが実装しなければならない 3 つのものが含まれています。Class
およびInstance
クラスは、それぞれオブジェクト内のクラスとインスタンスを表します。また、ここには 2 つの特別なクラスのインスタンスがあります:OBJECT
とTYPE
。OBJECT
は Python の継承システムの起点であるobject
クラスに対応しています(訳注:Python 2.x バージョンでは、実際には 2 つのクラスシステムがあり、一つはnew style classと呼ばれ、もう一つはold style classと呼ばれ、object
はnew style classの基底クラスです)。TYPE
は Python の型システムのtype
に対応しています。
Class
およびInstance
クラスのインスタンスに一般的な操作サポートを提供するために、これらのクラスはBase
クラスから継承し、一連のメソッドを実装します:
class Base(object):
""" すべてのオブジェクトモデルクラスが継承する基本クラス。 """
def __init__(self, cls, fields):
""" すべてのオブジェクトにはクラスがあります。 """
self.cls = cls
self._fields = fields
def read_attr(self, fieldname):
""" オブジェクトから'fieldname'フィールドを読み取る """
return self._read_dict(fieldname)
def write_attr(self, fieldname, value):
""" オブジェクトに'fieldname'フィールドを書き込む """
self._write_dict(fieldname, value)
def isinstance(self, cls):
""" オブジェクトがクラスclsのインスタンスである場合はTrueを返す """
return self.cls.issubclass(cls)
def callmethod(self, methname, *args):
""" オブジェクトの'methname'メソッドを'args'引数で呼び出す """
meth = self.cls._read_from_class(methname)
return meth(self, *args)
def _read_dict(self, fieldname):
""" オブジェクトの辞書から'fieldname'フィールドを読み取る """
return self._fields.get(fieldname, MISSING)
def _write_dict(self, fieldname, value):
""" オブジェクトの辞書に'fieldname'フィールドを書き込む """
self._fields[fieldname] = value
MISSING = object()
Base
はオブジェクトクラスのストレージを実装し、オブジェクトフィールドの値を保存するために辞書を使用します。現在、Class
およびInstance
クラスを実装する必要があります。Instance
のコンストラクタでは、クラスのインスタンス化とfields
およびdict
の初期化が行われます。言い換えれば、Instance
は単にBase
のサブクラスであり、追加のメソッドを提供することはありません。
Class
のコンストラクタは、クラス名、基底クラス、クラス辞書、およびメタクラスを受け取ります。クラスにとって、上記の変数はクラスの初期化時にユーザーによってコンストラクタに渡されます。また、コンストラクタはその基底クラスから変数のデフォルト値を取得します。この点については、次の章で説明します。
class Instance(Base):
""" ユーザー定義クラスのインスタンス。 """
def __init__(self, cls):
assert isinstance(cls, Class)
Base.__init__(self, cls, {})
class Class(Base):
""" ユーザー定義クラス。 """
def __init__(self, name, base_class, fields, metaclass):
Base.__init__(self, metaclass, fields)
self.name = name
self.base_class = base_class
また、あなたはこの点に気づくかもしれませんが、クラスは依然として特別なオブジェクトであり、間接的にBase
から継承しています。したがって、クラスも特別なクラスの特別なインスタンスであり、この特別なクラスはメタクラスと呼ばれます。
これで、最初のテストを無事に通過することができます。しかし、ここではまだType
およびOBJECT
という 2 つのClass
のインスタンスを定義していません。これらについては、Smalltalk のオブジェクトモデルに従って構築することはありません。なぜなら、Smalltalk のオブジェクトモデルは私たちにとってあまりにも複雑だからです。代わりに、ObjVlisp1 の型システムを採用します。Python の型システムはここから多くの要素を吸収しています。
ObjVlisp のオブジェクトモデルでは、OBJECT
とTYPE
は混在しています。OBJECT
はすべてのクラスの母クラスであり、つまりOBJECT
には母クラスがありません。TYPE
はOBJECT
のサブクラスです。一般的に、各クラスはTYPE
のインスタンスです。特定の状況では、TYPE
とOBJECT
の両方がTYPE
のインスタンスです。しかし、プログラマはTYPE
からクラスを派生させてメタクラスとして使用することができます:
# Pythonのように基本階層を設定する(ObjVLispモデル)
# 最終的な基底クラスはOBJECT
OBJECT = Class(name="object", base_class=None, fields={}, metaclass=None)
# TYPEはOBJECTのサブクラス
TYPE = Class(name="type", base_class=OBJECT, fields={}, metaclass=None)
# TYPEは自分自身のインスタンス
TYPE.cls = TYPE
# OBJECTはTYPEのインスタンス
OBJECT.cls = TYPE
新しいメタクラスを書くには、TYPE
から自分で派生する必要があります。しかし、この記事ではそうすることはありません。私たちは各クラスのメタクラスとしてTYPE
を使用するだけです。
さて、最初のテストは完全に通過しました。次に、2 番目のテストを見てみましょう。このテストでは、オブジェクトの属性の読み書きが正常に行われるかをテストします。このコードは非常に書きやすいです。
def test_read_write_field_class():
# クラスもオブジェクトです
# Pythonコード
class A(object):
pass
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
isinstance
チェック#
これまでのところ、私たちはオブジェクトがクラスを持つという特性を利用していませんでした。次のテストコードでは、isinstance
を自動的に実装します。
def test_isinstance():
# Pythonコード
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)
# オブジェクトモデルコード
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):
""" クラスのメソッド解決順序を計算する """
if self.base_class is None:
return [self]
else:
return [self] + self.base_class.method_resolution_order()
def issubclass(self, cls):
""" selfはclsのサブクラスですか? """
return cls in self.method_resolution_order()
さて、コードを修正した後、テストは完全に通過することができます。
メソッド呼び出し#
前に構築したオブジェクトモデルには、メソッド呼び出しという重要な特性が欠けています。この章では、シンプルな継承モデルを構築します。
def test_callmethod_simple():
# Pythonコード
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 # サブクラスでも動作します
# オブジェクトモデルコード
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コード
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
# オブジェクトモデルコード
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)
基本属性モデルは、呼び出しプロセスを 2 つのステップに分けます:属性を探し、実行結果を返します:
method = obj.f
result = method(arg1, arg2)
次のテストで前述の違いを体験できます:
def test_bound_method():
# Pythonコード
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 # サブクラスでも動作します
# オブジェクトモデルコード
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):
""" 'fieldname'フィールドをオブジェクトから読み取る """
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):
""" オブジェクトの'methname'メソッドを'args'引数で呼び出す """
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 では、これらのメソッド名は 2 つのアンダースコアで始まり、終わります(例:__init__
)。特殊メソッドは、通常の操作をオーバーロードするために使用され、カスタム機能を提供することができます。したがって、これらの存在はオブジェクトモデルがさまざまなことを自動的に処理する方法を示すことができます。Python における関連する特殊メソッドの説明は、こちらのドキュメントを参照してください。
メタオブジェクトプロトコルという概念は Smalltalk によって導入され、その後 CLOS のような一般的な Lisp のオブジェクトモデルでも広く使用されています。この概念は、特殊メソッドの集合を含んでいます(注:ここでは coined3 のジョークを見つけられませんでしたので、校正者の方に参考にしていただければと思います)。
この章では、オブジェクトモデルに 3 つのメタ呼び出し操作を追加します。これらは、オブジェクトの読み取りおよび変更操作をより詳細に制御するために使用されます。最初に追加する 2 つのメソッドは__getattr__
と__setattr__
であり、これらのメソッド名は Python で同様の機能を持つ関数の名前に非常に似ています。
カスタム属性の読み書き操作#
__getattr__
メソッドは、属性が通常の方法で見つからない場合に呼び出されます。言い換えれば、インスタンス辞書、クラス辞書、親クラス辞書などのオブジェクトの中で対応する属性が見つからないときに、このメソッドが呼び出されます。私たちは、検索された属性の名前をこのメソッドの引数として渡します。初期の Smalltalk4 では、このメソッドはdoesNotUnderstand:
と呼ばれていました。
__setattr__
では、状況が少し異なるかもしれません。まず、属性を設定することは通常、新しい属性を作成することを意味します。このため、属性を設定する際に__setattr__
メソッドがトリガーされることがよくあります。__setattr__
の存在を保証するために、OBJECT
オブジェクト内に__setattr__
メソッドを実装する必要があります。これにより、対応する辞書に属性を書き込む操作が完了します。これにより、ユーザーは自分で定義した__setattr__
をOBJECT.__setattr__
メソッドに委任できます。
これら 2 つの特殊メソッドのテスト用例は以下の通りです:
def test_getattr():
# Pythonコード
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:
# 基本実装を呼び出す
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
# オブジェクトモデルコード
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:
# 基本実装を呼び出す
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
の 2 つのメソッドを修正する必要があります:
class Base(object):
...
def read_attr(self, fieldname):
""" 'fieldname'フィールドをオブジェクトから読み取る """
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):
""" 'fieldname'フィールドをオブジェクトに書き込む """
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 におけるディスクリプタの最も重要な使用シーンの一つはstaticmethod
、classmethod
、property
です。
次のテキストでは、ディスクリプタを使用してオブジェクトをバインドする方法を紹介します。私たちは__get__
メソッドを使用してこの目標を達成できます。具体的には、以下のテストコードを参照してください:
def test_get():
# Pythonコード
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
# オブジェクトモデルコード
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 のオブジェクトモデルを示しているわけではないからです。モデルをさらに改善することで、この問題を効果的に解決できます。
インスタンスの最適化#
このオブジェクトモデルの最初の 3 つの部分の構築には多くの振る舞いの変化が伴いましたが、最後の部分の最適化作業は振る舞いの変化を伴いません。この最適化手法はマップと呼ばれ、自己ブートストラップ可能な言語の仮想マシンで広く使用されています。これは、PyPy や V8 の現代 JavaScript 仮想マシンで適用される最も重要なオブジェクトモデルの最適化手段です(V8 ではこの方法はhidden classesと呼ばれます)。
この最適化手法は、次の観察に基づいています:現在実装されているオブジェクトモデルでは、すべてのインスタンスが完全な辞書を使用して属性を保存しています。辞書はハッシュテーブルに基づいて実装されており、これにより大量のメモリを消費します。多くの場合、同じクラスのインスタンスは同じ属性を持ちます。たとえば、Point
というクラスがあり、そのすべてのインスタンスが同じ属性x
とy
を含んでいます。
Map
最適化はこの事実を利用します。各インスタンスの辞書を 2 つの部分に分割します。一部はすべてのインスタンスで共有できる属性名を保存します。もう一部は、最初の部分から生成されたMap
の参照と具体的な値を保存します。属性名を保存するマップは、値のインデックスとして機能します。
上記の要件に対していくつかのテストケースを作成します:
def test_maps():
# 実装を検査するホワイトボックステスト
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
のmap
には、x
とy
の 2 つの属性が含まれ、p1
内での値はそれぞれ 0 と 1 です。次に、2 番目のインスタンス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_index
とnext_map
の 2 つのメソッドがあります。前者は、オブジェクトのストレージ内で属性名に対応するインデックスを検索するために使用されます。新しい属性がオブジェクトに追加される場合は、後者を使用する必要があります。この場合、異なるインスタンスは異なるマッピングを計算するためにnext_map
を使用する必要があります。このメソッドは、すでに存在するマッピングをnext_maps
で検索します。これにより、類似のインスタンスは類似のMap
オブジェクトを使用します。
Figure 14.2 - マップの遷移
map
を使用したInstance
の実装は次のようになります:
class Instance(Base):
""" ユーザー定義クラスのインスタンス。 """
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
をオーバーロードする必要があります。新しいインスタンスが作成されるとき、EMPTY_MAP
が使用され、ここにはオブジェクトが何も保存されていません。_read_dict
を実装すると、インスタンスのmap
から属性名のインデックスを検索し、対応するストレージテーブルをマッピングします。
辞書にデータを書き込む操作は 2 つのケースに分かれます。最初のケースは、既存の属性値の変更であり、これは単にマッピングリスト内の対応する値を変更するだけで済みます。もう一つのケースは、対応する属性が存在しない場合であり、この場合はmap
の変換が必要です(上記の図のように)。この場合、next_map
メソッドを呼び出し、新しい値をストレージリストに保存します。
あなたはおそらく、この最適化手法が何を最適化したのかを尋ねるかもしれません。一般的に、同じ構造のインスタンスが多数存在する場合、メモリをうまく最適化できます。しかし、これは普遍的な最適化手法ではないことを覚えておいてください。コード内に異なる構造のインスタンスがあふれている場合、この手法はより多くのスペースを消費する可能性があります。
これは動的言語の最適化における一般的な問題です。一般的に、コードを最適化してより速く、よりスペースを節約するための万能の方法を見つけることは不可能です。したがって、具体的な状況に応じて最適化手法を選択する必要があります。
Map
最適化の興味深い点は、ここではメモリ使用量が削減されるだけでなく、JIT 技術を使用する VM ではプログラムのパフォーマンスも向上する可能性があることです。これを実現するために、JIT 技術はマッピングを使用して属性のストレージ内のオフセットを検索します。これにより、辞書検索の方法を完全に排除します。
潜在的な拡張#
私たちのオブジェクトモデルを拡張し、異なる言語の設計選択を導入することは非常に簡単です。以下にいくつかの可能な方向性を示します:
-
最も簡単なのは、
__init__
、__getattribute__
、__set__
のような、非常に簡単に実装できて興味深い特殊メソッドを追加することです。 -
モデルを拡張して多重継承をサポートします。これを実現するために、各クラスには親クラスのリストが必要です。そして、
Class.method_resolution_order
を修正してメソッド検索をサポートする必要があります。単純な MRO 計算ルールは深さ優先原則を使用できます。より複雑なものはC3 アルゴリズムを採用できます。このアルゴリズムは、ダイヤモンド継承構造によって引き起こされる問題をより良く処理できます。 -
より大胆なアイデアは、プロトタイプモードに切り替えることで、これにはクラスとインスタンスの違いを排除する必要があります。
まとめ#
オブジェクト指向プログラミング言語設計の核心は、そのオブジェクトモデルの詳細です。シンプルなオブジェクトモデルをいくつか書くことは非常に簡単で面白いことです。この方法で、既存の言語の動作メカニズムを理解し、オブジェクト指向言語の設計原則を深く理解することができます。異なるオブジェクトモデルを作成して異なるオブジェクト設計の考え方を検証することは非常に素晴らしい方法です。あなたはもはや、コードの解析や実行などの些細なことに注意を向ける必要はありません。
このようにオブジェクトモデルを書く作業は、実践においても非常に有用です。実験品としてだけでなく、他の言語でも使用される可能性があります。このような例はたくさんあります。たとえば、C 言語で書かれた GObject モデルは、GLib や他の Gnome で使用されています。また、JavaScript で実装されたさまざまなオブジェクトモデルもあります。
参考文献#
-
P. Cointe, “Metaclasses are first class: The ObjVlisp Model,” SIGPLAN Not, vol. 22, no. 12, pp. 156–162, 1987.↩
-
属性ベースのモデルは概念的により複雑であるようです。なぜなら、メソッドの検索と呼び出しの両方が必要だからです。実際には、何かを呼び出すことは、特別な属性
__call__
を検索して呼び出すことによって定義されるため、概念的な単純さが回復されます。ただし、これはこの章では実装されません。↩ -
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.↩
-
Python では、2 番目の引数は属性が見つかったクラスですが、ここでは無視します。↩
-
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.↩
-
それがどのように機能するかは、この章の範囲を超えています。数年前に書いた論文でそれについて合理的に読みやすい説明を試みました。それは、基本的にこの章のもののバリアントであるオブジェクトモデルを使用しています: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.↩