Python 良い慣用句、悪い慣用句
*****************************

著者:
   Moshe Zadka

This document is placed in the public domain. (この文書はパブリックド
メインです。)


概要
^^^^

この文書はチュートリアルのおまけと考えてもらって結構です。 Python をど
う使うべきか、そして更に重要なこととして、どう使うべきで *ない* かを例
示しています。


使うべきでない構文
==================

Python の落とし穴は他言語と比べればほとんどないようなものですが、中に
は特殊な場面でしか役に立たなかったり、単に危険なだけの構文も存在してい
ます。


from モジュール import *
------------------------


関数定義の中で
~~~~~~~~~~~~~~

関数定義内での "from モジュール import *" は *不正* です。多くの古い
Python では不正としてチェックされないものの、だからと言って有効になる
わけではありません。やり手の弁護士を雇って無罪になっても、潔白になれる
わけではないのと同じですね。ですから絶対にそういう使い方をしないでくだ
さい。不正にされないバージョンでも、コンパイラはどの名前がローカルでど
の名前がグローバルなのか判然としなくなるので、関数の実行が遅くなってし
まいます。Python 2.1 で、この構文は警告や、場合によってはエラーも出す
ようになりました。


モジュールレベルで
~~~~~~~~~~~~~~~~~~

モジュールレベルで "from モジュール import *" を使うのは有効ではありま
すが、大抵は、やめたほうが良いですよ。理由のひとつとして、それをやると
本来 Python が持っている大事な特徴を失ってしまうということが挙げられま
す。その特徴とは、トップレベルの名前がそれぞれどこで定義されているのか
、エディタの検索機能だけでわかるというものです。それに、将来モジュール
に関数やクラスが増えていくと、ややこしいことになりかねません。

ニュースグループで出てくる最低の質問には、なぜこのコード:

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

が動かないのか、というものがあります。もちろんこれで動きますよ (ただし
"www" というファイルがあれば)。でもモジュールのどこかに "from os
import *" があればダメです。 "os" モジュールは、整数を返す "open()" 関
数を持っているのです。これはとても便利ではありますが、ビルトインを隠し
てしまうのは、非常に不便な特徴のひとつと言えます。

モジュールがエクスポートする名前は確実にはわからないのですから、必要な
ものだけ "from モジュール import 名前1, 名前2" で取るか、モジュールか
ら出さずに "import モジュール;print モジュール.name" としておいて必要
に応じてアクセスするようにしましょう。


問題ない状況
~~~~~~~~~~~~

"from モジュール import *" が問題とならない状況もあります:

* 対話的プロンプト。たとえば "from math import *" すれば Python がス
  ー パー関数電卓に変身します。

* C のモジュールを Python のモジュールで拡張するとき。

* そのモジュールは "from import *" 可だ、と作者が言っているとき。


そのまんまな "exec", "execfile()" と仲間たち
--------------------------------------------

「そのまんま」という言葉は辞書を明示せずに使うという意味で、そういう構
文ではコードが *その時点の* 環境に対して評価されます。これは "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()


from モジュール import 名前1, 名前2
-----------------------------------

今回のは、これまでの「ダメ」よりかなり弱い「ダメ」ですが、やはりそれな
りの理由がなければ、やめておいたほうが良いことに変わりありません。これ
が大抵うまくないのは、いつの間にか二つ別々の名前空間に住む一つのオブジ
ェクトを持つことになるからです。一方の名前空間でそのバインディングが変
更されたとき、もう一方のバインディングは変更されないので、食い違いがで
きてしまいます。これが起こるのは、たとえば、モジュールを読み直したり、
ランタイムで関数の定義を変更したときなどです。

悪い見本:

   # 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


except:
-------

Python には "except:" 節があり、これはあらゆる例外を捕捉します。
Python のエラーは *すべて* 例外を出しますから、 "except:" を使うと各種
のプログラミングエラーがランタイムの問題のように見えてしまい、デバッグ
の邪魔になります。

以下のコードは、 "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:" は *全て* の例外を補足します。これには "SystemExit",
"KeyboardInterrupt", "GeneratorExit" (これはエラーではなく、通常はユー
ザーコードでキャッチするべきではない例外です) も含まれるので、ほとんど
全ての場合に "except:" を利用してはいけません。 "通常の" すべてのエラ
ーをキャッチする必要がある場合、たとえばコールバックを実行するようなフ
レームワークの場合は、全ての通常の例外の基底クラスである "Exception"
をキャッチすることができます。不幸なことに、 Python 2.x ではサードパー
ティのコードが "Exception" を継承していない例外を発生させる可能性があ
るので、 "except:" を使ってキャッチしたくない例外を手動で再度 raise す
る必要がある場合があるかもしれません。


例外
====

例外は 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" を
投げるでしょう。同じことは、 *file* は存在しているけれど読み出し権限が
なかった、という場合にも起こります。これをテストする際、ふつうのマシン
で、存在するファイルと存在しないファイルに対してだけやったのではバグが
ないように見えてしまい、テスト結果が大丈夫そうなのでコードはそのまま出
荷されてしまうことになります。こうして、対処されない "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()

呼び出した側は、望むなら例外を処理することもできますし (たとえばループ
で複数ファイルに試行するときとか)、そのまま *自分の* 呼び出し親まで上
げることもできます。

しかし、この最終バージョンにも深刻な問題があります --- CPython 実装の
細部に原因があるのですが、例外が起きたときにはその例外ハンドラが終了す
るまでファイルが閉じられないのです; しかも、なお悪いことに、他の実装 (
たとえば Jython) では例外の有無に関わらず閉じられません。

この関数の一番良いバージョンでは "open()" をコンテクストマネジャとして
使って、関数が返るとすぐにファイルが閉じられるようにしています:

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


使い捨てじゃなくて充電池を使う
==============================

どうも皆、Python ライブラリに最初からあるものを自分で書こうとして、大
抵うまくいっていないようです。そういう場当たりなモジュールには貧弱なイ
ンタフェースしかないことを考えると、ふつうは Python に付いてくる高機能
な標準ライブラリとデータ型を使うほうが、自分でひねり出すより格段に良い
ですよ。

便利なのにほとんど知られていないモジュールに "os.path" があります。こ
のモジュールには OS に合ったパス演算が備わっていて、大抵は自分でどれだ
け苦労して作ったものよりもずっと良いものです。

比べてください:

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

"os.path" にはさらに便利な関数が他にもあります: "basename()" や
"dirname()", "splitext()" などです。

There are also many useful built-in functions people seem not to be
aware of for some reason: "min()" and "max()" can find the
minimum/maximum of any sequence with comparable semantics, for
example, yet many people write their own "max()"/"min()". Another
highly useful function is "reduce()" which can be used to repeatedly
apply a binary operation to a sequence, reducing it to a single value.
For example, compute a factorial with a series of multiply operations:

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

数値の字句解析をするときには、 "float()", "int()", "long()" は全て文字
列の引数を受け取って、不正なフォーマットの文字列の場合には
"ValueError" を発生させることを覚えておくと便利です。


バックスラッシュで文を続ける
============================

Python は改行を文の終わりとして扱いますので、そして文は一行にうまく収
まらないことがよくありますので、こうする人が多いです:

   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))
