细节见真知

2020-11-30 09:42:43 浏览数 (1)

Python 虽然简单易学,但要真正掌握和精通也不是件容易的事情,比如本文将要分享的这些有趣的特性,如果你一眼就看穿了问题的本质,说明你已经非常精通了。如果没有那就多看几次,细节见真知,敲敲代码验证下,对于提升 Python 编程技能,非常有效。

1、小心链式操作

一开始我看到有人问为什么 Python 语句中True is False is False的结果是 False 时,我自己也产生了疑问?

代码语言:javascript复制
>>> 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 <= zx < yy <= z 具有相同的优先级,不存在先计算 x < y ,得到结果后再与 <=z 进行比较的情况,因此x < y <= zx < 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 也是合法的,虽然并不好看。

那么开始的问题就变得简单了:

代码语言:javascript复制
True is False is False

相当于

代码语言:javascript复制
(True is False) and (False is False)

结果自然就是 False。

类似的还有:

代码语言:javascript复制
>>> 1 in [0,1] == True
False
>>> not True in [True,False]
False

2、析构函数__del__的执行时机

先看一段代码现象:

代码语言:javascript复制
>>> class SomeClass:
...     def __del__(self):
...         print("Deleted!")
...
>>> x = SomeClass()
>>> y = x
>>> del x # 这里应该会输出 "Deleted!"
>>> del y
Deleted!
>>>

上述代码中有注释的部分,即 del x 的操作本应该会执行析构函数 __del__ 的,为什么没有被执行,直到 del y 时才被执行?

或者下面的代码,为什么调用了 globals() 后,才执行?

代码语言:javascript复制
>>> 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 交互解释器的特性, 它会自动让 _ 保存上一个表达式输出的值。

3、不要迭代列表自身时删除

代码语言:javascript复制
>>> 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 将被彻底的跳过. 类似的情况会交替发生在列表中的每个元素上。

4、当心默认的可变参数

看下面的代码,你会觉得困惑吗?

代码语言:javascript复制
>>> 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 的默认值, 所以函数会返回我们所期望的结果,可以运行以下代码进行验证。

代码语言:javascript复制
>>> 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 指定为参数的默认值,然后检查是否有值传给对应的参数:

代码语言:javascript复制
def some_func(default_arg=None):
    if not default_arg:
        default_arg = []
    default_arg.append("some_string")
    return default_arg

5、 =有什么不同

代码 a :

代码语言:javascript复制
>>> 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:

代码语言:javascript复制
>>> 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 仍然指向已被修改的同一列表。

6、类的作用域

代码语言:javascript复制
>>> 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。

7、Python 为什么没有 goto

也许你会问这个问题,之前我在学习 C 语言的时候就非常好奇,为什么要提供 goto,让程序跳转呢,用个函数调用不就行了,是的,Python 语言就回答了这个问题,完全没必要用 goto,它让程序严重的结构化,且难以理解。比如:

代码语言:javascript复制
void somefunc(int a)
{
    if (a == 1)
        goto label1;
    if (a == 2)
        goto label2;

    label1:
        ...
    label2:
        ...
}

完全可以用

代码语言:javascript复制
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

0 人点赞