web-dev-qa-db-ja.com

Force / ensure pythonクラス属性を特定のタイプにする

Pythonでクラスメンバー変数を特定の型に制限するにはどうすればよいですか?


長いバージョン:

クラスの外部に設定されているいくつかのメンバー変数を持つクラスがあります。使用方法により、intまたはlistのいずれかの特定のタイプである必要があります。これがC++の場合、単純にプライベートにし、「set」関数で型チェックを行います。それが不可能であることを考えると、変数のタイプを制限して、間違ったタイプの値が割り当てられた場合に実行時にエラー/例外が発生するようにする方法はありますか?または、それらを使用するすべての関数内でそれらの型を確認する必要がありますか?

ありがとう。

36
thornate

他の答えのようにプロパティを使用することができます-したがって、「bar」などの単一の属性を制約し、整数に制約する場合は、次のようなコードを記述できます。

class Foo(object):
    def _get_bar(self):
        return self.__bar
    def _set_bar(self, value):
        if not isinstance(value, int):
            raise TypeError("bar must be set to an integer")
        self.__bar = value
    bar = property(_get_bar, _set_bar)

そして、これは動作します:

>>> f = Foo()
>>> f.bar = 3
>>> f.bar
3
>>> f.bar = "three"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in _set_bar
TypeError: bar must be set to an integer
>>> 

(プロパティを書き込む新しい方法もあります。「プロパティ」ビルトインをゲッターメソッドのデコレータとして使用しますが、私は上記のように古い方法を好みます)。

もちろん、クラスに多くの属性があり、すべての属性をこの方法で保護したい場合、詳細になり始めます。心配することはありません-Pythonのイントロスペクション機能により、これを最小限の行で自動化できるクラスデコレータを作成できます。

def getter_setter_gen(name, type_):
    def getter(self):
        return getattr(self, "__" + name)
    def setter(self, value):
        if not isinstance(value, type_):
            raise TypeError("%s attribute must be set to an instance of %s" % (name, type_))
        setattr(self, "__" + name, value)
    return property(getter, setter)

def auto_attr_check(cls):
    new_dct = {}
    for key, value in cls.__dict__.items():
        if isinstance(value, type):
            value = getter_setter_gen(key, value)
        new_dct[key] = value
    # Creates a new class, using the modified dictionary as the class dict:
    return type(cls)(cls.__name__, cls.__bases__, new_dct)

そして、あなたはただauto_attr_checkクラスデコレータとして、クラス本体で必要な属性を、属性が制約する必要がある型と等しくなるように宣言します。

...     
... @auto_attr_check
... class Foo(object):
...     bar = int
...     baz = str
...     bam = float
... 
>>> f = Foo()
>>> f.bar = 5; f.baz = "hello"; f.bam = 5.0
>>> f.bar = "hello"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: bar attribute must be set to an instance of <type 'int'>
>>> f.baz = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: baz attribute must be set to an instance of <type 'str'>
>>> f.bam = 3 + 2j
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: bam attribute must be set to an instance of <type 'float'>
>>> 
40
jsbueno

一般的に、これは@yakが彼のコメントで言及した理由のために良い考えではありません。基本的に、正しい属性/動作を持っているが、ハードコーディングした継承ツリーにない有効な引数をユーザーが提供することを防止しています。

免責事項は別として、あなたがしようとしていることのために利用可能ないくつかのオプションがあります。主な問題は、Pythonにはプライベート属性がないことです。したがって、_self._a_などの単純な古いオブジェクト参照がある場合、それを型チェックするセッターを提供したとしても、ユーザーがそれを直接設定しないことは保証できません。以下のオプションは、タイプチェックを実際に実施する方法を示しています。

__setattr__をオーバーライドする

このメソッドは、これを行う(ごく少数の)属性に対してのみ便利です。 ___setattr___メソッドは、ドット表記を使用して通常の属性を割り当てるときに呼び出されます。例えば、

_class A:
    def __init__(self, a0):
        self.a = a0
_

A().a = 32を実行すると、A().__setattr__('a', 32)内部 が呼び出されます。実際、_self.a = a0_の___init___も_self.__setattr___を使用します。これを使用して、型チェックを実施できます。

_ class A:
    def __init__(self, a0):
        self.a = a0
    def __setattr__(self, name, value):
        if name == 'a' and not isinstance(value, int):
            raise TypeError('A.a must be an int')
        super().__setattr__(name, value)
_

このメソッドの欠点は、チェックする各タイプごとに個別の_if name == ..._が必要なことです(または、特定のタイプの複数の名前をチェックする場合は_if name in ..._)。利点は、ユーザーが型チェックを回避することをほぼ不可能にする最も簡単な方法であるということです。

物件を作る

プロパティは、通常の属性を記述子オブジェクトに置き換えるオブジェクトです(通常はデコレータを使用して)。記述子には、基礎となる属性へのアクセス方法をカスタマイズする___get___および___set___メソッドを含めることができます。これは、対応する___setattr___のifブランチを取得し、その属性に対してのみ実行されるメソッドに配置するようなものです。以下に例を示します。

_class A:
    def __init__(self, a0):
        self.a = a0
    @property
    def a(self):
        return self._a
    @a.setter
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
        self._a = value
_

同じことを行うわずかに異なる方法は、@ jsbuenoの回答にあります。

この方法でプロパティを使用するのは気の利いた方法であり、ほとんどの問題は解決しますが、いくつかの問題があります。 1つは、タイプチェックをバイパスして、ユーザーが直接変更できる「プライベート」__a_属性があることです。これは、プレーンゲッターとセッターを使用する場合とほぼ同じ問題です。ただし、aは、舞台裏でセッターにリダイレクトする「正しい」属性としてアクセスできるため、ユーザーが混乱する可能性が低くなります。 __a_。 2番目の問題は、プロパティを読み書き可能にするための余分なゲッターがあることです。これらの問題は this の質問の主題です。

真のセッター専用記述子を作成する

このソリューションは、おそらく全体で最も堅牢です。上記の質問に対する 受け入れられた答え で提案されています。基本的に、取り除くことができないフリルと便利さを備えたプロパティを使用する代わりに、独自の記述子(およびデコレータ)を作成し、型チェックを必要とする属性にそれを使用します。

_class SetterProperty:
    def __init__(self, func, doc=None):
        self.func = func
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        return self.func(obj, value)

class A:
    def __init__(self, a0):
        self.a = a0
    @SetterProperty
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
        self.__dict__['a'] = value
_

セッターは、実際の値をインスタンスの___dict___に直接格納して、無限に再帰するのを防ぎます。これにより、明示的なゲッターを提供せずに属性の値を取得できます。記述子aには___get___メソッドがないため、___dict___で属性が見つかるまで検索が続行されます。これにより、すべてのセットが記述子/セッターを通過し、属性値に直接アクセスできるようになります。

このようなチェックを必要とする属性が多数ある場合、行_self.__dict__['a'] = value_を記述子の___set___メソッドに移動できます。

_class ValidatedSetterProperty:
    def __init__(self, func, name=None, doc=None):
        self.func = func
        self.__= name if name is not None else func.__name__
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        ret = self.func(obj, value)
        obj.__dict__[self.__name__] = value

class A:
    def __init__(self, a0):
        self.a = a0
    @ValidatedSetterProperty
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
_

更新

Python3.6は、ほとんどすぐに使用できるようにこれを行います。 https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements

TL; DR

型チェックを必要とする非常に少数の属性については、___setattr___を直接オーバーライドします。より多くの属性については、上記のようにセッターのみの記述子を使用します。この種のアプリケーションにプロパティを直接使用すると、解決するよりも多くの問題が発生します。

13
Mad Physicist

Python 3.5なので、 type-hints を使用して、クラス属性が特定のタイプであることを示すことができます。その後、 MyPy 継続的な統合プロセスの一環として、すべての型契約が順守されていることを確認します。

たとえば、次のPythonスクリプト:

class Foo:
    x: int
    y: int

foo = Foo()
foo.x = "hello"

MyPyは次のエラーを出します:

6: error: Incompatible types in assignment (expression has type "str", variable has type "int")

実行時に型を強制する場合は、 enforce パッケージを使用できます。 READMEから:

>>> import enforce
>>>
>>> @enforce.runtime_validation
... def foo(text: str) -> None:
...     print(text)
>>>
>>> foo('Hello World')
Hello World
>>>
>>> foo(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/william/.local/lib/python3.5/site-packages/enforce/decorators.py", line 106, in universal
    _args, _kwargs = enforcer.validate_inputs(parameters)
  File "/home/william/.local/lib/python3.5/site-packages/enforce/enforcers.py", line 69, in validate_inputs
    raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError: 
  The following runtime type errors were encountered:
       Argument 'text' was not of type <class 'str'>. Actual type was <class 'int'>.
4
ostrokach

注1:@Blckknghtは、公正なコメントをありがとうございます。あまりにも単純なテストスイートで再帰の問題を見逃しました。

注2:この答えは、Pythonの学習を始めたばかりのときに書いたものです。現時点では、Pythonの記述子を使用します。たとえば、 link1link2 を参照してください。

以前の投稿といくつかの考えのおかげで、クラス属性を特定のタイプに制限する方法のはるかにユーザーフレンドリーな方法を見つけたと思います。

まず最初に、型を普遍的にテストする関数を作成します。

def ensure_type(value, types):
    if isinstance(value, types):
        return value
    else:
        raise TypeError('Value {value} is {value_type}, but should be {types}!'.format(
            value=value, value_type=type(value), types=types))

次に、セッターを介してクラスでそれを使用して適用します。これは比較的単純で、特にプロジェクト全体にフィードするために別のモジュールにエクスポートした後はDRYに従うと思います。以下の例を参照してください。

class Product:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    @property
    def name(self):
        return self.__dict__['name']

    @name.setter
    def name(self, value):
        self.__dict__['name'] = ensure_type(value, str)

    @property
    def quantity(self):
        return self.quantity

    @quantity.setter
    def quantity(self, value):
        self.__dict__['quantity'] = ensure_type(value, int)

テストは妥当な結果を生成します。最初にテストを参照してください。

if __== '__main__':
    from traceback import format_exc

    try:
        p1 = Product(667, 5)
    except TypeError as err:
        print(format_exc(1))

    try:
        p2 = Product('Knight who say...', '5')
    except TypeError as err:
        print(format_exc(1))

    p1 = Product('SPAM', 2)
    p2 = Product('...and Love', 7)
    print('Objects p1 and p2 created successfully!')

    try:
        p1.name = -191581
    except TypeError as err:
        print(format_exc(1))

    try:
        p2.quantity = 'EGGS'
    except TypeError as err:
        print(format_exc(1))

そして、テスト結果:

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 35, in <module>
    p1 = Product(667, 5)
TypeError: Value 667 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 40, in <module>
    p2 = Product('Knights who say...', '5')
TypeError: Value 5 is <class 'str'>, but should be <class 'int'>!

Objects p1 and p2 created successfully!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 49, in <module>
    p1.name = -191581
TypeError: Value -191581 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 54, in <module>
    p2.quantity = 'EGGS'
TypeError: Value EGGS is <class 'str'>, but should be <class 'int'>!
2
CapedHero

C++で実行すると言ったとおりに実行できます。それらへの割り当てにセッターメソッドを実行させ、セッターメソッドにタイプをチェックさせます。 Python)の「プライベートステート」および「パブリックインターフェイス」の概念は、ドキュメントと慣習によって行われ、誰にもforceすることはほとんど不可能です変数を直接割り当てるのではなく、セッターを使用します。ただし、アンダースコアで始まる属性名を指定し、クラスを使用する方法としてセッターを文書化する場合は、それを行う必要があります(2つの__namesを使用しないでください)アンダースコア;実際に設計された状況(継承階層で属性名が衝突する場合)でない限り、ほとんどの場合、それは価値があるよりも厄介です。開発者は、内部名が何であるかを理解し、それらを直接使用することを支持して、文書化された方法でクラスを使用する簡単な方法を避けます;or開発者クラスは異常に動作し(Pythonの場合)、リストの代わりにカスタムリストのようなクラスを使用することを許可しません。

他の回答で説明したように、プロパティを使用してこれを行うことができますが、属性に直接割り当てているように見えます。


個人的に、私はPythonで役に立たないように型安全を強制する試みを見つけました。静的型チェックは常に劣っていると思いますが、あなたのPython 100%の時間で動作する変数。は実行時に例外を発生させるだけなので、プログラムに型エラーがないという保証を維持するのに効果的ではありません。

それについて考えてください。静的にコンパイルされたプログラムがエラーなしで正常にコンパイルされると、コンパイラーが検出できるすべてのバグが完全にないことがわかります(HaskellやMercuryなどの言語の場合、それは完全ではありませんが、かなり良い保証です; C++やJavaなどの言語の場合... meh)。

しかし、Pythonでは、タイプエラーは、実行された場合にのみ認識されます。これは、プログラムの完全な静的型の強制everywhereを取得できたとしても、実際に100%のコードカバレッジでテストスイートを定期的に実行する必要があることを意味しますプログラムに型エラーがないことを確認してください。ただし、完全なカバレッジで定期的にテストを実行した場合は、型を強制しようとせずに、型エラーがあるかどうかを知っています!ですから、この恩恵は本当に価値がないように思えます。 Pythonの弱点(静的エラー検出)の1つ以上を得ることなく、Pythonの強さ(柔軟性)を捨てています。

1
Ben

私はこの議論が決まったことを知っていますが、はるかに簡単な解決策はPython以下に示す構造モジュールを使用することです。これには値を割り当てる前にデータのコンテナを作成する必要があります、しかし、データ型を静的に保つのに非常に効果的です https://pypi.python.org/pypi/structures

1
Jon

C++で言及したのと同じタイプのpropertyを使用できます。プロパティのヘルプは http://adam.gomaa.us/blog/2008/aug/11/the-python-property-builtin/ から入手できます。

1
Nilesh