Python中常见的陷阱

时间:2009-06-18 08:19:11

标签: python

  

可能重复:
  Python 2.x gotcha’s and landmines

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

我还应该在我的列表中添加什么才能避免?

  • 始终以相同的方式导入模块,例如from y import ximport xtreated as different modules

  • 不要使用 range 代替列表,因为无论如何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作为变量 TypeError,使用它来代替:

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

34 个答案:

答案 0 :(得分:69)

不要使用索引循环序列

不要:

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

使用“==”检查 True False

时要小心
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 :

它更好,因为它更通用。您可以对几乎任何事情应用“尝试/除外”。您无需关心如何防止它,只需要关注您所面临的错误。

不要检查类型

Python是动态类型的,因此检查类型会使您失去灵活性。相反,通过检查行为来使用duck typing。 E.G,你期望一个函数中的字符串,然后使用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中工作,但是,这并不像你想象的那样有效。

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

这意味着两大危害:

  • 如果更改了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()]

答案 1 :(得分:34)

如果需要一组数组,可能会输入类似这样的内容:

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

答案 2 :(得分:28)

Python语言陷阱 - 以非常模糊的方式失败的事情

  • 使用可变的默认参数。

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

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

Python Design Gotchas

  • 花时间进行内省(例如尝试自动确定类型或超类身份或其他内容)。首先,从阅读来源可以看出这一点。更重要的是,花在奇怪的Python内省上的时间通常表明了掌握多态性的根本失败。在SO上80%的Python内省问题都未能获得多态性。

  • 花时间打高尔夫球。仅仅因为你的应用程序的心理模型是四个关键词(“do”,“what”,“I”,“mean”),并不意味着你应该建立一个超复杂的内省装饰者驱动的框架来做到这一点。 Python允许您将DRY带到愚蠢的水平。关于SO的其余Python内省问题试图减少复杂问题以编码高尔夫练习。

  • 的Monkeypatching。

  • 未能真正阅读标准库并重新发明轮子。

  • 将交互式类型的Python与正确的程序相关联。当您以交互方式键入内容时,可能会丢失对变量的跟踪,并且必须使用globals()。此外,在您打字时,几乎所有内容都是全球性的。在适当的程序中,你永远不会“忘记”一个变量,没有什么是全局的。

答案 3 :(得分:25)

改变默认参数:

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

答案 4 :(得分:21)

我不知道这是否是一个常见的错误,但是虽然Python没有增量和减量运算符,但允许使用双重符号,所以

++i

--i

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

答案 5 :(得分:16)

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

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

当你可以使用它时:

from itertools import cycle

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

  • optparse用于创建命令行解析器
  • ConfigParser以标准方式阅读配置文件
  • tempfile用于创建和管理临时文件
  • shelve用于将Python对象存储到磁盘,当完整的数据库过度使用时很方便

答案 6 :(得分:13)

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

此外,不使用from somemodule import *总是好的。

答案 7 :(得分:13)

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

示例:

class MyClass:
  static_member = 1

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

答案 8 :(得分:12)

感到惊讶,没有人这么说:

  

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

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

答案 9 :(得分:11)

不使用功能工具。从风格的角度来看,这不仅仅是一个错误,从速度的角度来看,这是一个错误,因为很多功能工具都是用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)

答案 10 :(得分:10)

答案 11 :(得分:8)

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

答案 12 :(得分:8)

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

改为使用copy.deepcopy

答案 13 :(得分:7)

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

答案 14 :(得分:7)

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

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

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

打印此错误:

ERROR: Person wolever not found.

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

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

难以直观地解析并难以复制/粘贴到shell中。

然而,如果使用%r,则错误为:

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

易于视觉分析,易于复制粘贴,更好。

答案 15 :(得分:6)

我不得不训练自己的坏习惯是使用X and Y or Z进行内联逻辑。

除非您能100%始终保证Y将成为真正的价值,即使您的代码在18个月内发生变化,您也会为自己设置一些意想不到的行为。

值得庆幸的是,在以后的版本中,您可以使用Y if X else Z

答案 16 :(得分:6)

我会停止在2.6中使用弃用的方法,这样你的应用程序或脚本就可以准备好并且更容易转换为Python 3。

答案 17 :(得分:5)

来自C或Java背景的人

++n--n可能无法正常工作。

++n是正数的正数,只是n

--n是负数的负数,只是n

答案 18 :(得分:4)

import this    

美丽胜过丑陋 明确比隐含更好 简单比复杂更好 复杂比复杂更好 扁平比嵌套好。
稀疏比密集更好 可读性很重要 特殊情况不足以打破规则 虽然实用性超过了纯度 错误绝不应该默默无闻 除非明确沉默。
面对模棱两可,拒绝猜测的诱惑 应该有一个 - 最好只有一个 - 明显的方式来做到这一点 虽然这种方式起初可能并不明显,除非你是荷兰人 现在总比没有好。
虽然现在永远不会比正确更好 如果实施很难解释,那是个坏主意 如果实施很容易解释,那可能是个好主意 命名空间是一个很棒的主意 - 让我们做更多的事情吧!

import not_this

写丑陋的代码。
写隐式代码。
写复杂的代码 编写嵌套代码。
写密集代码。
写下难以理解的代码 写特殊情况。
争取纯洁。
忽略错误和例外。
在发布之前写出最佳代码 每个实施都需要一个流程图 不要使用名称空间。

答案 19 :(得分:4)

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

  • 使用已弃用的模块(对它们使用警告)

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

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

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

  • 为此使用功能(降低与旧版Python的兼容性)

  • 当你真的不需要使用元类时,通常会让事情太“神奇”

  • 避免使用发电机

  • (更个性化)尝试在低级别上微观优化CPython代码。更好地花时间在算法上然后通过制作一个由ctypes调用的小型C共享库进行优化(在内循环上获得5倍的增强效果非常容易)

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

  • 在您需要的库之前直接为3.x编写项目代码(现在这点可能有点争议!)

答案 20 :(得分:4)

我也开始学习Python,我犯的最大错误之一就是不断使用C ++ / C#索引“for”循环。 Python有(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]

答案 21 :(得分:4)

永远不要假设拥有多线程Python应用程序和支持SMP的计算机(例如配备多核CPU的计算机)将为您提供在应用程序中引入真正并行性的好处。很可能它不会因为GIL(全局解释器锁)而在字节码解释器级别上同步应用程序。

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

根据Python 3000常见问题解答(在Artima上提供),上述内容仍然适用于最新的Python版本。

答案 22 :(得分:3)

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

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

答案 23 :(得分:3)

与默认的可变参数有些相关,如何检查“缺失”的情况会导致传递空列表时出现差异:

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

答案 24 :(得分:3)

迭代时不要修改列表。

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

答案 25 :(得分:3)

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

答案 26 :(得分:3)

你提到了默认参数......一个几乎与可变默认参数一样糟糕:默认值不是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的超时意味着“永不超时”......正如您所见,这是不可能指定的。

答案 27 :(得分:2)

常见缺陷:默认参数评估一次

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

print x(1)
print x(2)

打印:

[1]
[1, 2]

即。你总是得到同样的名单。

答案 28 :(得分:1)

创建一个与stdlib中名称相同的本地模块。这几乎总是偶然地完成(如this question中所述),但通常会产生神秘的错误消息。

答案 29 :(得分:1)

混杂异常处理

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

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也可能是罪魁祸首。我认为这可能更加阴险,因为这是惯用的python ,你正在检查你的类型是否正确的方法。但是每个方法调用都有机会返回意外的东西并抑制应该引发异常的错误。

了解方法可以抛出的异常并不总是显而易见的。例如,urllib和urllib2使用socket,它有自己的异常,当你最不期望的时候会渗透并修复它们丑陋的头部。

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

答案 30 :(得分:0)

算法博客有一篇关于Python性能问题以及如何避免它们的好文章: 10 Python Optimization Tips and Issues

答案 31 :(得分:0)

这已经提到了,但我想详细说明类属性的可变性。

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

所以,如果你有像

这样的东西
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期间自动对所有类属性进行深度复制,但我不知道在任何地方提供的属性。

答案 32 :(得分:0)

类属性

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

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

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'和'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

我相信Python 3包含一个'外部'关键字,用于“此功能之外但不是全局”的情况。在Python 2.#中,你不得不将 y 设为全局,或者将其作为'inner'的可变参数。

答案 33 :(得分:0)

与mutable默认参数类似,是可变类属性。

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

不是你所期望的。