新しい型システムの探究
Python 3.0では、古いクラスが削除されています。Python 2.2で、組み込み型とユーザー定義型を統合する新しい形式のクラスが導入されましたが、後方互換性のために旧形式のクラスも残されました。Python 2.x(ただし2.2以上)では、クラスを次のように定義していました。
class OldStyleClass:
pass
class NewStyleClass(object):
pass
Python 3.0では、オブジェクトを継承する必要がなくなりました。旧形式のクラスではPythonの能力を十分に発揮できず、旧形式のクラスに適用できない新機能が数多くありました。これ以降の各セクションで、デコレータ、関数アノテーション、新しいメタクラス、および抽象基底クラスを詳しく見ていきます。
抽象基底クラス
抽象基底クラス(Abstract Base Class:ABC)機能は、Pythonのオブジェクトモデルを拡充し、クラスのメタデータを提供するという継続的な流れの一環です。抽象基底クラスを使うと、オブジェクトの機能(関数呼び出しのパラメータなど)を確実にテストできます。次に短い例を示します。
def foo(arg): assert isinstance(arg, dict) print arg['name']
foo()関数はarg引数に辞書としてアクセスする必要があるため、argがdictのインスタンスかどうかをチェックしています。しかし多くの場合、これには限界があります。次のように、__getitem__メソッドを定義しているクラスには、すべて辞書としてアクセスできるからです。
class A(object):
def __getitem__(key):
return len(key) * 'A'
>>> a = A()
>>> print a['123']
AAA
使う予定のすべての属性が存在するかどうかをテストすることも可能ですが、面倒で間違いが起きやすい上に、それでも正しいオブジェクトを扱っているという確信が得られないことがあります。
抽象基底クラスを使うと、この問題が見事に解決します。扱っているオブジェクトがABCに準拠していれば、クラスの構文上および(強制されない)セマンティック上のコントラクトである抽象クラスを割り当て、後からisinstance()やissubclass()を使ってオブジェクトをテストできます。
次に簡単な例を示します。例えば、宇宙船、惑星、小惑星、ミサイル、レーザー、その他いろいろなものが登場するスペースシューティングゲームを作成するとします。当然ながら、さまざまなオブジェクト同士の衝突を検出する必要があります。そのためには、コードでオブジェクトの位置や速度などのいくつもの属性にアクセスし、衝突後にオブジェクトがまだ「生きている」かどうかを確認する必要があります。そこで、必要なすべての属性とメソッドをオブジェクトがサポートしているかどうかをdetect_collision()関数で確認します。この関数を次に示します(メインロジックは省略します)。
def detect_collision(obj_1, obj_2): assert isinstance(obj_1, MovingObject) assert isinstance(obj_2, MovingObject) # Collision detection logic follows... ...
この関数ではisinstance()を使って、obj_1とobj_2の両方がMovingObjectのインスタンスであることを確認しています。MovingObjectは、detect_collision()に必要なメソッドと属性を定義しているABCです。MovingObjectの定義を次に示します。
from abc import ABCMeta, abstractmethod, abstractproperty
class MovingObject(metaclass=ABCMeta):
@abstractmethod
def is_alive(self):
return self._lifeCount > 0
@abstractmethod
def move(self, x, y):
pass
@abstractproperty
def speed(self):
pass
def get_position(self):
return self._position
def set_position(self, position):
self._position = position
position = abstractproperty(get_position, set_position)
最初に、新しいモジュールであるabcをインポートしています。このモジュールには、ABCMetaメタクラスと、ABCの抽象メソッドおよび抽象プロパティを示すために使われる@abstractmethodおよび@abstractpropertyデコレータが含まれています。抽象メソッドと抽象プロパティは、たとえABCが実装を提供している場合でも、ABCコントラクトを実装するクラスで実装しなければなりません。ただし、実装クラスはABCの実装をそのまま呼び出すことができます。これは、実装されたC++の純粋仮想関数に似ています(多くの人が、C++の純粋仮想関数は実装を持つことができないと誤解しています)。
MovingObjectクラスは、ABCMetaをメタクラスとして持っています(Python 3.0のメタクラスの新しい構文については後で説明します)。ABCMetaメタクラスはABCに必須ではありませんが、ABCの主要なメソッドである__instancecheck__()および__subclasscheck__()のデフォルトの実装を持っているので、これがあると非常に便利です。speedなどの読み取り専用プロパティは@abstractproperty「デコレータ」を使って宣言できますが、positionなどの読み書き可能プロパティはabstractproperty「クラス」を使って宣言しなければならないことに注意してください。
さて、MovingObject ABCを定義しましたが、このABCをインスタンス化しようとすると、次のエラーが表示されます。
>>> MovingObject() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class MovingObject with abstract methods is_alive, move, position, speed
エラーが起きるのは、これが抽象クラスだからです。MovingObjectのインスタンスを作成することはできません。しかし、MovingObjectは基底クラスの役割を果たします。例えば、MovingObjectを継承するSpaceshipクラスでは、次のようにis_alive()、move()、speed、positionの各抽象メソッドと抽象プロパティをすべて実装しなければなりません。
class Spaceship(MovingObject):
def __init__(self):
self._lifeCount = 1
self._speed = 5
self.position = (100, 100)
def speed(self):
return self._speed
def get_life_count(self):
return self._lifeCount
def is_alive (self):
return MovingObject.is_alive(self)
def move(self, x, y):
self.position = (self.position[0] + x, self.position[1] + y)
position = property(MovingObject.get_position, MovingObject.set_position)
サブクラスですべての抽象メソッドと抽象プロパティを実装しないと、そのサブクラスも抽象クラスと見なされます。このため、サブクラスをインスタンス化しようとすると、再びエラーメッセージが表示され、実装していない抽象メソッドや抽象プロパティの名前(次の例ではis_aliveとposition)が列挙されます。
class StillAbstractMovingObject(MovingObject):
def move(self):
pass
def speed(self):
pass
>>> StillAbstractMovingObject()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class StillAbstractMovingObject
with abstract methods is_alive, position
1つ問題となり得るのは、実装される抽象メソッドがABCの元のメソッドと同じシグネチャを持つ必要がないことです。Pythonではメソッド名しか確認されません。このため、ABCフレームワークはサブクラスが抽象コントラクトを完全に実装しているという保証にはなりません。
Python 3.0には、コンテナ、イテレータ、および数値用の一連のABCがあらかじめ組み込まれています。新しい数値型の階層とABCについては、後で詳しく説明します。collectionsモジュールにはコンテナおよびイテレータ用のABCの定義が含まれており、numbersモジュールでは数値用のABCが定義されています。都合のいいことに、どちらのモジュールでも、あるオブジェクトが必要な機能をサポートしているかどうかを確認できる抽象クラスの階層が定義されています。例えばcollectionsモジュールには、すべてのマッピング(辞書風)オブジェクトが従う必要があるMapping抽象クラスがあります。Mappingは、抽象メソッドの__len__、__iter__、__contains__を定義するSized、Iterable、およびContainer抽象クラスを継承します。Mappingクラス自体(リスト1を参照)は__getitem__という別の抽象メソッドを定義しており、この抽象メソッドで__contains__抽象メソッドを実装します。従って、Mappingには__getitem__、__iter__、__len__という未実装の抽象メソッド(__abstractmethods__属性に格納される)があることになります。インスタンス化できる具象マッピングオブジェクトでは、この3つのメソッドをすべて実装しなければなりません。
class Mapping(Sized, Iterable, Container)
Method resolution order:
Mapping
Sized
Iterable
Container
builtins.object
Methods defined here:
__contains__(self, key)
__eq__(self, other)
__getitem__(self, key)
__ne__(self, other)
get(self, key, default=None)
items(self)
keys(self)
values(self)
----------------------------------------------------------------------
Data and other attributes defined here:
__abstractmethods__ = frozenset(['__getitem__', '__iter__', '__len__']...
__hash__ = None
----------------------------------------------------------------------
Methods inherited from Sized:
__len__(self)
----------------------------------------------------------------------
Class methods inherited from Sized:
__subclasshook__(cls, C) from abc.ABCMeta
----------------------------------------------------------------------
Data descriptors inherited from Sized:
__dict__
dictionary for instance variables (if defined)
__weakref__
list of weak references to the object (if defined)
----------------------------------------------------------------------
Methods inherited from Iterable:
__iter__(self)
クラスは、この要件を満たし、isinstance()およびissubclass()呼び出しに正しく応答するために、必ずしもABCを継承する必要はありません。クラスと組み込み型を登録することもできます。例えば、組み込みのdictクラスはオブジェクトしか継承しませんが、次の照会に対してTrueを返します。
>>> issubclass(dict, collections.Mapping)
True
>>> issubclass(dict, collections.MutableMapping)
True
>>> isinstance({}, collections.Sized)
True
クラスの基底クラスのリストをいじりたくない場合は、登録が便利です。クラスをABCに登録する場合は注意が必要です。登録したクラスは、たとえ実際には必要な抽象メソッドや抽象プロパティを実装していなくても、issubclass()およびisinstance()のチェックに対して常にTrueを返すからです。例えば、必要な__len__および__getitem__抽象メソッドを実装していなくても、次のようにSpaceshipクラスはSequenceとして登録されます。
s = Spaceship() assert isinstance(s, collections.Sequence) == False collections.Sequence.register(Spaceship) assert isinstance(s, collections.Sequence) == True assert hasattr(s, '__len__') == False assert hasattr(s, '__getitem__') == False
詳細については、「PEP-3119」を参照ください。
