Pythonのイディオムとアンチイディオム—Pythonドキュメント

提供:Dev Guides
< PythonPython/docs/2.7/howto/doanddont
移動先:案内検索

Pythonのイディオムとアンチイディオム

著者
モシェザドカ

このドキュメントはパブリックドメインに置かれています。

概要

このドキュメントは、チュートリアルのコンパニオンと見なすことができます。 Pythonの使用方法、さらに重要なことに、Pythonの使用方法ではなくを示しています。


使用してはならない言語構造

Pythonには他の言語に比べて落とし穴が比較的少ないですが、それでも、コーナーケースでのみ役立つ、または明らかに危険な構造がいくつかあります。

モジュールインポートから*

内部関数定義

from module import *は、関数定義内の無効です。 Pythonの多くのバージョンは無効性をチェックしませんが、それをより有効にするわけではありません。賢い弁護士がいるだけで人は無実になります。 これまでのように使用しないでください。 受け入れられたバージョンでも、コンパイラーはどの名前がローカルでどれがグローバルであるかを判別できなかったため、関数の実行が遅くなりました。 Python 2.1では、この構成により警告が発生し、場合によってはエラーが発生することもあります。


モジュールレベルで

モジュールレベルでfrom module import *を使用することは有効ですが、通常はお勧めできません。 1つは、これによってPythonが持つ重要なプロパティが失われることです。つまり、お気に入りのエディターの単純な「検索」関数によって、各トップレベル名がどこで定義されているかを知ることができます。 また、一部のモジュールが追加の関数またはクラスを拡張した場合、将来的に問題が発生する可能性があります。

ニュースグループで尋ねられる最もひどい質問の1つは、なぜこのコードであるかです。

f = open("www")
f.read()

動作しません。 もちろん、これは問題なく機能します(「www」というファイルがあると仮定します)。ただし、モジュールのどこかにステートメントfrom os import *が存在する場合は機能しません。 os モジュールには、整数を返す open()という関数があります。 非常に便利ですが、ビルトインのシャドウイングは最も役に立たないプロパティの1つです。

モジュールがエクスポートする名前を確実に知ることはできないため、必要な名前(from module import name1, name2)を取得するか、モジュールに保持して必要に応じてアクセスする(import module;print module.name)ことを忘れないでください。


それがちょうど良いとき

from module import *で問題がない場合があります。

  • インタラクティブなプロンプト。 たとえば、from math import *は、Pythonをすばらしい関数電卓にします。
  • CのモジュールをPythonのモジュールで拡張する場合。
  • モジュールがfrom import *セーフとして自身をアドバタイズする場合。


装飾されていない exec 、 execfile()およびその仲間

「装飾されていない」という言葉は、明示的な辞書なしでの使用を指します。この場合、これらの構成は current 環境でコードを評価します。 これは、from import *が危険であるのと同じ理由で危険です。つまり、信頼している変数をステップオーバーして、コードの残りの部分を台無しにする可能性があります。 単にそれをしないでください。

悪い例:

>>> for name in sys.argv[1:]:
>>>     exec "%s=1" % name
>>> def func(s, **kw):
>>>     for var, val in kw.items():
>>>         exec "s.%s=val" % var  # invalid!
>>> execfile("handler.py")
>>> handle()

良い例:

>>> d = {}
>>> for name in sys.argv[1:]:
>>>     d[name] = 1
>>> def func(s, **kw):
>>>     for var, val in kw.items():
>>>         setattr(s, var, val)
>>> d={}
>>> execfile("handle.py", d, d)
>>> handle = d['handle']
>>> handle()

モジュールからインポートname1、name2

これは「してはいけない」であり、以前の「してはいけない」よりもはるかに弱いですが、それでも、そうする正当な理由がない場合はすべきではありません。 通常は悪い考えである理由は、2つの別々の名前空間にあるオブジェクトが突然あるためです。 一方の名前空間のバインディングが変更されても、もう一方の名前空間のバインディングは変更されないため、それらの間に不一致が生じます。 これは、たとえば、1つのモジュールがリロードされたとき、または実行時に関数の定義が変更されたときに発生します。

悪い例:

# foo.py
a = 1

# bar.py
from foo import a
if something():
    a = 2 # danger: foo.a != a

良い手本:

# foo.py
a = 1

# bar.py
import foo
if something():
    foo.a = 2

それ外:

Pythonにはexcept:句があり、すべての例外をキャッチします。 Pythonの every エラーは例外を発生させるため、except:を使用すると、多くのプログラミングエラーが実行時の問題のように見え、デバッグプロセスが妨げられる可能性があります。

次のコードは、これが悪い理由の良い例を示しています。

try:
    foo = opne("file") # misspelled "open"
except:
    sys.exit("could not open file!")

2行目は、NameErrorをトリガーします。これは、except句によってキャッチされます。 プログラムは終了し、プログラムが出力するエラーメッセージは、実際のエラーが"file"とは関係がないのに、問題は"file"の読みやすさであると思わせるでしょう。

上記を書くためのより良い方法は

try:
    foo = opne("file")
except IOError:
    sys.exit("could not open file")

これを実行すると、PythonはNameErrorを示すトレースバックを生成し、修正が必要なものがすぐに明らかになります。

except:は、SystemExitKeyboardInterruptGeneratorExitを含むすべての例外をキャッチするためです(これはエラーではなく、通常は裸のexcept:を使用することは、ほとんどの場合、良い考えではありません。 コールバックを実行するフレームワークなど、すべての「通常の」エラーをキャッチする必要がある状況では、すべての通常の例外Exceptionの基本クラスをキャッチできます。 残念ながら、Python 2.xでは、サードパーティのコードがExceptionから継承しない例外を発生させる可能性があるため、Python 2.xでは、ベアexcept:そして、キャッチしたくない例外を手動で再発生させます。


例外

例外はPythonの便利な機能です。 何か予期しないことが起こったときはいつでもそれらを育てることを学び、あなたがそれらについて何かをすることができる場所でのみそれらを捕まえる必要があります。

以下は非常に人気のある反イディオムです

def get_status(file):
    if not os.path.exists(file):
        print "file not found"
        sys.exit(1)
    return open(file).readline()

os.path.exists()が呼び出されてから、 open()が呼び出されるまでの間にファイルが削除される場合を考えてみます。 その場合、最後の行でIOErrorが発生します。 ファイルが存在するが、読み取り権限がない場合も同じことが起こります。 存在するファイルと存在しないファイルの通常のマシンでこれをテストするとバグがないように見えるため、テスト結果は正常に見え、コードが出荷されます。 その後、未処理のIOError(またはおそらく他のEnvironmentError)がユーザーに逃げ、ユーザーは醜いトレースバックを見るようになります。

これを行うには、いくらか良い方法があります。

def get_status(file):
    try:
        return open(file).readline()
    except EnvironmentError as err:
        print "Unable to open file: {}".format(err)
        sys.exit(1)

このバージョンでは、 ' ファイルが開かれ、行が読み取られるか(したがって、不安定なNFSまたはSMB接続でも機能します)、または開かれなかった理由に関するすべての利用可能な情報を提供するエラーメッセージが出力されます。 、およびアプリケーションは中止されます。

ただし、このバージョンのget_status()でさえ、あまりにも多くの仮定を行っています。つまり、実行時間の長いサーバーでは使用されず、実行時間の短いスクリプトでのみ使用されます。 確かに、発信者は次のようなことを行うことができます

try:
    status = get_status(log)
except SystemExit:
    status = None

しかし、もっと良い方法があります。 コードでは、できるだけ少ないexcept句を使用するようにしてください。通常、使用する句は、常に成功する必要のある呼び出し内、またはmain関数のキャッチオールです。

したがって、get_status()のさらに優れたバージョンはおそらく

def get_status(file):
    return open(file).readline()

呼び出し元は、必要に応じて(たとえば、ループ内で複数のファイルを試行する場合)例外を処理するか、例外を its 呼び出し元まで上方にフィルター処理することができます。

ただし、最後のバージョンには依然として深刻な問題があります。CPythonの実装の詳細により、例外が発生しても、例外ハンドラーが終了するまでファイルは閉じられません。 さらに悪いことに、他の実装(Jythonなど)では、例外が発生したかどうかに関係なく、まったく閉じられない場合があります。

この関数の最適なバージョンは、コンテキストマネージャーとしてopen()呼び出しを使用します。これにより、関数が戻るとすぐにファイルが閉じられます。

def get_status(file):
    with open(file) as fp:
        return fp.readline()

バッテリーの使用

時々、人々はPythonライブラリに何かを書き直しているように見えますが、通常は不十分です。 たまにあるモジュールのインターフェースは貧弱ですが、通常は、独自に作成するよりも、Pythonに付属している豊富な標準ライブラリとデータ型を使用する方がはるかに優れています。

os.path はほとんど知られていない便利なモジュールです。 これは常にオペレーティングシステムに適したパス演算を備えており、通常、自分で思いついたものよりもはるかに優れています。

比較:

# ugh!
return dir+"/"+file
# better
return os.path.join(dir, file)

os.path のより便利な関数:basename()dirname()splitext()

min()max()は、同等のシーケンスの最小値/最大値を見つけることができます。たとえば、セマンティクスは、まだ多くの人が独自の max() / min()を記述しています。 もう1つの非常に便利な関数は、 reduce()です。これを使用して、シーケンスに2項演算を繰り返し適用し、シーケンスを1つの値に減らすことができます。 たとえば、一連の乗算演算を使用して階乗を計算します。

>>> n = 4
>>> import operator
>>> reduce(operator.mul, range(1, n+1))
24

数値の解析に関しては、 float()int()、および long()はすべて文字列引数を受け入れ、不正な形式の文字列を拒否することに注意してください。 ValueErrorを上げます。


バックスラッシュを使用してステートメントを続行する

Pythonは改行をステートメントの終了子として扱い、ステートメントは1行に収めるのが快適であることが多いため、多くの人が次のように行います。

if foo.bar()['first'][0] == baz.quux(1, 2)[5:9] and \
   calculate_number(10, 20) != forbulate(500, 360):
      pass

これは危険であることを理解する必要があります。\の後の漂遊スペースはこの行を間違ったものにし、漂遊スペースはエディターで見づらいことで有名です。 この場合、少なくとも構文エラーになりますが、コードが次の場合:

value = foo.bar()['first'][0]*baz.quux(1, 2)[5:9] \
        + calculate_number(10, 20)*forbulate(500, 360)

それならそれは微妙に間違っているでしょう。

通常、括弧内に暗黙の継続を使用することをお勧めします。

このバージョンは防弾です:

value = (foo.bar()['first'][0]*baz.quux(1, 2)[5:9]
        + calculate_number(10, 20)*forbulate(500, 360))