最近刷题的时候遇到的几个奇妙特性。

短路特性

Python有一种短路特性,意思就是只要能确定与或非表达式的结果,后面的运算就会被忽略。更多例子可以参考如下,来自 python 的“短路”,Python(二手博客,找不到出处了)。

False or 5 # 输出 5 #False 删除线格式 为假,在 or 中,x 为假—>输出 y
5 or False # 输出 5 #5 为真,在 or 中,x 为真—>输出 x
0 or False # 输出 False # 0 是假
True or 3 # 输出 True #True 为真
2 or True # 输出 2 #2 为真
0 or True # 输出 True #0 为假
True or False # 输出 True #True 为真
True and 4 # 输出 4 #True 为真,在 and 中,x 为真—>输出 y
1 and True # 输出 True #1 为真
False and 1 # 输出 False #False 为假,在 and 中,x 为假—>输出 x
1 and False # 输出 False
0 and True # 输出 0
not 3 # 输出 False #3 为真,在 not 中,x 为真—>输出 False
not 0 # 输出 True

写代码的时候,某一处需要进行两个条件的“与”判断,其中一个是非常耗时的in判断,一开始没有在意and前后的顺序,而回头再看的时候想起了这个特性。

if current_list[i] in history and current_list[i]%2 == 0:
  pass

所以把复杂度低的判断放在and之前,就能减少复杂度高的判断次数,改完之后发现效率大概提升了 10%。

其实吧,想想也知道这种写法跟两个if嵌套没什么区别,但是如果在判断语句里有执行其他操作就要小心了。😒

Dict 查找

其实in并非都很耗时。问题出在一开始用 List 结构存储数据,改成 Set 发现速度提高十倍。

参考这一篇的代码可以画出一个直观的图像:Python3 中 in 操作在列表,字典,集合中的速度对比 2(改进版)

Dict 在其他语言中也称为 map,使用键-值(key-value)存储;Set 类似,也是一组 key 的集合,但不存储 value。

为什么 Dict 查找快?因为 dict 是 Hash Table,用空间换时间。优势“查找和插入的速度极快,不会随着 key 的增加而变慢”,带来的代价就是“需要占用大量的内存,内存浪费多”。非常通俗的解释可以看 Python 的 dict 不会随着 key 的增加而变慢吗? - 高天的回答 - 知乎

另一个关于 List 和 Dict 的小知识:字典在遍历的时候不能修改大小,看 Code:

>>> d = {'a':1, 'b':0, 'c':1, 'd':0}
>>> for k in d:
...   if d[k] == 0:
...     del(d[k])
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

解决办法就是改成遍历d.keys()这个 List。

赋值与拷贝

为了解释这个问题,首先回顾一下 Python 中的拷贝。

当传参的时候如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值——相当于通过“传引用”来传递对象;如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象——相当于通过“传值’来传递对象。(这种情况下一开始 a 和 b 的内存空间仍然相同,但当 b 的值发生变化会重新分配内存空间。)参考 Python 中 list 的删除与清空

为了解决这样的问题,可以关注下 Python 中的浅拷贝与深拷贝,参考 Python List 的赋值方法

平时测试可以用is判断 id 号,用==判断 value。

在某一题中写深度遍历的时候碰到一个问题,抽象过来是这样的:

>>> a = [1,2,3]
>>> b = a
>>> b[0] = 0
>>> print(a)
>>> b = []
>>> print(a)
[0, 2, 3]
[0, 2, 3]

按理说b=a的赋值操作把 b 变成了 a 的引用,两者 id 相同,执行b[0] = 0之后 a 也确实发生变化,但是执行b = []并没有把 a 清空。

这里的问题出在b = []操作,List 是可变对象,对其中元素重新赋值不会改变内存地址,但对整体赋值时相当于重新开了一个空的 List 空间,而原来的内存还有引用计数,所以并没有释放。参考 python 的内存管理机制

由此也学到了将列表清空的正确方式,即调用a.clear()方法,更有利于内存管理。参考 Python 将一个列表置为空

字符串驻留

还想玩玩id()方法,但是下面这代码又有点让人意外:

>>> a = "hello"
>>> b = "hello"
>>> print(id(a))
>>> print(id(b))
1683121644240
1683121644240

这里涉及的是 Python 的字符串驻留特性,为了优化字符串效率和内存,Python 会开一个类似字典的字符串池,一些满足条件的重复字符串在内存中只会存在一次。

参考 Python 札记 1:字符串驻留 (String Interning),这里提到的反汇编dis模块也很有意思。