web-dev-qa-db-ja.com

Pythonデータモデルと組み込み関数の関係は何ですか?

Python Stack Overflowの回答)を読んでいると、何人かの人々 ユーザーに伝えるデータモデルの使用 特別な メソッド または 属性 直接。

次に、矛盾するアドバイスを(時には自分自身から)しないで、代わりに組み込み関数と演算子を直接使用するように言っています。

何故ですか?特別な「dunder」メソッドとPython data model and builtin functions )の属性の関係は何ですか?

特別な名前を使用するのはいつですか?

36
Aaron Hall

Pythonデータモデルと組み込み関数の関係は何ですか?

  • ビルトインと演算子は、基になるdatamodelメソッドまたは属性を使用します。
  • ビルトインと演算子はよりエレガントな動作をしており、一般に上位互換性があります。
  • データモデルの特別なメソッドは、意味的に非公開のインターフェースです。
  • ビルトインおよび言語演算子は、特別なメソッドによって実装される動作のユーザーインターフェイスになることを特に目的としています。

したがって、データモデルの特別なメソッドと属性よりも、可能な場合は組み込み関数と演算子を使用することをお勧めします。

意味的に内部のAPIは、パブリックインターフェイスよりも変更される可能性が高くなります。 Pythonは実際には何も「プライベート」とは考えず、内部を公開しますが、それはそのアクセスを悪用するのが良い考えであることを意味しません。これには次のリスクがあります。

  • Python実行可能ファイルをアップグレードしたり、Pythonの他の実装(PyPy、IronPython、Jython、またはその他の予期しない実装など)に切り替えたりすると、さらに重大な変更が発生する場合があります。
  • あなたの同僚は、おそらくあなたの言語スキルと誠実さをあまりよく考えず、それをコード臭いと見なし、あなたとあなたのコードの残りの部分をより綿密に調査します。
  • 組み込み関数は、動作を簡単に傍受できます。特別なメソッドを使用すると、イントロスペクションとデバッグのためのPythonの能力が直接制限されます。

深く

組み込み関数と演算子は特別なメソッドを呼び出し、Pythonデータモデルの特別な属性を使用します。それらは、オブジェクトの内部を隠す、読み取り可能で保守可能なベニアです。一般に、ユーザーは、特別なメソッドを呼び出したり、特別な属性を直接使用したりするのではなく、言語で指定された組み込み関数と演算子を使用する必要があります。

組み込み関数と演算子は、より原始的なデータモデルの特別なメソッドよりもフォールバックまたはよりエレガントな動作を持つこともできます。例えば:

  • next(obj, default)では、イテレータが不足したときにStopIterationを発生させる代わりにデフォルトを提供できますが、obj.__next__()では発生しません。
  • str(obj)が利用できない場合、obj.__repr__()obj.__str__()にフォールバックします-obj.__str__()を直接呼び出すと属性エラーが発生します。
  • _obj != other_がない場合、Python 3の_not obj == other_フォールバック___ne___-obj.__ne__(other)を呼び出しても、これを利用できません。

(ビルトイン関数は、必要に応じて、または必要に応じて、モジュールのグローバルスコープまたはbuiltinsモジュールに簡単に影を付けて、動作をさらにカスタマイズすることもできます。)

ビルトインと演算子をデータモデルにマッピングする

以下は、組み込み関数と演算子の、使用または返されるそれぞれの特別なメソッドと属性へのメモ付きのマッピングです。通常のルールは、組み込み関数は通常、同じ名前の特別なメソッドにマッピングされるということですが、これは以下にこのマップを与えることを保証するのに十分な一貫性がありません:

_builtins/     special methods/
operators  -> datamodel               NOTES (fb == fallback)

repr(obj)     obj.__repr__()          provides fb behavior for str
str(obj)      obj.__str__()           fb to __repr__ if no __str__
bytes(obj)    obj.__bytes__()         Python 3 only
unicode(obj)  obj.__unicode__()       Python 2 only
format(obj)   obj.__format__()        format spec optional.
hash(obj)     obj.__hash__()
bool(obj)     obj.__bool__()          Python 3, fb to __len__
bool(obj)     obj.__nonzero__()       Python 2, fb to __len__
dir(obj)      obj.__dir__()
vars(obj)     obj.__dict__            does not include __slots__
type(obj)     obj.__class__           type actually bypasses __class__ -
                                      overriding __class__ will not affect type
help(obj)     obj.__doc__             help uses more than just __doc__
len(obj)      obj.__len__()           provides fb behavior for bool
iter(obj)     obj.__iter__()          fb to __getitem__ w/ indexes from 0 on
next(obj)     obj.__next__()          Python 3
next(obj)     obj.next()              Python 2
reversed(obj) obj.__reversed__()      fb to __len__ and __getitem__
other in obj  obj.__contains__(other) fb to __iter__ then __getitem__
obj == other  obj.__eq__(other)
obj != other  obj.__ne__(other)       fb to not obj.__eq__(other) in Python 3
obj < other   obj.__lt__(other)       get >, >=, <= with @functools.total_ordering
complex(obj)  obj.__complex__()
int(obj)      obj.__int__()
float(obj)    obj.__float__()
round(obj)    obj.__round__()
abs(obj)      obj.__abs__()
_

operatorモジュールには_length_hint_があり、___len___が実装されていない場合、それぞれの特別なメソッドによってフォールバックが実装されます。

_length_hint(obj)  obj.__length_hint__() 
_

点線ルックアップ

ドットルックアップは状況に応じて異なります。特別なメソッド実装がない場合は、最初にクラス階層でデータ記述子(プロパティやスロットなど)を探し、次にインスタンス___dict___(インスタンス変数など)を調べ、次にクラス階層で非データ記述子(メソッドなど)を探します。特別なメソッドは、次の動作を実装します。

_obj.attr      obj.__getattr__('attr')       provides fb if dotted lookup fails
obj.attr      obj.__getattribute__('attr')  preempts dotted lookup
obj.attr = _  obj.__setattr__('attr', _)    preempts dotted lookup
del obj.attr  obj.__delattr__('attr')       preempts dotted lookup
_

記述子

記述子は少し進んでいます-これらのエントリをスキップして後で戻って来てください-記述子インスタンスがクラス階層(メソッド、スロット、プロパティなど)にあることを思い出してください。データ記述子は___set___または___delete___を実装します。

_obj.attr        descriptor.__get__(obj, type(obj)) 
obj.attr = val  descriptor.__set__(obj, val)
del obj.attr    descriptor.__delete__(obj)
_

クラスがインスタンス化(定義)されると、記述子に属性名を通知する記述子がある場合、次の記述子メソッド___set_name___が呼び出されます。 (これはPython 3.6の新機能です。)clsは上記のtype(obj)と同じで、_'attr'_は属性名を表します:

_class cls:
    @descriptor_type
    def attr(self): pass # -> descriptor.__set_name__(cls, 'attr') 
_

アイテム(下付き表記)

下付き表記も文脈に応じて異なります。

_obj[name]         -> obj.__getitem__(name)
obj[name] = item  -> obj.__setitem__(name, item)
del obj[name]     -> obj.__delitem__(name)
_

___missing___がキーを見つけられない場合、dict、___getitem___のサブクラスの特別なケースが呼び出されます。

_obj[name]         -> obj.__missing__(name)  
_

オペレーター

+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |演算子には、次のような特別なメソッドもあります。

_obj + other   ->  obj.__add__(other), fallback to other.__radd__(obj)
obj | other   ->  obj.__or__(other), fallback to other.__ror__(obj)
_

および拡張代入のインプレース演算子_+=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=_、たとえば:

_obj += other  ->  obj.__iadd__(other)
obj |= other  ->  obj.__ior__(other)
_

(これらのインプレース演算子が定義されていない場合、Pythonは、たとえば、_obj += other_から_obj = obj + other_にフォールバックします)

と単項演算:

_+obj          ->  obj.__pos__()
-obj          ->  obj.__neg__()
~obj          ->  obj.__invert__()
_

コンテキストマネージャー

コンテキストマネージャーは、コードブロックの開始時に呼び出される___enter___を定義します(その戻り値、通常はselfはasでエイリアスされます)、および___exit___は呼び出されることが保証されています例外情報を含む、コードブロックの終了時。

_with obj as enters_return_value: #->  enters_return_value = obj.__enter__()
    raise Exception('message')
                                 #->  obj.__exit__(Exception, 
                                 #->               Exception('message'), 
                                 #->               traceback_object)
_

___exit___が例外を受け取ってからfalse値を返す場合、メソッドを終了すると例外が再発生します。

例外がない場合、___exit___はこれらの3つの引数の代わりにNoneを取得し、戻り値は無意味です。

_with obj:           #->  obj.__enter__()
    pass
                    #->  obj.__exit__(None, None, None)
_

いくつかのメタクラスの特別なメソッド

同様に、クラスは抽象基本クラスをサポートする(メタクラスからの)特別なメソッドを持つことができます。

_isinstance(obj, cls) -> cls.__instancecheck__(obj)
issubclass(sub, cls) -> cls.__subclasscheck__(sub)
_

重要なポイントは、nextboolのような組み込み関数はPython 2と3の間で変わらないが、基礎となる実装名変化します。

したがって、ビルトインを使用すると、上位互換性も提供されます。

特別な名前を使用するのはいつですか?

Pythonでは、アンダースコアで始まる名前は、意味的には非公開のユーザー名です。下線は、クリエイターの言い回しであり、「手を使わないで、触れないでください」です。

これは文化的なものだけでなく、PythonによるAPIの扱いにもあります。パッケージの___init__.py_が_import *_を使用してサブパッケージからAPIを提供する場合、サブパッケージが___all___を提供しない場合、アンダースコアで始まる名前は除外されます。サブパッケージの___name___も除外されます。

IDEのオートコンプリートツールは、アンダースコアで始まる名前が非公開であることを考慮して混合されています。ただし、___init___、___new___、___repr___、___str___、___eq___など(ユーザーが作成した非パブリックインターフェイスも表示されない) )オブジェクトの名前とピリオドを入力すると、.

したがって、私は断言します:

特別な「dunder」メソッドは、パブリックインターフェイスの一部ではありません。直接使用しないでください。

それで、それらをいつ使うのですか?

主な使用例は、独自のカスタムオブジェクトまたは組み込みオブジェクトのサブクラスを実装する場合です。

どうしても必要な場合にのみ使用してください。ここではいくつかの例を示します。

関数またはクラスで___name___特殊属性を使用する

関数を装飾すると、通常、関数に関する有用な情報を隠すラッパー関数が返されます。 @wraps(fn)デコレーターを使用して情報を失わないようにしますが、関数の名前が必要な場合は、___name___属性を直接使用する必要があります。

_from functools import wraps

def decorate(fn): 
    @wraps(fn)
    def decorated(*args, **kwargs):
        print('calling fn,', fn.__name__) # exception to the rule
        return fn(*args, **kwargs)
    return decorated
_

同様に、メソッドでオブジェクトのクラスの名前が必要な場合(たとえば、___repr___で使用)、次のようにします。

_def get_class_name(self):
    return type(self).__name__
          # ^          # ^- must use __name__, no builtin e.g. name()
          # use type, not .__class__
_

特別な属性を使用してカスタムクラスまたはサブクラス化されたビルトインを作成する

カスタム動作を定義する場合は、データモデル名を使用する必要があります。

これは理にかなっています。私たちが実装者であるため、これらの属性は私たちにはプライベートではありません。

_class Foo(object):
    # required to here to implement == for instances:
    def __eq__(self, other):      
        # but we still use == for the values:
        return self.value == other.value
    # required to here to implement != for instances:
    def __ne__(self, other): # docs recommend for Python 2.
        # use the higher level of abstraction here:
        return not self == other  
_

ただし、この場合でも、self.value.__eq__(other.value)またはnot self.__eq__(other)は使用しません(後者が予期しない動作を引き起こす可能性があることの証明については、my answer here を参照してください)。より高いレベルの抽象化を使用する必要があります。

特別なメソッド名を使用する必要があるもう1つのポイントは、子の実装で親に委任する場合です。例えば:

_class NoisyFoo(Foo):
    def __eq__(self, other):
        print('checking for equality')
        # required here to call the parent's method
        return super(NoisyFoo, self).__eq__(other) 
_

結論

特別なメソッドにより、ユーザーはオブジェクト内部のインターフェースを実装できます。

組み込み関数と演算子をできる限り使用します。ドキュメント化されたパブリックAPIがない特別なメソッドのみを使用します。

43
Aaron Hall

あなたがどうやら考えていなかったいくつかの使用法を示し、示した例にコメントし、あなた自身の答えからのプライバシー主張に反対します。


たとえば、len(a)ではなくa.__len__()を使用する必要があるというあなた自身の回答に同意します。私はそれを次のように記述します:lenが存在するために使用でき、__len__が存在するためlenが使用できる。または、実際にはlen(a)は実際には多くのfasterになる可能性があるため、内部的には機能します。たとえば、リストや文字列の場合は次のようになります。

>>> timeit('len(a)', 'a = [1,2,3]', number=10**8)
4.22549770486512
>>> timeit('a.__len__()', 'a = [1,2,3]', number=10**8)
7.957335462257106

>>> timeit('len(s)', 's = "abc"', number=10**8)
4.1480574509332655
>>> timeit('s.__len__()', 's = "abc"', number=10**8)
8.01780160432645

しかし、これらのメソッドを組み込み関数や演算子で使用するために自分のクラスで定義するほかに、次のように使用することもあります。

ある関数にフィルター関数を与える必要があり、フィルターとしてセットsを使用したいとします。追加の関数lambda x: x in sまたはdef f(x): return x in sを作成するつもりはありません。いいえ、私はすでに使用できる完全に優れた関数を持っています。それは、セットの__contains__メソッドです。よりシンプルで直接的です。そして、ここに示されているように、さらに高速です(ここではfとして保存していることを無視してください。これは、このタイミングデモのためだけです)。

>>> timeit('f(2); f(4)', 's = {1, 2, 3}; f = s.__contains__', number=10**8)
6.473739433621368
>>> timeit('f(2); f(4)', 's = {1, 2, 3}; f = lambda x: x in s', number=10**8)
19.940786514456924
>>> timeit('f(2); f(4)', 's = {1, 2, 3}\ndef f(x): return x in s', number=10**8)
20.445680107760325

したがって、私は直接呼び出しs.__contains__(x)のような魔法のメソッドを使用しませんが、時々passsome_function_needing_a_filter(s.__contains__)のような魔法のメソッドを使用します。そして、それは完全に問題なく、lambda/defの代替よりも優れていると思います。


あなたが示した例についての私の考え:

  • 例1 :リストのサイズを取得する方法を尋ねたところ、items.__len__()と回答しました。どんな理由もなく。私の評決:それは間違っています。 len(items)である必要があります。
  • 例2 :最初にd[key] = valueについて言及していますか?次に、d.__setitem__(key, value)を理由付きで追加します "キーボードに角かっこキーがない場合" 、これはほとんど適用されず、私が深刻であったとは思えません。それが最後のポイントのドアの足元にすぎなかったと私は思います、それが私たち自身のクラスで角括弧構文をサポートする方法であると言及しました。角括弧を使用するという提案に戻ります。
  • obj.__dict__を提案します。悪い、__len__の例のように。しかし、私は彼がvars(obj)を知らなかったのではないかと思います。varsはあまり一般的ではないため、名前は__dict__の「dict」とは異なります。
  • 例4__class__を提案します。 type(obj)である必要があります。 typeの方がよく知られていると思いますが、それは__dict__の話に似ていると思います。

プライバシーについて:あなた自身の答えでは、これらの方法は「意味的にプライベート」であると言います。私は強く反対します。シングルおよびダブルleadingアンダースコアはそのためのものですが、データモデルの特別な「dunder/magic」メソッドではなく、ダブルリーディング+トレーリングアンダースコアを使用します。

  • 引数として使用する2つのことは、インポート動作とIDEのオートコンプリートです。しかし、インポートとこれらの特別なメソッドは異なる領域であり、1つIDE私が試した(人気のあるPyCharm)はあなたに同意しません。メソッド_fooおよび__bar__でクラス/オブジェクトを作成しましたが、オートコンプリートは失敗しましたtは_fooを提供しますが、-did__bar__を提供します。両方の方法を使用した場合、PyCharmは_foo(「保護されたメンバー」と呼ぶ)についてのみ警告しましたnot__bar__について。
  • PEP 8 'weak "internal use" indicator' を明示的にsingleアンダースコアを明示的に示し、明示的にdouble を示しますLeadingアンダースコアはマングリングの名前を示し、後で "サブクラスに使用させたくない属性のためのものである" と説明しています。しかし、double leading + trailingアンダースコアに関するコメントは、そのようなことを何も言いません。
  • データモデルページ あなた自身がリンクしていると、これらの 特別なメソッド名"演算子のオーバーロードに対するPythonのアプローチ"であると述べています。プライバシーについては何もありません。 private/privacy/protectedという単語は、そのページのどこにも表示されません。

    私はまた、これらのメソッドについて Andrew Montalentiによるこの記事 を読んで、 "を強調することをお勧めします" dunderの規則は、コア用に予約されている名前空間ですPython team " and "決して、あなた自身のダンダーを発明しない "なぜなら"コアPythonチームは自分用にやや醜い名前空間を予約しました "。これはすべてPEP 8の命令に一致します" [dunder/magic]の名前を発明することはありません。文書化されているとおりに使用してください "。私はAndrewがスポットを当てていると思います-これはコアチームの醜い名前空間です。プライバシーの問題ではなく、オペレーターのオーバーロードを目的としています(Andrewの要点ではなく、私のデータモデルページの問題です)。

Andrewの記事以外にも、これらの「マジック」/「ダンダー」メソッドについてさらにいくつかチェックしましたが、プライバシーについて話しているものはまったく見つかりませんでした。それだけではありません。

繰り返しますが、len(a)ではなくa.__len__()を使用する必要があります。プライバシーのためではありません。

11
Stefan Pochmann