Python初心者のプログラミング過程でよくある問題をどのように回避するかについてのアドバイス

8761 ワード

この文章はPythonの初心者開発者が書いたコードで見た規範的ではないが、たまに微妙な問題を集めている.本稿の目的は,初心者開発者が醜いPythonコードを書く段階を乗り越えるのを助けることである.ターゲット読者の世話をするために,本論文ではいくつかの簡略化を行った(例えば,反復器を議論する際にジェネレータと強力な反復ツールitertoolsを無視した).
初心者開発者には、反パターンを使う理由がいくつかあります.私は可能な場所でこれらの理由を与えようとしました.しかし、通常、これらの逆モードは、コードの可読性に欠け、バグが発生しやすく、Pythonのコードスタイルに合わないことをもたらします.もっと関連資料を探したいなら、The Python TutorialやDive into Pythonをお勧めします.反復
rangeの使用
Pythonプログラミング初心者はrangeを使用して簡単な反復を実現するのが好きで、反復器の長さの範囲内で反復器の各要素を取得します.

for i in range(len(alist)):
  print alist[i]


rangeはシーケンスの簡単な反復を実現するためではないことを覚えておいてください.数値で定義されたforループに比べて、rangeで実現されたforループは自然に見えますが、シーケンスの反復ではバグが発生しやすく、反復器を直接構築するほうがはっきりしています. 

for item in alist:
  print item

rangeの乱用は、Javaのsubstringや他の多くのこのようなタイプの関数のように、rangeが生成したオブジェクトがrangeの最初のパラメータを含むことを忘れたため、予想外のサイズ差1(off-by-one)エラーを引き起こしやすい.シーケンスの最後を超えていないと考えているプログラミング初心者はバグを作成します. 

#            
alist = ['her', 'name', 'is', 'rio']
for i in range(0, len(alist) - 1): #     (Off by one)!
  print i, alist[i]

rangeを適切に使用しない一般的な理由:1.ループでインデックスを使用する必要がある.これは合理的な理由ではありません.インデックスの代わりに次の方法で使用できます. 

for index, value in enumerate(alist):
  print index, value

2.2つのループを同時に反復し、同じインデックスで2つの値を取得する必要があります.この場合zipで実現できます. 

for word, number in zip(words, numbers):
  print word, number

3.反復シーケンスの一部が必要です.この場合、シーケンススライスを反復するだけで実現できます.必要な注釈を追加して意図を明記することに注意してください. 

for word in words[1:]: #         
  print word

例外があります.大きなシーケンスを反復すると、スライス操作によるオーバーヘッドが大きくなります.シーケンスに10要素しかない場合は、問題はありません.しかし,1000万個の要素がある場合,あるいは1つの個性に敏感な内ループでスライス操作を行う場合,オーバーヘッドは非常に重要になる.この場合rangeの代わりにxrangeを用いることが考えられる[1].
反復シーケンスに使用される以外に、rangeの重要な使い方は、インデックスを生成するのではなく、本当に数値シーケンスを生成したい場合です. 

# Print foo(x) for 0<=x<5
for x in range(5):
  print foo(x)

リスト解析を正しく使用
このようなサイクルがあれば 

# An ugly, slow way to build a list
words = ['her', 'name', 'is', 'rio']
alist = []
for word in words:
  alist.append(foo(word))

リスト解析を使用して書き換えることができます. 

words = ['her', 'name', 'is', 'rio']
alist = [foo(word) for word in words]

どうしてそうするの?リストを正しく初期化することによるエラーを回避する一方で、コードを書くことできれいに見え、きれいに見えます.関数式プログラミングの背景がある人にとってmap関数を使うのはもっとよく知っているかもしれませんが、私から見ればこのやり方はあまりPython化されていません.
リスト解析を使用しない一般的な理由は、次のとおりです.
1.ループネストが必要です.このとき、リスト解析全体をネストしたり、リスト解析でループを複数行使用したりすることができます. 

words = ['her', 'name', 'is', 'rio']
letters = []
for word in words:
  for letter in word:
    letters.append(letter)

リスト解析を使用するには: 

words = ['her', 'name', 'is', 'rio']
letters = [letter for word in words
         for letter in word]

注意:複数のループがあるリスト解析では、リスト解析を使用していないように、ループには同じ順序があります.
2.ループの内部には条件判断が必要です.この条件判断をリスト解析に追加するだけです. 

words = ['her', 'name', 'is', 'rio', '1', '2', '3']
alpha_words = [word for word in words if isalpha(word)]

リスト解析を使用しない合理的な理由は、リスト解析で異常処理を使用できないことです.反復中にいくつかの要素が異常を引き起こす可能性がある場合は、リスト解析で関数呼び出しで可能な異常処理を移行するか、リスト解析を使用しない必要があります.せいのうけっかん
線形時間内に内容をチェックする
文法的には、listまたはset/dictに要素が含まれているかどうかをチェックします.表面的には違いはありませんが、表面的にはまったく違います.データ構造に要素が含まれているかどうかを繰り返しチェックする必要がある場合は、リストの代わりにsetを使用したほうがいいです.(値をチェックする要素に関連付ける場合はdictを使用します.これにより定数チェック時間も実現できます.)

#    list  
lyrics_list = ['her', 'name', 'is', 'rio']
 
#        
words = make_wordlist() #             
for word in words:
  if word in lyrics_list: #       
    print word, "is in the lyrics"
 
#      
lyrics_set = set(lyrics_list) #       set
words = make_wordlist() #             
for word in words:
  if word in lyrics_set: #       
    print word, "is in the lyrics"


[注:Pythonのsetの要素とdictのキー値はハッシュ可能であるため、検索時間の複雑さはO(1)である.
セットの作成には使い捨てのオーバーヘッドが導入されており、作成プロセスにはメンバーのチェックに定数時間がかかる場合でも線形時間がかかります.そのため、ループでメンバーをチェックする必要がある場合は、セットの作成に時間がかかるほうがいいです.一度だけ作成する必要があるからです.変数が漏洩します.
ループ
通常、Pythonでは、変数の役割ドメインが他の言語で期待されるよりも広くなります.たとえば、Javaでは次のコードはコンパイルできません. 

// Get the index of the lowest-indexed item in the array
// that is > maxValue
for(int i = 0; i < y.length; i++) {
  if (y[i] > maxValue) {
    break;
  }
}
// i        :   i
processArray(y, i);

しかし、Pythonでは、同じコードが順調に実行され、予想外の結果が得られます. 

for idx, value in enumerate(y):
  if value > max_value:
    break
 
processList(y, idx)

このコードは正常に動作します.サブyが空の場合を除き、ループは実行されません.processList関数の呼び出しは、idxが定義されていないため、NameError例外を放出します.Pylintコードチェックツールを使用すると、定義されていない可能性のある変数idxを使用すると警告されます.
解決策は永遠に明らかで、サイクルの前にidxをいくつかの特殊な値に設定することができて、このようにあなたはサイクルが永遠に実行されていない時あなたが何を探しているかを知っています.このモードを哨兵モードと言います.では、どの値が哨兵として使用することができますか?C言語の時代あるいはそれ以前に、intがプログラミングの世界を支配する時、1つの期待の間違いに戻る必要がありますエラー結果の関数の一般的なパターンは、-1を返します.たとえば、リストの要素のインデックス値を返したい場合は、次のようになります. 

def find_item(item, alist):
  # None -1  Python 
  result = -1
  for idx, other_item in enumerate(alist):
    if other_item == item:
      result = idx
      break
 
  return result

通常、PythonではNoneはPython標準タイプで一貫して使用されていなくても比較的良い哨兵値である(例えばstr.find[2])
がいぶさようりょういき
Pythonプログラマーは、いわゆる外部役割ドメインであるpythonファイルにコードブロック(関数やクラスなど)に含まれない部分にすべてを置くのが好きです.外部役割ドメインはグローバルネーミングスペースに相当します.この部分の議論のために、グローバル役割ドメインの内容は単一のPythonファイルのどこでもアクセスできると仮定する必要があります.
モジュール全体にアクセスする必要があるファイルの上部に宣言された定数を定義する場合、外部の役割ドメインは非常に強力に見えます.外部の役割ドメイン内の任意の変数に固有の名前を使用するのは賢明です.たとえば、IN_ALL_CAPSという定数名を使用します.これは、次のようなバグを発生させることは容易ではありません. 

import sys
 
# See the bug in the function declaration?
def print_file(filenam):
  """Print every line of a file."""
  with open(filename) as input_file:
    for line in input_file:
      print line.strip()
 
if __name__ == "__main__":
  filename = sys.argv[1]
  print_file(filename)

近くを見るとprint_file関数の定義ではfilenamでパラメータ名が付けられていますが、関数体はfilenameを参照しています.しかし、このプログラムはよく機能しています.なぜですか.print_file関数では、ローカル変数filenameが見つからない場合、次はグローバルな役割ドメインで探します.print_fileの調整によりインデントがあっても外部役割ドメインで使用され、ここで宣言されたfilenameはprint_file関数に対して表示されます.
では、このようなエラーを回避するにはどうすればよいのでしょうか.まず、外用ドメインにおいてIN_ALL_CAPSのようなグローバル変数でなければ値を設定しない[3].パラメータ解析はmain関数に任せることが望ましいため、関数内の任意の内部変数は外用ドメインでは生存しない.
グローバルキーワードglobalにも注目してください.グローバル変数の値を読み取るだけで、グローバルキーワードglobalは必要ありません.グローバル変数名参照のオブジェクトを変更する場合にのみglobalキーワードを使用する必要があります.ここで詳細な関連情報を取得できます.this discussion of the global keyword on Stack Overflow.コードスタイル
PEP 8に敬意を表します
PEP 8はPythonコードの共通スタイルガイドで、心に刻み、できるだけそれに従うべきです.一部の人は、スペースの数を縮めたり、空行を使ったりするなど、細かいスタイルに同意しない十分な理由がありますが、PEP 8に従わない場合は、「私はそのようなスタイルが好きではありません」を除くべきです.それ以外にもっと良い理由です.下のスタイルガイドはPEP 8から抜粋されていて、プログラミング者がよく覚えているようです.
テストが空かどうか
コンテナタイプ(リスト、辞書、コレクションなど)が空であるかどうかを確認するには、len(x)>0のようなチェック方法ではなく、簡単にテストする必要があります. 

numbers = [-1, -2, -3]
# This will be empty
positive_numbers = [num for num in numbers if num > 0]
if positive_numbers:
  # Do something awesome

positive_numbersが空であるかどうかを他の場所で保存したい場合は、bool(positive_number)を結果として保存し、boolはif条件判断文の真値を判断するために使用します.
Noneのテスト
前述したように、Noneは良い哨兵値として使用することができます.では、どのように検査しますか?
Falseの他の値を持つアイテム(空のコンテナや0など)をテストするだけでなく、Noneをテストしたい場合は、次の操作を行います. 

if x is not None:
  # Do something with x

ホイッスルとしてNoneを使用する場合は、Noneと0を区別したい場合など、Pythonスタイルが望んでいるモードでもあります.
変数が役に立つ値であるかどうかをテストするだけであれば、簡単なifモードで十分です. 

if x:
  # Do something with x

たとえば、xがコンテナタイプであることが望ましいが、xが別の関数の戻り値としてNoneになる可能性がある場合は、すぐにこの状況を考慮する必要があります.xに渡される値を変更したかどうかに注意する必要があります.そうしないと、Trueまたは0.0が有用な値だと思っているかもしれませんが、プログラムはあなたの望むように実行されません.
翻訳者注:
[1]Python 2.xでrangeが生成したのはlistオブジェクト、xrangeが生成したのはrangeオブジェクトである;Python 3.xはxrangeを廃棄し、rangeが生成したのはrangeオブジェクトに統一され、listファクトリ関数でlistを明示的に生成することができる;[2]string.find(str)はstringでstringが開始したインデックス値を返し、存在しなければ-1を返す;[3]関数内のローカル変数名に値を設定しないでください.これにより、関数内でローカル変数を呼び出すときにエラーが発生し、外部の役割ドメイン内の同じ名前の変数が呼び出されないようにします.