GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及不为人知的功能特性,并尝试讨论这些现象背后真正的原理! 原版地址:github.com/satwikkansa…python
最近,一位名为“暮晨”的贡献者将其翻译成了中文。 中文版地址:github.com/leisurelich…git
本来每一个的标题都是原版中的英文,有些取名比较奇怪,不直观,我换成了能够描述主题的中文形式,有些是本身想的,不足之处请指正。另一些 Python 中的彩蛋被我去掉了。github
我将全部代码都亲自试过了,加入了一些本身的理解和例子,因此会和原文稍有不一样。express
下篇已发布:Python:不为人知的功能特性(下)数组
①bash
>>> a = '!'
>>> b = '!'
>>> a is b
True
复制代码
②闭包
>>> a = 'some_string'
>>> id(a)
140420665652016
>>> id('some' + '_' + 'string') # 注意两个的id值是相同的.
140420665652016
复制代码
③app
>>> a = 'wtf'
>>> b = 'wtf'
>>> a is b
True
>>> a = 'wtf!'
>>> b = 'wtf!'
>>> a is b
False
>>> a, b = 'wtf!', 'wtf!'
>>> a is b
True
复制代码
④函数
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False
复制代码
说明: 这些行为是因为 CPython 在编译优化时,某些状况下会尝试使用已经存在的不可变对象而不是每次都建立一个新对象。这种行为被称做字符串的驻留 string interning。发生驻留以后, 许多变量可能指向内存中的相同字符串对象从而节省内存。post
有一些方法能够用来猜想字符串是否会被驻留:
'wtf'
将被驻留,可是 ''.join(['w', 't', 'f']
将不会被驻留)'wtf!'
因为包含 '!'
而未被驻留a
和 b
的值设置为 'wtf!'
的时候,Python 解释器会建立一个新对象,而后两个变量同时指向这个对象。若是你在不一样的行上进行赋值操做,它就不会“知道”已经有一个 'wtf!'
对象(由于 'wtf!'
不是按照上面提到的方式被隐式驻留的)。'a' * 20
会被替换为 'aaaaaaaaaaaaaaaaaaaa'
以减小运行时的时钟周期。只有长度小于 20 的字符串才会发生常量折叠。(为啥?想象一下因为表达式 'a' * 10 ** 10
而生成的 .pyc 文件的大小)。若是你在 .py 文件中尝试这个例子,则不会看到相同的行为,由于文件是一次性编译的。
>>> 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(1) == hash(1.0)
必须也为真,但其实两个数字(整数和浮点数)的内部结构是彻底不同的。
def some_func():
try:
return 'from_try'
finally:
return 'from_finally'
复制代码
Output:
>>> some_func()
'from_finally'
复制代码
说明: 函数的返回值由最后执行的 return
语句决定。因为 finally
子句必定会执行,因此 finally
子句中的 return
将始终是最后执行的语句。
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 值。
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
复制代码
正如你所看到的,对象销毁的顺序是形成全部不一样之处的缘由。
>>> 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(4):
print(i)
i = 10
复制代码
①
>>> array = [1, 8, 15]
>>> g = (x for x in array if array.count(x) > 0)
>>> array = [2, 8, 22]
>>> list(g)
[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(g1)
[1, 2, 3, 4]
>>> list(g2)
[1, 2, 3, 4, 5]
复制代码
说明: 在生成器表达式中 in
子句在声明时执行,而条件子句则是在运行时执行。 ①中,在运行前 array
已经被从新赋值为 [2, 8, 22]
,所以对于以前的 1, 8, 15,只有 count(8)
的结果是大于 0 ,因此生成器只会生成 8。 ②中,g1
和 g2
的输出差别则是因为变量 array_1
和 array_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]
。>>> 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
表明引用相同,==
表明值相等。下面的例子能够很好的说明这点:
>>> [] == []
True
>>> [] is [] # 这两个空列表位于不一样的内存地址
False
复制代码
当启动 Python 的时候,-5 到 256 的数值就已经被分配好了。这些数字由于常用因此适合被提早准备好。
当前的实现为 -5 到 256 之间的全部整数保留一个整数对象数组,当你建立了一个该范围内的整数时,你只须要返回现有对象的引用。因此改变 1 的值是有可能的。
可是,当 a
和 b
在同一行中使用相同的值初始化时,会指向同一个对象。
>>> 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
>>> a, b = 257, 257
>>> id(a)
140640774013296
>>> id(b)
140640774013296
复制代码
这是一种特别为交互式环境作的编译器优化,当你在实时解释器中输入两行的时候,他们会单独编译,所以也会单独进行优化, 若是你在 .py 文件中尝试这个例子,则不会看到相同的行为,由于文件是一次性编译的。
>>> row = [''] * 3
>>> board = [row] * 3
>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]
复制代码
说明: 咱们来输出 id 看下:
>>> id(row[0])
7536232
>>> id(row[1])
5143216
>>> id(row[2])
5143216
>>> id(board[0])
7416840
>>> id(board[1])
7416840
>>> id(board[2])
7416840
复制代码
row
是一个 list,其中三个元素都指向地址 5143216,当对 board[0][0]
进行赋值之后,row
的第一个元素指向 7536232。而 board
中的三个元素都指向 row
,row
的地址并无改变。
咱们能够经过不使用变量 row
生成 board
来避免这种状况。
>>> board = [[''] * 3 for _ in range(3)]
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['', '', ''], ['', '', '']]
复制代码
这里用了推导式,每次迭代都会生成一个新的 _
,因此 board
中三个元素指向的是不一样的变量。
funcs = []
results = []
for x in range(7):
def some_func():
return x
funcs.append(some_func)
results.append(some_func())
funcs_results = [func() for func in funcs]
复制代码
Output:
>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]
复制代码
说明: 当在循环内部定义一个函数时,若是该函数在其主体中使用了循环变量,则闭包函数将与循环变量绑定,而不是它的值。所以,全部的函数都是使用最后分配给变量的值来进行计算的。
能够经过将循环变量做为命名变量传递给函数来得到预期的结果。为何这样可行?由于这会在函数内再次定义一个局部变量。
funcs = []
for x in range(7):
def some_func(x=x):
return x
funcs.append(some_func)
复制代码
Output:
>>> funcs_results = [func() for func in funcs]
>>> funcs_results
[0, 1, 2, 3, 4, 5, 6]
复制代码
>>> print("\\ C:\\")
\ C:\
>>> print(r"\ C:")
\ C:
>>> print(r"\ C:\")
File "<stdin>", line 1
print(r"\ C:\")
^
SyntaxError: EOL while scanning string literal
复制代码
说明: 在以 r
开头的原始字符串中,反斜杠并无特殊含义。解释器所作的只是简单的改变了反斜杠的行为,所以会直接传递反斜杠及后一个的字符。这就是反斜杠在原始字符串末尾不起做用的缘由。
>>> not x == y
True
>>> x == not y
File "<input>", line 1
x == not y
^
SyntaxError: invalid syntax
复制代码
说明: 一句话,==
运算符的优先级要高于 not
运算符。
>>> print('wtfpython''')
wtfpython
>>> print("wtfpython""")
wtfpython
>>> # 下面的语句会抛出 `SyntaxError` 异常
>>> # print('''wtfpython')
>>> # print("""wtfpython")
复制代码
说明: '''
和 """
在 Python 中也是字符串定界符,Python 解释器在先遇到三个引号的的时候会尝试再寻找三个终止引号做为定界符,若是不存在则会致使 SyntaxError
异常。
而 Python 提供隐式的字符串连接:
>>> print("wtf" "python")
wtfpython
>>> print("wtf""") # 至关于 "wtf" ""
wtf
复制代码
from datetime import datetime
midnight = datetime(2018, 1, 1, 0, 0)
midnight_time = midnight.time()
noon = datetime(2018, 1, 1, 12, 0)
noon_time = noon.time()
if midnight_time:
print("Time at midnight is", midnight_time)
if noon_time:
print("Time at noon is", noon_time)
复制代码
Output:
Time at noon is 12:00:00
复制代码
midnight_time
并无被输出。
说明: 在 Python 3.5 以前,若是 datetime.time
对象存储的 UTC 的午夜 0 点, 那么它的布尔值会被认为是 False。 这个我特地下了个 python 3.4 验证了下,真是这样。
mixed_list = [False, 1.0, "some_string", 3, True, [], False]
integers_found_so_far = 0
booleans_found_so_far = 0
for item in mixed_list:
if isinstance(item, int):
integers_found_so_far += 1
elif isinstance(item, bool):
booleans_found_so_far += 1
复制代码
Output:
>>> booleans_found_so_far
0
>>> integers_found_so_far
4
复制代码
说明: 布尔值是 int
的子类
>>> isinstance(True, int)
True
>>> isinstance(False, int)
True
复制代码
在引入实际 bool
类型以前,0 和 1 是真值的官方表示。为了向下兼容,新的 bool
类型须要像 0 和 1 同样工做。
①
class A:
x = 1
class B(A):
pass
class C(A):
pass
复制代码
Output:
>>> A.x, B.x, C.x
(1, 1, 1)
>>> B.x = 2
>>> A.x, B.x, C.x
(1, 2, 1)
>>> A.x = 3
>>> A.x, B.x, C.x
(3, 2, 3)
>>> a = A()
>>> a.x, A.x
(3, 3)
>>> a.x += 1
>>> a.x, A.x
(4, 3)
复制代码
②
class SomeClass:
some_var = 15
some_list = [5]
another_list = [5]
def __init__(self, x):
self.some_var = x + 1
self.some_list = self.some_list + [x]
self.another_list += [x]
复制代码
Output:
>>> some_obj = SomeClass(420)
>>> some_obj.some_list
[5, 420]
>>> some_obj.another_list
[5, 420]
>>> another_obj = SomeClass(111)
>>> another_obj.some_list
[5, 111]
>>> another_obj.another_list
[5, 420, 111]
>>> another_obj.another_list is SomeClass.another_list
True
>>> another_obj.another_list is some_obj.another_list
True
复制代码
说明:
__dict__
属性),若是在当前类的字典中找不到的话就去它的父类中寻找。+=
运算符会在原地修改可变对象,而不是建立新对象。所以,修改一个实例的属性会影响其余实例和类属性。some_iterable = ('a', 'b')
def some_func(val):
return "something"
复制代码
Output:
>>> [x for x in some_iterable]
['a', 'b']
>>> [(yield x) for x in some_iterable]
<generator object <listcomp> at 0x7f70b0a4ad58>
>>> list([(yield x) for x in some_iterable])
['a', 'b']
>>> list((yield x) for x in some_iterable)
['a', None, 'b', None]
>>> list(some_func((yield x)) for x in some_iterable)
['a', 'something', 'b', 'something']
复制代码
说明: 这是 CPython 在理解和生成器表达式中处理 yield
的一个错误,在 Python 3.8 中修复,在 Python 3.7 中有弃用警告。 请参阅 Python 错误报告和 Python 3.7 和 Python 3.8 的新增条目。
来源和解释能够在这里找到: stackoverflow.com/questions/3… 相关错误报告: bugs.python.org/issue10544
>>> some_tuple = ("A", "tuple", "with", "values")
>>> another_tuple = ([1, 2], [3, 4], [5, 6])
>>> some_tuple[2] = "change this"
TypeError: 'tuple' object does not support item assignment
>>> another_tuple[2].append(1000) # 这里不出现错误
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000])
>>> another_tuple[2] += [99, 999]
TypeError: 'tuple' object does not support item assignment
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000, 99, 999])
复制代码
说明: 元组中不可变的元素的标识(即元素的地址),若是元素是引用类型,元组的值会随着引用的可变对象的变化而变化。因此 another_tuple[2].append(1000)
是能够的。 +=
操做符在原地修改了列表。元素赋值操做并不工做,可是当异常抛出时,元素已经在原地被修改了。+=
并非原子操做,而是 extend
和 =
两个动做,这里 =
操做虽然会抛出异常,但 extend
操做已经修改为功了。
e = 7
try:
raise Exception()
except Exception as e:
pass
复制代码
Output: python2
>>> print(e)
# prints nothing
复制代码
Output: python3
>>> print(e)
NameError: name 'e' is not defined
复制代码
说明: 当使用 as
为目标分配异常的时候,将在 except
子句的末尾清除该异常。
这就好像:
except E as N:
foo
复制代码
会被翻译成:
except E as N:
try:
foo
finally:
del N
复制代码
这意味着必须将异常分配给其余名称才能在 except
子句以后引用它。而异常之因此会被清除,是由于附加了回溯信息(trackback),它们与栈帧(stack frame)造成一个引用循环,使得该栈帧中的全部本地变量在下一次垃圾回收发生以前都处于活动状态(不会被回收)。
子句在 Python 中并无独立的做用域。示例中的全部内容都处于同一做用域内,因此变量 e
会因为执行了 except
子句而被删除。而对于有独立的内部做用域的函数来讲状况就不同了。下面的例子说明了这一点:
def f(x):
del(x)
print(x)
x = 5
y = [5, 4, 3]
复制代码
Output:
>>>f(x)
UnboundLocalError: local variable 'x' referenced before assignment
>>>f(y)
UnboundLocalError: local variable 'x' referenced before assignment
>>> x
5
>>> y
[5, 4, 3]
复制代码
True = False
if True == False:
print("I've lost faith in truth!")
复制代码
Output:
I've lost faith in truth!
复制代码
说明: 最初,Python 并无 bool
型(人们用 0 表示假值, 用非零值好比 1 做为真值)。后来他们添加了 True
, False
, 和 bool
型,可是,为了向后兼容,他们无法把 True
和 False
设置为常量,只是设置成了内置变量。 Python 3 因为再也不须要向后兼容,终于能够修复这个问题了,因此这个例子没法在 Python 3.x 中执行。
some_list = [1, 2, 3]
some_dict = {
"key_1": 1,
"key_2": 2,
"key_3": 3
}
some_list = some_list.append(4)
some_dict = some_dict.update({"key_4": 4})
复制代码
Output:
>>> print(some_list)
None
>>> print(some_dict)
None
复制代码
说明: 大多数修改序列/映射对象的方法,好比 list.append
,dict.update
,list.sort
等等,都是原地修改对象并返回 None
,这样能够避免建立对象的副原本提升性能。