Python のクラスをハックして型付き immutable / protected 属性を作った
はじめに
Python のクラスの属性(attribute: メンバ、メソッド、インスタンス変数なども指す)は基本的に const にしたり private にしたりすることができませんし、型もありません。
- でもこの属性は一度定義したら書き換えたくないなー、とか
- この属性はユーザに勝手に書き換えられたくないなー、とか
- この属性は自動的にこの型に制約してほしいなー、とか
そんなときありませんか? ありますよね? そう、あるんです。
あったので作りました。
$ pip install nobus
で使えます。
参考文献
- dynamically defined properties in python
- Adding a method to an existing object instance in Python
- Avoiding infinite loops in
__getattribute__
[duplicate]
- Pythonのデコレータを理解するまで
Example
自分でクラスを定義するときに nobus.safeattr
の SafeAttrABC
を継承し、
-
immutable
: 一度定義したら書き換えられない(型チェック可能) -
protected
: 通常アクセスでは書き換えられない、隠蔽アクセスで書き換え可能(型チェック可能) -
typed
: アクセス方法については制御せず型チェックのみ実施
の3つから選んで使うだけです。
from nobus.safeattr import immutable, protected, typed, SafeAttrABC
class Person(SafeAttrABC):
def __init__(self, name, age):
# 通常通りの定義も可能
self.species = 'human'
fst, lst = name.split(' ')
# アクセス制御や型チェックが可能
self.first_name = immutable(fst, str)
self.last_name = protected(lst, str)
self.age = typed(age, int)
person = Person('Josh Nobus', 27)
immutable 属性: 一度定義したら書き換え不可能
# 通常アクセス
print(person.first_name)
# 隠蔽アクセス
print(person._first_name)
# 隠蔽された実体に対するアクセス
print(person._safeattr_first_name)
# 通常アクセスと隠蔽アクセスのどちらから書き換えてもエラー
person.first_name = 'Jane'
person._first_name = 'Jane'
Josh
Josh
Immutable(value='Josh', type_=<class 'str'>, optional=False)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-5-5888de4903e3> in <cell line: 11>()
9
10 # 通常アクセスと隠蔽アクセスのどちらから書き換えてもエラー
---> 11 person.first_name = 'Jane'
12 person._first_name = 'Jane'
...
/usr/local/lib/python3.9/dist-packages/nobus/safeattr.py in setter(self, value)
241 if isinstance(attr, Immutable):
242 # immutable を書き換えようとしているのでエラー
--> 243 raise AttributeError(f"immutable attribute '{name}' cannot be rewritten.")
244 elif isinstance(attr, Protected):
245 # protected に通常の名前でアクセスして書き換えようとしていたのでエラー
AttributeError: immutable attribute 'first_name' cannot be rewritten.
protected 属性: 隠蔽アクセスで書き換え可能
# 通常アクセス
print(person.last_name)
# 隠蔽アクセス
print(person._last_name)
# 隠蔽された実体に対するアクセス
print(person._safeattr_last_name)
# 隠蔽アクセスによる書き換えはエラーにならない
person._last_name = 'Washington'
print(person.last_name)
# 通常アクセスによる書き換えはエラー
person.last_name = 'Washington'
Nobus
Nobus
Protected(value='Nobus', type_=<class 'str'>, optional=False)
Washington
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-b55af4fef567> in <cell line: 15>()
13
14 # 通常アクセスによる書き換えはエラー
---> 15 person.last_name = 'Washington'
...
/usr/local/lib/python3.9/dist-packages/nobus/safeattr.py in setter(self, value)
244 elif isinstance(attr, Protected):
245 # protected に通常の名前でアクセスして書き換えようとしていたのでエラー
--> 246 raise AttributeError(f"protected attribute '{name}' cannot be rewritten.")
247
248 if not isinstance(attr, Typed):
AttributeError: protected attribute 'last_name' cannot be rewritten.
typed 属性: 代入時に自動で型チェック
# 通常アクセス
print(person.age)
# 隠蔽アクセス
print(person._age)
# 隠蔽された実体に対するアクセス
print(person._safeattr_age)
# 通常アクセス、隠蔽アクセスどちらで書き換えてもエラーにならない
person.age = 28
print(person.age)
person._age = 29
print(person.age)
# ただし型チェックが入っており、
# 指定した型と異なるとエラーになる
person.age = 'dog'
27
27
Typed(value=27, type_=<class 'int'>, optional=False)
28
29
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-12-8b8aa9b18d2f> in <cell line: 19>()
17 # ただし型チェックが入っており、
18 # 指定した型と異なるとエラーになる
---> 19 person.age = 'dog'
...
/usr/local/lib/python3.9/dist-packages/nobus/safeattr.py in typecheck(value, type_, optional)
35 if isinstance(value, type_):
36 return True
---> 37 raise TypeError(f"value must be instance of {type_} but actual type {type(value)}.")
38
39 return
TypeError: value must be instance of <class 'int'> but actual type <class 'str'>.
仕組み
SafeAttrABC
クラスが、継承された子クラスの __setattr__()
メソッドと __getattribute__()
メソッドをジャックして、インスタンスの属性に対するアクセスを監視しています。
immutable
, protected
, typed
などの関数で作られる変数はそれぞれ Immutable
, Protected
, Typed
というクラスのインスタンスになっていて、Immutable
と Protected
は Typed
の子クラスです。Immutable
と Protected
も型チェック可能です。
SafeAttrABC
は通常の属性、たとえば person.x
に対して Typed
クラスのインスタンス(Immutable
と Protected
も含む)が割り当てられそうになると、その実体を person._safeattr_x
に隠蔽して管理下に置きます[1]。
属性が管理下に置かれると、アクセスポイントとして person.x
, person._x
というプロパティが作成され、person._safeattr_x
から情報を読み取って型チェック&アクセス制御を行います。このおかげでユーザはあたかも person.x
に読み書きを行ったかのように感じます。
Python ではユーザから見える名前は person.x
のようにしておき、開発者がいじり回す実体には person._x
という名前をつける風習があります。型チェックやアクセス制御をしたくなるようなメンバにはどうせ ._x
を作るので、SafeAttrABC
は制御対象とするメンバに自動的に ._x
のアクセスポイントも作成します。
まとめると、アクセス方法は以下の3種類です。
アクセス方法 | 属性名 | 型チェック | 読取り可能 | 書込み可能 |
---|---|---|---|---|
通常アクセス | .x |
可 | 無制限 |
Typed のみ |
隠蔽アクセス | ._x |
可 | 無制限 |
Typed , Protected のみ |
実体アクセス | ._safeattr_x |
不可 | 無制限 | 無制限だけど書き換えてはダメ(管理できなくなるから) |
Usage
Immutable
と Protected
は Typed
から派生したクラスであり、アクセス制御が異なるだけで型チェックの機構は Typed
と同様です。したがって使い方の解説ではどれかひとつについてのみ解説し、他をハショることがあります。
インストール
$ pip install nobus
インポート
from nobus.safeattr import SafeAttrABC
アクセス制御済み属性の定義
名前が '_'
で始まらない通常の属性を定義するときに Typed
クラス、およびその派生クラスである Immutable
, Protected
のインスタンスが与えられると、その属性を管理対象とします。
視覚的ノイズを軽減するために immutable
, protected
, typed
という関数を用意しているのでそれを使うのがオススメです。また、名前空間を汚染したくないときは SafeAttrABC
に .immutable()
, .protected()
, .typed()
という staticmethod を定義してあるのでそれを使うとよいです。機能に違いはないので好きなものを使ってください。
from nobus.safeattr import SafeAttrABC
from nobus.safeattr import immutable, protected, typed
from nobus.safeattr import Immutable, Protected, Typed
class MyClass(SafeAttrABC):
def __init__(self):
# 管理対象とならない定義方法
self.species = 'human'
# 管理対象となる定義方法1(オススメ)
self.first_name = immutable('Josh')
self.last_name = protected('Nobus')
self.age = typed(27)
# 管理対象となる定義方法2(お好みで)
self.first_name = Immutable('Josh')
self.last_name = Protected('Nobus')
self.age = Typed(27)
# 管理対象となる定義方法3(名前空間を汚したくないとき)
self.first_name = self.immutable('Josh')
self.last_name = self.protected('Nobus')
self.age = self.typed(27)
inst.x = immutable(3)
のように属性が定義されると、SafeAttr は自動的に以下の属性を作成します。
self._safeattr_x
# self._x は作成されませんが存在しているように見えます
@property
def x(self):
...
@x.setter
def x(self, value):
...
def arg_x(self, value):
...
._safeattr_x
は SafeAttr が管理しているデータの実体であり、ユーザからの書き換えは(可能ですが)想定されていません。
._x
はクラスの開発者からのアクセスを想定したインターフェースになります。._x
という名前のメンバやプロパティは作成されませんが、SafeAttr によってアクセスをインターセプトされ、._safeattr_x
へリダイレクトされるようになります。この時点でユーザからは本物の ._x
が unreachable になります。もともと ._x
という名前の属性が存在していた場合は自動的に削除されます[2]。
.x
という名前の getter と setter が作成され、ユーザはあたかも定義した属性がそのまま存在しているように感じます。実際のアクセスは ._safeattr_x
へリダイレクトされます。
.arg_x()
の機能は型チェックの章で説明します。
-
.x
でアクセスしたとき、Immutable
,Protected
であれば書き換えが禁止されます。 -
._x
でアクセスしたとき、Immutable
であれば書き換えが禁止されます。 -
._safeattr_x
でアクセスしたとき、任意に書き換えが可能ですがユーザによる書き換えは想定されていません。 -
.arg_x()
は属性に対して書き換えを行いませんので任意に使用していただいて構いません。
型チェック
型チェックは以下のタイミングで自動的に実行されます。
-
Immutable
,Protected
,Typed
などの初期化時 - SafeAttr の管理下にある属性に代入が実行されたとき
型チェックには Python の isinstance
関数が用いられますが、ユーザ定義の TypeChecker
を用いることも可能です。
型を指定する場合はイニシャライザの第二引数に与えます。複数の型を許容する場合は、複数の型をタプルで与えます(isinstance
の仕様上、リストなどではダメなので注意)。属性が None
であることを許容する場合は第三引数に optional=True
を指定します。
あるいはユーザ定義の TypeChecker
を定義することも可能です。TypeChecker
はひとつの引数を取り、許容される型なら True
、それ以外は False
を返します。SafeAttr は TypeChecker
が False
を返したという情報しか知らないため、エラーメッセージがわかりにくい場合は TypeChecker
の中で自前の TypeError
を吐くように実装してください。
from nobus.safeattr import SafeAttrABC
from nobus.safeattr import typed
from nobus.safeattr import typechecker
@typechecker
def even(x):
if isinstance(x, int) and x % 2 == 0:
return True
return False
class MyClass(SafeAttrABC):
def __init__(self):
self.x = typed(1, (int, str))
self.y = typed(None, int, optional=True)
self.z = typed(6, even)
inst = MyClass()
# 型が合うので OK
inst.x = 5
inst.x = 'hoge'
inst.y = None
inst.y = 6
inst.z = 4
# 型が合わないので NG
inst.x = 3.0
inst.x = None
inst.y = '7'
inst.z = '9'
inst.z = 11
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-71-09c246687ae7> in <cell line: 13>()
11
12 # 型が合わないので NG
---> 13 inst.x = 3.0
14 inst.x = None
15
...
/usr/local/lib/python3.9/dist-packages/nobus/safeattr.py in typecheck(value, type_, optional)
35 if isinstance(value, type_):
36 return True
---> 37 raise TypeError(f"value must be instance of {type_} but actual type {type(value)}.")
38
39 return
TypeError: value must be instance of (<class 'int'>, <class 'str'>) but actual type <class 'float'>.
メソッドを呼び出すときに以下のような挙動になってほしいことがよくあります。
- 引数に None が与えられたら自身の属性で上書きする
- 引数に None 以外が与えられたらそのメソッド内では与えられた値を使う
- このとき型チェックが実行される
これを実現するのが SafeAttr の管理下にある属性 .x
に対して自動的に実装される .arg_x()
メソッドです。
from nobus.safeattr import SafeAttrABC
from nobus.safeattr import typed
class MyClass(SafeAttrABC):
def __init__(self):
self.x = typed('Bow!', str)
def hello(self, x=None):
x = self.arg_x(x)
print(x)
inst = MyClass()
inst.hello()
inst.hello(x='Meow!')
# 型エラーになります
inst.hello(x=5)
Bow!
Meow!
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-9884fd282648> in <cell line: 18>()
16
17 # 型エラーになります
---> 18 inst.hello(x=5)
...
/usr/local/lib/python3.9/dist-packages/nobus/safeattr.py in _typecheck(value, type_, optional)
35 if isinstance(value, type_):
36 return True
---> 37 raise TypeError(f"value must be instance of {type_} but actual type {type(value)}.")
38
39 return
TypeError: value must be instance of <class 'str'> but actual type <class 'int'>.
与えられた引数に型キャストを施すには第二引数にキャスト用の関数を与えます。
from nobus.safeattr import SafeAttrABC
from nobus.safeattr import typed
from pathlib import Path
class MyClass(SafeAttrABC):
def __init__(self):
self.x = typed(Path('Bow!'), (str, Path))
def hello(self, x=None):
x = self.arg_x(x, Path)
print(x, type(x))
inst = MyClass()
inst.hello()
inst.hello(x='Meow!')
Bow! <class 'pathlib.PosixPath'>
Meow! <class 'pathlib.PosixPath'>
第三引数の typecheck
を False
にすると、引数に対する型チェックは行いません。
from nobus.safeattr import SafeAttrABC
from nobus.safeattr import typed
class MyClass(SafeAttrABC):
def __init__(self):
self.x = typed('Bow!', str)
def hello(self, x=None):
x = self.arg_x(x, typecheck=False)
print(x)
inst = MyClass()
inst.hello()
inst.hello(x='Meow!')
# 型エラーになりません
inst.hello(x=5)
Bow!
Meow!
5
以上で SafeAttrABC
が持つ機能は全部になります。
解説
今回は以前作った kette
ほどの黒魔術はありません。Python の仕様のせいで妙なやり方になっている部分はありますが、基本的には地道な条件分岐です。個々の条件分岐は全体の整合性を取るためのものであり特に解説するようなことはないので、今回は実装のときに困った Python の仕様とそれを回避するためのテクニックについて紹介します。
マングリング機構と継承
当初は隠蔽するメンバは、アンダースコア2つを名前の先頭に持つマングリング機構によって隠蔽されたメンバを使おうと思っていました。これを使えるならばプライベート変数も作れたかもしれません。
class MyClass:
def __init__(self):
self.__x = 3
def show(self):
# ここからは見える
print(self.__x)
inst = MyClass()
# ここからは見えない
print(inst.__x)
表示してみるとわかりますが、.__x
というメンバはクラス定義の外からは ._MyClass__x
という名前で見えているので外で .__x
にアクセスしても AttributeError
になります(._MyClass__x
に直アクセスすることは可能です)。このようにして擬似的にプライベート変数のような挙動を実現する仕組みを Python のマングリング機構といいます。
print(dir(inst))
['_MyClass__x',
'__class__',
'__delattr__',
'__dict__',
...
pathlib
の Path
から派生した子クラスを作ろうとしたときに知ったのですが、このマングリング機構はクラスの継承の際にも有効になります。つまり他のプログラミング言語で言うところの private 変数に相当する機構になっていて、継承した子クラスからも隠蔽されてしまいます。
class MyClass:
def __init__(self):
self.__x = 3
def show(self):
# ここからは見える
print(self.__x)
class MyChildClass:
def __init__(self):
super().__init__()
def show(self):
# ここからは見えない
print(self.__x)
inst = MyChildClass()
# エラーになる
inst.show()
エラーを見てみればわかりますが、MyChildClass
内での .__x
は ._MyChildClass__x
に変換されてしまうので、MyClass
内で定義された ._MyClass__x
とは名前が食い違ってしまいます。
この性質は SafeAttrABC
という抽象クラスを継承することでメンバに対するアクセス制御を実現する機構とは相性が悪かったです。ユーザから隠蔽された実体にアクセスしにくくするためには .__x
という名前にしたほうがよかったのですが、それだと継承先の子クラスで動作しなくなるので実体は ._safeattr_x
のような名前にせざるを得ませんでした。
__setattr__()
をオーバーライドするときの注意
__setattr__()
は以下のようにクラスにメンバを追加するとき(代入時)に呼び出されるメソッドです。
self.x = 2
inst.x = 3
setattr(self, 'x', 5)
setattr(inst, 'x', 7)
__setattr__()
メソッドをオーバーライドするときは、注意しないとクラスが機能不全を起こしたり無限再帰になってエラーになったりします。例を示します。
class MyClass:
def __setattr__(self, name, value):
pass
inst = MyClass()
inst.x = 3
# メンバが追加されておらず AttributeError になる
print(inst.x)
これは inst.x = 3
のときに __setattr__()
をインターセプトして何もしていないために、メンバの追加が不可能になっています。かといって以下のように、与えられた値を文字列に変換して自身にセットしようとすると __setattr__()
が無限に再帰的に呼び出されてやはりエラーになります。
class MyClass:
def __setattr__(self, name, value):
value = str(value)
setattr(self, name, value)
inst = MyClass()
# maximum recursion error になる
inst.x = 3
これを回避するには、__setattr__()
の中では親クラスの __setattr__()
を呼び出すようにしないといけません。
class MyClass:
def __setattr__(self, name, value):
if isinstance(value, int):
value = str(value)
# 自分のではなく親クラスの __setattr__() を呼び出す
super().__setattr__(name, value)
inst = MyClass()
inst.x = 3
print(inst.x)
print(type(inst.x))
3
<class 'str'>
意図した通りの動作になりました。基本的には監視対象としたいメンバに対するアクセスや、監視したい型の値が入ってこない限りは何もせずに親クラスの __setattr__()
に渡すべきです。
__getattribute__()
をオーバーライドするときの注意
__getattribute__()
は __setattr__()
と同様に、メンバへの属性アクセス(参照時)に呼び出されるメソッドです。__getattr__()
という似たような名前の特殊メソッドも存在し、こちらはデフォルトの属性アクセスが失敗したときのみ呼び出されるという違いがあります。
__getattribute__()
は __setattr__()
同様に親クラスの __getattribute__()
を呼び出さないとクラスが機能不全になります。一方で __getattr__()
は検索している属性が存在するならば呼び出されないので、実装に失敗してもクラスが機能不全になることはありません。
いずれにせよインスタンスが保持する情報にアクセスしようとすると再帰の可能性が生じるため __setattr__()
よりも無限再帰の回避が難しいです。
たとえば「インスタンスにその属性が存在するかどうかをチェック」しただけでも無限再帰になりえます。
class MyClass:
def __init__(self):
self.x = 3
def __getattribute__(self, name):
pass
inst = MyClass()
# __getattribute__() 常に None を返すので機能不全になる
print(inst.x)
class MyClass:
def __init__(self):
self.x = 3
def __getattribute__(self, name):
if hasattr(self, name):
print(f"'{name}' という名前の属性を持っています")
inst = MyClass()
# hasattr が __getattribute__() を呼び出すので無限再帰する
print(inst.x)
class MyClass:
def __init__(self):
self.x = 3
def __getattr__(self, name):
if hasattr(self, name):
print(f"'{name}' という名前の属性を持っています")
inst = MyClass()
# このメンバは発見できるので __getattr__() が呼び出されず機能不全にはならない
print(inst.x)
# hasattr が __getattr__() を呼び出すので無限再帰する
print(inst.y)
これは hasattr
が「getattr
を呼び出したときに AttributeError
が発生するかどうかで属性を持っているかどうかを判断している」という仕様によるものです実装したやつを殴ったほうがいい[3]。
したがって再帰させないように属性の存在確認を行うには、属性に対してアクセスしてみて AttributeError
が発生しないかどうかチェックするという hasattr
相当のことを自分でやる必要があります。
class MyClass:
def __init__(self):
self.x = 1
self._y = 2
def __getattribute__(self, name):
try:
attr = super().__getattribute__(name)
print(f"'{name}' という名前の属性を持っています")
return attr
except AttributeError:
_name = '_' + name
print(f"'{name}' という名前の属性はありません")
print(f"代わりに '{_name}' という名前の属性を探してみます")
return super().__getattribute__(_name)
inst = MyClass()
print(inst.x)
print(inst.y)
'x' という名前の属性を持っています
1
'y' という名前の属性はありません
代わりに '_y' という名前の属性を探してみます
2
__setattr__()
, __getattribute__()
は文字通りクラスの心臓部であり、ここがバグると継承したクラスはすべて心不全を起こして死ぬのでめちゃくちゃ気を遣います。
インスタンスに動的にメソッドを追加する
.arg_x()
メソッドは属性が SafeAttr
の管理下に置かれるときに動的にインスタンスに追加されています。これにもちょっとしたコツが必要です。
というのも関数をインスタンスに追加してもそれは自動的にメソッドにはならないからです。以下で inst.morning
はあくまでも関数であり、第1引数に自動的に self
が渡されることはありません。
def morning(self):
print('morning')
class MyClass:
def hello(self):
print('hello')
inst = MyClass()
# 単純に関数を追加しただけではメソッドにならない
inst.morning = morning
print(type(inst.morning))
print(type(inst.hello))
<class 'function'>
<class 'method'>
メソッドとして追加するには MethodType
に変換してから追加する必要があります。
from types import MethodType
def morning(self):
print('morning')
class MyClass:
def hello(self):
print('hello')
inst = MyClass()
# inst のメソッドにする
inst.morning = MethodType(morning, inst)
print(type(inst.morning))
print(type(inst.hello))
inst.morning()
inst.hello()
<class 'method'>
<class 'method'>
morning
hello
インスタンスに動的にプロパティを追加する
メソッドでさえちょっとした面倒がありましたが、プロパティの追加はもっと厄介です。まず Python のプロパティはクラスに結びついたものであるため、インスタンスを変更しただけでプロパティを追加することはできません。
つまりプロパティを追加するにはもとのクラスを書き換える必要があるのですが、これをやってしまうとそのクラスのインスタンスすべてに影響が及んでしまいます。
以下はクラスに動的にプロパティを追加する例ですが、複数のインスタンスにプロパティの追加が反映されてしまうことがわかります。
class MyClass:
def create_new_property(self, name):
def getter_of(name):
def getter(self):
print(f"'{name}' の getter が呼び出されました")
return getattr(self, '_' + name)
return getter
# getter をセットする
setattr(self.__class__, name, property(getter_of(name)))
inst = MyClass()
# inst.x に対する getter をセット
inst.create_new_property('x')
# 隠蔽した属性をセット
inst._x = 3
# getter が呼び出される
print(inst.x)
'x' の getter が呼び出されました
3
class MyClass:
def create_new_property(self, name):
def getter_of(name):
def getter(self):
print(f"'{name}' の getter が呼び出されました")
return getattr(self, '_' + name)
return getter
# getter をセットする
setattr(self.__class__, name, property(getter_of(name)))
inst1 = MyClass()
inst2 = MyClass()
# inst1.x にのみ getter をセットしたつもり
inst1.create_new_property('x')
# 隠蔽した属性をセット
inst1._x = 1
inst2._x = 2
# inst2 にも getter がセットされてしまっている
print(inst1.x)
print(inst2.x) # ← こっちはエラーになってほしいが、ならない
'x' の getter が呼び出されました
1
'x' の getter が呼び出されました
2
これを防ぐためにはインスタンスの親クラスの情報を、親クラスから動的に派生させた子クラスで上書きするという荒技を使わなければなりません。
型の情報を取得するために使われる type
関数ですが、実は3つ変数を与えることで子クラスを派生させることができます。第1引数には子クラスの名前、第2引数には親クラスのタプル、第3引数には追加する属性を辞書で与えます。
class MyClass:
pass
new_class = type('MyChildClass', (MyClass, ), {})
print(new_class.__name__)
print(new_class)
MyChildClass
<class '__main__.MyChildClass'>
あとはこれを使って親クラスをすり替えれば完璧です。今回は結果をわかりやすくするために派生させた子クラスに 'Child'
という名前を付加して結果をわかりやすくしていますが、親クラスとまったく同じ名前で派生させても正しく動作します(黒魔術みがある)。
class MyClass:
def __init__(self):
# 複数回派生させないための制御フラグ
self.is_derived_class = False
def create_new_property(self, name):
def getter_of(name):
def getter(self):
print(f"'{name}' の getter が呼び出されました")
return getattr(self, '_' + name)
return getter
# 親クラスの情報を書き換える
if not self.is_derived_class:
cls = self.__class__
self.__class__ = type(cls.__name__ + 'Child', (cls, ), {})
# getter をセットする
setattr(self.__class__, name, property(getter_of(name)))
inst1 = MyClass()
inst2 = MyClass()
# inst1.x にのみ getter をセットした
inst1.create_new_property('x')
# クラスが書きかわっていることを確認
print(inst1.__class__)
print(inst2.__class__)
# 隠蔽した属性をセット
inst1._x = 1
inst2._x = 2
# getter によるアクセス
print(inst1.x)
print(inst2.x) # ← こっちがエラーになる!!
<class '__main__.MyClassChild'>
<class '__main__.MyClass'>
'x' の getter が呼び出されました
1
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-52-0706c4d9b2f3> in <cell line: 37>()
35 # getter によるアクセス
36 print(inst1.x)
---> 37 print(inst2.x) # ← こっちがエラーになる!!
AttributeError: 'MyClass' object has no attribute 'x'
おしまい
そんなこんなで試行錯誤しながらできたのが safeattr
モジュールになります。この記事がみなさんの Python 黒魔術ライフの充実に繋がれば幸いです。
safeattr
モジュールもよかったら使ってね!!
Discussion