>  기사  >  백엔드 개발  >  Python 프로그래밍의 안티패턴

Python 프로그래밍의 안티패턴

黄舟
黄舟원래의
2017-02-04 16:53:511335검색

이 기사는 초보 Python 개발자가 작성한 코드에서 본 불규칙하지만 때때로 미묘한 문제를 수집합니다. 이 기사의 목적은 초보 개발자가 보기 흉한 Python 코드를 작성하는 단계를 통과하도록 돕는 것입니다. 대상 독자를 배려하기 위해 이 기사에서는 몇 가지 단순화를 수행했습니다(예를 들어 반복자에 대해 논의할 때 생성자 및 강력한 반복 도구인 itertools는 무시됩니다).

초보 개발자에게는 항상 안티 패턴을 사용해야 하는 몇 가지 이유가 있으며 가능한 경우 이를 제공하려고 노력했습니다. 그러나 일반적으로 이러한 안티패턴으로 인해 코드는 읽기 어렵고 버그가 발생하기 쉬우며 Python의 코딩 스타일을 따르지 않습니다. 보다 관련성이 높은 소개 자료를 찾고 있다면 The Python 튜토리얼 또는 Python에 대해 알아보세요.

반복

범위 사용

파이썬 프로그래밍 초보자는 범위를 사용하여 간단한 반복을 구현하고, 길이 범위 내에서 반복을 얻는 것을 좋아합니다. iterator 컨테이너의 각 요소:

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


range는 시퀀스의 단순 반복을 구현하기 위한 것이 아니라는 점을 기억해야 합니다. 숫자로 정의된 for 루프와 비교할 때 range로 구현된 for 루프는 자연스러워 보이지만 시퀀스 반복에 사용될 때 버그가 발생하기 쉽고 반복자를 직접 구성하는 것만큼 명확하지 않습니다.

for item in alist:
    print item


범위를 남용하면 예상치 못한 일대일 오류가 쉽게 발생할 수 있습니다. 이는 일반적으로 새로운 프로그래머가 범위에 의해 생성된 개체에 범위의 첫 번째 매개 변수가 포함되어 있지만 첫 번째 매개 변수는 포함되어 있지 않다는 사실을 잊어버렸기 때문입니다. 두 번째는 하위 문자열 및 Java의 이 유형의 다른 많은 함수와 유사합니다. 시퀀스의 끝이 초과되지 않았다고 생각하는 새로운 프로그래머는 버그를 만들 것입니다:

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

범위를 부적절하게 사용하는 일반적인 이유:


1. 루프에서 인덱스를 사용합니다. 이는 유효한 이유가 아닙니다. 인덱스를 사용하는 대신 다음 방법을 사용할 수 있습니다.

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

2. 두 개의 루프를 동시에 반복하고 동일한 인덱스를 사용하여 두 개의 값을 가져와야 합니다. 이 경우 zip을 사용할 수 있습니다:

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

3. 시퀀스의 일부를 반복해야 합니다. 이 경우에는 시퀀스 슬라이스를 반복하여 달성할 수 있습니다. 목적을 나타내는 데 필요한 주석을 추가하세요.

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

예외가 있습니다. 대규모 시퀀스를 반복하면 오버헤드가 발생합니다. 슬라이싱 작업으로 인해 상대적으로 크기가 큽니다. 시퀀스에 요소가 10개만 있으면 문제가 되지 않습니다. 그러나 요소가 천만 개이거나 슬라이싱 작업이 성능에 민감한 내부 루프에서 수행되는 경우 오버헤드가 매우 중요해집니다. 이 경우 range 대신 xrange 사용을 고려할 수 있습니다. [1].


시퀀스를 반복하는 데 사용되는 것 외에도 범위의 중요한 용도는 실제로 인덱스를 생성하기보다는 숫자 시퀀스를 생성하려는 경우입니다.

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


사용할 수 있습니다 다시 작성할 목록 구문 분석:

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


이 작업을 수행하는 이유는 무엇입니까? 한편으로는 목록을 올바르게 초기화함으로써 발생할 수 있는 오류를 피할 수 있습니다. 반면에 이런 방식으로 코드를 작성하면 목록이 깨끗하고 깔끔하게 보입니다. 함수형 프로그래밍 배경이 있는 사람들에게는 map 함수를 사용하는 것이 더 친숙하게 느껴질 수 있지만 제 생각에는 이 접근 방식이 덜 Pythonic합니다.


리스트 컴프리헨션을 사용하지 않는 몇 가지 일반적인 이유:


1. 루프 중첩이 필요합니다. 이때 전체 목록 분석을 중첩하거나 목록 분석에서 여러 줄에 루프를 사용할 수 있습니다.

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)


목록 분석 사용:

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


참고: 다중 루프가 있는 List Comprehension에서 루프는 List Comprehension을 사용하지 않는 것과 동일한 순서로 이루어집니다.


2. 루프 내부에는 조건부 판단이 필요합니다. List Comprehension에 다음 조건을 추가하면 됩니다:

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


List Comprehension을 사용하지 않는 타당한 이유는 List Comprehension에서 예외 처리를 사용할 수 없다는 것입니다. 반복의 일부 요소가 예외를 일으킬 수 있는 경우 리스트 컴프리헨션의 함수 호출을 통해 가능한 예외 처리를 전달하거나 리스트 컴프리헨션을 전혀 사용하지 않아야 합니다.

성능 결함

선형 시간으로 내용 확인

구문적으로 목록이나 집합/딕셔너리에 요소가 포함되어 있는지 확인하는 것은 표면적으로는 별 차이가 없는 것처럼 보이지만, 내부적으로는 표면이 완전히 다릅니다. 특정 데이터 구조에 요소가 포함되어 있는지 반복적으로 확인해야 하는 경우 목록 대신 집합을 사용하는 것이 가장 좋습니다. (값을 검사할 요소와 연결하려면 dict를 사용할 수 있습니다. 이 방법으로도 검사 시간이 일정합니다.)

# 
假设以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 집합의 요소와 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函数的调用将会抛出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] 함수 내부에서 지역 변수를 호출하고 외부 스코프에서 같은 이름의 변수를 호출할 때 오류가 발생하지 않도록 외부 스코프의 함수에서는 지역 변수 이름에 어떤 값도 설정하지 마세요.

위 내용은 파이썬 프로그래밍의 안티패턴 내용입니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(www.php.cn)를 참고해주세요!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.