>  기사  >  백엔드 개발  >  Python 프로그래머가 저지르는 10가지 일반적인 실수

Python 프로그래머가 저지르는 10가지 일반적인 실수

高洛峰
高洛峰원래의
2016-10-18 09:49:43904검색

Python 정보

Python은 동적 의미 체계를 갖춘 해석된 객체 지향 고급 프로그래밍 언어입니다. 고급 데이터 구조가 내장되어 있으며 동적 타이핑과 동적 바인딩의 장점을 결합하여 신속한 애플리케이션 개발에 매우 ​​매력적이며 기존 구성 요소나 서비스를 연결하는 스크립트 또는 글루 언어로 사용할 수 있습니다. Python은 모듈과 패키지를 지원하므로 프로그램 모듈화 및 코드 재사용을 장려합니다.

이 기사 정보

Python의 배우기 쉬운 구문으로 인해 Python 개발자, 특히 프로그래밍에 익숙하지 않은 개발자는 Python의 미묘함을 간과하고 언어 능력을 과소평가할 수 있습니다.

이를 염두에 두고 이 기사에서는 고급 Python 개발자도 때때로 파악하기 어려운 오류의 "상위 10개" 목록을 제시합니다.

일반적인 실수 1: 표현식을 함수 매개변수의 기본값으로 남용

Python에서는 함수 매개변수에 기본 선택적 값을 제공할 수 있습니다. 이는 언어의 기능이지만 변경 가능한 기본값으로 인해 약간의 혼란을 초래할 수 있습니다. 예를 들어 다음 Python 함수의 정의를 살펴보세요.

>>> def foo(bar=[]):        # bar is optional and defaults to [] if not specified  
...    bar.append("baz")    # but this line could be problematic, as we'll see...  
...    return bar

일반적인 실수는 선택적 매개변수가 함수가 실행될 때마다 선택적 매개변수라고 생각하는 것입니다. 매개변수 없이 호출되면 매개변수는 지정된 기본값으로 설정됩니다. 예를 들어, 위의 코드에서 foo()에 대한 반복 호출(즉, bar 매개변수를 명시적으로 지정하지 않고)은 항상 'baz'를 반환할 것으로 예상할 수 있습니다. 왜냐하면 foo()에 대한 각 호출은 (bar 매개변수를 지정하지 않고) 다음과 같이 가정하기 때문입니다. bar는 [](즉, 빈 목록)으로 설정됩니다.

하지만 이렇게 하면 정확히 무슨 일이 일어나는지 볼까요?

>>> foo()  
["baz"]>>> foo()  
["baz", "baz"]>>> foo()  
["baz", "baz", "baz"]

 

그래요? 새 목록을 만드는 대신 foo()가 호출될 때마다 기존 목록에 기본값 "baz"를 추가하는 이유는 무엇입니까?

대답은 함수 매개변수의 기본값이 함수가 정의될 ​​때 한 번만 평가된다는 것입니다. 따라서 bar 매개변수는 기본값(즉, 빈 목록)으로 초기화됩니다. 즉, foo()가 처음 정의될 때, foo()가 호출될 때(즉, bar 매개변수가 지정되지 않을 때) 계속 사용됩니다. ) bar는 원래 초기화된 매개변수였습니다.

일반적인 해결책은 다음과 같습니다.

>>> def foo(bar=None):  
...    if bar is None:        # or if not bar:  
...        bar = []  
...    bar.append("baz")  
...    return bar  
...  
>>> foo()  
["baz"]  
>>> foo()  
["baz"]  
>>> foo()  
["baz"]

일반적인 실수 2: 클래스 변수의 잘못된 사용

다음 예:

>>> class A(object):  
...     x = 1 
...  
>>> class B(A):  
...     pass 
...  
>>> class C(A):  
...     pass 
...  
>>> print A.x, B.x, C.x  
1 1 1

정기적으로 사용하세요.

1

2

3

>>> B.x = 2

>>> print A.x, B.x, C.x

1 2 1

음, 다시 시도해도 마찬가지입니다.

1

2

3

>>> A.x = 3

>>> 인쇄 A.x, B.x, C.x

3 2 3

What$%#!&?? 왜 C가 있지? 따라서 위 코드에서는 C 클래스의 x 속성을 찾을 수 없으므로 해당 기본 클래스를 찾습니다(Python은 다중 상속을 지원하지만 위 예에서는 A만 있습니다). 즉, 클래스 C에는 A와 독립적인 자체 x 속성이 없습니다. 따라서 C.x는 실제로 A.x에 대한 참조입니다.

일반적인 실수 3: 제외에 대해 잘못된 매개변수 지정

다음과 같은 코드가 있다고 가정해 보겠습니다.

>>> try:  
...     l = ["a", "b"]  
...     int(l[2])  
... except ValueError, IndexError:  # To catch both exceptions, right?  
...     pass 
...  
Traceback (most recent call last):  
  File "<stdin>", line 3, in <module>  
IndexError: list index out of range

여기서 문제는 Except 문이 이런 방식으로 지정된 예외 목록을 허용하지 않는다는 것입니다. 대조적으로, Python 2.x에서는 Exception을 제외한 구문을 사용하여 e는 나중에 사용할 수 있도록 두 번째 선택적 인수(이 예에서는 e)에 예외 객체를 바인딩합니다. 따라서 위의 예에서 IndexError 예외는 Except 문에 의해 포착되지 않지만 IndexError라는 매개 변수에 바인딩될 때 발생합니다.

예외 문에서 여러 예외를 포착하는 올바른 방법은 첫 번째 매개변수를 포착할 모든 예외를 포함하는 튜플로 지정하는 것입니다. 그리고 코드 이식성을 위해 as 키워드를 사용하세요. Python 2와 Python 3 모두 다음 구문을 지원하기 때문입니다.

>>> try:  
...     l = ["a", "b"]  
...     int(l[2])  
... except (ValueError, IndexError) as e:    
...     pass 
...  
>>>

일반적인 오류 4: 하지 마세요. Python의 범위 이해

Python은 Parsing을 위해 LEGB를 기반으로 합니다. LEGB는 Local, Enclosing, Global, Builder의 약자입니다. "텍스트의 의미를 아는 것"인 것 같죠? 사실 Python에는 주의가 필요한 몇 가지 사항이 있습니다. 먼저 다음 코드를 살펴보겠습니다.

>>> x = 10 
>>> def foo():  
...     x += 1 
...     print x  
...  
>>> foo()  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
  File "<stdin>", line 2, in foo  
UnboundLocalError: local variable &#39;x&#39; referenced before assignment

여기서 무엇이 문제인가요?

위 문제는 범위 내의 변수에 값을 할당할 때 Python이 자동으로 해당 값을 현재 범위의 지역 변수로 처리하여 동일한 변수를 가진 외부 범위의 변수를 숨기기 때문에 발생합니다. 이름.

이전에는 정상적으로 실행되던 코드의 함수 본문 어딘가에 할당문을 추가한 후 UnboundLocalError가 발생하면 많은 사람들이 놀랐습니다. (자세한 내용은 여기에서 확인하세요)

尤其是当开发者使用 lists 时,这个问题就更加常见.  请看下面这个例子:

>>> lst = [1, 2, 3]  
>>> def foo1():  
...     lst.append(5)   # 没有问题...  
...  
>>> foo1()  
>>> lst  
[1, 2, 3, 5]  
  
>>> lst = [1, 2, 3]  
>>> def foo2():  
...     lst += [5]      # ... 但是这里有问题!  
...  
>>> foo2()

 

Traceback (most recent call last):  

  File "", line 1, in  

  File "", line 2, in foo  

UnboundLocalError: local variable 'lst' referenced before assignment 

嗯?为什么 foo2 报错,而foo1没有问题呢?

原因和之前那个例子的一样,不过更加令人难以捉摸。foo1 没有对 lst 进行赋值操作,而 foo2 做了。要知道, lst += [5] 是 lst = lst + [5] 的缩写,我们试图对 lst 进行赋值操作(Python把他当成了局部变量)。此外,我们对 lst 进行的赋值操作是基于 lst 自身(这再一次被Python当成了局部变量),但此时还未定义。因此出错!

常见错误 5:当迭代时修改一个列表(List)

下面代码中的问题应该是相当明显的:

>>> odd = lambda x : bool(x % 2)  
>>> numbers = [n for n in range(10)]  
>>> for i in range(len(numbers)):  
...     if odd(numbers[i]):  
...         del numbers[i]  # BAD: Deleting item from a list while iterating over it  
...

   

Traceback (most recent call last):  

        File "", line 2, in  

IndexError: list index out of range  

当迭代的时候,从一个 列表 (List)或者数组中删除元素,对于任何有经验的开发者来说,这是一个众所周知的错误。尽管上面的例子非常明显,但是许多高级开发者在更复杂的代码中也并非是故意而为之的。

幸运的是,Python包含大量简洁优雅的编程范例,若使用得当,能大大简化和精炼代码。这样的好处是能得到更简化和更精简的代码,能更好的避免程序中出现当迭代时修改一个列表(List)这样的bug。一个这样的范例是递推式列表(list comprehensions)。而且,递推式列表(list comprehensions)针对这个问题是特别有用的,通过更改上文中的实现,得到一段极佳的代码:

>>> odd = lambda x : bool(x % 2)  
>>> numbers = [n for n in range(10)]  
>>> numbers[:] = [n for n in numbers if not odd(n)]  # ahh, the beauty of it all  
>>> numbers  
[0, 2, 4, 6, 8]

   

常见错误 6: 不明白Python在闭包中是如何绑定变量的

看下面这个例子:

>>> def create_multipliers():  
...     return [lambda x : i * x for i in range(5)]  
>>> for multiplier in create_multipliers():  
...     print multiplier(2)  
...

   

你也许希望获得下面的输出结果:

但实际的结果却是:

8  

8  

8  

8  

8  

惊讶吧!

这之所以会发生是由于Python中的“后期绑定”行为——闭包中用到的变量只有在函数被调用的时候才会被赋值。所以,在上面的代码中,任何时候,当返回的函数被调用时,Python会在该函数被调用时的作用域中查找 i 对应的值(这时,循环已经结束,所以 i 被赋上了最终的值——4)。

解决的方法有一点hack的味道:

>>> def create_multipliers():  
...     return [lambda x, i=i : i * x for i in range(5)]  
...  
>>> for multiplier in create_multipliers():  
...     print multiplier(2)  
...

   

在这里,我们利用了默认参数来生成一个匿名的函数以便实现我们想要的结果。有人说这个方法很巧妙,有人说它难以理解,还有人讨厌这种做法。但是,如果你是一个 Python 开发者,理解这种行为很重要。

常见错误 7: 创建循环依赖模块

让我们假设你有两个文件,a.py 和 b.py,他们之间相互引用,如下所示:

a.py:

import b  
def f():  
    return b.x    
print f()

   

b.py:

import a  
x = 1 
def g():  
    print a.f()

   

首先,让我们尝试引入 a.py:

>>> import a  

可以正常工作。这也许是你感到很奇怪。毕竟,我们确实在这里引入了一个循环依赖的模块,我们推测这样会出问题的,不是吗?

答案就是在Python中,仅仅引入一个循环依赖的模块是没有问题的。如果一个模块已经被引入了,Python并不会去再次引入它。但是,根据每个模块要访问其他模块中的函数和变量位置的不同,就很可能会遇到问题。

所以,回到我们这个例子,当我们引入 a.py 时,再引入 b.py 不会产生任何问题,因为当引入的时候,b.py 不需要 a.py 中定义任何东西。b.py 中唯一引用 a.py 中的东西是调用 a.f()。 但是那个调用是发生在g() 中的,并且 a.py 和 b.py 中都没有调用 g()。所以运行正常。

但是,如果我们尝试去引入b.py 会发生什么呢?(在这之前不引入a.py),如下所示:

>>> import b

   

Traceback (most recent call last):  

        File "", line 1, in  

        File "b.py", line 1, in  

    import a  

        File "a.py", line 6, in  

    print f()  

        File "a.py", line 4, in f  

    return b.x  

AttributeError: 'module' object has no attribute 'x' 

啊哦。 出问题了!此处的问题是,在引入b.py的过程中,Python尝试去引入 a.py,但是a.py 要调用f(),而f() 有尝试去访问 b.x。但是此时 b.x 还没有被定义呢。所以发生了 AttributeError 异常。

至少,解决这个问题很简单,只需修改b.py,使其在g()中引入 a.py:

x = 1 
def g():  
    import a    # 只有当g()被调用的时候才会引入a  
    print a.f()

   

现在,当我们再引入b,没有任何问题:

>>> import b  
>>> b.g()  
1    # Printed a first time since module 'a' calls 'print f()' at the end  
1    # Printed a second time, this one is our call to 'g'

    

常见错误 8: 与Python标准库中的模块命名冲突

   

Python一个令人称赞的地方是它有丰富的模块可供我们“开箱即用”。但是,如果你没有有意识的注意的话,就很容易出现你写的模块和Python自带的标准库的模块之间发生命名冲突的问题(如,你也许有一个叫 email.py 的模块,但这会和标准库中的同名模块冲突)。

这可能会导致很怪的问题,例如,你引入了另一个模块,但这个模块要引入一个Python标准库中的模块,由于你定义了一个同名的模块,就会使该模块错误的引入了你的模块,而不是 stdlib 中的模块。这就会出问题了。

因此,我们必须要注意这个问题,以避免使用和Python标准库中相同的模块名。修改你包中的模块名要比通过 Python Enhancement Proposal (PEP) 给Python提建议来修改标准库的模块名容易多了。

常见错误 #9: 未能解决Python 2和Python 3之间的差异

请看下面这个 filefoo.py:

import sys  
def bar(i):  
    if i == 1:  
        raise KeyError(1)  
    if i == 2:  
        raise ValueError(2)  
  
def bad():  
    e = None 
    try:  
        bar(int(sys.argv[1]))  
    except KeyError as e:  
        print(&#39;key error&#39;)  
    except ValueError as e:  
        print(&#39;value error&#39;)  
    print(e)  
  
bad()

   

在Python 2中运行正常:

$ python foo.py 1 

key error  

$ python foo.py 2 

value error  

但是,现在让我们把它在Python 3中运行一下:

$ python3 foo.py 1 

key error  

Traceback (most recent call last):  

  File "foo.py", line 19, in  

    bad()  

  File "foo.py", line 17, in bad  

    print(e)  

UnboundLocalError: local variable 'e' referenced before assignment  

出什么问题了? “问题”就是,在 Python 3 中,异常的对象在 except 代码块之外是不可见的。(这样做的原因是,它将保存一个对内存中堆栈帧的引用周期,直到垃圾回收器运行并且从内存中清除掉引用。了解更多技术细节请参考这里) 。

一种解决办法是在 except 代码块的外部作用域中定义一个对异常对象的引用,以便访问。下面的例子使用了该方法,因此最后的代码可以在Python 2 和 Python 3中运行良好。

import sys  
def bar(i):  
    if i == 1:  
        raise KeyError(1)  
    if i == 2:  
        raise ValueError(2)  
def good():  
    exception = None 
    try:  
        bar(int(sys.argv[1]))  
    except KeyError as e:  
        exception = e  
        print(&#39;key error&#39;)  
    except ValueError as e:  
        exception = e  
        print(&#39;value error&#39;)  
    print(exception)  
  
good()

   

在Py3k中运行:

$ python3 foo.py 1 

key error  

$ python3 foo.py 2 

value error  

正常!

(顺便提一下, 我们的 Python Hiring Guide 讨论了当我们把代码从Python 2 迁移到 Python 3时的其他一些需要知道的重要差异。)

常见错误 10: 误用__del__方法

假设你有一个名为 calledmod.py 的文件:

import foo  
class Bar(object):  
           ...  
    def __del__(self):  
        foo.cleanup(self.myhandle)

   

并且有一个名为 another_mod.py 的文件:

import mod  

mybar = mod.Bar()  

你会得到一个 AttributeError 的异常。

为什么呢?因为,正如这里所说,当解释器退出的时候,模块中的全局变量都被设置成了 None。所以,在上面这个例子中,当 __del__ 被调用时,foo 已经被设置成了None。

解决方法是使用 atexit.register() 代替。用这种方式,当你的程序结束执行时(意思是正常退出),你注册的处理程序会在解释器退出之前执行。

了解了这些,我们可以将上面 mod.py 的代码修改成下面的这样:

import foo  
import atexit  
def cleanup(handle):  
    foo.cleanup(handle)  
class Bar(object):  
    def __init__(self):  
        ...  
        atexit.register(cleanup, self.myhandle)

   

这种实现方式提供了一个整洁并且可信赖的方法用来在程序退出之前做一些清理工作。很显然,它是由foo.cleanup 来决定对绑定在 self.myhandle 上对象做些什么处理工作的,但是这就是你想要的。

总结

Python是一门强大的并且很灵活的语言,它有很多机制和语言规范来显著的提高你的生产力。和其他任何一门语言或软件一样,如果对它能力的了解有限,这很可能会给你带来阻碍,而不是好处。正如一句谚语所说的那样 “knowing enough to be dangerous”(译者注:意思是自以为已经了解足够了,可以做某事了,但其实不是)。

熟悉Python的一些关键的细微之处,像本文中所提到的那些(但不限于这些),可以帮助我们更好的去使用语言,从而避免一些常见的陷阱。


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