Python 的 51 个秘密曝光,Github 获 2 万星

论坛 期权论坛 期权     
Python那些事   2019-7-13 18:02   2654   0
(点击上方快速关注并设置为星标,一起学Python)
来源:https://github.com/leisurelicht/wtfpython-cn
编辑:Python那些事



Python, 是一个设计优美的解释型高级语言, 它提供了很多能让程序员感到舒适的功能特性. 但有的时候, Python 的一些输出结果对于初学者来说似乎并不是那么一目了然.
一个解析51项堪称是"秘密"的Python特性项目,在GitHub上彻底火了。
英文原版已经拿到了近15000星,中文翻译版也获得了7600+星。





这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性, 并尝试讨论这些现象背后真正的原理!
虽然下面的有些例子并不一定会让你觉得 WTFs, 但它们依然有可能会告诉你一些你所不知道的 Python 有趣特性. 我觉得这是一种学习编程语言内部原理的好办法, 而且我相信你也会从中获得乐趣!
如果您是一位经验比较丰富的 Python 程序员, 你可以尝试挑战看是否能一次就找到例子的正确答案. 你可能对其中的一些例子已经比较熟悉了, 那这也许能唤起你当年踩这些坑时的甜蜜回忆

这个项目的中文版全文大约2万字,干货多的快要溢出来了,大家可以先看一下目录。


示例结构
所有示例的结构都如下所示:


我个人建议, 最好依次阅读下面的示例, 并对每个示例:
  • 仔细阅读设置例子最开始的代码. 如果您是一位经验丰富的 Python 程序员, 那么大多数时候您都能成功预期到后面的结果.
  • 阅读输出结果,
    • 如果不知道, 深呼吸然后阅读说明 (如果你还是看不明白, 别沉默! 可以在这提个 issue).
    • 如果知道, 给自己点奖励, 然后去看下一个例子.
    • 确认结果是否如你所料.
    • 确认你是否知道这背后的原理.
PS: 你也可以在命令行阅读 WTFpython. 我们有 pypi 包 和 npm 包(支持代码高亮).(译: 这两个都是英文版的)
示例
Strings can be tricky sometimes/微妙的字符串
1、
  1. >>> a = "some_string">>> id(a)140420665652016>>> id("some" + "_" + "string") # 注意两个的id值是相同的.140420665652016
复制代码
2、
  1. >>> a = "wtf">>> b = "wtf">>> a is bTrue>>> a = "wtf!">>> b = "wtf!">>> a is bFalse>>> a, b = "wtf!", "wtf!">>> a is b # 仅适用于3.7版本以下, 3.7以后的返回结果为False.True
复制代码
3、
  1. >>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'True>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'False
复制代码

说明:
  • 这些行为是由于 Cpython 在编译优化时, 某些情况下会尝试使用已经存在的不可变对象而不是每次都创建一个新对象. (这种行为被称作字符串的驻留[string interning])
  • 发生驻留之后, 许多变量可能指向内存中的相同字符串对象. (从而节省内存)
  • 在上面的代码中, 字符串是隐式驻留的. 何时发生隐式驻留则取决于具体的实现. 这里有一些方法可以用来猜测字符串是否会被驻留:
    • 所有长度为 0 和长度为 1 的字符串都被驻留.
    • 字符串在编译时被实现 (
      1. 'wtf'
      复制代码
      将被驻留, 但是
      1. ''.join(['w', 't', 'f']
      复制代码
      将不会被驻留)
    • 字符串中只包含字母,数字或下划线时将会驻留. 所以
      1. 'wtf!'
      复制代码
      由于包含
      1. !
      复制代码
      而未被驻留. 可以在这里找到 CPython 对此规则的实现.


  • 当在同一行将
    1. a
    复制代码
    1. b
    复制代码
    的值设置为
    1. "wtf!"
    复制代码
    的时候, Python 解释器会创建一个新对象, 然后同时引用第二个变量(译: 仅适用于3.7以下, 详细情况请看这里). 如果你在不同的行上进行赋值操作, 它就不会“知道”已经有一个
    1. wtf!
    复制代码
    对象 (因为
    1. "wtf!"
    复制代码
    不是按照上面提到的方式被隐式驻留的). 它是一种编译器优化, 特别适用于交互式环境.
  • 常量折叠(constant folding) 是 Python 中的一种 窥孔优化(peephole optimization) 技术. 这意味着在编译时表达式
    1. 'a'*20
    复制代码
    会被替换为
    1. 'aaaaaaaaaaaaaaaaaaaa'
    复制代码
    以减少运行时的时钟周期. 只有长度小于 20 的字符串才会发生常量折叠. (为啥? 想象一下由于表达式
    1. 'a'*10**10
    复制代码
    而生成的
    1. .pyc
    复制代码
    文件的大小). 相关的源码实现
https://github.com/python/cpython/blob/3.6/Python/peephole.c#L288
[h2]Time for some hash brownies!/是时候来点蛋糕了![/h2]
  • hash brownie指一种含有大麻成分的蛋糕, 所以这里是句双关
1、
  1. some_dict = {}some_dict[5.5] = "Ruby"some_dict[5.0] = "JavaScript"some_dict[5] = "Python"
复制代码
Output:
  1. >>> some_dict[5.5]"Ruby">>> some_dict[5.0]"Python">>> some_dict[5]"Python"
复制代码
"Python" 消除了 "JavaScript" 的存在?

说明:
  • Python 字典通过检查键值是否相等和比较哈希值来确定两个键是否相同.
  • 具有相同值的不可变对象在Python中始终具有相同的哈希值.
  1. >>> 5 == 5.0True>>> hash(5) == hash(5.0)True
复制代码
  • 注意: 具有不同值的对象也可能具有相同的哈希值(哈希冲突).
  • 当执行
    1. some_dict[5] = "Python"
    复制代码
    语句时, 因为Python将
    1. 5
    复制代码
    1. 5.0
    复制代码
    识别为
    1. some_dict
    复制代码
    的同一个键, 所以已有值 "JavaScript" 就被 "Python" 覆盖了.
  • 这个 StackOverflow的 回答 漂亮的解释了这背后的基本原理.
https://stackoverflow.com/questions/32209155/why-can-a-floating-point-dictionary-key-overwrite-an-integer-key-with-the-same-v
[h2]Return return everywhere!/到处返回![/h2]
  1. def some_func():    try:        return 'from_try'     finally:        return 'from_finally'
复制代码
Output:
  1. >>> some_func()'from_finally'
复制代码
[h3]
说明:[/h3]
  • 当在 "try...finally" 语句的
    1. try
    复制代码
    中执行
    1. return
    复制代码
    ,
    1. break
    复制代码
    1. continue
    复制代码
    后,
    1. finally
    复制代码
    子句依然会执行.
  • 函数的返回值由最后执行的
    1. return
    复制代码
    语句决定. 由于
    1. finally
    复制代码
    子句一定会执行, 所以
    1. finally
    复制代码
    子句中的
    1. return
    复制代码
    将始终是最后执行的语句.
[h2] Deep down, we're all the same./本质上,我们都一样. [/h2]
  1. class WTF:  pass
复制代码
Output:
  1. >>> WTF() == WTF() # 两个不同的对象应该不相等False>>> WTF() is WTF() # 也不相同False>>> hash(WTF()) == hash(WTF()) # 哈希值也应该不同True>>> id(WTF()) == id(WTF())True
复制代码
[h3]
说明:[/h3]
  • 当调用
    1. id
    复制代码
    函数时, Python 创建了一个
    1. WTF
    复制代码
    类的对象并传给
    1. id
    复制代码
    函数. 然后
    1. id
    复制代码
    函数获取其id值 (也就是内存地址), 然后丢弃该对象. 该对象就被销毁了.
  • 当我们连续两次进行这个操作时, Python会将相同的内存地址分配给第二个对象. 因为 (在CPython中)
    1. id
    复制代码
    函数使用对象的内存地址作为对象的id值, 所以两个对象的id值是相同的.
  • 综上, 对象的id值仅仅在对象的生命周期内唯一. 在对象被销毁之后, 或被创建之前, 其他对象可以具有相同的id值.
  • 那为什么
    1. is
    复制代码
    操作的结果为
    1. False
    复制代码
    呢? 让我们看看这段代码.
  1. class WTF(object):  def __init__(self): print("I")  def __del__(self): print("D")
复制代码
Output:
  1. >>> WTF() is WTF()IIDDFalse>>> id(WTF()) == id(WTF())IDIDTrue
复制代码
       正如你所看到的, 对象销毁的顺序是造成所有不同之处的原因.
[h2]For what?/为什么?[/h2]
  1. some_string = "wtf"some_dict = {}for i, some_dict[i] in enumerate(some_string):    pass
复制代码
Output:
  1. >>> some_dict # 创建了索引字典.{0: 'w', 1: 't', 2: 'f'}
复制代码
[h3]
说明:[/h3]
  • Python 语法 中对
    1. for
    复制代码
    的定义是:
    1. for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
    复制代码
    其中
    1. exprlist
    复制代码
    指分配目标. 这意味着对可迭代对象中的每一项都会执行类似
    1. {exprlist} = {next_value}
    复制代码
    的操作.
    一个有趣的例子说明了这一点:
    1. for i in range(4):    print(i)    i = 10
    复制代码
    Output:
    1. 0123
    复制代码
    你可曾觉得这个循环只会运行一次?

    说明:
    • 由于循环在Python中工作方式, 赋值语句
      1. i = 10
      复制代码
      并不会影响迭代循环, 在每次迭代开始之前, 迭代器(这里指
      1. range(4)
      复制代码
      ) 生成的下一个元素就被解包并赋值给目标列表的变量(这里指
      1. i
      复制代码
      )了.
  • 在每一次的迭代中,
    1. enumerate(some_string)
    复制代码
    函数就生成一个新值
    1. i
    复制代码
    (计数器增加) 并从
    1. some_string
    复制代码
    中获取一个字符. 然后将字典
    1. some_dict
    复制代码
    1. i
    复制代码
    (刚刚分配的) 的值设为该字符. 本例中循环的展开可以简化为:
    1. >>> i, some_dict[i] = (0, 'w')>>> i, some_dict[i] = (1, 't')>>> i, some_dict[i] = (2, 'f')>>> some_dict
    复制代码
[h2] Evaluation time discrepancy/执行时机差异[/h2]1、
  1. array = [1, 8, 15]g = (x for x in array if array.count(x) > 0)array = [2, 8, 22]
复制代码
Output:
  1. >>> print(list(g))[8]
复制代码
2、
  1. array_1 = [1,2,3,4]g1 = (x for x in array_1)array_1 = [1,2,3,4,5]array_2 = [1,2,3,4]g2 = (x for x in array_2)array_2[:] = [1,2,3,4,5]
复制代码
Output:
  1. >>> print(list(g1))[1,2,3,4]>>> print(list(g2))[1,2,3,4,5]
复制代码
[h3][/h3]
说明:
  • 在生成器表达式中,
    1. in
    复制代码
    子句在声明时执行, 而条件子句则是在运行时执行.
  • 所以在运行前,
    1. array
    复制代码
    已经被重新赋值为
    1. [2, 8, 22]
    复制代码
    , 因此对于之前的
    1. 1
    复制代码
    ,
    1. 8
    复制代码
    1. 15
    复制代码
    , 只有
    1. count(8)
    复制代码
    的结果是大于
    1. 0
    复制代码
    的, 所以生成器只会生成
    1. 8
    复制代码
    .
  • 第二部分中
    1. g1
    复制代码
    1. g2
    复制代码
    的输出差异则是由于变量
    1. array_1
    复制代码
    1. array_2
    复制代码
    被重新赋值的方式导致的.
  • 在第一种情况下,
    1. array_1
    复制代码
    被绑定到新对象
    1. [1,2,3,4,5]
    复制代码
    , 因为
    1. in
    复制代码
    子句是在声明时被执行的, 所以它仍然引用旧对象
    1. [1,2,3,4]
    复制代码
    (并没有被销毁).
  • 在第二种情况下, 对
    1. array_2
    复制代码
    的切片赋值将相同的旧对象
    1. [1,2,3,4]
    复制代码
    原地更新为
    1. [1,2,3,4,5]
    复制代码
    . 因此
    1. g2
    复制代码
    1. array_2
    复制代码
    仍然引用同一个对象(这个对象现在已经更新为
    1. [1,2,3,4,5]
    复制代码
    ).

本文内容来自中文版项目,项目全文2万多字,以及海量代码。
因为篇幅原因,本文就只为大家展示这6个案例了,更多案例大家可以在项目中查看。
英文版项目名称:wtfpython
链接:https://github.com/satwikkansal/wtfpython
中文项目名称:wtfpython-cn
链接:https://github.com/leisurelicht/wtfpython-cn

(完)


看完本文有收获?请转发分享给更多人
关注「Python那些事」,做全栈开发工程师



点「在看」的人都变好看了哦
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:100
帖子:20
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP