当前位置: 美高梅棋牌 > 智能家电 > 正文

Python:What the f*ck Python(上)

时间:2019-09-24 16:46来源:智能家电
GitHub 上有一个名为《What the f*ckPython!》的项目,这个有趣的项目意在收集 Python中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理! 原

GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理!
原版地址:

最近,一位名为“暮晨”的贡献者将其翻译成了中文。
中文版地址:

GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理!
原版地址:
最近,一位名为“暮晨”的贡献者将其翻译成了中文。
中文版地址:

我将所有代码都亲自试过了,加入了一些自己的理解和例子,所以会和原文稍有不同。

上一篇 Python:What the f*ck Python

1. 字符串驻留

>>> a = '!'>>> b = '!'>>> a is bTrue

>>> a = 'some_string'>>> id140420665652016>>> id('some' + '_' + 'string') # 注意两个的id值是相同的.140420665652016

>>> a = 'wtf'>>> b = 'wtf'>>> a is bTrue>>> a = 'wtf!'>>> b = 'wtf!'>>> a is bFalse>>> a, b = 'wtf!', 'wtf!'>>> a is bTrue

>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'True>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'False

说明:
这些行为是由于 CPython 在编译优化时,某些情况下会尝试使用已经存在的不可变对象而不是每次都创建一个新对象。这种行为被称作字符串的驻留 string interning。发生驻留之后, 许多变量可能指向内存中的相同字符串对象从而节省内存。

有一些方法可以用来猜测字符串是否会被驻留:

  • 所有长度为 0 和长度为 1 的字符串都被驻留
  • 字符串在编译时被实现('wtf' 将被驻留,但是 ''.join(['w', 't', 'f'] 将不会被驻留)
  • 字符串中只包含字母、数字或下划线时将会驻留,所以 'wtf!' 由于包含 '!' 而未被驻留
  • 当在同一行将 ab 的值设置为 'wtf!' 的时候,Python 解释器会创建一个新对象,然后两个变量同时指向这个对象。如果你在不同的行上进行赋值操作,它就不会“知道”已经有一个 'wtf!' 对象(因为 'wtf!' 不是按照上面提到的方式被隐式驻留的)。
  • 常量折叠(constant folding)是 Python 中的一种窥孔优化(peephole optimization)技术。这意味着在编译时表达式 'a' * 20 会被替换为 'aaaaaaaaaaaaaaaaaaaa' 以减少运行时的时钟周期。只有长度小于 20 的字符串才会发生常量折叠。(为啥?想象一下由于表达式 'a' * 10 ** 10 而生成的 .pyc 文件的大小)。

如果你在 .py 文件中尝试这个例子,则不会看到相同的行为,因为文件是一次性编译的。

原本每个的标题都是原版中的英文,有些取名比较奇怪,不直观,我换成了可以描述主题的中文形式,有些是自己想的,不足之处请指正。另外一些 Python 中的彩蛋被我去掉了。

2. 字典的键

>>> some_dict = {}>>> some_dict[5.5] = "Ruby">>> some_dict[5.0] = "JavaScript">>> some_dict[5] = "Python">>> some_dict[5.5]"Ruby">>> some_dict[5.0]"Python">>> some_dict[5]"Python"

说明:
Python 字典检查键值是否相等是通过比较哈希值是否相等来确定的。如果两个对象在比较的时候是相等的,那它们的散列值必须相等,否则散列表就不能正常运行了。例如,如果 1 == 1.0 为真,那么 hash == hash 必须也为真,但其实两个数字的内部结构是完全不一样的。

我将所有代码都亲自试过了,加入了一些自己的理解和例子,所以会和原文稍有不同。

3. finally 子句中的 return

def some_func():    try:        return 'from_try'    finally:        return 'from_finally'

Output:

>>> some_func()'from_finally'

说明:
函数的返回值由最后执行的 return 语句决定。由于 finally 子句一定会执行,所以 finally 子句中的 return 将始终是最后执行的语句。

21. 子类关系

>>> from collections import Hashable>>> issubclass(list, object)True>>> issubclass(object, Hashable)True>>> issubclass(list, Hashable)False

子类关系应该是可传递的,对吧?即,如果 AB 的子类,BC 的子类,那么 A 应该 是 C 的子类。
说明:

  • Python 中的子类关系并不必须是传递的,任何人都可以在元类中随意定义 __subclasscheck__
  • issubclass(cls, Hashable) 被调用时,它只是在 cls 中寻找 __hash__() 方法或继承自 __hash__() 的方法。
  • 由于 object 是可散列的,而 list 是不可散列的,所以它打破了这种传递关系。

4. 同一个对象

class WTF:    pass

Output:

>>> WTF() == WTF() # 两个不同的对象应该不相等False>>> WTF() is WTF() # 也不相同False>>> hash == hash # 哈希值也应该不同True>>> id == idTrue

说明:
当调用 id() 函数时,Python 创建了一个 WTF 类的对象并传给 id() 函数,然后 id() 函数获取其 id 值,然后丢弃该对象,该对象就被销毁了。

当我们连续两次进行这个操作时,Python会将相同的内存地址分配给第二个对象,因为在 CPythonid() 函数使用对象的内存地址作为对象的 id 值,所以两个对象的 id 值是相同的。

综上,对象的 id 值仅仅在对象的生命周期内唯一,在对象被销毁之后或被创建之前,其他对象可以具有相同的 id 值。

class WTF:  def __init__: print("I")  def __del__: print("D")

Output:

>>> WTF() is WTF()IIDDFalse>>> id == idIDIDTrue

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

22. 神秘的键型转换

class SomeClass:    passsome_dict = {'s': 42}

Output:

>>> type(list(some_dict.keys<class 'str'>>>> s = SomeClass('s')>>> some_dict[s] = 40>>> some_dict # 预期: 两个不同的键值对{'s': 40}>>> type(list(some_dict.keys<class 'str'>

说明:

  • 由于 SomeClass 会从 str 自动继承 __hash__() 方法,所以 s 对象和 's' 字符串的哈希值是相同的。
  • SomeClass == 's'True 是因为 SomeClass 也继承了 str__eq__() 方法。
  • 由于两者的哈希值相同且相等,所以它们在字典中表示相同的键。

如果想要实现期望的功能, 我们可以重定义 SomeClass__eq__() 方法.

class SomeClass:  def __eq__(self, other):      return (          type is SomeClass          and type is SomeClass          and super().__eq__      )  # 当我们自定义 __eq__() 方法时, Python 不会再自动继承 __hash__() 方法  # 所以我们也需要定义它  __hash__ = str.__hash__some_dict = {'s':42}

Output:

>>> s = SomeClass('s')>>> some_dict[s] = 40>>> some_dict{'s': 40, 's': 42}>>> keys = list(some_dict.keys>>> type, type<class 'str'> <class '__main__.SomeClass'>

5. for 循环分配目标赋值

>>> some_string = "wtf">>> some_dict = {}>>> for i, some_dict[i] in enumerate(some_string): pass>>> some_dict{0: 'w', 1: 't', 2: 'f'}

说明:
这一条仔细看一下很好理解,for 循环每次迭代都会给分配目标赋值,some_dict[i] = value 就相当于给字典添加键值对了。
有趣的是下面这个例子,你可曾觉得这个循环只会运行一次?

for i in range:    print    i = 10

23. 链式赋值表达式

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

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

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

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

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

这话看着非常的晦涩,我们来看一个简单的例子:

a, b = b, c = 1, 2print

Output:

1 1 2

在这个简单的例子中,目标列表是 a, bb, c,表达式是 1, 2。将表达式从左到右赋给目标列表,上述例子就可以拆分成:

a, b = 1, 2b, c = 1, 2

所以结果就是 1 1 2

那么,原例子就不难理解了,拆解开来就是:

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

这里不能写作 a[b] = {}, 5,因为这样第一句中的 {} 和第二句中的 {} 其实就是不同的对象了,而实际他们是同一个对象。这就形成了循环引用,输出中的 {...} 指与 a 引用了相同的对象。

我们来验证一下:

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

可见确实是同一个对象。

以下是一个简单的循环引用的例子:

>>> 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_listTrue

6. 执行时机差异

>>> array = [1, 8, 15]>>> g = (x for x in array if array.count > 0)>>> array = [2, 8, 22]>>> list[8]

>>> 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]>>> list[1, 2, 3, 4]>>> list[1, 2, 3, 4, 5]

说明:
在生成器表达式中 in 子句在声明时执行,而条件子句则是在运行时执行。
①中,在运行前 array 已经被重新赋值为 [2, 8, 22],因此对于之前的 1, 8, 15,只有 count 的结果是大于 0 ,所以生成器只会生成 8。
②中,g1g2 的输出差异则是由于变量 array_1array_2 被重新赋值的方式导致的。

  • 在第一种情况下,array_1 被绑定到新对象 [1, 2, 3, 4, 5],因为 in 子句是在声明时被执行的,所以它仍然引用旧对象 [1, 2, 3, 4]
  • 在第二种情况下,对 array_2 的切片赋值将相同的旧对象 [1, 2, 3, 4] 原地更新为 [1, 2, 3, 4, 5]。因此 g2 和 array_2 仍然引用同一个对象[1, 2, 3, 4, 5]

24. 空间移动

import numpy as npdef energy_send:    # 初始化一个 numpy 数组    np.array])def energy_receive():    # 返回一个空的 numpy 数组    return np.empty, dtype=np.float).tolist()

Output:

>>> energy_send>>> energy_receive()123.456

说明:
energy_send() 函数中创建的 numpy 数组并没有返回,因此内存空间被释放并可以被重新分配。
numpy.empty() 直接返回下一段空闲内存,而不重新初始化。而这个内存点恰好就是刚刚释放的那个(通常情况下,并不绝对)。

7. 整数的预分配

>>> a = 256>>> b = 256>>> a is bTrue>>> a = 257>>> b = 257>>> a is bFalse>>> a = 257; b = 257>>> a is bTrue

25. 不要混用制表符和空格

tab 是 8 个空格,而用空格表示则一个缩进是 4 个空格,混用就会出错。python3 里直接不允许这种行为了,会报错:

TabError: inconsistent use of tabs and spaces in indentation

很多编辑器,例如 pycharm,可以直接设置 tab 表示 4 个空格。
图片 1

is 和 == 的区别

  • is 运算符检查两个运算对象是否引用自同一对象
  • == 运算符比较两个运算对象的值是否相等

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

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

26. 迭代字典时的修改

x = {0: None}for i in x:    del x[i]    x[i+1] = None    print

Output(Python 2.7- Python 3.5):

01234567

说明:
Python 不支持 对字典进行迭代的同时修改它,它之所以运行 8 次,是因为字典会自动扩容以容纳更多键值(译: 应该是因为字典的初始最小值是8,扩容会导致散列表地址发生变化而中断循环)。
在不同的 Python 实现中删除键的处理方式以及调整大小的时间可能会有所不同,python3.6 开始,到 5 就会扩容。

而在 list 中,这种情况是允许的,listdict 的实现方式是不一样的,list 虽然也有扩容,但 list 的扩容是整体搬迁,并且顺序不变。

list = [1]j = 0for i in list:    print    list.append

这个代码可以一直运行下去直到 int 越界。但一般不建议在迭代的同时修改 list

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

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

当前的实现为 -5 到 256 之间的所有整数保留一个整数对象数组,当你创建了一个该范围内的整数时,你只需要返回现有对象的引用。所以改变 1 的值是有可能的。

但是,当 ab 在同一行中使用相同的值初始化时,会指向同一个对象。

>>> id10922528>>> a = 256>>> b = 256>>> id10922528>>> id10922528>>> id140084850247312>>> x = 257>>> y = 257>>> id140084850247440>>> id140084850247344>>> a, b = 257, 257>>> id140640774013296>>> id140640774013296

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

27. __del__

class SomeClass:    def __del__:        print("Deleted!")

Output:

>>> x = SomeClass()>>> y = x>>> del x  # 这里应该会输出 "Deleted!">>> del yDeleted!

说明:
del x 并不会立刻调用x.__del__(),每当遇到del xPython 会将 x 的引用数减 1,当 x 的引用数减到 0 时就会调用x.__del__()

我们再加一点变化:

>>> x = SomeClass()>>> y = x>>> del x>>> y  # 检查一下y是否存在<__main__.SomeClass instance at 0x7f98a1a67fc8>>>> del y # 像之前一样,这里应该会输出 "Deleted!">>> globals() # 好吧, 并没有。让我们看一下所有的全局变量Deleted!{'__builtins__': <module '__builtin__' >, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__package__': None, '__name__': '__main__', '__doc__': None}

y.__del__()之所以未被调用,是因为前一条语句(>>> y)对同一对象创建了另一个引用,从而防止在执行del y后对象的引用数变为 0。(这其实是 Python 交互解释器的特性,它会自动让 _ 保存上一个表达式输出的值。)
调用globals()导致引用被销毁,因此我们可以看到 Deleted! 终于被输出了。

8. 容易疏忽的引用类型赋值

>>> row = [''] * 3>>> board = [row] * 3>>> board[['', '', ''], ['', '', ''], ['', '', '']]>>> board[0]['', '', '']>>> board[0][0]''>>> board[0][0] = "X">>> board[['X', '', ''], ['X', '', ''], ['X', '', '']]

说明:
我们来输出 id 看下:

>>> id7536232>>> id5143216>>> id5143216>>> id7416840>>> id7416840>>> id7416840

row 是一个 list,其中三个元素都指向地址 5143216,当对 board[0][0] 进行赋值以后,row 的第一个元素指向 7536232。而 board 中的三个元素都指向 rowrow 的地址并没有改变。

我们可以通过不使用变量 row 生成 board 来避免这种情况。

>>> board = [[''] * 3 for _ in range]>>> board[0][0] = "X">>> board[['X', '', ''], ['', '', ''], ['', '', '']]

这里用了推导式,每次迭代都会生成一个新的 _ ,所以 board 中三个元素指向的是不同的变量。

28. 迭代列表时删除元素

在前面我附加了一个迭代列表时添加元素的例子,现在来看看迭代列表时删除元素。

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:    del itemfor idx, item in enumerate:    list_2.removefor idx, item in enumerate(list_3[:]):    list_3.removefor idx, item in enumerate:    list_4.pop

Output:

>>> list_1[1, 2, 3, 4]>>> list_2[2, 4]>>> list_3[]>>> list_4[2, 4]

说明:
在迭代时修改对象是一个很愚蠢的主意,正确的做法是迭代对象的副本,list_3[:]就是这么做的。

编辑:智能家电 本文来源:Python:What the f*ck Python(上)

关键词: