cover_effective_python

【 Effective Python, 2nd Edition 】今回も懲りずにメタクラス ( metaclass ) - __init_subclass__() 特殊関数でメタクラスをもっと活用しよう! 投稿一覧へ戻る

Tags: Python , Effective , __new__ , metaclass , __init_subclass__ , super

Published 2020年7月23日18:23 by T.Tsuyoshi

前回 は、メタクラスでどんなことができるのか、どんな動作をしているのか、ということを少しでも理解しようとモガいてみましたが、今回も懲りずに続きです。


今回はメタクラスの一般的な使い途の1つである「サブクラス定義の検証」を見ていきたいと思います。


「多角形 ( multisided polygon ) 」を象徴するクラスを考えます。


各サブクラスでは「何」角形であるのかを示す sides 属性が定義されていなければならず、その値はベースクラスを除いて 3 以上でなければエラーを投げるようなメタクラスを定義します。


今回の例の Polygon クラス階層において、ValidatePolygon メタクラス によって作成される MyPolygon がベースクラスになります。


ですから、MyPlygon クラスの作成時に __new__() 特殊関数に渡される親クラスの羅列である bases パラメータの値は、空のタプル、です。
この場合は検証を行わず、クラス ( 型オブジェクト ) の作成のみを行います。


class ValidatePolygon(type):
"""
このメタクラスを利用するベースクラスのサブクラスが
1: sides 属性を定義しているか?
2: sides 属性の値は 3 以上か?
を検証します
"""

def __new__(mcls, name, bases, attrs):
if bases:
if not (sides := attrs.get('sides', 0)):
raise NotImplementedError("'sides' 属性が定義されていないか、属性の値が不正ですよ!")

if sides < 3:
raise ValueError("'sides' 属性の値は 3 以上じゃないと多角形にならないよ!")

return type.__new__(mcls, name, bases, attrs)


class MyPolygon(metaclass=ValidatePolygon):
"""
このクラスから派生させるクラスには、多角形の辺の数を象徴する sides 属性を定義する必要があります。
sides 属性の値は 3 以上でなければいけません
"""

@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180


class Triangle(MyPolygon):
sides = 3


class Rectangle(MyPolygon):
sides = 4


print(Triangle.interior_angles())

# 180


print(Rectangle.interior_angles())

# 360



ここでもし、sides 属性を定義しないサブクラスを作ってしまうと...


class NoSidesAttr(MyPolygon):
dif_attr = 12

# Traceback (most recent call last):
# NotImplementedError: 'sides' 属性が定義されていないか、属性の値が不正ですよ!



怒られます。


今度は辺の数を 2 としてクラスを定義してみます。


class Line(MyPolygon):
sides = 2

# Traceback (most recent call last):
# ValueError: 'sides' 属性の値は 3 以上じゃないと多角形にならないよ!



ちゃんと怒られます。


そしてメタクラスの __new__ 特殊関数内に記述したクラス検証は、各クラス定義ブロック処理直後に実行されますから、もしエラーが見つかれば、クラス定義ブロックが終了すると同時にエラーとなります。


つまり、通常のクラスの __init__() 関数内で検証を行う場合と異なり、プログラム自体が開始されることなくエラー発生を知らせてくるんですね。


print("クラス定義前")


class Line(MyPolygon):
print("'sides' 属性定義前")
sides = 2
print("'sides' 属性定義後")


print("クラス定義後") # このステートメント以前にエラーが投げられるので、実行されません

# クラス定義前
# 'sides' 属性定義前
# 'sides' 属性定義後
# Traceback (most recent call last):
# ValueError: 'sides' 属性の値は 3 以上じゃないと多角形にならないよ!



ただ、この例のようなことを実行するためだけにわざわざメタクラスを定義するのは何とも大袈裟です。


そこで Python 3.6 で組み込まれたのが __init_subclass__ 特殊関数です。


__init_subclass__ 特殊関数を利用すると、メタクラスを一切記述することなく同様の機能を実現することができちゃいます。


class BetterPolygon:
"""
このクラスのサブクラスが
1: sides 属性を定義しているか?
2: sides 属性の値は 3 以上か?
を検証します
"""

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if 'sides' not in cls.__dict__:
raise NotImplementedError("'sides' 属性を定義しなくちゃダメですよ!")

if cls.sides < 3:
raise ValueError("'sides' 属性の値は 3 以上じゃないと多角形にならないよ!")

@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180


class Pentagon(BetterPolygon):
sides = 5


print(Pentagon.interior_angles())

# 540



ちゃんとエラーも投げてくれます。


class NoSidesAttr(BetterPolygon):
dif_attr = 12

# Traceback (most recent call last):
# NotImplementedError: 'sides' 属性を定義しなくちゃダメですよ!



class Line(BetterPolygon):
sides = 2

# Traceback (most recent call last):
# ValueError: 'sides' 属性の値は 3 以上じゃないと多角形にならないよ!



いうなれば、メタクラスの機能をベースクラスに押し込んじゃった、という感じです。


メタクラスの記述が一切必要なくなったばかりか、クラス属性に cls インスタンスを利用してアクセス可能になるので、その値をわざわざ属性辞書から取得する必要もなくなっています。


さて、ちょっと戻りまして、メタクラスの短所、というか、制約をもう1つあげるとすると、クラス定義で継承可能なメタクラスは1つだけ、ということです。


例として「多角形を塗りつぶす色」を検証するもう1つのメタクラスを定義します。


class ValidateColor(type):
"""
このメタクラスを利用するベースクラスのサブクラスが
1: color 属性を定義しているか?
2: color 属性の値は 'red' もしくは 'green' か?
を検証します
"""

def __new__(mcls, name, bases, attrs):
if bases:
if (color := attrs.get('color', "")) == "":
raise NotImplementedError("'color' 属性が定義されていないか、属性の値が不正ですよ!")

if bases and color not in ('red', 'green'):
raise ValueError("'color' 属性の値は 'red' か 'green' じゃないとダメでーす!")

return type.__new__(mcls, name, bases, attrs)


class MyColor(metaclass=ValidateColor):
@classmethod
def color_length(cls):
return len(cls.color)


class Green(MyColor):
color = 'green'


print(Green.color_length())

# 5


class NoColorAttr(MyColor):
dif_attr = 'green'

# Traceback (most recent call last):
# NotImplementedError: 'color' 属性が定義されていないか、属性の値が不正ですよ!



class InvalidColor(MyColor):
color = 'blue'

# Traceback (most recent call last):
# ValueError: 'color' 属性の値は 'red' か 'green' じゃないとダメでーす!



ここで、辺の数と多角形の色を同時に検証したいので、2つのメタクラスを共に継承するクラスを作成します。


class RedHexagon(MyPolygon, MyColor):
sides = 6
color = 'red'

# Traceback (most recent call last):
# TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases



だめなんです。


こういうときは、メタクラスを階層化する必要があります。


class ValidatePolygon(type):
"""
このメタクラスを利用するベースクラスのサブクラスが
1: sides 属性を定義しているか?
2: sides 属性の値は 3 以上か?
を検証します
"""

def __new__(mcls, name, bases, attrs):
if bases:
if not (sides := attrs.get('sides', 0)):
raise NotImplementedError("'sides' 属性が定義されていないか、属性の値が不正ですよ!")

if sides < 3:
raise ValueError("'sides' 属性の値は 3 以上じゃないと多角形にならないよ!")

return type.__new__(mcls, name, bases, attrs)


class ValidateColor(ValidatePolygon):
"""
このメタクラスを利用するベースクラスのサブクラスが
1: color 属性を定義しているか?
2: color 属性の値は 'red' もしくは 'green' か?
を検証します
"""

def __new__(mcls, name, bases, attrs):
if bases:
if (color := attrs.get('color', "")) == "":
raise NotImplementedError("'color' 属性が定義されていないか、属性の値が不正ですよ!")

if bases and color not in ('red', 'green'):
raise ValueError("'color' 属性の値は 'red' か 'green' じゃないとダメでーす!")

return ValidatePolygon.__new__(mcls, name, bases, attrs)


class TwoAttributesPolygon(metaclass=ValidateColor):
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180

@classmethod
def color_length(cls):
return len(cls.color)


class RedHeptagon(TwoAttributesPolygon):
color = 'red'
sides = 7


print(RedHeptagon.interior_angles())
# 900

print(RedHeptagon.color_length())
# 3



属性が定義されていないと...


class NoSidesAttribute(TwoAttributesPolygon):
color = 'red'

# Traceback (most recent call last):
# NotImplementedError: 'sides' 属性が定義されていないか、属性の値が不正ですよ!



class NoColorAttribute(TwoAttributesPolygon):
sides = 3

# Traceback (most recent call last):
# NotImplementedError: 'color' 属性が定義されていないか、属性の値が不正ですよ!



属性の値が不正だと...


class WrongSidesValue(TwoAttributesPolygon):
sides = 2
color = "green"

# Traceback (most recent call last):
# ValueError: 'sides' 属性の値は 3 以上じゃないと多角形にならないよ!



class WrongColorAttribute(TwoAttributesPolygon):
sides = 3
color = "blue"

# Traceback (most recent call last):
# ValueError: 'color' 属性の値は 'red' か 'green' じゃないとダメでーす!



ちゃんと期待通りの検証が行われていますね。


ただ、こうしてメタクラスを階層化して実現する必要がある場合にも、前出の __init_subclass__ 特殊関数を利用することでわざわざ階層化することもなく、複数のクラスを継承する際の通常の書式で記述することが可能です。


class BetterPolygon:
"""
このクラスのサブクラスが
1: sides 属性を定義しているか?
2: sides 属性の値は 3 以上か?
を検証します
"""

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if 'sides' not in cls.__dict__:
raise NotImplementedError("'sides' 属性を定義しなくちゃダメですよ!")

if cls.sides < 3:
raise ValueError("'sides' 属性の値は 3 以上じゃないと多角形にならないよ!")

@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180


class BetterColor:
"""
このクラスのサブクラスが
1: color 属性を定義しているか?
2: color 属性の値は 'red'、'green'、'rainbow' のいずれかか?
を検証します
"""

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if 'color' not in cls.__dict__:
raise NotImplementedError("'color' 属性を定義しなくちゃダメですよ!")

if cls.color not in ('red', 'green', 'rainbow'):
raise ValueError("'color' 属性の値は 'red' か 'green' か 'rainbow' じゃないとダメでーす!")

@classmethod
def color_length(cls):
return len(cls.color)



__init_subclass__ 特殊関数をこのような形で利用する際の注意点は、必ず super() 組み込み関数を使って親クラスの __init_subclass__ を呼び出すことです。


では、2つのクラスを継承して動作を確認してみましょう。


class RedOctagon(BetterPolygon, BetterColor):
sides = 8
color = 'rainbow'


print(RedOctagon.interior_angles())
# 1080

print(RedOctagon.color_length())
# 7



属性が定義されていないと...


class NoSidesAttribute(BetterPolygon, BetterColor):
color = 'red'

# Traceback (most recent call last):
# NotImplementedError: 'sides' 属性を定義しなくちゃダメですよ!



class NoColorAttribute(BetterPolygon, BetterColor):
sides = 3

# Traceback (most recent call last):
# NotImplementedError: 'color' 属性を定義しなくちゃダメですよ!



属性の値が不正だと...


class WrongSidesValue(BetterPolygon, BetterColor):
sides = 2
color = "green"

# Traceback (most recent call last):
# ValueError: 'sides' 属性の値は 3 以上じゃないと多角形にならないよ!



class WrongColorAttribute(BetterPolygon, BetterColor):
sides = 3
color = "blue"

# Traceback (most recent call last):
# ValueError: 'color' 属性の値は 'red' か 'green' か 'rainbow' じゃないとダメでーす!



__init_subclass__ と super() を併用することで、ダイヤモンド継承 ( diamond hierarchy ) のような複雑な継承関係であっても問題なく動作させることができます。


まとめ:

1: メタクラス内の __new__ 特殊関数は、サブクラス定義ブロック全体の処理終了後に実行されます。

2: メタクラスは、サブクラスが、実際に利用される「型オブジェクト」として作成される前の検証や変更に利用することができます。

3: __init_subclass__ 特殊関数を利用すると、メタクラス自体を記述することなくメタクラスと同様の機能を提供できるようになります。

4: __init_subclass__ を定義した場合は、その定義内で必ず super().__init_subclass__() を呼び出さなければなりません。

5: __init_subclass__ を利用することで、「メタクラスの多重継承」を通常の記述で実現することが可能です。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)