おそらく、厳密に型指定された言語 (Java) を使用した日々の名残りとして、関数を記述した後で型チェックを強制することがよくあります。例えば:
def orSearch(d, query):
assert (type(d) == dict)
assert (type(query) == list)
これを続けるべきですか?これを行う/行わないことの利点は何ですか?
あんな事はしないで。
「動的」言語 (値に関しては強く型付けされ、変数に関しては型付けされておらず、レイト バウンド) を使用するポイントは、インターフェイスをサポートする任意のオブジェクトに対応するという点で、関数が適切にポリモーフィックになることです。関数は (「ダックタイピング」) に依存します。
Python は、さまざまなタイプのオブジェクトが相互に関連することなく実装できる多くの一般的なプロトコル (反復可能プロトコルなど) を定義しています。プロトコル自体は言語機能ではありません (Java インターフェースとは異なります)。
これの実際的な結果は、一般に、言語の型を理解し、適切にコメントしている限り (docstring を含めて、他の人もあなたのプログラムの型を理解できるようにする)、一般的にはより少ないコードを書くことができるということです。型システムをコーディングする必要はありません。型宣言が異なるだけで (クラスがバラバラの階層にある場合でも)、異なる型に対して同じコードを書くことにはなりません。コードを 1 つだけ書いてみたいと思います。
理論的に同じことを提供する他の言語があります: 型推論言語です。最も一般的なのは C++ (テンプレートを使用) と Haskell です。理論的には (そしておそらく実際には)、型は静的に解決されるため、コードをさらに少なくすることができます。そのため、間違った型が渡された場合に対処するために例外ハンドラーを記述する必要はありません。プログラムの実際の型ではなく、型システムにプログラミングする必要があることがわかりました(型システムは定理証明者であり、扱いやすいように、プログラム全体を分析しません)。それがいいと思うなら、python (または ruby、smalltalk、または Lisp の変種) の代わりにこれらの言語の 1 つを使用することを検討してください。
Python (または同様の動的言語) では、型テストの代わりに、例外を使用して、オブジェクトが特定のメソッドをサポートしていない場合にキャッチする必要があります。その場合、それをスタックに上げるか、キャッチして、不適切な型に関する例外を発生させます。このタイプの「許可よりも許しを求めるほうがよい」コーディングは慣用的な python であり、コードの単純化に大きく貢献します。
*
実際には。クラスの変更は Python と Smalltalk で可能ですが、まれです。また、低水準言語でのキャストと同じではありません。
更新: mypy を使用して、本番環境以外で Python を静的にチェックできます。コードが一貫していることを確認できるようにコードに注釈を付けることで、必要に応じてそれを行うことができます。または、必要に応じてヨロします。
ほとんどの場合、ダックタイピングと継承に干渉します。
継承:あなたは確かに次の効果で何かを書くつもりでした.
assert isinstance(d, dict)
コードが のサブクラスでも正しく動作することを確認しますdict
。これはJavaでの使い方と似ていると思います。しかし、Python には Java にないものがあります。
ダックタイピング:ほとんどの組み込み関数は、オブジェクトが特定のクラスに属していることを必要とせず、正しい方法で動作する特定のメンバー関数を持っていることのみを必要とします。たとえば、for
ループは、ループ変数がiterableであることのみを必要とします。これは、メンバー関数__iter__()
およびnext()
があり、それらが正しく動作することを意味します。
したがって、Python の全機能への扉を閉じたくない場合は、本番コードで特定の型をチェックしないでください。(とはいえ、デバッグには役立つかもしれません。)
コードに型チェックを追加することを主張する場合は、注釈と、記述しなければならないものをどのように簡素化できるかを調べることができます。StackOverflowに関する質問の 1 つで、注釈を利用した難読化された小さな型チェッカーが導入されました。あなたの質問に基づいた例を次に示します。
>>> def statictypes(a):
def b(a, b, c):
if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
return c
return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
>>> @statictypes
def orSearch(d: dict, query: dict) -> type(None):
pass
>>> orSearch({}, {})
>>> orSearch([], {})
Traceback (most recent call last):
File "<pyshell#162>", line 1, in <module>
orSearch([], {})
File "<pyshell#155>", line 5, in <lambda>
return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
File "<pyshell#155>", line 5, in <listcomp>
return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
File "<pyshell#155>", line 3, in b
if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
TypeError: d should be <class 'dict'>, not <class 'list'>
>>> orSearch({}, [])
Traceback (most recent call last):
File "<pyshell#163>", line 1, in <module>
orSearch({}, [])
File "<pyshell#155>", line 5, in <lambda>
return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
File "<pyshell#155>", line 5, in <listcomp>
return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
File "<pyshell#155>", line 3, in b
if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
TypeError: query should be <class 'dict'>, not <class 'list'>
>>>
型チェッカーを見て、「これは一体何をしているのだろう?」と疑問に思うかもしれません。私は自分で調べて、それを読みやすいコードに変換することにしました。2 番目のドラフトでは、b
関数が削除されました ( と呼ぶことができますverify
)。3 番目の最終ドラフトでは、いくつかの改善が行われました。
import functools
def statictypes(func):
template = '{} should be {}, not {}'
@functools.wraps(func)
def wrapper(*args):
for name, arg in zip(func.__code__.co_varnames, args):
klass = func.__annotations__.get(name, object)
if not isinstance(arg, klass):
raise TypeError(template.format(name, klass, type(arg)))
result = func(*args)
klass = func.__annotations__.get('return', object)
if not isinstance(result, klass):
raise TypeError(template.format('return', klass, type(result)))
return result
return wrapper
編集:
この回答が書かれてから 4 年以上が経過しましたが、それ以来 Python では多くの変更がありました。これらの変更と言語の個人的な成長の結果として、型チェック コードを再検討し、新しい機能と改善されたコーディング テクニックを活用するために書き直すことは有益であると思われます。statictypes
そのため、 (現在は に名前が変更されたstatic_types
) 関数デコレータにいくつかのわずかな改善を加える次のリビジョンが提供されています。
#! /usr/bin/env python3
import functools
import inspect
def static_types(wrapped):
def replace(obj, old, new):
return new if obj is old else obj
signature = inspect.signature(wrapped)
parameter_values = signature.parameters.values()
parameter_names = tuple(parameter.name for parameter in parameter_values)
parameter_types = tuple(
replace(parameter.annotation, parameter.empty, object)
for parameter in parameter_values
)
return_type = replace(signature.return_annotation, signature.empty, object)
@functools.wraps(wrapped)
def wrapper(*arguments):
for argument, parameter_type, parameter_name in zip(
arguments, parameter_types, parameter_names
):
if not isinstance(argument, parameter_type):
raise TypeError(f'{parameter_name} should be of type '
f'{parameter_type.__name__}, not '
f'{type(argument).__name__}')
result = wrapped(*arguments)
if not isinstance(result, return_type):
raise TypeError(f'return should be of type '
f'{return_type.__name__}, not '
f'{type(result).__name__}')
return result
return wrapper
これは非慣用的な方法です。通常、Python ではtry/except
テストを使用します。
def orSearch(d, query):
try:
d.get(something)
except TypeError:
print("oops")
try:
foo = query[:2]
except TypeError:
print("durn")
型チェックを行う必要がある場合は、Steve のアプローチに同意します。Python で型チェックを行う必要があるとはあまり思いませんが、少なくとも 1 つの状況で型チェックを行うことがあります。ここで、型をチェックしないと間違った答えが返され、後の計算でエラーが発生する可能性があります。この種のエラーは追跡が難しい場合があり、Python で何度も経験しています。あなたと同じように、私は最初に Java を学びました。
配列を期待し、最初の要素を返す単純な関数があるとします。
def func(arr): return arr[0]
配列で呼び出すと、配列の最初の要素が取得されます。
>>> func([1,2,3])
1
また、文字列またはgetitemマジック メソッドを実装する任意のクラスのオブジェクトで呼び出した場合にも、応答が返されます。
>>> func("123")
'1'
これにより応答が得られますが、この場合は間違ったタイプです。これは、同じメソッド シグネチャを持つオブジェクトで発生する可能性があります。計算のかなり後になるまで、エラーに気付かない場合があります。独自のコードでこれが発生した場合は、通常、以前の計算でエラーがあったことを意味しますが、そこにチェックを入れておくと、それを早期に検出できます。ただし、他の人のために python パッケージを作成している場合は、おそらく考慮すべきものです。
チェックによってパフォーマンスが大幅に低下することはありませんが、コードが読みにくくなります。これは Python の世界では大きな問題です。