一些有趣且鲜为人知的 Python 特性
共 5395字,需浏览 11分钟
·
2021-05-23 12:38
Output (Python version):
>>> 触发语句
出乎意料的输出结果(可选): 对意外输出结果的简短描述.
💡 说明:
简要说明发生了什么以及为什么会发生.
如有必要, 举例说明Output:
>>> 触发语句 # 一些让魔法变得容易理解的例子# 一些正常的输入
注意: 所有的示例都在 Python 3.5.2 版本的交互解释器上测试过, 如果不特别说明应该适用于所有 Python 版本.
我个人建议, 最好依次阅读下面的示例, 并对每个示例:
仔细阅读设置例子最开始的代码. 如果您是一位经验丰富的 Python 程序员, 那么大多数时候您都能成功预期到后面的结果.
阅读输出结果,
如果不知道, 深呼吸然后阅读说明 (如果你还是看不明白, 别沉默! 可以在这提个 issue).
如果知道, 给自己点奖励, 然后去看下一个例子.
确认结果是否如你所料.
确认你是否知道这背后的原理.
PS: 你也可以在命令行阅读 WTFpython. 我们有 pypi 包 和 npm 包(支持代码高亮).(译: 这两个都是英文版的)
安装 npm 包 wtfpython
$ npm install -g wtfpython
或者, 安装 pypi 包 wtfpython
$ pip install wtfpython -U
现在, 在命令行中运行 wtfpython
, 你就可以开始浏览了.
Section: Strain your brain!/大脑运动!
> Strings can be tricky sometimes/微妙的字符串 *
1.
>>> a = "some_string"
>>> id(a)
140420665652016
>>> id("some" + "_" + "string")# 注意两个的id值是相同的.
# 140420665652016
2.
>>> a = "wtf"
>>> b = "wtf"
>>> a is b
True>>> a = "wtf!"
>>> b = "wtf!"
>>> a is b
False>>> a, b = "wtf!", "wtf!"
>>> a is b
True # 3.7 版本返回结果为 False.
3.
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False # 3.7 版本返回结果为 True
很好理解, 对吧?
💡 说明:
这些行为是由于 Cpython 在编译优化时, 某些情况下会尝试使用已经存在的不可变对象而不是每次都创建一个新对象. (这种行为被称作字符串的驻留[string interning])
发生驻留之后, 许多变量可能指向内存中的相同字符串对象. (从而节省内存)
在上面的代码中, 字符串是隐式驻留的. 何时发生隐式驻留则取决于具体的实现. 这里有一些方法可以用来猜测字符串是否会被驻留:
所有长度为 0 和长度为 1 的字符串都被驻留.
字符串在编译时被实现 (
'wtf'
将被驻留, 但是''.join(['w', 't', 'f'])
将不会被驻留)字符串中只包含字母,数字或下划线时将会驻留. 所以
'wtf!'
由于包含!
而未被驻留. 可以在这里找到 CPython 对此规则的实现.当在同一行将
a
和b
的值设置为"wtf!"
的时候, Python 解释器会创建一个新对象, 然后同时引用第二个变量(译: 仅适用于3.7以下, 详细情况请看这里). 如果你在不同的行上进行赋值操作, 它就不会“知道”已经有一个wtf!
对象 (因为"wtf!"
不是按照上面提到的方式被隐式驻留的). 它是一种编译器优化, 特别适用于交互式环境.常量折叠(constant folding) 是 Python 中的一种 窥孔优化(peephole optimization) 技术. 这意味着在编译时表达式
'a'*20
会被替换为'aaaaaaaaaaaaaaaaaaaa'
以减少运行时的时钟周期. 只有长度小于 20 的字符串才会发生常量折叠. (为啥? 想象一下由于表达式'a'*10**10
而生成的.pyc
文件的大小). 相关的源码实现在这里.如果你是使用 3.7 版本中运行上述示例代码, 会发现部分代码的运行结果与注释说明相同. 这是因为在 3.7 版本中, 常量折叠已经从窥孔优化器迁移至新的 AST 优化器, 后者可以以更高的一致性来执行优化. (由 Eugene Toder 和 INADA Naoki 在 bpo-29469 和 bpo-11549 中贡献.)
(译: 但是在最新的 3.8 版本中, 结果又变回去了. 虽然 3.8 版本和 3.7 版本一样, 都是使用 AST 优化器. 目前不确定官方对 3.8 版本的 AST 做了什么调整.)
> Time for some hash brownies!/是时候来点蛋糕了!
hash brownie指一种含有大麻成分的蛋糕, 所以这里是句双关
1.
some_dict = {}
some_dict[5.5] = "Ruby"
some_dict[5.0] = "JavaScript"
some_dict[5] = "Python"
Output:
>>> some_dict[5.5]
"Ruby"
>>> some_dict[5.0]
"Python"
>>> some_dict[5]
"Python"
"Python" 消除了 "JavaScript" 的存在?
💡 说明:
Python 字典通过检查键值是否相等和比较哈希值来确定两个键是否相同.
具有相同值的不可变对象在Python中始终具有相同的哈希值.
>>> 5 == 5.0
True
>>> hash(5) == hash(5.0)
True
注意: 具有不同值的对象也可能具有相同的哈希值(哈希冲突).
当执行
some_dict[5] = "Python"
语句时, 因为Python将5
和5.0
识别为some_dict
的同一个键, 所以已有值 "JavaScript" 就被 "Python" 覆盖了.这个 StackOverflow的 回答 漂亮地解释了这背后的基本原理.
> Return return everywhere!/到处返回!
def some_func():
try:
return 'from_try'
finally:
return 'from_finally'
Output:
>>> some_func()
'from_finally'
💡 说明:
当在 "try...finally" 语句的
try
中执行return
,break
或continue
后,finally
子句依然会执行.函数的返回值由最后执行的
return
语句决定. 由于finally
子句一定会执行, 所以finally
子句中的return
将始终是最后执行的语句.
> Deep down, we're all the same./本质上,我们都一样. *
class WTF:
pass
Output:
>>> WTF() == WTF() # 两个不同的对象应该不相等
False
>>> WTF() is WTF() # 也不相同
False
>>> hash(WTF()) == hash(WTF()) # 哈希值也应该不同
True
>>> id(WTF()) == id(WTF())
True
💡 说明:
当调用
id
函数时, Python 创建了一个WTF
类的对象并传给id
函数. 然后id
函数获取其id值 (也就是内存地址), 然后丢弃该对象. 该对象就被销毁了.当我们连续两次进行这个操作时, Python会将相同的内存地址分配给第二个对象. 因为 (在CPython中)
id
函数使用对象的内存地址作为对象的id值, 所以两个对象的id值是相同的.综上, 对象的id值仅仅在对象的生命周期内唯一. 在对象被销毁之后, 或被创建之前, 其他对象可以具有相同的id值.
那为什么
is
操作的结果为False
呢? 让我们看看这段代码.
class WTF(object):
def __init__(self): print("I")
def __del__(self): print("D")
Output:
>>> WTF() is WTF()
I
I
D
D
False
>>> id(WTF()) == id(WTF())
I
D
I
D
True
正如你所看到的, 对象销毁的顺序是造成所有不同之处的原因.
> For what?/为什么?
some_string = "wtf"
some_dict = {}
for i, some_dict[i] in enumerate(some_string):
pass
Output:
>>> some_dict # 创建了索引字典.
{0: 'w', 1: 't', 2: 'f'}
💡 说明:
Python 语法 中对
for
的定义是:
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite
其中 exprlist
指分配目标. 这意味着对可迭代对象中的每一项都会执行类似 {exprlist} = {next_value}
的操作.
一个有趣的例子说明了这一点:
for i in range(4): print(i) i = 10
Output:
0
1
2
3
你可曾觉得这个循环只会运行一次?
由于循环在Python中工作方式, 赋值语句
i = 10
并不会影响迭代循环, 在每次迭代开始之前, 迭代器(这里指range(4)
) 生成的下一个元素就被解包并赋值给目标列表的变量(这里指i
)了.在每一次的迭代中,
enumerate(some_string)
函数就生成一个新值i
(计数器增加) 并从some_string
中获取一个字符. 然后将字典some_dict
键i
(刚刚分配的) 的值设为该字符. 本例中循环的展开可以简化为:
>>> i, some_dict[i] = (0, 'w')
>>> i, some_dict[i] = (1, 't')
>>> i, some_dict[i] = (2, 'f')
>>> some_dict