内容简介:GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理! 原版地址:上一篇原本每个的标题都是原版中的英文,有些取名比较奇怪,不直观,我换成了可以描述主题的中文形式,有些是自己想的,不足之处请指正。另外一些
GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理! 原版地址: github.com/satwikkansa… 。 最近,一位名为“暮晨”的贡献者将其翻译成了中文。 中文版地址: github.com/leisurelich…
原本每个的标题都是原版中的英文,有些取名比较奇怪,不直观,我换成了可以描述主题的中文形式,有些是自己想的,不足之处请指正。另外一些 Python 中的彩蛋被我去掉了。
我将所有代码都亲自试过了,加入了一些自己的理解和例子,所以会和原文稍有不同。
21. 子类关系
>>> from collections import Hashable >>> issubclass(list, object) True >>> issubclass(object, Hashable) True >>> issubclass(list, Hashable) False 复制代码
子类关系应该是可传递的,对吧?即,如果 A
是 B
的子类, B
是 C
的子类,那么 A
应该 是 C
的子类。 说明:
- Python 中的子类关系并不必须是传递的,任何人都可以在元类中随意定义
__subclasscheck__
。 - 当
issubclass(cls, Hashable)
被调用时,它只是在cls
中寻找__hash__()
方法或继承自__hash__()
的方法。 - 由于
object
是可散列的(hashable),而list
是不可散列的,所以它打破了这种传递关系。
22. 神秘的键型转换
class SomeClass(str): pass some_dict = {'s': 42} 复制代码
Output:
>>> type(list(some_dict.keys())[0]) <class 'str'> >>> s = SomeClass('s') >>> some_dict[s] = 40 >>> some_dict # 预期: 两个不同的键值对 {'s': 40} >>> type(list(some_dict.keys())[0]) <class 'str'> 复制代码
说明:
- 由于
SomeClass
会从str
自动继承__hash__()
方法,所以s
对象和's'
字符串的哈希值是相同的。 - 而
SomeClass('s') == 's'
为True
是因为SomeClass
也继承了str
类__eq__()
方法。 - 由于两者的哈希值相同且相等,所以它们在字典中表示相同的键。
如果想要实现期望的功能, 我们可以重定义 SomeClass
的 __eq__()
方法.
class SomeClass(str): def __eq__(self, other): return ( type(self) is SomeClass and type(other) is SomeClass and super().__eq__(other) ) # 当我们自定义 __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(keys[0]), type(keys[1]) <class 'str'> <class '__main__.SomeClass'> 复制代码
23. 链式赋值表达式
>>> a, b = a[b] = {}, 5 >>> a {5: ({...}, 5)} 复制代码
说明:根据Python 语言参考,赋值语句的形式如下:
(target_list "=")+ (expression_list | yield_expression) 复制代码
赋值语句计算表达式列表( expression list )(请记住,这可以是单个表达式或以逗号分隔的列表,后者返回元组)并将单个结果对象从左到右分配给目标列表中的每一项。
(target_list "=")+
中的 +
意味着可以有一个或多个目标列表。在这个例子中,目标列表是 a, b
和 a[b]
。表达式列表只能有一个,是 {}, 5
。
这话看着非常的晦涩,我们来看一个简单的例子:
a, b = b, c = 1, 2 print(a, b, c) 复制代码
Output:
1 1 2 复制代码
在这个简单的例子中,目标列表是 a, b
和 b, c
,表达式是 1, 2
。将表达式从左到右赋给目标列表,上述例子就可以拆分成:
a, b = 1, 2 b, c = 1, 2 复制代码
所以结果就是 1 1 2
。
那么,原例子就不难理解了,拆解开来就是:
a, b = {}, 5 a[b] = a, b 复制代码
这里不能写作 a[b] = {}, 5
,因为这样第一句中的 {}
和第二句中的 {}
其实就是不同的对象了,而实际他们是同一个对象。这就形成了循环引用,输出中的 {...}
指与 a
引用了相同的对象。
我们来验证一下:
>>> a[b][0] is a True 复制代码
可见确实是同一个对象。
以下是一个简单的循环引用的例子:
>>> 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 复制代码
24. 空间移动
import numpy as np def energy_send(x): # 初始化一个 numpy 数组 np.array([float(x)]) def energy_receive(): # 返回一个空的 numpy 数组 return np.empty((), dtype=np.float).tolist() 复制代码
Output:
>>> energy_send(123.456) >>> energy_receive() 123.456 复制代码
说明: energy_send()
函数中创建的 numpy
数组并没有返回,因此内存空间被释放并可以被重新分配。 numpy.empty()
直接返回下一段空闲内存,而不重新初始化。而这个内存点恰好就是刚刚释放的那个(通常情况下,并不绝对)。
25. 不要混用制表符(tab)和空格(space)
tab 是 8 个空格,而用空格表示则一个缩进是 4 个空格,混用就会出错。 python3 里直接不允许这种行为了,会报错:
TabError: inconsistent use of tabs and spaces in indentation
很多编辑器,例如 pycharm ,可以直接设置 tab 表示 4 个空格。
26. 迭代字典时的修改
x = {0: None} for i in x: del x[i] x[i+1] = None print(i) 复制代码
Output(Python 2.7- Python 3.5):
0 1 2 3 4 5 6 7 复制代码
说明: Python 不支持 对字典进行迭代的同时修改它,它之所以运行 8 次,是因为字典会自动扩容以容纳更多键值(译: 应该是因为字典的初始最小值是8,扩容会导致散列表地址发生变化而中断循环)。 在不同的 Python 实现中删除键的处理方式以及调整大小的时间可能会有所不同, python3.6 开始,到 5 就会扩容。
而在 list
中,这种情况是允许的, list
和 dict
的实现方式是不一样的, list
虽然也有扩容,但 list
的扩容是整体搬迁,并且顺序不变。
list = [1] j = 0 for i in list: print(i) list.append(i + 1) 复制代码
这个代码可以一直运行下去直到 int
越界。但一般不建议在迭代的同时修改 list
。
27. _ del _
class SomeClass: def __del__(self): print("Deleted!") 复制代码
Output:
>>> x = SomeClass() >>> y = x >>> del x # 这里应该会输出 "Deleted!" >>> del y Deleted! 复制代码
说明: del x
并不会立刻调用 x.__del__()
,每当遇到 del x
, Python 会将 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__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__package__': None, '__name__': '__main__', '__doc__': None} 复制代码
y.__del__()
之所以未被调用,是因为前一条语句( >>> y
)对同一对象创建了另一个引用,从而防止在执行 del y
后对象的引用数变为 0。(这其实是 Python 交互解释器的特性,它会自动让 _
保存上一个表达式输出的值。) 调用 globals()
导致引用被销毁,因此我们可以看到 Deleted!
终于被输出了。
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(list_1): del item for idx, item in enumerate(list_2): list_2.remove(item) for idx, item in enumerate(list_3[:]): list_3.remove(item) for idx, item in enumerate(list_4): list_4.pop(idx) 复制代码
Output:
>>> list_1 [1, 2, 3, 4] >>> list_2 [2, 4] >>> list_3 [] >>> list_4 [2, 4] 复制代码
说明:在迭代时修改对象是一个很愚蠢的主意,正确的做法是迭代对象的副本, list_3[:]
就是这么做的。
del、remove、pop 的不同:
-
del var_name
只是从本地或全局命名空间中删除了 var_name(这就是为什么list_1
没有受到影响)。 -
remove
会删除第一个匹配到的指定值,而不是特定的索引,如果找不到值则抛出ValueError
异常。 -
pop
则会删除指定索引处的元素并返回它,如果指定了无效的索引则抛出IndexError
异常。
为什么输出是 [2, 4]?列表迭代是按索引进行的,所以当我们从 list_2
或 list_4
中删除 1 时,列表的内容就变成了 [2, 3, 4]
。剩余元素会依次位移,也就是说, 2
的索引会变为 0, 3
会变为 1。由于下一次迭代将获取索引为 1 的元素(即 3
), 因此 2
将被彻底的跳过。类似的情况会交替发生在列表中的每个元素上。
29. 循环变量泄漏!
①
for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global') 复制代码
Output:
6 : for x inside loop 6 : x in global 复制代码
②
# 这次我们先初始化x x = -1 for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global') 复制代码
Output:
6 : for x inside loop 6 : x in global 复制代码
③
x = 1 print([x for x in range(5)]) print(x, ': x in global') 复制代码
Output(Python 2):
[0, 1, 2, 3, 4] (4, ': x in global') 复制代码
Output(Python 3):
[0, 1, 2, 3, 4] 1 : x in global 复制代码
说明:在 Python 中, for
循环使用所在作用域并在结束后保留定义的循环变量。如果我们曾在全局命名空间中定义过循环变量,它会重新绑定现有变量。 Python 2.x 和 Python 3.x 解释器在列表推导式示例中的输出差异,在文档 What’s New In Python 3.0 中可以找到相关的解释:
"列表推导不再支持句法形式 [... for var in item1, item2, ...]
。使用 [... for var in (item1, item2, ...)]
代替。另外注意,列表推导具有不同的语义:它们更接近于 list()
构造函数中生成器表达式的语法糖,特别是循环控制变量不再泄漏到周围的作用域中。"
简单来说,就是 python2 中,列表推导式依然存在循环控制变量泄露,而 python3 中不存在。
30. 当心默认的可变参数!
def some_func(default_arg=[]): default_arg.append("some_string") return default_arg 复制代码
Output:
>>> some_func() ['some_string'] >>> some_func() ['some_string', 'some_string'] >>> some_func([]) ['some_string'] >>> some_func() ['some_string', 'some_string', 'some_string'] 复制代码
说明: Python 中函数的默认可变参数并不是每次调用该函数时都会被初始化。相反,它们会使用最近分配的值作为默认值。当我们明确的将 []
作为参数传递给 some_func
的时候,就不会使用 default_arg
的默认值, 所以函数会返回我们所期望的结果。
>>> some_func.__defaults__ # 这里会显示函数的默认参数的值 ([],) >>> some_func() >>> some_func.__defaults__ (['some_string'],) >>> some_func() >>> some_func.__defaults__ (['some_string', 'some_string'],) >>> some_func([]) >>> some_func.__defaults__ (['some_string', 'some_string'],) 复制代码
避免可变参数导致的错误的常见做法是将 None
指定为参数的默认值,然后检查是否有值传给对应的参数。例:
def some_func(default_arg=None): if not default_arg: default_arg = [] default_arg.append("some_string") return default_arg 复制代码
31. 捕获异常
这里讲的是 python2
some_list = [1, 2, 3] try: # 这里会抛出异常 ``IndexError`` print(some_list[4]) except IndexError, ValueError: print("Caught!") try: # 这里会抛出异常 ``ValueError`` some_list.remove(4) except IndexError, ValueError: print("Caught again!") 复制代码
Output:
Caught! ValueError: list.remove(x): x not in list 复制代码
说明:如果你想要同时捕获多个不同类型的异常时,你需要将它们用括号包成一个元组作为第一个参数传递。第二个参数是可选名称,如果你提供,它将与被捕获的异常实例绑定。 也就是说,代码原意是捕获 IndexError, ValueError
两种异常,但在 python2 中,必须写成 (IndexError, ValueError)
,示例中的写法解析器会将 ValueError
理解成绑定的异常实例名。 在 python3 中,不会有这种误解,因为必须使用 as
关键字。
32. +=就地修改
①
a = [1, 2, 3, 4] b = a a = a + [5, 6, 7, 8] 复制代码
Output:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4] 复制代码
②
a = [1, 2, 3, 4] b = a a += [5, 6, 7, 8] 复制代码
Output:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4, 5, 6, 7, 8] 复制代码
说明: a += b
并不总是与 a = a + b
表现相同。 表达式 a = a + [5,6,7,8]
会生成一个新列表,并让 a
引用这个新列表,同时保持 b
不变。 表达式 a += [5, 6, 7, 8]
实际上是使用的是 extend()
函数,就地修改列表,所以 a
和 b
仍然指向已被修改的同一列表。
33. 外部作用域变量
a = 1 def some_func(): return a def another_func(): a += 1 return a 复制代码
Output:
>>> some_func() 1 >>> another_func() UnboundLocalError: local variable 'a' referenced before assignment 复制代码
说明:当在函数中引用外部作用域的变量时,如果不对这个变量进行修改,则可以直接引用,如果要对其进行修改,则必须使用 global
关键字,否则解析器将认为这个变量是局部变量,而做修改之前并没有定义它,所以会报错。
def another_func() global a a += 1 return a 复制代码
Output:
>>> another_func() 2 复制代码
34. 小心链式操作
>>> (False == False) in [False] # 可以理解 False >>> False == (False in [False]) # 可以理解 False >>> False == False in [False] # 为毛? True >>> True is False == False False >>> False is False is False True >>> 1 > 0 < 1 True >>> (1 > 0) < 1 False >>> 1 > (0 < 1) False 复制代码
根据 docs.python.org/2/reference…
形式上,如果 a, b, c, ..., y, z 是表达式,而 op1, op2, ..., opN 是比较运算符,那么 a op1 b op2 c ... y opN z 就等于 a op1 b and b op2 c and ... y opN z,除了每个表达式最多被评估一次。
-
False == False in [False]
就相当于False == False and False in [False]
-
1 > 0 < 1
就相当于1 > 0 and 0 < 1
虽然上面的例子似乎很愚蠢,但是像 a == b == c
或 0 <= x <= 100
就很棒了。
35. 忽略类作用域的名称解析
① 生成器表达式
x = 5 class SomeClass: x = 17 y = (x for i in range(10)) 复制代码
Output:
>>> list(SomeClass.y) [5, 5, 5, 5, 5, 5, 5, 5, 5, 5] 复制代码
② 列表推导式
x = 5 class SomeClass: x = 17 y = [x for i in range(10)] 复制代码
Output(Python 2):
>>> SomeClass.y [17, 17, 17, 17, 17, 17, 17, 17, 17, 17] 复制代码
Output(Python 3):
>>> SomeClass.y [5, 5, 5, 5, 5, 5, 5, 5, 5, 5] 复制代码
说明:
- 类定义中嵌套的作用域会忽略类内的名称绑定。
- 生成器表达式有它自己的作用域。
- 从 Python 3 开始,列表推导式也有自己的作用域。
36. 元组
①
x, y = (0, 1) if True else None, None 复制代码
Output:
>>> x, y # 期望的结果是 (0, 1) ((0, 1), None) 复制代码
②
t = ('one', 'two') for i in t: print(i) t = ('one') for i in t: print(i) t = () print(t) 复制代码
Output:
one two o n e tuple() 复制代码
说明:
- 对于 1,正确的语句是
x, y = (0, 1) if True else (None, None)
。 - 对于 2,正确的语句是
t = ('one',)
或者t = 'one'
, (缺少逗号) 否则解释器会认为t
是一个字符串,并逐个字符对其进行迭代。 -
()
是一个特殊的标记,表示空元组。
37. else
① 循环末尾的 else
def does_exists_num(l, to_find): for num in l: if num == to_find: print("Exists!") break else: print("Does not exist") 复制代码
Output:
>>> some_list = [1, 2, 3, 4, 5] >>> does_exists_num(some_list, 4) Exists! >>> does_exists_num(some_list, -1) Does not exist 复制代码
② try 末尾的 else
try: pass except: print("Exception occurred!!!") else: print("Try block executed successfully...") 复制代码
Output:
Try block executed successfully... 复制代码
说明:循环后的 else
子句只会在循环执行完成(没有触发 break、return
语句)的情况下才会执行。 try
之后的 else
子句也被称为 "完成子句",因为在 try
语句中到达 else
子句意味着 try
块实际上已成功完成。
38. 名称改写
class Yo(object): def __init__(self): self.__honey = True self.bitch = True 复制代码
Output:
>>> Yo().bitch True >>> Yo().__honey AttributeError: 'Yo' object has no attribute '__honey' >>> Yo()._Yo__honey True 复制代码
说明: python 中不能像 Java 那样使用 private
修饰符创建私有属性。但是,解释器会通过给类中以 __(双下划线)开头且结尾最多只有一个下划线的类成员名称加上 __类名_
来修饰。这能避免子类意外覆盖父类的“私有”属性。
举个例子:有人编写了一个名为 Dog
的类,这个类的内部用到了 mood
实例属性,但是没有将其开放。现在,你创建了 Dog
类的子类 Beagle
,如果你在毫不知情的情况下又创建了一个 mood
实例属性,那么在继承的方法中就会把 Dog
类的 mood
属性覆盖掉。
为了避免这种情况,python 会将 __mood
变成 _Dog__mood
,而对于 Beagle 类来说,会变成 _Beagle__mood
。这个语言特性就叫名称改写(name mangling)。
39. +=更快
>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 0.25748300552368164 # 用 "+=" 连接三个字符串: >>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 0.012188911437988281 复制代码
说明:连接两个以上的字符串时 +=
比 +
更快,因为在计算过程中第一个字符串(例如, s1 += s2 + s3
中的 s1
)不会被销毁。(就是 +=
执行的是追加操作,少了一个销毁新建的动作。)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Python:鲜为人知的功能特性(上)
- 一些有趣且鲜为人知的 Python 特性
- WTF Python:有趣且鲜为人知的Python特性
- 鲜为人知的混沌工程,到底哪里好?
- 六个鲜为人知的 Composer 命令
- 11 个鲜为人知的 Linux 命令(1)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。