1 滥用表达式作为函数参数的默认值
python允许通过为函数提供默认值来指定函数参数的,但是当默认值是可变的时,就会产生一些问题:
1 | def foo(bar=[]): |
上面的代码中,期望的是 foo()
重复调用(即不指定bar参数)将始终返回 'baz'
,因此假设每次 foo()
调用 bar
被设置为 []
。
但是,让我们来看看执行次操作时实际发生的情况:
1 | >> foo() |
咦,为什么每次调用都会默认值附加 'baz'
到现有的列表中,而不是每次都创建一个新列表?
答案就是: 函数参数的默认值仅在定义函数时计算一次。因此 bar
仅在 foo()
首次定义时将参数初始化为其默认值,但随后调用 foo()
(即未指定bar
参数),将继续使用 bar
最初初始化的相同列表。
仅供参考,一个常见的解决方法如下:
1 | def foo(bar=None): |
2 错误的使用类变量
请考虑一下示例:
1 | class A(object): |
以上的输出是没有问题的,请继续往下看:
1 | 2 B.x = |
输出还是如预期的那样,那接下来:
1 | >>> A.x = 3 |
可以思考一下上面输出的结果:
3 2 3
这是什么情况?我们只改变了A.x,为什么C.x也改变了呢?
在python中,类变量在内部作为字典处理,并遵循通常成为方法解析顺序(MRO)的方法,因此在上面的代码中,由于在C
中找不到x
属性,因此将在其基类中查找它。换句话数,C
没有自己的x
属性,因此引用C.x实际上值得是A.x。
3 错误的为异常块指定参数
假如你用一下代码:
1 | # 这段代码是python 2.7版本的 |
这里的问题是except语句没有采用这种方式指定的异常列表,相反,在python2.x中,语法 except Exception, e
用于将异常绑定到指定的可选的第二个参数(本例中e),以用于进一步检查。结果在上面的代码中,IndexError异常没有被except
语句捕获,相反,异常最终被绑定到一个名为IndexError
的参数。
在except
语句中,捕获多个异常的正确方法是将第一个参数指定为包含要捕获所有异常的元祖,此外为了获得最大的可移植性,请使用as
关键字,因为Python2和Python3都支持该语法。
1 | try: |
4 误解Python范围规范
Python范围解析是基于所谓的LEGB规则。在Python的工作方式中有一些细微之处,让我们看看常见的更高级的Python编程问题:
1 | >>> x = 10 |
上述问题出现的原因是:当你对作用域中的变量进行赋值时,Python会自动将变量视为该作用域的本地变量,并在任何外部作用域中隐藏任何类似命名的变量。
但在使用列表时,有一个特殊的现象,请看以下代码示例:
1 | 1, 2, 3] lst = [ |
咦?为什么foo1良好的运行,但是foo2却报错了??
答案和前面示例问题相同,但无疑更微妙一些。foo1
不是分配值到lst,而foo2却是。记住lst += [5]
实际是lst = lst + [5]
的简写,我们看到foo2正在分配一个值给lst,因此Python推测它是本地范围内。但是我们要分配的值lst是lst自身,因此是未定义。
5 在迭代时修改列表
以下代码的问题是相当明显的:
1 | >>> odd = lambda x: bool(x % 2) |
在迭代时,从列表或数组中删除项是Python常见的问题。幸运的是Python结合许多优雅的编程范例,如果使用得当可以简化代码。另外一个好处是更简单的代码不太可能被意外删除列表项而导致迭代问题。它完美的工作:
1 | >>> odd = lambda x : bool(x % 2) |
6 混淆Python如何绑定闭包中的变量
参考以下示例:
1 | def create_multipliers(): |
你可能期望以下输出:
0
2
4
6
8
但是你得到的是:
8
8
8
8
8
这是因为Python调用内部函数时,闭包中使用的变量值是后期绑定行为导致的。所以上面的代码中,每当调用任何返回的函数时,在调用i它时,在周围的作用域中查找值,那是循环已经完成,因此i已经分配了它的最终值4。
这个常见问题的解决是有点像黑客的做法:
1 | def create_multipliers(): |
这里利用了默认参数来生成匿名函数,以实现所需的行为,有些人称之为优雅,有些人会认为微免,有些人会讨厌它。但是作为Python的开发人员,无论如何都要理解它。
7 创建循环引用
假设你有两个文件,a.py
和b.py
而且每个文件都导入另一个文件,如下所示:
在a.py
中:
1 | import b |
在b.py
中:
1 | import a |
首先让我们尝试导入a.py
1 | >>> import a |
到此,没有出现异常,也许这个会给你带来惊喜,毕竟,我们这里有一个循环导入的问题,大概应该是一个问题,不应该?答案是,仅仅存在循环导入本身并不是Python的一个问题。如果已导入的模块,Python足够聪明,不会尝试重新导入它。但是根据每个模块尝试访问另一个模块中定义的函数或变量,你可能会遇到一些问题。
所以回到例子中,当我们导入a.py
,它导入b.py
有没有问题?因为b.py
不需要从a.py
中导入任何变量,这是因为唯一调用的a.f()
还是在调用g()
时被调用,所以此时a.py
或b.py
中没有任何内容调用g()
,所以一切看起来是美好的。
如果我们尝试导入b.py
,看看会发生什么?(前提是没有先导入a.py
)
1 | >>> import b |
这就出现问题了。 在导入b.py
中,他会尝试导入a.py
,而后者又会调用f()
尝试访问的内容b.x
,但b.x
尚未定义,因此出现AttributeError
问题。
这里提供一个简单的方案处理这个问题,只需要修改b.py
,在g()
中导入a.py
:
1 | x = 1 |
当我们导入它时,一切都会变得美好:
1 | import b |
8 名称与Python标准库模块冲突
Python的优点之一是它提供了“开箱即用”的丰富的库模块。但是如果你没有意识的避开它,那么在发成自定义模块与Python标准库模块冲突的几率会增大很多。
9 未能解决Python2和Python3之间的差异
考虑一下文件 foo.py
1 | import sys |
在Python2上,正常运行:
1 | $ python foo.py 1 |
但是在Python3上:
1 | $ python3 foo.py 1 |
“问题”是,在Python 3中,异常对象超出except块的范围是不可访问的。(原因是,否则,它会在内存中保持堆栈帧的引用循环,直到垃圾收集器运行并从内存中清除引用。
避免此问题的一种方法是在块的范围之外维护对异常对象的引用,以except使其保持可访问状态。这是使用此技术的上一个示例的一个版本,从而产生兼容Python 2和Python 3的代码:
1 | import sys |
在Python3上运行:
1 | $ python3 foo.py 1 |
10 滥用__del__
方法
假设你在一个名为的文件中有这个mod.py
:
1 | import foo |
然后你试着这样做 another_mod.py
:
1 | import mod |
你会得到一个丑陋的AttributeError
。
当解释器关闭时,模块的全局变量都被设置为None。因此,在上面的示例中,在__del__
调用的位置,名称foo
已设置为None
。
解决方案是使用atexit.register()
。这样,当您的程序完成执行时(正常退出时),您的注册处理程序将在解释器关闭之前启动:
1 | import foo |
此实现提供了一种干净可靠的方法,可在正常程序终止时调用任何所需的清理功能。显然,foo.cleanup要决定如何处理绑定到名称的对象self.myhandle,但是你明白了。