Heim  >  Artikel  >  Backend-Entwicklung  >  Antimuster in der Python-Programmierung

Antimuster in der Python-Programmierung

黄舟
黄舟Original
2017-02-04 16:53:511333Durchsuche

In diesem Artikel werden unregelmäßige, aber gelegentlich subtile Probleme gesammelt, die ich in Code gesehen habe, der von unerfahrenen Python-Entwicklern geschrieben wurde. Der Zweck dieses Artikels besteht darin, unerfahrenen Entwicklern dabei zu helfen, das Stadium des Schreibens von hässlichem Python-Code zu überwinden. Um die Zielgruppe zu berücksichtigen, wurden in diesem Artikel einige Vereinfachungen vorgenommen (bei der Diskussion von Iteratoren werden beispielsweise Generatoren und das leistungsstarke Iterationstool itertools ignoriert).

Für die neuen Entwickler da draußen gibt es immer einige Gründe, Anti-Patterns zu verwenden, und ich habe versucht, sie nach Möglichkeit anzugeben. Aber normalerweise führen diese Anti-Patterns zu Code, der weniger lesbar, anfälliger für Fehler ist und nicht dem Codierungsstil von Python entspricht. Wenn Sie nach relevanteren Einführungsmaterialien suchen, kann ich The wärmstens empfehlen Python-Tutorial oder Tauchen Sie ein in Python.

Iteration

Verwendung von Bereichen

Neulinge in der Python-Programmierung verwenden gerne Bereiche, um einfache Iterationen zu implementieren und Iterationen innerhalb des Längenbereichs der zu erhalten iterator Jedes Element im Container:

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


Es sollte beachtet werden, dass der Bereich nicht dazu gedacht ist, eine einfache Iteration der Sequenz zu implementieren. Im Vergleich zu den mit Zahlen definierten For-Schleifen scheint die mit Range implementierte For-Schleife zwar sehr natürlich zu sein, ist jedoch bei der Verwendung in der Sequenziteration fehleranfällig und nicht so klar wie die direkte Konstruktion eines Iterators:

for item in alist:
    print item


Der Missbrauch des Bereichs kann leicht zu unerwarteten Off-by-One-Fehlern führen. Dies wird normalerweise dadurch verursacht, dass neue Programmierer vergessen, dass das von Range generierte Objekt den ersten Parameter des Bereichs enthält Es ähnelt Teilzeichenfolgen und vielen anderen Funktionen dieses Typs in Java. Neue Programmierer, die glauben, dass das Ende der Sequenz nicht überschritten wird, erzeugen Fehler:

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

Häufige Gründe für die unangemessene Verwendung von Bereichen:


1. Der Index muss in der Schleife verwendet werden. Dies ist kein gültiger Grund. Anstatt den Index zu verwenden, können Sie die folgende Methode verwenden:

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

2. Sie müssen zwei Schleifen gleichzeitig durchlaufen und denselben Index verwenden, um zwei Werte zu erhalten . In diesem Fall kann zip verwendet werden:

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

3. Ein Teil der Sequenz muss iteriert werden. In diesem Fall kann dies erreicht werden, indem einfach über das Sequenz-Slice iteriert wird. Bitte fügen Sie die erforderlichen Kommentare hinzu, um den Zweck anzugeben:

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

Es gibt eine Ausnahme: Wenn Sie über eine große Sequenz iterieren, erfolgt das Slicing Betriebsursachen Die Kosten sind relativ hoch. Wenn die Sequenz nur 10 Elemente hat, ist das kein Problem; wenn es aber 10 Millionen Elemente sind oder wenn der Slicing-Vorgang in einer leistungsempfindlichen inneren Schleife ausgeführt wird, wird der Overhead sehr groß. In diesem Fall können Sie die Verwendung von xrange anstelle von range in Betracht ziehen [1].


Range wird nicht nur zum Durchlaufen einer Sequenz verwendet, sondern auch dann, wenn Sie tatsächlich eine Zahlenfolge und nicht einen Index generieren möchten:

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

Listenverständnisse richtig verwenden


Wenn Sie eine Schleife wie diese haben:

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


Sie können Listenverständnisse verwenden, um Folgendes umzuschreiben:

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


Warum das tun? Einerseits vermeiden Sie mögliche Fehler, die durch die korrekte Initialisierung der Liste entstehen können. Andererseits sieht das Schreiben des Codes auf diese Weise sauber und aufgeräumt aus. Für diejenigen mit einem Hintergrund in funktionaler Programmierung mag die Verwendung der Kartenfunktion vertrauter sein, aber meiner Meinung nach ist dieser Ansatz weniger pythonisch.


Einige andere häufige Gründe, Listenverständnisse nicht zu verwenden:


1. Schleifenverschachtelung ist erforderlich. Zu diesem Zeitpunkt können Sie die gesamte Listenanalyse verschachteln oder Schleifen in mehreren Zeilen in der Listenanalyse verwenden:

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)


Listenanalyse verwenden:

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


Hinweis: In einem Listenverständnis mit mehreren Schleifen sind die Schleifen in der gleichen Reihenfolge, als ob Sie kein Listenverständnis verwenden würden.


2. Sie benötigen eine bedingte Beurteilung innerhalb der Schleife. Sie müssen nur diese Bedingung zum Listenverständnis hinzufügen:

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


Ein guter Grund, Listenverständnisse nicht zu verwenden, besteht darin, dass Sie in Listenverständnissen keine Ausnahmen verwenden können mit. Wenn einige Elemente in der Iteration Ausnahmen verursachen können, müssen Sie eine mögliche Ausnahmebehandlung durch Funktionsaufrufe in das Listenverständnis übertragen oder das Listenverständnis überhaupt nicht verwenden.

Leistungsmängel

Überprüfung von Inhalten in linearer Zeit

Syntaktisch scheint die Überprüfung, ob eine Liste oder ein Set/Dikt ein Element enthält, an der Oberfläche keinen Unterschied zu machen, aber darunter Die Oberfläche ist völlig anders. Wenn Sie wiederholt überprüfen müssen, ob eine bestimmte Datenstruktur ein Element enthält, verwenden Sie am besten eine Menge anstelle einer Liste. (Wenn Sie dem zu prüfenden Element einen Wert zuordnen möchten, können Sie ein Diktat verwenden; dadurch wird auch eine konstante Prüfzeit erreicht.)

# 
假设以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"


[Anmerkung des Übersetzers : Die Elemente von set und die Schlüsselwerte von dict in Python sind hashbar, daher beträgt die zeitliche Komplexität der Suche O(1). ]


Es sollte beachtet werden, dass das Erstellen eines Sets einen einmaligen Mehraufwand mit sich bringt und der Erstellungsprozess lineare Zeit in Anspruch nimmt, selbst wenn die Mitgliederüberprüfung eine konstante Zeit in Anspruch nimmt. Wenn Sie also Mitglieder in einer Schleife überprüfen müssen, ist es besser, sich zuerst die Zeit zu nehmen, den Satz zu erstellen, da Sie ihn nur einmal erstellen müssen.

Variablenleck

Schleife


Im Allgemeinen ist in Python der Umfang einer Variablen größer, als man es in anderen Sprachen erwarten würde großzügig sein. Beispiel: Der folgende Code in Java lässt sich nicht kompilieren:

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


In Python wird derselbe Code jedoch immer reibungslos ausgeführt und liefert die erwarteten Ergebnisse:

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


这段代码将会正常运行,除非子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] Legen Sie keinen Wert für den lokalen Variablennamen in der Funktion im externen Bereich fest, um einen Fehler beim Aufrufen der lokalen Variablen innerhalb der Funktion und beim Aufrufen der Variablen mit demselben Namen im externen Bereich zu verhindern.

Das Obige ist der Inhalt von Anti-Patterns in der Python-Programmierung. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (www.php.cn)!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn