Python 中常见的陷阱

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

还有什么是我必须避免的?

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

  • 不要使用 射程代替列表,因为 range()无论如何都会成为迭代器,下面的代码将会失败:

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

    同样的事情可以错误地做 Xrange:

      myIndexList == xrange(3)
    
  • 小心捕获多个异常类型:

      try:
    raise KeyError("hmm bug")
    except KeyError, TypeError:
    print TypeError
    

    这会打印出“嗯,bug”,尽管它不是 bug; 看起来我们正在捕获这两种类型的异常,但是我们只捕获 KeyError 变量 输入错误,使用:

      try:
    raise KeyError("hmm bug")
    except (KeyError, TypeError):
    print TypeError
    
21566 次浏览

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

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

而且,不使用 from somemodule import *总是好的。

一些个人观点,但我认为最好的 没有是:

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

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

  • 显式地使用声明性算法(作为使用 for的迭代与使用 itertools)

  • 重新实现标准库中的函数,“因为我不需要所有这些特性”

  • 使用特性(减少与旧版 Python 的兼容性)

  • 使用元类,当你真的不需要的时候,更一般地使事情太“神奇”

  • 避免使用发电机

  • (更个性化)尝试在低级别基础上对 CPython 代码进行微优化。最好花时间在算法上,然后通过制作一个由 ctypes调用的小型 C 共享库进行优化(在内部循环中很容易获得5倍的性能提升)

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

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

Python 语言陷阱——以非常模糊的方式失败的东西

  • 使用可变的默认参数。

  • 前导零表示八进制。 09是 Python2.x 中一个非常隐晦的语法错误

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

Python 设计陷阱

  • 花时间进行自我反省(例如,尝试自动确定类型或超类标识或其他东西)。首先,从阅读来源可以明显看出。更重要的是,花费在奇怪的 Python 自省上的时间通常表明在掌握多态性方面存在根本性的失败。80% 的关于 SO 的 Python 自省问题都没有得到多态性。

  • 花时间打电码高尔夫。仅仅因为应用程序的思维模型是四个关键词(“ do”、“ what”、“ I”、“ mean”) ,并不意味着您应该构建一个超复杂的内省式装饰驱动框架来实现这一点。Python 允许您将 DRY 提升到一个非常愚蠢的层次。Python 关于 SO 的其余自省问题试图将复杂问题简化为编写高尔夫练习代码。

  • 猴子拼图。

  • 未能实际阅读标准库和重造轮子。

  • 将交互式类型与适当的程序合并到 Python 中。当您以交互方式输入时,您可能会丢失对变量的跟踪,因此必须使用 globals()。而且,当你打字的时候,几乎所有的东西都是全局的。在正确的程序中,您永远不会“失去对”变量的跟踪,而且没有什么是全局的。

当您需要数组的填充时,您可能会想要输入以下内容:

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

毫无疑问,当你看着它的时候,它会给你你想要的

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

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

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

除非这就是你需要的。

值得一提的是一个变通办法:

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

美丽总比丑陋好。
显式比隐式好。
简单比复杂好。
复杂比复杂好。
平坦比嵌套好。
稀疏总比密集好。
可读性很重要。
特殊情况不足以破坏规则。
尽管实用胜过纯洁。
错误永远不会悄无声息地过去。
除非明确地让他闭嘴。
面对模棱两可的情况,拒绝猜测的诱惑。
应该有一种——最好只有一种——显而易见的方法来做到这一点。
虽然一开始可能看不出来,除非你是荷兰人。
现在总比没有好。
虽然现在从来没有比 更好的了。
如果实现很难解释,那就是个坏主意。
如果实现容易解释,那么它可能是一个好主意。
名称空间是一个非常棒的想法——让我们做更多这样的事情吧!

import not_this

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

++n--n可能不会像 C 或 Java 背景的人所期望的那样工作。

++n是正数的正数,也就是简单的 n

--n是负数的负数,也就是简单的 n

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

除非您能够100% 地保证 Y将是一个真值,即使您的代码在18个月的时间内发生了变化,否则您可能会遇到一些意想不到的行为。

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

最后一个链接是原来的一个,这个 SO 问题是重复的。

与默认的可变参数有些相关的是,当传递一个空列表时,检查“丢失”的情况会导致不同的结果:

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)

输出如下:

func1
before: []
after: []
func2
before: []
after: ['bar']

没人这么说真是奇怪:

缩进时混合使用制表符和空格。

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

我不知道这是否是一个常见的错误,但是尽管 Python 没有增值和减值操作符,但是双符号是允许的,所以

++i

还有

--i

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

我也开始学习 Python,我犯过的最大的错误之一就是经常使用 C + +/C # 索引的“ for”循环。Python 有 for (i; i < length; i + +)类型的循环,这是有原因的——大多数情况下,都有更好的方法可以做同样的事情。

例如: 我有一个方法,它迭代一个列表并返回所选项的索引:

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

相反,Python 的列表内涵以一种更优雅、更容易阅读的方式解决了同样的问题:

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

修改默认参数:

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

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

如果你想变异酒吧做这样的事情:

def foo(bar=None):
if bar is None:
bar = []


bar.append('baz')
return bar

或者,如果你希望争论是最终的:

def foo(bar=[]):
not_bar = bar[:]


not_bar.append('baz')
return not_bar

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

使用 copy.deepcopy代替。

开始之前的第一个错误: 不要害怕空格

当您向某人展示一段 Python 代码时,他们会对您印象深刻,直到您告诉他们 正确缩进为止。出于某种原因,大多数人认为一种语言不应该强制他们使用某种样式,尽管如此,他们所有人都会缩进代码。

不要使用 index 在序列上循环

不要:

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

做:

for elem in tab :
print elem

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

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

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

使用“ = =”检查 没错假的 时要小心

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

不要检查,如果你可以,只是这样做,并处理错误

Pythonistas 通常说“请求原谅比请求许可更容易”。

不要:

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

做:

try :
file =  open(file_path)
except OSError as e:
# do something

或者使用 python 2.6 +/3更好:

with open(file_path) as file :

这样更好,因为它更通用。您几乎可以对任何事情应用“ try/竹篮打水一场空”。您不需要关心如何防止它,只需要关心您正在冒的错误。

不要检查 类型

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

不要:

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)

做:

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 中有新的和旧的类。那些老的,嗯,老的。它们缺乏某些特征,在遗传方面可能会有尴尬的行为。为了便于使用,您的任何类都必须是“新样式”的。要做到这一点,让它从“ object”继承:

不要:

class Father :
pass


class Child(Father) :
pass

做:

class Father(object) :
pass




class Child(Father) :
pass

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

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

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

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

这意味着两大风险:

  • 如果 class 属性发生更改,则初始值也会发生更改。
  • 如果您将一个可变对象设置为默认值,那么您将获得跨实例共享的相同对象。

不要(除非你想要静电干扰) :

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

做:

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

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

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

def x(a, l=[]):
l.append(a)
return l


print x(1)
print x(2)

印刷品:

[1]
[1, 2]

也就是说,你总是得到相同的列表。

如果您来自 C + + ,请意识到在类定义中声明的变量是静态的。可以在 Init方法中初始化非静态成员。

例如:

class MyClass:
static_member = 1


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

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

>>> class Classy:
...    foo = []
...    def add(self, value):
...        self.foo.append(value)
...
>>> instance1 = Classy()
>>> instance2 = Classy()
>>> instance1.add("Foo!")
>>> instance2.foo
['Foo!']

不是你想的那样。

在查看标准库之前先滚动您自己的代码:

def repeat_list(items):
while True:
for item in items:
yield item

当你可以用这个的时候:

from itertools import cycle

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

没有使用功能性工具。从样式的角度来看,这不仅是一个错误,从速度的角度来看也是一个错误,因为很多函数工具都是用 C 语言优化的。

这是最常见的例子:

temporary = []
for item in itemlist:
temporary.append(somefunction(item))
itemlist = temporary

正确的做法是:

itemlist = map(somefunction, itemlist)

正确的做法是:

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

如果一次只需要一个可用的处理项,而不是一次性全部可用,则可以通过使用等效的迭代来节省内存和提高速度

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

永远不要认为拥有一个多线程 Python 应用程序和一台能够使用 SMP 的机器(例如,一台配备了多核 CPU 的机器)将会给您的应用程序带来真正的并行性。最有可能的原因是 GIL (GIL)在字节码解释器级别同步你的应用程序。

有一些解决方案,比如利用 SMP,把并发代码放在 C API 调用中,或者通过包装器使用多个进程(而不是线程)(例如 http://www.parallelpython.org中的那个) ,但是如果需要真正的 Python 多线程,应该看看 Jython,IronPython 等(GIL 是 CPython 解释器的一个特性,所以其他实现不受影响)。

根据 Python3000FAQ (可在 Artima 上获得) ,上面的内容对于最新的 Python 版本来说仍然是站得住脚的。

这已经提到过了,但是我想稍微阐述一下类属性可变性。

当您定义一个成员属性时,那么每次您实例化该类时,它都会得到一个属性,这个属性是 class 属性的浅表副本。

所以如果你有

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

它会像预期的那样运转。

>>> instA.myAttr
1
>>> instB.myAttr
2

当您拥有可变的类属性时,问题就出现了。因为实例化只是做了一个浅表复制,所有的实例都只有一个指向同一个对象的引用。

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

但是引用实例的 实际成员,所以只要您实际上为属性分配了新的内容,就没有问题。

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

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]

也许可以编写一个装饰器,在 init 期间自动深拷贝所有的 class 属性,但是我不知道任何地方都提供了这样的装饰器。

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 不会以任何方式警告您,在第二次赋值时,您拼错了变量名并创建了一个新变量。

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

你提到过缺省参数... 一个几乎和可变的缺省参数一样糟糕的参数: 不是 None的缺省值。

考虑一个将烹饪一些食物的函数:

def cook(breakfast="spam"):
arrange_ingredients_for(breakfast)
heat_ingredients_for(breakfast)
serve(breakfast)

因为它指定了 breakfast的默认值,所以其他函数不可能说“煮你的默认早餐”,除非有特殊情况:

def order(breakfast=None):
if breakfast is None:
cook()
else:
cook(breakfast)

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

def cook(breakfast=None):
if breakfast is None:
breakfast = "spam"


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

一个很好的例子是 Django bug # 6988。Django 的缓存模块有一个“保存到缓存”的功能,看起来像这样:

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

但是,对于 memcached 后端,0的超时意味着“永远不超时”... ... 正如您所看到的,这是不可能指定的。

在错误消息中使用 %s格式化程序。

例如,想象这样的代码:

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

输出这个错误:

ERROR: Person wolever not found.

It's impossible to tell if the person variable is the string "wolever", the unicode string u"wolever" or an instance of the Person class (which has __str__ defined as def __str__(self): return self.name). Whereas, if %r was used, there would be three different error messages:

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

会产生更有帮助的错误:

ERROR: Person 'wolever' not found.
ERROR: Person u'wolever' not found.
ERROR: Person  not found.

另一个很好的理由是路径更容易复制/粘贴,想象一下:

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

如果 pathsome path/with 'strange' "characters",则错误消息将是:

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

Which is hard to visually parse and hard to copy/paste into a shell.

Whereas, if %r is used, the error would be:

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

易于视觉解析,易于复制粘贴,全方位更好。

创建与 stdlib 中的本地模块同名的本地模块。这几乎总是出于偶然(如 这个问题中所报告的) ,但通常会导致隐晦的错误消息。

迭代时不要修改列表。

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

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

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

但更好的办法是用一个列表内涵来建立一个新的名单来取代旧的名单:

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

类属性

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

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

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

类属性可以用作实例属性的某种默认值,稍后用具有不同值的同名实例属性掩盖。

中间作用域局部变量

更难理解的是嵌套函数中变量的作用域。

在下面的示例中,除了函数“外部”之外,在任何地方都是不可写的。X在任何地方都是可读和可写的,因为它在每个函数中都声明为全局的。Z只能在‘ inner *’中读写。是可读的’外’和’内 *’, 但不能写,除非在“外部”。

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

我相信 Python3包含了一个‘ out’关键字,用于表示‘在这个函数之外,但不是全局的’情况。在 Python 2中。# ,要么让 全局化,要么让它成为‘ inner’的可变参数。

混杂异常处理

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

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

这个异常是你想要抑制的,还是更严重的?但还有更微妙的情况。这会让你揪头发,想弄清楚。

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

问题是 Foo 或 Baz 也可能是罪魁祸首。我认为这可能更加隐蔽,因为这是 成语巨蟒,您在其中检查类型以获得正确的方法。但是每个方法调用都有机会返回一些意想不到的东西,并抑制应该引发异常的 bug。

知道方法可以抛出什么异常并不总是显而易见的。例如,urllib 和 urllib2使用套接字,它们有自己的异常,在您最不希望的时候,它们会渗透并抬起它们丑陋的头。

异常处理是处理系统级语言(如 C 语言)错误时的一个生产力优势。但是我发现,不正确地抑制异常可能会创建真正神秘的调试会话,并带走解释语言提供的一个主要优势。