検索ガイド -Search Guide-

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

【 Effective Python, 2nd Edition 】サブクラス定義に付随させて必ず行いたい操作がある場合は、メタクラス ( metaclass )、または、__init_subclass__ 特殊関数を利用してド忘れを防止しよう! 投稿一覧へ戻る

Published 2020年7月25日18:42 by mootaro23

SUPPORT UKRAINE

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

メタクラスの用途として、ある属性の有無や属性値の値によってクラスの機能を変化させる、サブクラス定義に必要な属性が含まれているか、属性値は有効かを検証する、等がありますが、型を記録しておき将来的に復元可能にする、という操作にも最適です。


例えば、Python オブジェクトの「型紙」をパラメータの値も含めて JSON 形式にシリアル化しておき、将来的な復元に備えて保存したいとします。


そのための第一段階として、あるクラスのインスタンス化時のパラメータ値を JSON 形式にシリアル化しましょう。


import json


class Serializable:
def __init__(self, *args):
self.args = args

def __repr__(self):
cls_name = self.__class__.__name__
param_str = [f"'{name}'" for name in self.args]
return f"{cls_name}({', '.join(param_str)})"

def serialize(self):
return json.dumps({'class': self.__class__.__name__, 'args': self.args})


class User(Serializable):
def __init__(self, *args):
super().__init__(*args)


user = User('Nana', 'Saki', 'Yuka')


print(f"オブジェクト象徴文字列: {user}")

# オブジェクト象徴文字列: User('Nana', 'Saki', 'Yuka')


print(f"JSON 形式にシリアル化したパラメータ値: {user.serialize()}")

# JSON 形式にシリアル化したパラメータ値: {"class": "User", "args": ["Nana", "Saki", "Yuka"]}



ここで作成したシリアル化したデータから元のオブジェクトを復元できるようにします。


class Deserializable(Serializable):
def deserialize(self, data):
params = json.loads(data)
return self.__class__(*params['args'])


class BetterUser(Deserializable):
def __init__(self, *args):
super().__init__(*args)


before = BetterUser('Nana', 'Hana', 'Megu')


print(f"Before: {before}")

# Before: BetterUser('Nana', 'Hana', 'Megu')


data = before.serialize()


print(data)

# {"class": "BetterUser", "args": ["Nana", "Hana", "Megu"]}


after = before.deserialize(data)


print(f"After: {after}")

# After: BetterUser('Nana', 'Hana', 'Megu')



さて、何となく目的は達しているような気がしますが、実は Python オブジェクトを JSON 形式にシリアル化しているだけで、ほとんど使い途がないプログラムなんです。


だって、保存しておいた JSON データから Python オブジェクトを復元するときに何をやっていますか?


結局復元したいクラスのインスタンスの deserialize() メソッドにデータを渡してオブジェクトを取得しています。


もし deserialize() を @classmethod デコレータでクラスメソッドとして定義しておいたとしても、元のクラス ( BetterUser クラス ) を介してアクセスする必要がありますから、結局元のクラスをこちらが把握しておかなければいけない ことに変わりはありません。


それだったら、その都度クラスインスタンスを作ればいいじゃん、という話です。


やりたいことは、保存しておいた JSON データを一般的に利用可能な復元関数に渡せば元の Python オブジェクトを取得できる、ということなんです。


そのためにはどうしたらいいでしょう?


まずは、クラスのインスタンスメソッドとして定義している deserialize() を一般の関数として定義することです。


registry = {}


def deserialize(json_data):
params = json.loads(json_data)

class_name = params['class']
class_type = registry[class_name]

return class_type(*params['args'])



このときに、クラス名とクラスの実体を結び付けるための仕組みが必要です。


クラスの「型紙 (ここではクラス名とパラメータ) 」は JSON 形式にシリアル化して保存しておけますが、クラス本体をシリアル化して保存しておくことはできませんから。


そこで registry という名前の辞書を deserialize() と同じグローバルスコープ内に用意しておき、将来的に復元したいクラスを定義したらそこに登録しておくことにします。


クラスを登録するための関数も用意しましょう。


def register_class(class_type):
registry[class_type.__name__] = class_type



準備ができました。試してみましょう。


class EvenBetterUser(Serializable):
def __init__(self, *args):
super().__init__(*args)


register_class(EvenBetterUser)


class EvenBetterEmail(Serializable):
def __init__(self, *args):
super().__init__(*args)


register_class(EvenBetterEmail)


before_user = EvenBetterUser('Nana')
before_email = EvenBetterEmail('aa@bb.com', 'cc@dd.org')


print(f"Before User: {before_user}")

# Before User: EvenBetterUser('Nana')


print(f"Before Email: {before_email}")

# Before Email: EvenBetterEmail('aa@bb.com', 'cc@dd.org')


user_data = before_user.serialize()
email_data = before_email.serialize()


print(f"User data: {user_data}")

# User data: {"class": "EvenBetterUser", "args": ["Nana"]}


print(f"Email data: {email_data}")

# Email data: {"class": "EvenBetterEmail", "args": ["aa@bb.com", "cc@dd.org"]}



復元しましょう!


after_user = deserialize(user_data)
after_email = deserialize(email_data)


print(f"After User: {after_user}")

# After User: EvenBetterUser('Nana')


print(f"After Email: {after_email}")

# After Email: EvenBetterEmail('aa@bb.com', 'cc@dd.org')



おー、成功です、なんらクラスの指定をすることなく JSON データを渡すだけで元の Python オブジェクトを復元できました、パチパチ!!


図に乗って、もう1つクラスを作って復元してみましょう。


class EvenBetterCountry(Serializable):
def __init__(self, *args):
super().__init__(*args)


before_country = EvenBetterCountry('Taiwan', 'Mexico')


print(f"Before Country: {before_country}")

# Before Country: EvenBetterCountry('Taiwan', 'Mexico')


country_data = before_country.serialize()


print(f"Country Data: {country_data}")

# Country Data: {"class": "EvenBetterCountry", "args": ["Taiwan", "Mexico"]}


after_country = deserialize(country_data)

# Traceback (most recent call last):
# KeyError: 'EvenBetterCountry'



あっ、エラーです。あっ、そうかっ!


EvenBetterCountry クラスを定義した後で register_class() を呼んでクラスを登録しておくのを忘れていました。


このように、あれをやったらこれをして、これをしたらそれをしなければいけません、的な処理の流れの強制はエラーを生む温床 になり得ます。
やり忘れたら全てが機能しなくなっちゃいます。


では、Serializable クラスを継承したサブクラスを作成した場合には必ず register_class() で登録しなければいけない、というプログラムの意図を確実に実行するようにするにはどうしたらいいでしょう?


このような状況でもメタクラスが役に立ちます。


class MyMeta(type):
def __new__(mcls, name, bases, attrs):
class_type = type.__new__(mcls, name, bases, attrs)
register_class(class_type)
return class_type



Serializable クラスのメタクラスとして MyMeta クラスを指定することで、サブクラス作成時には必ず register_class() が呼び出されるようになります。


class Serializable(metaclass=MyMeta):
def __init__(self, *args):
self.args = args

def __repr__(self):
cls_name = self.__class__.__name__
param_str = [f"'{name}'" for name in self.args]
return f"{cls_name}({', '.join(param_str)})"

def serialize(self):
return json.dumps({'class': self.__class__.__name__, 'args': self.args})


class AutoRegisterUser(Serializable):
pass


before = AutoRegisterUser('Nana', 'Hana')


print(f"Before: {before}")

# Before: AutoRegisterUser('Nana', 'Hana')


data = before.serialize()


print(f"Data: {data}")

# Data: {"class": "AutoRegisterUser", "args": ["Nana", "Hana"]}


after = deserialize(data)


print(f"After: {after}")

# After: AutoRegisterUser('Nana', 'Hana')



AutoRegisterUser クラスを定義した後に register_class() を呼び出さなくてもちゃんと機能しています。


そして同様の機能を実現するためのよりよい実装方法は __init_subclass__ 特殊関数を利用することです。


そうすることでわざわざメタクラスを用意することなく、Serializable クラス内に __init_subclass__ 定義を追加するだけでOKになります。


class Serializable:
def __init_subclass__(cls):
super().__init_subclass__()
register_class(cls)

def __init__(self, *args):
self.args = args

def __repr__(self):
cls_name = self.__class__.__name__
param_str = [f"'{name}'" for name in self.args]
return f"{cls_name}({', '.join(param_str)})"

def serialize(self):
return json.dumps({'class': self.__class__.__name__, 'args': self.args})


class BetterAutoRegisterUser(Serializable):
pass


before = BetterAutoRegisterUser('Ichiro', 'Jiro', 'Saburo', 'Shiro')


print(f"Before: {before}")

# Before: BetterAutoRegisterUser('Ichiro', 'Jiro', 'Saburo', 'Shiro')


data = before.serialize()


print(f"Data: {data}")

# Data: {"class": "BetterAutoRegisterUser", "args": ["Ichiro", "Jiro", "Saburo", "Shiro"]}


after = deserialize(data)


print(f"After: {after}")

# After: BetterAutoRegisterUser('Ichiro', 'Jiro', 'Saburo', 'Shiro')



今回のメタクラスの利用方法は、サブクラス定義時におけるデコレータ ( decorator ) の適用し忘れ等、あるステップを踏む必要がある場合のド忘れによるエラー発生を抑える上で非常に効果があります。


まとめ:

1: サブクラスを定義した際に必ず実行するべき付随操作がある場合、メタクラス内でその操作を実行するようにしておくことでエラー発生を防ぐことができます。

2: 通常のメタクラス機能であれば __init_subclass__ 特殊関数を利用して実装するようにした方がコードが簡潔に、かつ、読み易くなります。

この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Effective Python, 2nd Edition 】今回も懲りずにメタクラス ( metaclass ) - __init_subclass__() 特殊関数でメタクラスをもっと活用しよう!
【 Effective Python, 2nd Edition 】__set_name__ デスクリプタ専用特殊関数 ( special method for descriptor ) を利用してデスクリプタインスタンスを割り当てたクラス変数名を取得し、コードの冗長性を排除しよう!
【Python 雑談・雑学】メタクラス ( metaclass ) とデコレータ ( decorator ) で遊んでみる、考えてみる!
【 Effective Python, 2nd Edition 】__getattr__、__getattribute__、__setattr__ メソッドを利用して、事前に定義していないインスタンス属性を操作しよう!
【 Effective Python, 2nd Edition 】クラスインスタンスを関数として利用可能にする __call__ 特殊関数を含んだクラスを定義してフック ( hook ) として利用することで、既存の API の機能拡張を計ろう!
【 Effective Python, 2nd Edition 】Python のスレッド ( thread ) はブロッキング I/O ( blocking I/O ) 対策で存在しています。決して並行処理 ( parallelism ) を実現するためではありません!
【 Effective Python, 2nd Edition 】引数として受け取った値を関数内で複数回「消費」する場合には要注意! イテレータ ( iterator ) とコンテナ ( container ) の違いをちゃんと認識しよう!