Python中常见的陷阱

80

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

还有什么其他的东西我必须避免吗?

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

  • 不要使用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
    

    这会输出“hmm bug”,尽管它并不是一个错误。看起来我们抓住了两种异常,但实际上我们只捕获了 KeyError 作为变量的 TypeError,请使用以下代码:

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

1
这个问题不是原始的吗?https://dev59.com/wXRB5IYBdhLWcg3wvpic - zeroDivisible
3
在Python 2.x中,不要使用range()作为列表。在Python 3.x中,应该使用list(range())来代替,可以使用2to3脚本进行转换。请注意,翻译后的内容仍然保留原意,同时更加易于理解,没有解释和额外的内容。 - jfs
4
“普通”的编程语言是什么? - Esteban Küber
普通的编程语言是快速、舒适、功能丰富、拥有许多库、帮助、书籍、论坛和其他互联网参考资料。它易于学习和使用!普通的编程语言是一种在使用时你不会讨厌它,而是越来越喜欢它的语言! - Narek
它应该处于适当的级别:不要太高,以至于为了获得舒适和易用性而限制了许多重要功能,也不要太低级(如汇编语言),以至于编写一个简单的程序时你就开始“与金属交流”(我的意思是硬件),在大多数情况下这是不必要的。当然,你还应该考虑你的任务:对于每个任务,对于每个方面,你都必须优先考虑不同的编程语言! - Narek
显示剩余5条评论
34个回答

73
不要使用索引来遍历序列。
不要:
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

使用"=="检查是否等于TrueFalse时要小心

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 程序员通常说“宁愿请求后谅解,也不要事先获得许可”。

不要:

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/except"。你不需要考虑如何预防错误的发生,只需要关注你可能会遇到的错误。

不要检查类型

Python是动态类型语言,因此检查类型会使你失去灵活性。相反,通过检查行为来使用鸭子类型。比如,在函数中需要一个字符串,那么使用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将接受字符串、元组、集合、列表等等。DRY很实惠 :-)

不要混用空格和制表符

千万不要这么做。你会哭的。

object用作第一个父类

这有点棘手,但随着程序的增长,它会咬你一口。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 Car(object):
    color = "red"
    wheels = [wheel(), Wheel(), Wheel(), Wheel()]

做:

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

6
在你的“只需执行并更正错误”段落中,你有一个except:块,这是邪恶可怕的。为避免在较大的程序中出现奇怪的调试问题,始终捕获最小的错误子集,因此在你的示例中,将except:替换为except IOError as E:(始终值得获取异常实例以供检查;) - richo
你说得对,这只是一个打字错误。已经修复了。当然,如果你期望列表在函数外部被更新,它是不起作用的。但在Python中,通常不使用具有副作用的函数。有一些方法可以操作可变类型。 - Bite code
1
这些大多数都是通用的编程陷阱:它们并不特别针对Python。 - Eric O. Lebigot
不,危险在于如果将self.wheels [0]设置为类属性,则它将成为所有实例共享的列表,因为任何类属性都被用作默认实例属性。我看到我的所有学生都创建了类属性,而他们想要的是实例属性,因为它似乎适用于不可变类型。 - Bite code
在大多数情况下,直接与 TrueFalse 进行比较是不正确的;应该使用 if varif not var 代替,这可能会调用 __len__()__nonzero__()(在 Python 3 中为 __bool__)方法。始终使用 if var is None 而不是 == None(一个类可能会覆盖 __eq__ 方法)。 - jfs
显示剩余7条评论

39

当你需要一个数组群体时,你可能会想要输入以下内容:

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

6
我不会在表达式中使用下划线。首先,它在交互式解释器中具有特殊含义。其次,人们并不完全清楚你的意思是忽略循环变量。那么把它改成ignored或者其他类似的词怎么样? - SingleNegationElimination
1
我只会使用“i”来表示那个变量。 - Tyler
3
翻译:对于口译员来说,_ 的意义是什么? - orokusaki
1
在交互式解释器中,下划线是您上一条命令的输出。 - Jorisslob
1
与此陷阱相关的问题会定期发布。这里是一个常被引用的重复目标:https://dev59.com/jHVC5IYBdhLWcg3wnCaA - unutbu

28

Python语言中的坑点 -- 非常隐晦的错误

  • 使用可变默认参数。

  • 前导零表示八进制。在Python 2.x中,09是一个非常难以发现的语法错误。

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

Python设计上的坑点

  • 花时间进行内省(例如试图自动确定类型、超类标识或其他内容)。首先,从源代码阅读就可以明显看出。更重要的是,在奇怪的Python内审方面花费的时间通常表明对多态性的基本理解失败了。80%的Python内审问题都是由于无法理解多态性造成的。

  • 花时间进行代码高尔夫比赛。仅仅因为你对应用程序的心理模型只有四个关键字(“做”,“什么”,“我的”,“意思”),并不意味着你应该构建一个超复杂的内省装饰器驱动框架来实现它。Python允许您将DRY提高到荒谬的水平。其余的Python内省问题试图将复杂问题简化为代码高尔夫练习。

  • 猴子补丁。

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

  • 将交互式逐步输入的Python与正确的程序混淆。当您交互式键入时,您可能会追踪不住变量,必须使用globals()。另外,在您输入时,几乎所有东西都是全局的。在正确的程序中,您永远不会“追踪不住”一个变量,并且任何事情都不会是全局的。


1
当第三方库存在一些错误时,除了MonkeyPatching之外还有哪些其他选择? - Anurag Uniyal
8
这是Python语言:你可以看到源代码。如果第三方库出现问题,你可以直接修复它。 - S.Lott
Python 3.0修复了前导零八进制错误。数字前的前导零不再使其成为八进制数。但您仍然可以使用带有0o(零小写oh)前缀的八进制字面量。 - Chris Connett
1
@Chris: "修复"?哇,Python开发者真是个愚蠢的决定。让所有调用chmod的程序都崩溃! - Draemon
1
@Draemon:只有当你没有遵循文档时才会出现这种情况。http://docs.python.org/library/os.html#os.chmod。文档中指出应使用提供的常量构建模式,而不是使用神奇的八进制常量。 - S.Lott
显示剩余5条评论

27

修改默认参数:

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

默认值只会被评估一次,而不是每次调用函数时都会重新评估。对 foo() 的重复调用将返回 ['baz']['baz', 'baz']['baz', 'baz', 'baz'] 等等。

如果你想改变 bar 的值,请这样做:

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

同时,改变类属性。原因相同,效果类似。 - Lennart Regebro
+1 我也遇到了这个问题:https://dev59.com/c0fSa4cB1Zd3GeqPA-RR - gotgenes

22

我不知道这是否是一个常见错误,但是尽管 Python 没有递增和递减运算符,但允许双加号,因此:

++i

并且

--i

这是语法正确的代码,但它并不执行任何“有用”的操作或者你可能期望的操作。


5
需要注意的是,Python 中的自增/自减操作使用 i += 1 和 i -= 1,而不是 ++i、i++ 或者 i--。请留意。 - monkut
哇,从未知道这个。这怎么可能在语法上是正确的?我很困惑。 - Otto Allmendinger
1
Python语法的相关生产规则为:u_expr ::= power | "-" u_expr | "+" u_expr | "~" u_expr(请参见http://docs.python.org/reference/expressions.html#unary-arithmetic-and-bitwise-operations) - Jochen Walter
1
整个意思是,这里的“--”和“++”不是递增/递减运算符,而是后面数字的双重符号。因此,--i 等同于 -(-i) == i。因此,您也可以写成 ---i = -i,这就是 Python 没有递增/递减运算符的原因(如果有的话,会导致未定义的行为)。 - Boldewyn

18

在查看标准库之前自己编写代码。例如,编写以下内容:

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

如果可以使用这个方法:

from itertools import cycle

除了itertools之外,常常被忽略的模块示例包括:

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

1
我会将shutil和glob添加到那个列表中。 - André
1
shelve是一个非常糟糕的例子,它不适用于在不同操作系统之间移植代码。 - Massimo
1
optparse已被弃用,请改用argparse - chunyang.wen

15

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

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


1
在编写模块时,请避免使用 from ... import *。但在顶层脚本中,我认为这样做是可以的。 - SingleNegationElimination

14

没有使用函数工具。这不仅是从风格的角度来看错误,而且从速度的角度来看也是错误的,因为许多函数工具都在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 3.x中,内置函数map不会返回一个列表,而是返回一个迭代器(类似于Python 2.x中的itertools.imap)。如果要从map中获取一个列表,您必须使用list(map(...)) - Demian Wolf

14

惊讶于没有人提到这一点:

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

确实,这是一个致命的问题。相信我。特别是,如果代码需要运行。


3
如果您按照时间顺序阅读回答的话,就不会出现这种情况。 - Boldewyn
简单的解决方案:在源代码开头加上 #!/usr/bin/env python -t - Massimo

13

如果您是从C++转过来的,请注意类定义中声明的变量是静态的。您可以在init方法中初始化非静态成员。

示例:

class MyClass:
  static_member = 1

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接