Python中常见的陷阱

Common pitfalls in Python

本问题已经有最佳答案,请猛点这里访问。

Possible Duplicate:
Python 2.x gotcha’s and landmines

今天,多年后我又被可变的默认参数咬了一口。我通常不使用可变的默认参数,除非需要,但我想随着时间的推移,我忘记了这一点。今天在应用程序中,我在一个PDF生成函数的参数列表中添加了tocElements=[],现在在每次调用"生成PDF"之后,"目录"变得越来越长。:)

我还应该在必须避免的事情列表中添加什么?

  • 始终以相同的方式导入模块,例如,from y import ximport x被视为不同的模块。

  • 不要用range代替list,因为range()无论如何都将成为迭代器,以下操作将失败:

    1
    2
    3
    myIndexList = [0, 1, 3]
    isListSorted = myIndexList == range(3)  # will fail in 3.0
    isListSorted = myIndexList == list(range(3))  # will not

    同样的事情也可以用xrange错误地完成:

    1
    myIndexList == xrange(3)
  • 注意捕获多个异常类型:

    1
    2
    3
    4
    try:
        raise KeyError("hmm bug")
    except KeyError, TypeError:
        print TypeError

    这将打印"hmm bug",尽管它不是bug;看起来我们正在捕获这两种类型的异常,但是我们只捕获keyerror作为变量typeerror,请使用它:

    1
    2
    3
    4
    try:
        raise KeyError("hmm bug")
    except (KeyError, TypeError):
        print TypeError


不使用索引循环序列

不要:

1
2
for i in range(len(tab)) :
    print tab[i]

做:

1
2
for elem in tab :
    print elem

for将为您自动化大多数迭代操作。

如果确实需要索引和元素,请使用enumerate

1
2
for i, elem in enumerate(tab):
     print i, elem

使用"=="检查"对"或"错"时要小心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (var == True) :
    # this will execute if var is True or 1, 1.0, 1L

if (var != True) :
    # this will execute if var is neither True nor 1

if (var == False) :
    # this will execute if var is False or 0 (or 0.0, 0L, 0j)

if (var == None) :
    # only execute if var is None

if var :
    # execute if var is a non-empty string/list/dictionary/tuple, non-0, etc

if not var :
    # execute if var is"", {}, [], (), 0, None, etc.

if var is True :
    # only execute if var is boolean True, not 1

if var is False :
    # only execute if var is boolean False, not 0

if var is None :
    # same as var == None

不要检查是否可以,只需检查并处理错误即可。

Python教徒通常说"请求宽恕比允许更容易"。

不要:

1
2
3
4
if os.path.isfile(file_path) :
    file = open(file_path)
else :
    # do something

做:

1
2
3
4
try :
    file =  open(file_path)
except OSError as e:
    # do something

或者用python 2.6+/3更好:

1
with open(file_path) as file :

它更好,因为它更通用。您可以将"尝试/排除"应用于几乎所有内容。你不需要关心该怎么做来防止它,只关心你所冒的错误。

不检查类型

python是动态类型的,因此检查类型会使您失去灵活性。相反,通过检查行为来使用duck类型。例如,您期望函数中有一个字符串,然后使用str()来转换字符串中的任何对象。如果需要列表,请使用list()转换列表中的任何ITerable。

不要:

1
2
3
4
5
6
7
8
def foo(name) :
    if isinstance(name, str) :
        print name.lower()

def bar(listing) :
    if isinstance(listing, list) :
        listing.extend((1, 2, 3))
        return",".join(listing)

做:

1
2
3
4
5
6
7
def foo(name) :
    print str(name).lower()

def bar(listing) :
    l = list(listing)
    l.extend((1, 2, 3))
    return",".join(l)

使用最后一种方法,foo将接受任何对象。BAR将接受字符串、元组、集合、列表等等。廉价干燥:-)

不要混合空格和制表符

别这样,你会哭的。

将对象用作第一个父级

这很棘手,但随着程序的发展,它会咬你。在python 2.x中有新旧类,旧类是旧的。它们缺少一些特性,并且在继承时会有笨拙的行为。为了可用,任何类都必须是"新样式"。为此,使其从"对象"继承:

不要:

1
2
3
4
5
class Father :
    pass

class Child(Father) :
    pass

做:

1
2
3
4
5
6
class Father(object) :
    pass


class Child(Father) :
    pass

在python 3.x中,所有类都是新样式,因此可以声明class Father:是好的。

不要在__init__方法之外初始化类属性

来自其他语言的人觉得这很诱人,因为你在Java或PHP上做的工作。编写类名,然后列出属性并给它们一个默认值。它似乎在Python中工作,但是,这并不像您想象的那样工作。

这样做将设置类属性(静态属性),然后当您尝试获取对象属性时,它将给出它的值,除非它是空的。在这种情况下,它将返回类属性。

这意味着两大危险:

  • 如果更改了class属性,则更改初始值。
  • 如果将可变对象设置为默认值,则可以在实例之间共享相同的对象。

不要(除非你想要静态的):

1
2
3
class Car(object):
    color ="red"
    wheels = [wheel(), Wheel(), Wheel(), Wheel()]

做:

1
2
3
4
class Car(object):
    def __init__(self):
        self.color ="red"
        self.wheels = [wheel(), Wheel(), Wheel(), Wheel()]


当需要数组填充时,您可能会尝试键入如下内容:

1
>>> a=[[1,2,3,4,5]]*4

当然,当你看到它的时候,它会给你期望的东西。

1
2
3
4
5
6
7
>>> from pprint import pprint
>>> pprint(a)

[[1, 2, 3, 4, 5],
 [1, 2, 3, 4, 5],
 [1, 2, 3, 4, 5],
 [1, 2, 3, 4, 5]]

但不要期望群体中的元素是独立的对象:

1
2
3
4
5
6
7
>>> a[0][0] = 2
>>> pprint(a)

[[2, 2, 3, 4, 5],
 [2, 2, 3, 4, 5],
 [2, 2, 3, 4, 5],
 [2, 2, 3, 4, 5]]

除非这是你需要的…

值得一提的是解决方法:

1
a = [[1,2,3,4,5] for _ in range(4)]


python语言gotchas——以非常模糊的方式失败的东西

  • 使用可变的默认参数。

  • 前导零表示八进制。在python 2.x中,09是一个非常模糊的语法错误。

  • 在超类或子类中拼错重写的方法名。超类拼写错误更严重,因为没有任何子类正确地覆盖它。

python设计Gotchas

  • 花时间反省(例如尝试自动确定类型或超类身份或其他东西)。首先,从阅读资料中可以明显看出。更重要的是,花在奇怪的python自省上的时间通常表示无法掌握多态性。80%的python自省问题都是因为无法获得多态性。

  • 花时间打代码高尔夫。仅仅因为您的应用程序的心理模型是四个关键字("do"、"what"、"i"、"mean"),并不意味着您应该构建一个超复杂的自省装饰驱动框架来实现这一点。Python可以让你把干到一个愚蠢的水平。剩下的python自省问题就这样试图减少复杂的问题来编码高尔夫练习。

  • 打补丁。

  • 没有真正读完标准库,重新发明轮子。

  • 将交互式类型与适当的程序结合在一起。当您以交互方式键入时,可能会丢失变量的跟踪,因此必须使用globals()。而且,当你打字时,几乎所有的东西都是全球性的。在适当的程序中,您永远不会"丢失"变量,也不会有全局变量。


改变默认参数:

1
2
3
def foo(bar=[]):
    bar.append('baz')
    return bar

默认值只计算一次,而不是每次调用函数时。重复调用foo()会返回['baz']['baz', 'baz']['baz', 'baz', 'baz']……

如果你想改变条形图,可以这样做:

1
2
3
4
5
6
def foo(bar=None):
    if bar is None:
        bar = []

    bar.append('baz')
    return bar

或者,如果您希望论点是最终的:

1
2
3
4
5
def foo(bar=[]):
    not_bar = bar[:]

    not_bar.append('baz')
    return not_bar


我不知道这是否是一个常见的错误,但是虽然python没有递增和递减操作符,但是允许使用双符号,所以

1
++i

1
--i

在语法上是正确的代码,但不做任何"有用的"或您可能期望的事情。


在查看标准库之前滚动您自己的代码。例如,写下:

1
2
3
4
def repeat_list(items):
    while True:
        for item in items:
            yield item

当你可以使用这个的时候:

1
from itertools import cycle

经常被忽视的模块(除itertools外)包括:

  • 用于创建命令行分析器的optparse
  • 用于以标准方式读取配置文件的ConfigParser
  • 用于创建和管理临时文件的tempfile
  • 用于将python对象存储到磁盘的shelve,当一个成熟的数据库被过度杀戮时非常方便。


避免使用关键字作为自己的标识符。

另外,最好不要使用from somemodule import *


如果你来自C++,那么在类定义中声明的变量是静态的。可以在init方法中初始化非静态成员。

例子:

1
2
3
4
5
class MyClass:
  static_member = 1

  def __init__(self):
    self.non_static_member = random()

很惊讶没人这么说:

Mix tab and spaces when indenting.

真的,这是个杀手。相信我。尤其是如果它运行的话。


不使用功能性工具。从风格的角度来看,这不仅仅是一个错误,从速度的角度来看,这是一个错误,因为很多功能工具都是在C中优化的。

这是最常见的例子:

1
2
3
4
temporary = []
for item in itemlist:
    temporary.append(somefunction(item))
itemlist = temporary

正确的方法:

1
itemlist = map(somefunction, itemlist)

同样正确的方法是:

1
itemlist = [somefunction(x) for x in itemlist]

如果您一次只需要一个可用的已处理项目,而不是同时需要所有项目,那么您可以使用不可重复的等效项来节省内存并提高速度。

1
2
3
4
# itertools-based iterator
itemiter = itertools.imap(somefunction, itemlist)
# generator expression-based iterator
itemiter = (somefunction(x) for x in itemlist)

类似于pythonista的代码:惯用的python


正常复制(分配)是通过引用来完成的,因此通过调整同一对象并插入来填充容器,最终得到一个引用最后添加的对象的容器。

copy.deepcopy代替。


导入re并使用完整正则表达式方法进行字符串匹配/转换,前提是对于每个常见操作(如大写、简单匹配/搜索)都存在完美的字符串方法。


  • 不要将大输出消息写入标准输出
  • 字符串是不可变的-不使用"+"运算符生成字符串,而是使用str.join()函数。
  • 阅读这些文章:
    • Python
    • 要避免的事情
    • 针对python用户的gotchas
    • Python地雷

最后一个链接是原始链接,所以这个问题是重复的。


在错误消息中使用%s格式化程序。在几乎所有情况下,都应该使用%r

例如,想象一下这样的代码:

1
2
3
4
try:
    get_person(person)
except NoSuchPerson:
    logger.error("Person %s not found." %(person))

打印此错误:

1
ERROR: Person wolever not found.

无法判断person变量是字符串"wolever"、unicode字符串u"wolever"还是person类(其中__str__定义为def __str__(self): return self.name的实例)。然而,如果使用%r,则会出现三条不同的错误消息:

1
2
...
logger.error("Person %r not found." %(person))

会产生更有用的错误:

1
2
3
ERROR: Person 'wolever' not found.
ERROR: Person u'wolever' not found.
ERROR: Person  not found.

另一个很好的原因是路径更容易复制/粘贴。想象:

1
2
3
4
try:
    stuff = open(path).read()
except IOError:
    logger.error("Could not open %s" %(path))

如果pathsome path/with 'strange'"characters",则错误信息为:

1
ERROR: Could not open some path/with 'strange'"characters"

这很难从视觉上解析,也很难复制/粘贴到shell中。

鉴于,如果使用%r,则错误为:

1
ERROR: Could not open 'some path/with \'strange\'"characters"'

易于可视化分析,易于复制粘贴,四周更好。


我将停止在2.6中使用不推荐使用的方法,这样您的应用程序或脚本就可以准备好并更容易转换为Python3。


我必须训练自己摆脱的一个坏习惯是使用X and Y or Z作为内联逻辑。

除非你能100%地保证Y是一个真正的值,即使你的代码在18个月内发生了变化,你也会为一些意想不到的行为做好准备。

谢天谢地,在以后的版本中,您可以使用Y if X else Z


EDCOX1×0和EDCOX1〔1〕可能不是来自C或Java背景的人所期望的。

++n是正数的正数,即n

--n是负数的负数,简单地说就是n


不要假设拥有一个多线程的python应用程序和一台支持SMP的机器(例如一台配备了多核CPU的机器)会给您带来将真正的并行性引入应用程序的好处。很可能不是因为gil(全局解释器锁)在字节码解释器级别上同步您的应用程序。

有一些解决方法,比如利用SMP在C API调用中放置并发代码,或者通过包装器(例如http://www.parallelpython.org上提供的包装器)使用多个进程(而不是线程),但是如果需要在python中真正的多线程,应该查看jython、ironpython等(gil是一个cpython解释器的特性,因此其他实现不会受到影响)。

根据python 3000常见问题解答(可在artima上找到),上述内容仍然适用于最新的python版本。


一些个人意见,但我认为最好不要:

  • 使用不推荐使用的模块(使用警告)

  • 过度使用类和继承(可能是静态语言遗产的典型)

  • 明确使用声明性算法(与for的迭代相比)itertools

  • 从标准lib重新实现函数,"因为我不需要所有这些特性"

  • 为它使用特性(降低与旧的Python版本的兼容性)

  • 当你真的不需要时使用元类,通常会使事情变得"神奇"

  • 避免使用发电机

  • (更个人化)尝试在底层对cpython代码进行微优化。最好花时间在算法上,然后通过制作一个由ctypes调用的小型C共享lib进行优化(这样很容易在内部循环上获得5倍的性能提升)

  • 当迭代器足够时使用不必要的列表

  • 在你需要的libs全部可用之前,直接为3.x代码一个项目(这一点现在可能有点争议!)


1
import this

美胜于丑。显式优于隐式。简单胜于复杂。复杂总比复杂好。平的比嵌套的好。稀疏胜于稠密。可读性计数。特殊情况不足以打破规则。尽管实用性胜过纯洁性。错误永远不会悄悄地过去。除非明确沉默。面对歧义,拒绝猜测的诱惑。应该有一种——最好只有一种——显而易见的方法来做到这一点。不过,如果不是荷兰语的话,这种方式一开始可能并不明显。现在总比没有好。虽然从来没有比现在更好。如果实现很难解释,那是个坏主意。如果实现很容易解释,这可能是一个好主意。名称空间是一个非常好的主意——让我们做更多的事情吧!

1
import not_this

写丑陋的代码。编写隐式代码。编写复杂代码。编写嵌套代码。写密集的代码。写不可读的代码。写一些特殊的案例。追求纯洁。忽略错误和异常。在释放之前编写最佳代码。每个实现都需要一个流程图。不要使用名称空间。


我也开始学习Python,我犯下的最大错误之一就是不断使用C++/C。python有for(i;i

例子:我有一个方法,它遍历一个列表并返回选定项的索引:

1
2
3
for i in range(len(myList)):
    if myList[i].selected:
        retVal.append(i)

相反,python具有列表理解能力,能够以更优雅、更易于阅读的方式解决相同的问题:

1
retVal = [index for index, item in enumerate(myList) if item.selected]

迭代列表时不要修改它。

1
2
3
4
5
odd = lambda x : bool(x % 2)
numbers = range(10)
for i in range(len(numbers)):
    if odd(numbers[i]):
        del numbers[i]

解决此问题的一个常见建议是反向迭代列表:

1
2
3
for i in range(len(numbers)-1,0,-1):
    if odd(numbers[i]):
        del numbers[i]

但更好的是使用列表理解来构建一个新的列表来替换旧的列表:

1
numbers[:] = [n for n in numbers if not odd(n)]

在你开始之前的第一个错误是:不要害怕空白。

当您向某人展示一段Python代码时,他们会留下深刻印象,直到您告诉他们必须正确缩进。出于某种原因,大多数人认为一种语言不应该强迫他们使用某种风格,尽管所有人都会缩进代码。


你提到了默认参数…一个几乎和可变的默认参数一样糟糕的参数:不是None的默认值。

考虑一个烹饪食物的功能:

1
2
3
4
def cook(breakfast="spam"):
    arrange_ingredients_for(breakfast)
    heat_ingredients_for(breakfast)
    serve(breakfast)

因为它为breakfast指定了一个默认值,所以在没有特殊情况下,其他函数不可能说"烹饪默认早餐":

1
2
3
4
5
def order(breakfast=None):
    if breakfast is None:
        cook()
    else:
        cook(breakfast)

但是,如果cook使用None作为默认值,则可以避免:

1
2
3
4
5
6
def cook(breakfast=None):
    if breakfast is None:
        breakfast ="spam"

def order(breakfast=None):
    cook(breakfast)

一个很好的例子就是Django bug 6988。Django的缓存模块具有"保存到缓存"功能,如下所示:

1
2
3
4
def set(key, value, timeout=0):
    if timeout == 0:
        timeout = settings.DEFAULT_TIMEOUT
    _caching_backend.set(key, value, timeout)

但是,对于memcached后端,EDOCX1的超时(9)意味着"永不超时"…正如您所见,这是不可能指定的。


1
2
3
4
5
my_variable = <something>
...
my_varaible = f(my_variable)
...
use my_variable and thinking it contains the result from f, and not the initial value

python不会以任何方式警告您,在第二次赋值时,您拼错了变量名并创建了一个新的变量名。


与默认可变参数有点关联,当传递空列表时,如何检查"丢失"大小写会导致差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def func1(toc=None):
    if not toc:
        toc = []
    toc.append('bar')

def func2(toc=None):
    if toc is None:
        toc = []
    toc.append('bar')

def demo(toc, func):
    print func.__name__
    print '  before:', toc
    func(toc)
    print '  after:', toc

demo([], func1)
demo([], func2)

输出结果如下:

1
2
3
4
5
6
func1
  before: []
  after: []
func2
  before: []
  after: ['bar']


常见陷阱:默认参数只计算一次:

1
2
3
4
5
6
def x(a, l=[]):
    l.append(a)
    return l

print x(1)
print x(2)

印刷品:

1
2
[1]
[1, 2]

也就是说,你总是得到相同的清单。


创建与stdlib中的本地模块同名的本地模块。这几乎总是意外完成的(如本问题中所报告的),但通常会导致隐藏的错误消息。


混杂异常处理

这是我在生产代码中看到的惊人数量的东西,它让我感到害怕。

1
2
3
4
try:
    do_something() # do_something can raise a lot errors e.g. files, sockets
except:
    pass # who cares we'll just ignore it

例外是您想要抑制的例外,还是更严重?但还有更多微妙的案例。那会让你想办法把头发拔出来。

1
2
3
4
try:
    foo().bar().baz()
except AttributeError: # baz() may return None or an incompatible *duck type*
    handle_no_baz()

问题是foo或baz也可能是罪魁祸首。我认为这可能更阴险一些,因为这是一个惯用的python,您在这里检查您的类型是否有正确的方法。但是每个方法调用都有机会返回一些意外的结果,并抑制应该引发异常的错误。

知道一个方法可以抛出哪些异常并不总是显而易见的。例如,urllib和urllib2使用的插座有其自身的例外,当你最不期待的时候,它会渗透到它们丑陋的头部后面。

异常处理在处理系统级语言(如C)的错误方面是一种提高效率的方法。但我发现,不正确地抑制异常可以创建真正神秘的调试会话,并消除解释语言提供的主要优势。


类似于可变默认参数的是可变类属性。

1
2
3
4
5
6
7
8
9
10
>>> class Classy:
...    foo = []
...    def add(self, value):
...        self.foo.append(value)
...
>>> instance1 = Classy()
>>> instance2 = Classy()
>>> instance1.add("Foo!")
>>> instance2.foo
['Foo!']

不是你所期望的。


算法博客有一篇关于Python性能问题以及如何避免这些问题的好文章:10 python优化提示和问题


这已经被提到过了,但是我想详细介绍一下类属性的可变性。

定义成员属性时,每次实例该类时,它都会得到一个属性,该属性是类属性的浅副本。

所以如果你有

1
2
3
4
5
class Test(object):
   myAttr = 1
instA = Test()
instB = Test()
instB.myAttr = 2

它将按预期运行。

1
2
3
4
>>> instA.myAttr
  1
>>> instB.myAttr
  2

当您具有可变的类属性时,问题就会出现。由于实例化只是做了一个简单的复制,所以所有实例都将只具有指向同一对象的引用。

1
2
3
4
5
6
7
class Test(object):
   myAttr=[1,2,3]
instA = Test()
instB = Test()
instB.myAttr[0]=2
>>> instA.myAttr
   [2,2,3]

但是引用是实例的实际成员,只要您实际为属性分配了一些新的内容,就可以了。

您可以通过在init函数期间对可变变量进行深度复制来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
import copy
class Test(object):
   myAttr = [1,2,3]
   def __init__(self):
      self.myAttr = copy.deepcopy(self.myAttr)
instA = Test()
instB = Test()
instB.myAttr[0] = 5
>>> instA.myAttr
   [1,2,3]
>>> instB.myAttr
   [5,2,3]

也许可以编写一个decorator,在init期间自动地deepcopy您的所有类属性,但我不知道在任何地方提供的这个decorator。


类属性

上面的一些答案是不正确的,或者对于类属性不清楚。

它们不会成为实例属性,但可以使用与实例属性相同的语法进行读取。可以通过类名访问它们来更改它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass:
    attrib = 1                         # class attributes named 'attrib'
    another = 2                        # and 'another'
    def __init__(self):
        self.instattr = 3              # creates instance attributes
        self.attrib = 'instance'      

mc0 = MyClass()
mc1 = MyClass()

print mc.attrib    # 'instance'
print mc.another   # '2'

MyClass.another = 5  # change class attributes
MyClass.attrib = 21  # <- masked by instance attribute of same name

print mc.attrib    # 'instance'   unchanged instance attribute
print mc.another   # '5'          changed class attribute

类属性可以用作实例属性的默认值排序,稍后由具有不同值的同名实例属性屏蔽。

中间作用域局部变量

更难理解的是嵌套函数中变量的范围。

在下面的示例中,Y不可从函数"outer"以外的任何位置进行编译。x在任何地方都是可读和可写的,因为它在每个函数中都被声明为全局的。Z仅在"inner*"中可读和可写。Y在"外部"和"内部"中可读,但不可写,除非在"outer"中。

1
2
3
4
5
6
7
8
9
10
x = 1
def outer():
    global x
    y = 2
    def inner1():
        global x, y
        y = y+1  # creates new global variable with value=3
    def inner2():
        global x
        y = y+1  # creates new local variable with value=3

我相信python 3包含一个"outer"关键字,用于"outside this function but not global"这样的情况。在python 2.中,要么将y设置为全局,要么将其设置为"inner"的可变参数。