- 原文作者: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 は、施楽パーク研究所の Alan Kay が率いるグループによって 1970 年代に開発されたオブジェクト指向言語です。オブジェクト指向プログラミングを普及させ、今日のプログラミング言語にも当時の多くの特徴が見られます。Smalltalk のコア設計原則の一つは「すべてはオブジェクトである」です。Smalltalk の最も広く知られている後継者は Ruby であり、C 言語の構文を使用しながら Smalltalk のオブジェクトモデルを保持しています。
このセクションでは、実装するオブジェクトモデルにはクラス、インスタンス、属性の呼び出しと変更、メソッドの呼び出しが含まれ、サブクラスの存在も許可されます。始める前に、ここでのクラスはそれぞれ独自の属性とメソッドを持つ通常のクラスであることを宣言します。
良い習慣は、具体的な実装の振る舞いを制約するために、テストコードを優先して書くことです。この記事で書かれたテストコードは 2 つの部分で構成されています。最初の部分は通常の Python コードで構成され、Python のクラスやその他の高度な機能を使用する可能性があります。2 番目の部分では、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
クラスのインスタンスに一般的な操作サポートを提供するために、これらの 2 つのクラスは、さまざまなメソッドを提供する基底クラスである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 # __getattr__をテスト
obj.celsius = 40
assert obj.fahrenheit == 104
obj.fahrenheit = 86 # __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 # __getattr__をテスト
obj.write_attr("celsius", 40)
assert obj.read_attr("fahrenheit") == 104
obj.write_attr("fahrenheit", 86) # __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 でのディスクリプタの最も重要な使用シーンの 1 つは、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
には、attrs
が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 つのケースに分かれます。最初のケースは、既存の属性値を変更する場合で、これは単にマッピングされたリスト内の対応する値を変更するだけです。もう 1 つのケースは、対応する属性が存在しない場合で、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.↩