Python 虽然简单易学,但要真正掌握和精通也不是件容易的事情,比如本文将要分享的这些有趣的特性,如果你一眼就看穿了问题的本质,说明你已经非常精通了。如果没有那就多看几次,细节见真知,敲敲代码验证下,对于提升 Python 编程技能,非常有效。
一开始我看到有人问为什么 Python 语句中True is False is False
的结果是 False
时,我自己也产生了疑问?
>>> True is False is False
False
>>> (True is False) is False
True
>>> True is (False is False)
True
>>>
于是就搜索了下 stackoverflow,然后索引到官方文档[1]对比较操作的说明,一次子就长知识了,发现 Python 中的比较运算与 C 语言不同,这些比较操作具有相同的优先级,该优先级低于任何算术,移位或按位运算。
这些比较操作in, not in, is, is not, <, <=, >, >=, !=, ==
操作符,会产生 True 或 False 的结果,这些比较操作符号可以任意的链式比较,比如:x < y <= z
,x < y
与 y <= z
具有相同的优先级,不存在先计算 x < y
,得到结果后再与 <=z
进行比较的情况,因此x < y <= z
与 x < y and y <= z
是等价的。
x < y and y <= z
中,如果 x < y
的结果是 False,那么 y <= z
根本不会被计算。
也就是说a op1 b op2 c ... y opN z
等价于 a op1 b and b op2 c and ... y opN z
,每一个表达式最多被执行一次。
注意,a op1 b op2 c
并不代表 a 和 c 有必然的关系,比如这样写x < y > z
也是合法的,虽然并不好看。
那么开始的问题就变得简单了:
True is False is False
相当于
(True is False) and (False is False)
结果自然就是 False。
类似的还有:
>>> 1 in [0,1] == True
False
>>> not True in [True,False]
False
__del__
的执行时机先看一段代码现象:
>>> class SomeClass:
... def __del__(self):
... print("Deleted!")
...
>>> x = SomeClass()
>>> y = x
>>> del x # 这里应该会输出 "Deleted!"
>>> del y
Deleted!
>>>
上述代码中有注释的部分,即 del x
的操作本应该会执行析构函数 __del__
的,为什么没有被执行,直到 del y
时才被执行?
或者下面的代码,为什么调用了 globals() 后,才执行?
>>> x = SomeClass()
>>> y = x
>>> del x
>>> y
<__main__.SomeClass object at 0x7fa8e1cb94c0>
>>> del y
>>> globals()
Deleted!
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'some_func': <function some_func at 0x7fa8dedacd30>...
原因是:首先 del x
并不会立刻调用 x.__del__()
,而是每当遇到 del x
, Python 会将 x 的引用数减 1, 当 x 的引用数减到 0 时就会调用 x.__del__()
。因此第一个示例代码中 del x
前 x 的引用技术为 2,执行后变为 1,并不执行 x.__del__()
。
在第二个例子中, y.__del__()
之所以未被调用, 是因为前一条语句 (>>> y) 对同一对象创建了另一个引用, 从而防止在执行 del y
后对象的引用数变为 0。
调用 globals 导致引用被销毁, 因此我们可以看到 "Deleted!" 终于被输出了,这其实是 Python 交互解释器的特性, 它会自动让 _ 保存上一个表达式输出的值。
>>> list_1 = [1, 2, 3, 4]
>>> list_2 = [1, 2, 3, 4]
>>> list_3 = [1, 2, 3, 4]
>>> list_4 = [1, 2, 3, 4]
>>>
>>> for idx, item in enumerate(list_1):
... del item
...
>>> for idx, item in enumerate(list_2):
... list_2.remove(item)
...
>>> for idx, item in enumerate(list_3[:]):
... list_3.remove(item)
...
>>> for idx, item in enumerate(list_4):
... list_4.pop(idx)
...
1
3
>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]
>>>
为什么 list_1 没有被删除,为什么 list_2 和 list_4 还会有元素 [2,4]?
list_1 这个很好理解,item 只是 for 循环内部的一个临时变量,删除这个根本不影响原始列表。
在迭代时修改对象是一个很愚蠢的主意,正确的做法是迭代对象的副本, list_3[:] 就相当于完整的复制了 list_3,因此可以全部删除。
那么为什么输出是 [2, 4]?
因为列表迭代是按索引进行的, 所以当我们从 list_2 或 list_4 中删除 1 时, 列表的内容就变成了 [2, 3, 4],剩余元素会依次位移, 也就是说, 2 的索引会变为 0, 3 会变为 1. 由于下一次迭代将获取索引为 1 的元素 (即 3), 因此 2 将被彻底的跳过. 类似的情况会交替发生在列表中的每个元素上。
看下面的代码,你会觉得困惑吗?
>>> def some_func(default_arg=[]):
... default_arg.append("some_string")
... return default_arg
...
>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']
>>>
Python 中函数的默认可变参数并不是每次调用该函数时都会被初始化,相反,它们会使用最近分配的值作为默认值。当我们明确的将 [] 作为参数传递给 some_func 的时候, 就不会使用 default_arg 的默认值, 所以函数会返回我们所期望的结果,可以运行以下代码进行验证。
>>> some_func.__defaults__ # 这里会显示函数的默认参数的值
([],)
>>> some_func()
>>> some_func.__defaults__
(['some_string'],)
>>> some_func()
>>> some_func.__defaults__
(['some_string', 'some_string'],)
>>> some_func([])
>>> some_func.__defaults__
(['some_string', 'some_string'],)
避免可变参数导致的错误的常见做法是将 None 指定为参数的默认值,然后检查是否有值传给对应的参数:
def some_func(default_arg=None):
if not default_arg:
default_arg = []
default_arg.append("some_string")
return default_arg
代码 a :
>>> a = [1, 2, 3, 4]
>>> b = a
>>> a = a + [5, 6, 7, 8]
>>>
>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4]
代码 b:
>>> a = [1, 2, 3, 4]
>>> b = a
>>> a += [5, 6, 7, 8]
>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]
两者的区别仅仅在于 a = a + [5,6,7,8] 和 a += [5,6,7,8],结果却完全不同,这是为什么呢?
因为:a += b 并不总是与 a = a + b 表现相同,类实现 op= 运算符的方式也许是不同的,列表就是这样做的:表达式 a = a + [5,6,7,8] 会生成一个新列表,并让 a 引用这个新列表,同时保持 b 不变。表达式 a += [5,6,7,8] 实际上是使用的是 "extend" 函数,所以 a 和 b 仍然指向已被修改的同一列表。
>>> x = 5
>>> class SomeClass:
... x = 17
... y = (x for i in range(10))
...
>>>
>>> list(SomeClass.y)[0]
5
原因是:类定义中嵌套的作用域会忽略类内的名称绑定,生成器表达式有它自己的作用域,因此生成器表达式忽略了类内部定义的 17 而使用全局变量 5,从 Python 3.X 开始, 列表推导式也有自己的作用域,因此 () 换成 [] 在 Python 3.X 的结果也是 5,Python 2.X 则是 17。
也许你会问这个问题,之前我在学习 C 语言的时候就非常好奇,为什么要提供 goto,让程序跳转呢,用个函数调用不就行了,是的,Python 语言就回答了这个问题,完全没必要用 goto,它让程序严重的结构化,且难以理解。比如:
void somefunc(int a)
{
if (a == 1)
goto label1;
if (a == 2)
goto label2;
label1:
...
label2:
...
}
完全可以用
def func1():
...
def func2():
...
funcmap = {1 : func1, 2 : func2}
def somefunc(a):
funcmap[a]() #Ugly! But it works.
替代。
编程细节藏着魔鬼,搞懂了就豁然开朗,希望这些知识对你有用。
[1]
官方文档: https://docs.python.org/3/reference/expressions.html#comparisons