Python错误代码:开发人员最常犯的10个错误

关于Python


Python是一种具有动态语义的解释性,面向对象的高级编程语言。 内置的高级数据结构与动态类型和动态绑定相结合,使其对于BRPS(应用程序工具的快速开发)以及用作连接现有组件或服务的脚本和连接语言非常有吸引力。 Python支持模块和包,从而鼓励程序模块化和代码重用。

关于本文


掌握这种语言的简便性会使开发人员(尤其是刚开始学习Python的开发人员)感到困惑,因此您可能会忽略一些重要的细微之处,并低估了使用Python的各种可能解决方案的强大功能。

考虑到这一点,本文介绍了即使是高级Python开发人员也可能犯的细微,难以发现的“十大错误”。

错误1:将表达式误用作函数参数的默认值


Python允许您通过为函数设置默认值来指示函数可以具有可选参数。 当然,这是该语言非常方便的功能,但是如果此值的类型可变,则可能导致不愉快的后果。 例如,考虑以下函数定义:

>>> def foo(bar=[]): # bar -    #      . ... bar.append("baz") #     ... ... return bar 

在这种情况下,一个常见的错误是认为每次调用不带参数值的函数时,可选参数的值都会被设置为默认值。 例如,在上面的代码中,我们可以假定通过重复调用函数foo()(即,不指定bar参数的值),它将始终返回“ 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"] 

错误2:滥用类变量


考虑以下示例:

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

一切似乎井井有条。

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1 

是的,一切都如预期。

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3 

什么鬼?! 我们只是换了Ax,为什么Cx也要换?

在Python中,类变量被视为字典,并遵循通常称为“方法解析顺序(MRO)”的方法。 因此,在上面的代码中,由于在C类中找不到x属性,因此它将在其基类中找到(尽管Python支持多重继承,但在上面的示例中只有A属性)。 换句话说,C没有独立于A的自身属性x,因此,对Cx的引用实际上是对Ax的引用,如果这些情况处理不当,将会引起问题。 因此,在学习Python时,请特别注意类属性并使用它们。

错误之三:异常块的参数不正确


假设您具有以下代码:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range 

这里的问题是异常表达式不接受以此方式指定的异常列表。 而是在Python 2.x中,使用表达式“ Exception,e除外”将异常绑定到可选的第二个给定的第二个参数(在本例中为e),以使其可用于进一步检查。 结果,在上面的代码中,except语句未捕获IndexError异常; 相反,异常以绑定到名为IndexError的参数结束。

使用异常表达式捕获多个异常的正确方法是将第一个参数指定为包含要捕获的所有异常的元组。 另外,为了获得最大的兼容性,请使用as关键字,因为Python 2和Python 3支持该语法:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>> 

错误#4:误解Python作用域规则


Python中的作用域基于所谓的LEGB规则,它是Local(在函数内部以任何方式分配的名称(def或lambda)的缩写,并且在此函数中未声明为全局)的缩写,Enclosing(在任何静态包含函数的局部作用域中的名称( def或lambda)(从内部到外部),全局(在模块文件的顶级分配的名称,或通过在文件内部def中执行全局指令分配的名称),内置(以前在内置名称模块中分配的名称:打开,范围, SyntaxError,...)。 看起来很简单,对吧? 好吧,实际上,它在Python中的工作方式有一些微妙之处,这使我们想到了下面更常见的更复杂的Python编程问题。 考虑以下示例:

 >>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment 

怎么了

发生上述错误的原因是,当您在范围内分配变量时,Python会自动将其视为该范围的局部变量,并在任何父范围内隐藏具有相同名称的任何变量。

因此,当许多人在先前运行的代码中收到UnboundLocalError时,通过在函数体中的某个位置添加赋值运算符对其进行了修改,感到非常惊讶。

使用列表时,此功能特别会使开发人员感到困惑。 考虑以下示例:

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

?? 为什么在foo1正常工作时foo2崩溃?

答案与前面的示例相同,但是根据流行的看法,这里的情况更加微妙。 foo1不会将赋值运算符应用于lst,而foo2则不会。 记住lst + = [5]实际上只是lst = lst + [5]的简写,我们看到我们正在尝试分配值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>", 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) ... 

您可以期待以下输出:

 0 2 4 6 8 

但是实际上,您得到以下信息:

 8 8 8 8 8 

惊喜!

这是由于Python中的后期绑定,这意味着在调用内部函数期间会查找闭包中使用的变量的值。 因此,在上面的代码中,每当调用任何返回的函数时,在调用它的过程中都会在周围的范围内搜索值i(并且到那时该循环已经完成,因此我已经被分配了最终结果-值4) 。

解决此常见Python问题的方法是:

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

瞧! 我们在此处使用默认参数来生成匿名函数以实现所需的行为。 有人会将此解决方案称为优雅。 有些是
薄。 有些人讨厌这些东西。 但是,无论如何,如果您是Python开发人员,了解这一点很重要。

错误7:创建循环模块依赖项


假设您有两个文件a.py和b.py,每个文件都导入另一个文件,如下所示:

在a.py中:

 import b def f(): return bx print f() 

在b.py中:

 import a x = 1 def g(): print af() 

首先,尝试导入a.py:

 >>> import a 1 

工作正常。 这可能会让您感到惊讶。 毕竟,模块会周期性地互相导入,这可能应该是个问题,对吧?

答案是,简单地循环导入模块本身并不是Python中的问题。 如果模块已经被导入,那么Python足够聪明,不会尝试重新导入它。 但是,根据每个模块试图访问另一个模块中定义的函数或变量的不同点,实际上可能会遇到问题。

因此,回到我们的示例,当我们导入a.py时,导入b.py没有问题,因为b.py不需要在导入过程中定义任何a.py。 b.py中对a的唯一引用是对af()的调用。 但是,在g()中的此调用而在a.py或b.py中的任何内容都不调用g()。 因此,一切正常。

但是,如果我们尝试导入b.py(即不先导入a.py),会发生什么:

 >>> import b Traceback (most recent call last): File "<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 bx AttributeError: 'module' object has no attribute 'x' 

哦,哦。 这不好! 这里的问题是,在b.py的导入过程中,它尝试导入a.py,而后者又调用f(),后者尝试访问bx,但是bx尚未定义。 因此,AttributeError异常。

至少有一个解决这个问题的方法很简单。 只需修改b.py即可将a.py导入g():

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

现在,当我们导入它时,一切都很好:

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

错误8:Python标准库中的名称与模块名称相交


Python的魅力之一是其现成的许多模块。 但是结果是,如果您不自觉地遵循此规则,则可能会发现模块的名称可能与Python随附的标准库中的模块名称相同(例如,在您的代码中可能有一个模块名称为email.py,它将与具有相同名称的标准库模块冲突)。

这会导致严重的问题。 例如,如果任何模块尝试从Python标准库中导入模块的版本,并且您在项目中有一个具有相同名称的模块,那么它将被错误地导入,而不是从标准库中导入该模块。

因此,应注意不要使用与Python标准库的模块相同的名称。 与在标准库中提交更改模块名称并获得批准的请求相比,更改项目中模块的名称要容易得多。

错误9:无法考虑Python 2和Python 3之间的差异


考虑以下foo.py文件:

 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('key error') except ValueError as e: print('value error') 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 'e' referenced before assignment 

刚刚发生什么事了? “问题”是在Python 3中,异常块中的对象不在其外部。 (这样做的原因是,否则该块中的对象将被存储在内存中,直到垃圾回收器启动并从那里删除对其的引用为止)。

避免此问题的一种方法是将对异常块对象的引用保持在该块之外,以使其保持可用。 这是使用该技术的先前示例的版本,从而获得了适用于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('key error') except ValueError as e: exception = e print('value error') 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() 

并得到一个可怕的AttributeError。

怎么了 因为,如此处报告的那样,当解释器关闭时,模块全局变量的值都为None。 结果,在上面的示例中,当调用__del__时,名称foo已经设置为None。

解决此“带有星号的任务”的方法是使用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) 

这样的实现提供了一种简单可靠的方法,可以在正常程序终止后调用任何必要的清除操作。 显然,有关如何处理与self.myhandle名称关联的对象的决定留给了foo.cleanup,但我认为您可以理解。

结论


Python是一种功能强大且灵活的语言,具有许多可以显着提高性能的机制和范例。 但是,与任何软件工具或语言一样,在对其功能的了解或评估有限的情况下,开发过程中可能会出现无法预料的问题。

对本文所涵盖的Python细微差别的介绍将有助于优化对语言的使用,同时避免一些常见的错误。

Source: https://habr.com/ru/post/zh-CN466441/


All Articles