cover_effective_python

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

Tags: Python , Effective , metaclass , __init_subclass__

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

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


例えば、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__ 特殊関数を利用して実装するようにした方がコードが簡潔に、かつ、読み易くなります。

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

0 comments

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

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