検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
img_for_tre_tron

Tré Thộn を食べたことがありますか?
ベトナム・ビンズオン滞在中の方は是非注文して食べてみて!
絶対に美味しいです!
ホーチミン市内へも配達可能です。お問い合わせください。

Have you ever had "Tré Thộn" before?
If you're living at Bình Dương in Vietnam, you "must" try to order and eat it.
I'm sure you're very surprised how delicious it is!!
If you're in Hồ Chí Minh, you have a chance to get it too. Please call!!
>>
effective_python

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

Published 2020年7月23日18:23 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

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


今回はメタクラスの一般的な使い途の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__ を利用することで、「メタクラスの多重継承」を通常の記述で実現することが可能です。

この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Effective Python, 2nd Edition 】サブクラス定義に付随させて必ず行いたい操作がある場合は、メタクラス ( metaclass )、または、__init_subclass__ 特殊関数を利用してド忘れを防止しよう!
【Python 雑談・雑学】メタクラス ( metaclass ) とデコレータ ( decorator ) で遊んでみる、考えてみる!
【 Effective Python, 2nd Edition 】__set_name__ デスクリプタ専用特殊関数 ( special method for descriptor ) を利用してデスクリプタインスタンスを割り当てたクラス変数名を取得し、コードの冗長性を排除しよう!
【 Effective Python, 2nd Edition 】@classmethod ポリモーフィズム ( polymorphism ) を利用して、複数の派生クラスをよりジェネリック ( generic ) に活用しよう!
【 Effective Python, 2nd Edition 】引数として受け取った値を関数内で複数回「消費」する場合には要注意! イテレータ ( iterator ) とコンテナ ( container ) の違いをちゃんと認識しよう!
【 Effective Python, 2nd Edition 】ジェネレータ ( generator ) に値を注入したいなら、yield from 式と send() 関数の併用よりも、注入する値を提供するイテレータ ( iterator ) を渡しましょう、の巻
【 Effective Python, 2nd Edition 】クラス作成時の setter メソッド、getter メソッドの利用は最小限に。可能な限り public 属性によるインターフェース構築を目指しましょう