你对Python一无所知!

204 阅读9分钟

    Python语法简洁精炼,很容易上手,我们也误以为很容易掌握,看完这篇文章后你恐怕觉得自己一无所知。


> 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的 回答 漂亮的解释了这背后的基本原理.



> 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

    正如你所看到的, 对象销毁的顺序是造成所有不同之处的原因.

> is is not what it is!/出人意料的is!

下面是一个在互联网上非常有名的例子.

>>> a = 256

>>> b = 256

>>> a is b

True

>>> a = 257

>>> b = 257

>>> a is b

False

>>> a = 257; b = 257

>>> a is b

True

💡 说明:

is 和 == 的区别

  • is 运算符检查两个运算对象是否引用自同一对象 (即, 它检查两个运算对象是否相同).

  • == 运算符比较两个运算对象的值是否相等.

  • 因此 is 代表引用相同, == 代表值相等. 下面的例子可以很好的说明这点,

    >>> [] == []

    True

    >>> [] is [] # 这两个空列表位于不同的内存地址.

    False

256 是一个已经存在的对象, 而 257 不是

当你启动Python 的时候, 数值为 -5 到 256 的对象就已经被分配好了. 这些数字因为经常被使用, 所以会被提前准备好.

Python 通过这种创建小整数池的方式来避免小整数频繁的申请和销毁内存空间.

引用自 https://docs.python.org/3/c-api/long.html

当前的实现为-5到256之间的所有整数保留一个整数对象数组, 当你创建了一个该范围内的整数时, 你只需要返回现有对象的引用. 所以改变1的值是有可能的. 我怀疑这种行为在Python中是未定义行为. :-)

>>> id(256)

10922528

>>> a = 256

>>> b = 256

>>> id(a)

10922528

>>> id(b)

10922528

>>> id(257)

140084850247312

>>> x = 257

>>> y = 257

>>> id(x)

140084850247440

>>> id(y)

140084850247344

这里解释器并没有智能到能在执行 y = 257 时意识到我们已经创建了一个整数 257, 所以它在内存中又新建了另一个对象.

当 a 和 b 在同一行中使用相同的值初始化时,会指向同一个对象.

>>> a, b = 257, 257

>>> id(a)

140640774013296

>>> id(b)

140640774013296

>>> a = 257

>>> b = 257

>>> id(a)

140640774013392

>>> id(b)

140640774013488

  • 当 a 和 b 在同一行中被设置为 257 时, Python 解释器会创建一个新对象, 然后同时引用第二个变量. 如果你在不同的行上进行, 它就不会 "知道" 已经存在一个 257 对象了.

  • 这是一种特别为交互式环境做的编译器优化. 当你在实时解释器中输入两行的时候, 他们会单独编译, 因此也会单独进行优化. 如果你在 .py 文件中尝试这个例子, 则不会看到相同的行为, 因为文件是一次性编译的.

> Let's see if you can guess this?/看看你能否猜到这一点?

a, b = a[b] = {}, 5

Output:

>>> a

{5: ({...}, 5)}

💡 说明:

  • 根据 Python 语言参考, 赋值语句的形式如下

    (target_list "=")+ (expression_list | yield_expression)

    赋值语句计算表达式列表(expression list)(牢记 这可以是单个表达式或以逗号分隔的列表, 后者返回元组)并将单个结果对象从左到右分配给目标列表中的每一项.

  • (target_list "=")+ 中的 + 意味着可以有一个或多个目标列表. 在这个例子中, 目标列表是 a, b 和 a[b] (注意表达式列表只能有一个, 在我们的例子中是 {}, 5).

  • 表达式列表计算结束后, 将其值自动解包后从左到右分配给目标列表(target list). 因此, 在我们的例子中, 首先将 {}, 5 元组并赋值给 a, b, 然后我们就可以得到 a = {} 且 b = 5.

  • a 被赋值的 {} 是可变对象.

  • 第二个目标列表是 a[b] (你可能觉得这里会报错, 因为在之前的语句中 a 和 b 都还没有被定义. 但是别忘了, 我们刚刚将 a 赋值 {} 且将 b 赋值为 5).

  • 现在, 我们将通过将字典中键 5 的值设置为元组 ({}, 5) 来创建循环引用 (输出中的 {...} 指与 a 引用了相同的对象). 下面是一个更简单的循环引用的例子

    >>> some_list = some_list[0] = [0]

    >>> some_list

    [[...]]

    >>> some_list[0]

    [[...]]

    >>> some_list is some_list[0]

    True

    >>> some_list[0][0][0][0][0][0] == some_list

    True

    我们的例子就是这种情况 (a[b][0] 与 a 是相同的对象)

  • 总结一下, 你也可以把例子拆成

    a, b = {}, 5

    a[b] = a, b

    并且可以通过 a[b][0] 与 a 是相同的对象来证明是循环引用

    >>> a[b][0] is a

    True


本文大量内容来自于: https://github.com/leisurelicht/wtfpython-cn


大家记得关注微信公众号支持哦~