ホームページ >バックエンド開発 >Python チュートリアル >Python プログラミングにおけるアンチパターン

Python プログラミングにおけるアンチパターン

黄舟
黄舟オリジナル
2017-02-04 16:53:511372ブラウズ

この記事では、初心者の Python 開発者によって書かれたコードで見られた、不規則ではあるが時折微妙な問題を集めています。この記事の目的は、初心者の開発者が醜い Python コードを作成する段階を乗り越えられるようにすることです。対象読者に配慮するために、この記事ではいくつかの簡略化を行っています (たとえば、イテレーター、ジェネレーター、および強力な反復ツール itertools について説明する場合は無視されます)。

初心者の開発者のために、アンチパターンを使用する理由は常にいくつかあり、私は可能な限りそれらを提供するように努めてきました。しかし通常、これらのアンチパターンにより、コードは読みにくくなり、バグが発生しやすくなり、Python のコーディング スタイルに準拠しないものになります。より関連性の高い紹介資料をお探しの場合は、次の資料を強くお勧めします。 Python チュートリアルまたは Python の詳細。

反復

range の使用

Python プログラミングの初心者は、range を使用して単純な反復を実装し、反復子の長さの範囲内で反復子の各要素を取得することを好みます。 : Range は、シーケンスの単純な反復を実装することを目的としたものではありません。数値で定義された for ループと比較すると、range で実装された for ループは自然に見えますが、シーケンスの反復に使用するとバグが発生しやすく、反復子を直接構築する場合ほど明確ではありません:

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


range乱用すると、予期しない off-by-one エラーが簡単に発生する可能性があります。これは、Java やその他の関数の部分文字列と同様に、range によって生成されたオブジェクトに範囲の最初のパラメーターが含まれていることを忘れることが原因です。このタイプのもの。シーケンスの終わりを超えていないと考える新人プログラマーはバグを作成します:

for item in alist:
    print item

範囲を不適切に使用する一般的な理由:

1 ループ内でインデックスを使用する必要がある。これは正当な理由ではありません。インデックスを使用する代わりに以下を使用できます:

# 
迭代整个序列错误的方法
alist = ['her', 'name', 'is', 'rio']
for i in range(0, len(alist) - 1): # 大小差一(Off by one)!
    print i, alist[i]

2. 2 つの値を取得するには、2 つのループを同時に繰り返し、同じインデックスを使用する必要があります。この場合、zip を使用できます:

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

3. シーケンスの一部を反復する必要があります。この場合、シーケンス スライスを反復するだけで実現できます。目的を示すために必要なコメントを追加してください。

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

例外が 1 つあります。大規模なシーケンスを反復する場合、スライス操作によって生じるオーバーヘッドは比較的大きくなります。大きい。シーケンスに要素が 10 個しかない場合は問題ありませんが、要素が 1,000 万個ある場合、またはパフォーマンスが重視される内部ループでスライス操作が実行される場合、オーバーヘッドが非常に重要になります。この場合、range の代わりに xrange を使用することを検討できます。 [1]。

range の重要な使い方は、シーケンスを反復処理する以外に、インデックスを生成するのではなく実際に数値シーケンスを生成したい場合です。

for word in words[1:]: # 不包括第一个元素
    print word

リスト内包表記を正しく使用してください

If次のようなループがあります:

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


リスト内包表記を使用して書き換えることができます:

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


なぜこれを行うのでしょうか?一方では、リストを正しく初期化することによって発生する可能性のあるエラーを回避できますが、他方では、この方法でコードを記述すると、見た目がすっきりします。関数型プログラミングのバックグラウンドを持つ人にとっては、map 関数の使用の方が馴染みがあるかもしれませんが、私の意見では、このアプローチは Python 的ではありません。


リスト内包表記を使用しないその他の一般的な理由:


1. ループのネストが必要。現時点では、リスト解析全体をネストすることも、リスト解析の複数行でループを使用することもできます:

words = [&#39;her&#39;, &#39;name&#39;, &#39;is&#39;, &#39;rio&#39;]
alist = [foo(word) for word in words]


リスト解析を使用します:

words = [&#39;her&#39;, &#39;name&#39;, &#39;is&#39;, &#39;rio&#39;]
letters = []
for word in words:
    for letter in word:
        letters.append(letter)


注: 複数のループを含むリスト解析では、ループはリスト内包表記を使用しない場合と同じ順序です。


2. ループ内で条件判定が必要です。この条件をリスト内包表記に追加するだけです:

words = [&#39;her&#39;, &#39;name&#39;, &#39;is&#39;, &#39;rio&#39;]
letters = [letter for word in words
                  for letter in word]


リスト内包表記を使用しない正当な理由は、リスト内包表記では例外処理を使用できないことです。反復内の一部の要素が例外を引き起こす可能性がある場合は、リスト内包表記の関数呼び出しを通じて発生する可能性のある例外処理を転送するか、リスト内包表記をまったく使用しない必要があります。

パフォーマンスの欠陥

線形時間でのコンテンツのチェック

構文的には、リストまたは set/dict に要素が含まれているかどうかのチェックは、表面的には同じように見えるかもしれませんが、表面下ではまったく異なります。特定のデータ構造に要素が含まれているかどうかを繰り返し確認する必要がある場合は、リストの代わりにセットを使用するのが最善です。 (値をチェック対象の要素に関連付けたい場合は、dict を使用できます。これにより、一定のチェック時間を実現することもできます。)

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

[翻訳者注: Python の set の要素とキーの値の dict はハッシュ可能であるため、検索の時間計算量は O(1) です。 ]


覚えておくべきこと: セットの作成には 1 回限りのオーバーヘッドが発生し、メンバーのチェックに一定の時間がかかる場合でも、作成プロセスには直線的な時間がかかります。したがって、ループ内のメンバーをチェックする必要がある場合は、セットを作成する必要があるのは 1 回だけであるため、時間をかけて最初にセットを作成することをお勧めします。

変数リーク

ループ

一般に、Python では、変数のスコープは他の言語で予想されるよりも広いです。例: Java の次のコードはコンパイルされません:

# 
假设以list开始
lyrics_list = [&#39;her&#39;, &#39;name&#39;, &#39;is&#39;, &#39;rio&#39;]
 
# 
避免下面的写法
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 では、同じコードが常にスムーズに実行され、期待される結果が得られます:

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


这段代码将会正常运行,除非子y为空的情况下,此时,循环永远不会执行,而且processList函数的调用将会抛出NameError异常,因为idx没有定义。如果你使用Pylint代码检查工具,将会警告:使用可能没有定义的变量idx。


解决办法永远是显然的,可以在循环之前设置idx为一些特殊的值,这样你就知道如果循环永远没有执行的时候你将要寻找什么。这种模式叫做哨兵模式。那么什么值可以用来作为哨兵呢?在C语言时代或者更早,当int统治编程世界的时候,对于需要返回一个期望的错误结果的函数来说为通用的模式为返回-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 这个常量名。 这将不容易造成如下bug:

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(http://stackoverflow.com/questions/4693120/use-of-global-keyword-in-python/4693170#4693170)。

代码风格

向PEP8致敬

PEP 8是Python代码的通用风格指南,你应该牢记在心并且尽可能去遵循它,尽管一些人有充分的理由不同意其中一些细小的风格,例如缩进的空格个数或使用空行。如果你不遵循PEP8,你应该有除“我只是不喜欢那样的风格”之外更好的理由。下边的风格指南都是从PEP8中摘取的,似乎是编程者经常需要牢记的。

测试是否为空

如果你要检查一个容器类型(例如:列表,词典,集合)是否为空,只需要简单测试它而不是使用类似检查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可以作为一个很好的哨兵值。那么如何检查它呢?

如果你明确的想要测试None,而不只是测试其他一些值为False的项(如空容器或者0),可以使用:

if x is not None:
    # Do something with x

如果你使用None作为哨兵,这也是Python风格所期望的模式,例如在你想要区分None和0的时候。

如果你只是测试变量是否为一些有用的值,一个简单的if模式通常就够用了:

if x:
    # Do something with x

例如:如果期望x是一个容器类型,但是x可能作另一个函数的返回结果值变为None,你应该立即考虑到这种情况。你需要留意是否改变了传给x的值,否则可能你认为True或0. 0是个有用的值,程序却不会按照你想要的方式执行。

译者注:


[1] 在Python2.x 中 range生成的是list对象,xrange生成的则是range对象;Python 3.x 废除了xrange,range生成的统一为range对象,用list工厂函数可以显式生成list;

[2] string.find(str)返回str在string中开始的索引值,如果不存在则返回-1;

[3] 関数内でローカル変数を呼び出し、外部スコープで同名の変数を呼び出す際のエラーを防ぐため、外部スコープの関数内のローカル変数名には値を設定しないでください。

上記は Python プログラミングのアンチパターンの内容です。さらに関連する内容については、PHP 中国語 Web サイト (www.php.cn) に注目してください。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。