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

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

高洛峰
高洛峰원래의
2016-10-17 13:16:26920검색

공부를 하든, 일을 하든 사람은 실수를 하게 마련입니다. Python의 구문은 간단하고 유연하지만 여전히 몇 가지 큰 함정이 있습니다. 주의하지 않으면 초보자와 숙련된 Python 프로그래머 모두 넘어질 수 있습니다. 이 기사는 10가지 일반적인 실수를 공유합니다. 필요한 친구는 이를 참조할 수 있습니다

일반적인 실수 1: 표현식을 함수의 기본 매개변수로 잘못 사용

Python에서는 기본값을 설정할 수 있습니다. 매개변수 값을 지정하여 매개변수를 선택사항으로 만듭니다. 이는 훌륭한 언어 기능이지만 기본값이 변경 가능한 유형인 경우 혼란스러운 상황을 초래할 수도 있습니다. 다음 Python 함수 정의를 살펴보겠습니다.

>>> def foo(bar=[]):    # bar是可选参数,如果没有提供bar的值,则默认为[],
...  bar.append("baz")  # 但是稍后我们会看到这行代码会出现问题。
...  return bar

Python 프로그래머가 저지르는 일반적인 실수는 함수가 호출될 때마다 선택적 매개변수에 대해 값이 전달되지 않는 경우를 당연하게 여기는 것입니다. , 그러면 이 선택적 매개변수가 지정된 기본값으로 설정됩니다. 위 코드에서 foo() 함수를 반복적으로 호출하면 항상 'baz'가 반환되어야 한다고 생각할 수도 있습니다. 기본적으로 foo() 함수가 실행될 때마다(bar 변수의 값이 지정되지 않음) bar가 반환되기 때문입니다. 변수는 [ ](즉, 새로운 빈 목록)로 설정됩니다.

그런데 실제 실행 결과는 이렇습니다.

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

이상하지 않나요? 새로운 빈 목록을 생성하는 대신 foo() 함수가 호출될 때마다 기본값 "baz"가 기존 목록에 추가되는 이유는 무엇입니까?

답은 선택적 매개변수의 기본값 설정은 Python에서 한 번, 즉 함수가 정의될 ​​때에만 실행된다는 것입니다. 따라서 foo() 함수가 정의된 경우에만 bar 매개변수가 기본값(즉, 빈 리스트)으로 초기화되지만 이후 foo() 함수가 호출될 때마다 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"]

FAQ 2: 클래스 변수의 잘못된 사용

다음 예를 살펴보겠습니다.

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

이 결과는 정상입니다.

>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1

그럼 결과는 예상대로군요.

>>> A.x = 3
>>> print A.x, B.x, C.x
3 2 3

파이썬 언어에서 클래스 변수는 딕셔너리 형식으로 처리되며 MRO(메소드 결정 순서)를 따릅니다. 따라서 위 코드에서는 클래스 C에 x 속성이 없기 때문에 인터프리터는 해당 기본 클래스를 찾습니다(파이썬은 다중 상속을 지원하지만 이 예에서는 C의 기본 클래스는 A뿐입니다). 즉, C에는 A와 독립적이고 실제로 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>"</stdin>, line 3, in <module>
IndexError: list index out of range

이 코드의 문제점은 의 경우, Except 문은 이러한 방식으로 예외를 지정하는 것을 지원하지 않습니다. Python 2.x에서는 예외를 더 자세히 보려면 ​​변수 e를 사용하여 예외를 선택적 두 번째 매개변수에 바인딩해야 합니다. 따라서 위 코드에서 Except 문은 IndexError 예외를 포착하지 않고 대신 IndexError라는 매개 변수에 발생하는 예외를 바인딩합니다.

예외 문에서 여러 예외를 올바르게 포착하려면 첫 번째 매개변수를 튜플로 지정한 다음 포착하려는 예외 유형을 튜플에 작성해야 합니다. 또한 이식성을 향상하려면 Python 2와 Python 3 모두에서 지원되는 as 키워드를 사용하세요.

>>> try:
...   l = ["a", "b"]
...   int(l[2])
... except (ValueError, IndexError) as e: 
...   pass
...
>>>
흔한 실수 4: Python의 변수 이름 확인에 대한 오해
Python의 변수 이름 확인은 소위 LEGB 원칙, 즉 "L: 로컬 범위; E: def 상위 구조에서 또는 람다의 로컬 범위; G: 전역 범위; B: 내장 범위" (Local, Enclosing, Global, 내장), 순서대로 검색합니다. 간단해 보이지 않나요? 그러나 실제로 이 원칙이 적용되는 방식에는 몇 가지 특별한 기능이 있습니다. 이에 관해 다음과 같은 일반적인 Python 프로그래밍 오류를 언급해야 합니다. 아래 코드를 살펴보세요.
>>> x = 10
>>> def foo():
...   x += 1
...   print x
...
>>> foo()
Traceback (most recent call last):
 File "<stdin>"</stdin>, line 1, in <module>
 File "<stdin>"</stdin>, line 2, in foo
UnboundLocalError: local variable &#39;x&#39; referenced before assignment

무슨 문제가 있나요?

위 오류는 특정 범위의 변수에 값을 할당하면 해당 변수가 Python 인터프리터에 의해 자동으로 해당 범위의 로컬 변수로 간주되어 모든 상위 범위 변수를 대체하기 때문에 발생합니다. 같은 이름으로.

이 때문에 잘 시작된 코드가 나타나는데 함수 내부에 할당문을 추가하면 UnboundLocalError가 발생하여 많은 사람들이 놀라는 것은 당연합니다.

Python 프로그래머는 특히 목록 작업을 할 때 이러한 함정에 빠지기 쉽습니다.

다음 코드 예제를 참조하세요.

>>> 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 "<stdin>"</stdin>, line 1, in <module>
 File "<stdin>"</stdin>, line 2, in foo
UnboundLocalError: local variable &#39;lst&#39; referenced before assignment

어? foo1 함수는 정상적으로 실행되는데 foo2에서는 오류가 발생하는 이유는 무엇입니까?

答案与上一个示例相同,但是却更难捉摸清楚。foo1函数并没有为lst变量进行赋值,但是foo2却有赋值。我们知道,lst += [5]只是lst = lst + [5]的简写,从中我们就可以看出,foo2函数在尝试为lst赋值(因此,被Python解释器认为是函数本地作用域的变量)。但是,我们希望为lst赋的值却又是基于lst变量本身(这时,也被认为是函数本地作用域内的变量),也就是说该变量还没有被定义。这才出现了错误。

常见错误5:在遍历列表时更改列表

下面这段代码的问题应该算是十分明显:

>>> 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 "<stdin>"</stdin>, line 2, in <module>
IndexError: list index out of range

在遍历列表或数组的同时从中删除元素,是任何经验丰富的Python开发人员都会注意的问题。但是尽管上面的示例十分明显,资深开发人员在编写更为复杂代码的时候,也很可能会无意之下犯同样的错误。

幸运的是,Python语言融合了许多优雅的编程范式,如果使用得当,可以极大地简化代码。简化代码还有一个好处,就是不容易出现在遍历列表时删除元素这个错误。能够做到这点的一个编程范式就是列表解析式。而且,列表解析式在避免这个问题方面尤其有用,下面用列表解析式重新实现上面代码的功能:

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

你可能觉得输出结果应该是这样的:

但是,实际的输出结果却是:

吓了一跳吧!

这个结果的出现,主要是因为Python中的迟绑定(late binding )机制,即闭包中变量的值只有在内部函数被调用时才会进行查询。因此,在上面的代码中,每次create_multipliers()所返回的函数被调用时,都会在附近的作用域中查询变量i的值(而到那时,循环已经结束,所以变量i最后被赋予的值为4)。

要解决这个常见Python问题的方法中,需要使用一些hack技巧:

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

请注意!我们在这里利用了默认参数来实现这个lambda匿名函数。有人可能认为这样做很优雅,有人会觉得很巧妙,还有人会嗤之以鼻。但是,如果你是一名Python程序员,不管怎样你都应该要了解这种解决方法。

常见错误7:模块之间出现循环依赖(circular dependencies)

假设你有两个文件,分别是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模块:

代码运行正常。也许这出乎了你的意料。毕竟,我们这里存在循环引用这个问题,想必应该是会出现问题的,难道不是吗?

答案是,仅仅存在循环引用的情况本身并不会导致问题。如果一个模块已经被引用了,Python可以做到不再次进行引用。但是如果每个模块试图访问其他模块定义的函数或变量的时机不对,那么你就很可能陷入困境。

那么回到我们的示例,当我们导入a.py模块时,它在引用b.py模块时是不会出现问题的,因为b.py模块在被引用时,并不需要访问在a.py模块中定义的任何变量或函数。b.py模块中对a模块唯一的引用,就是调用了a模块的foo()函数。但是那个函数调用发生在g()函数当中,而a.py或b.py模块中都没有调用g()函数。所以,不会出现问题。

但是,如果我们试着导入b.py模块呢(即之前没有引用a.py模块的前提下):

>>> import b
Traceback (most recent call last):
   File "<stdin>"</stdin>, line 1, in <module>
   File "b.py", line 1, in <module>
  import a
   File "a.py", line 6, in <module>
 print f()
   File "a.py", line 4, in f
 return b.x
AttributeError: &#39;module&#39; object has no attribute &#39;x&#39;

糟糕。情况不太妙!这里的问题是,在导入b.py的过程中,它试图引用a.py模块,而a.py模块接着又要调用foo()函数,这个foo()函数接着又试图去访问b.x变量。但是这个时候,b.x变量还没有被定义,所以才出现了AttributeError异常。

解决这个问题有一种非常简单的方法,就是简单地修改下b.py模块,在g()函数内部才引用a.py:

x = 1
def g():
  import a # This will be evaluated only when g() is called
  print a.f()

现在我们再导入b.py模块的话,就不会出现任何问题了:

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

   

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

Python语言的一大优势,就是其本身自带的强大标准库。但是,正因为如此,如果你不去刻意注意的话,你也是有可能为自己的模块取一个和Python自带标准库模块相同的名字(例如,如果你的代码中有一个模块叫email.py,那么这就会与Python标准库中同名的模块相冲突。)

这很可能会给你带来难缠的问题。举个例子,在导入模块A的时候,假如该模块A试图引用Python标准库中的模块B,但却因为你已经有了一个同名模块B,模块A会错误地引用你自己代码中的模块B,而不是Python标准库中的模块B。这也是导致一些严重错误的原因。

因此,Python程序员要格外注意,避免使用与Python标准库模块相同的名称。毕竟,修改自己模块的名称比提出PEP提议修改上游模块名称且让提议通过,要来得容易的多。

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

假设有下面这段代码:

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
1
$ python foo.py 2
value error
2

   

但是现在,我们换成Python 3再运行一遍:

   
$ python3 foo.py 1
key error
Traceback (most recent call last):
 File "foo.py", line 19, in <module>
  bad()
 File "foo.py", line 17, in bad
  print(e)
UnboundLocalError: local variable &#39;e&#39; referenced before assignment

   

这到底是怎么回事?这里的“问题”是,在Python 3中,异常对象在except代码块作用域之外是无法访问的。(这么设计的原因在于,如果不这样的话,堆栈帧中就会一直保留它的引用循环,直到垃圾回收器运行,将引用从内存中清除。)

避免这个问题的一种方法,就是在except代码块的作用域之外,维持一个对异常对象的引用(reference),这样异常对象就可以访问了。下面这段代码就使用了这种方法,因此在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()


在Python 3下运行代码:

$ python3 foo.py 1
key error
1
$ python3 foo.py 2
value error
2

太棒了!

常见错误10:错误使用del方法

假设你在mod.py的文件中编写了下面的代码:

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

之后,你在another_mod.py文件中进行如下操作:

import mod
mybar = mod.Bar()

   


如果你运行another_mod.py模块的话,将会出现AttributeError异常。

为什么?因为当解释器结束运行的时候,该模块的全局变量都会被设置为None。因此,在上述示例中,当__del__方法被调用之前,foo已经被设置成了None。

要想解决这个有点棘手的Python编程问题,其中一个办法就是使用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是一门强大而又灵活的编程语言,提供的许多编程机制和范式可以极大地提高工作效率。但是与任何软件工具或语言一样,如果对该语言的能力理解有限或无法欣赏,那么有时候自己反而会被阻碍,而不是受益了。正如一句谚语所说,“自以为知道够多,但实则会给自己或别人带来危险。

不断地熟悉Python语言的一些细微之处,尤其是本文中提到的10大常见错误,将会帮助你有效地使用这门语言,同时也能避免犯一些比较常见的错误。


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