wtfPython:14个反直觉的Python代码陷阱与原理解析
本文基于wtfPython项目,深入解析14个Python中反直觉的代码现象。内容涵盖字典键隐式转换、生成器求值时机、迭代中删除元素、循环与try-else机制、is与==区别、变量泄露、+=与+差异、finally返回值及True赋值等核心问题,揭示Python内部机制,帮助开发者避开常见误区。
wtfPython是一个托管在GitHub上的开源项目,作者整理了一系列看似奇妙且输出结果往往与直觉相悖的Python代码片段。
通过深入探究这些现象背后的内部机制,开发者能够对Python语言中许多容易被忽视的细节建立更全面的认知。
1. 字典键的隐式类型转换
some_dict = {}
some_dict[5.5] = "Ruby"
some_dict[5.0] = "JavaScript"
some_dict[5] = "Python"
代码执行输出如下:
>>> some_dict {5.0: 'Python', 5.5: 'Ruby'} >>> some_dict[5.5] 'Ruby' >>> some_dict[5.0] 'Python' >>> some_dict[5] 'Python'
产生该现象的原因在于:
Python字典在比较键时,依据的是哈希值(hash value)。
在Python中,若两个不可变对象的值相等,则它们的哈希值必然相同。
因此,hash(5) 与 hash(5.0) 的比较结果为 True,导致整型键 5 被隐式转换为浮点型键 5.0 并覆盖了原有值。
2. 生成器表达式中执行时间的差异
array = [1, 8, 15] g = (x for x in array if array.count(x) > 0) array = [2, 8, 22]
代码输出结果:
>>> print(list(g)) [8]
原因分析:
在生成器表达式中,in 子句在表达式声明时即被求值,而 if 子句则在生成器运行期间逐个求值。
因此,在生成器实际运行之前,变量 array 已被重新赋值为 [2, 8, 22],此时 x 的值依次为 2、8、22。
3. 在列表迭代过程中删除元素
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)
代码输出结果:
>>> list_1 [1, 2, 3, 4] >>> list_2 [2, 4] >>> list_3 [] >>> list_4 [2, 4]
原因分析:
实际上,只有 list_3 的处理方式符合规范。直接修改正在迭代的对象并非良好实践,正确做法是创建该对象的副本进行迭代。
对于 list_1,del item 仅删除了局部变量 item 的引用,并未影响列表本身的结构。
对于 list_2 和 list_4,由于列表迭代基于索引进行:第一次删除索引 0 的元素 1 后,列表变为 [2, 3, 4];接着移除索引 1 的元素(此时为 3),列表变为 [2, 4];此时仅剩两个元素,循环结束。
4. else 子句的不同处理逻辑
针对 for 循环的 else 子句:
def does_exists_num(l, to_find):
for num in l:
if num == to_find:
print("Exists!")
break
else:
print("Does not exist")
代码输出如下:
>>> 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...")
代码输出如下:
Try block executed successfully...
原因分析:
循环后的 else 子句仅在遍历完所有迭代项且未遇到 break 语句时才会执行。
try 模块后的 else 子句仅在 try 块中的代码成功执行且未抛出异常时才会执行。
5. Python 中 is 运算符的行为
>>> a = 256 >>> b = 256 >>> a is b True >>> a = 257 >>> b = 257 >>> a is b False
原因分析:
is 与 == 存在本质区别:is 用于判断两个对象是否为同一实例(引用一致),而 == 用于判断两个对象的值是否相等。即 is 要求值相等且引用一致。
在 Python 中,整数 -5 到 256 因被频繁使用,被设计为预分配的固定对象,因此小整数比较时 is 往往返回 True。
6. 循环变量在作用域中的泄露
代码段 1:
for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global')
代码输出如下:
6 : for x inside loop 6 : x in global
代码段 2:
# This time let's initialize x first x = -1 for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global')
代码输出如下:
6 : for x inside loop 6 : x in global
代码段 3:
x = 1 print([x for x in range(5)]) print(x, ': x in global')
在 Python 2.x 环境下的输出:
[0, 1, 2, 3, 4] (4, ': x in global')
在 Python 3.x 环境下的输出:
[0, 1, 2, 3, 4] 1 : x in global
原因分析:
对于代码段 1,在 Python 中,for 循环会复用其所在命名空间的变量,并将循环定义的变量保留在局部作用域中。对于代码段 2,若在全局命名空间中显式定义了 for 循环变量,则循环变量会重新绑定到该现有变量上。
对于代码段 3,Python 3.x 对列表解析的语法形式进行了调整。Python 2.x 中列表解析语法为:[… for var in item1, item2, …];而 Python 3.x 的列表解析式为:[… for var in (item1, item2, …)],这种元组形式的解析不会导致循环变量泄露到外部作用域。
7. + 与 += 运算符的区别
代码段 1:
a = [1, 2, 3, 4] b = a a = a + [5, 6, 7, 8]
代码输出如下:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4]
代码段 2:
a = [1, 2, 3, 4] b = a a += [5, 6, 7, 8]
代码输出如下:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4, 5, 6, 7, 8]
原因分析:
a = a + b 的操作会生成一个新的列表对象,并建立一个新的引用指向该新对象。
a += b 则是在原列表 a 上执行 extend 操作,直接修改原对象。
8. try…finally 结构中 return 的行为
def some_func():
try:
return 'from_try'
finally:
return 'from_finally'
代码输出如下:
>>> some_func() 'from_finally'
原因分析:
在 try…finally 结构内部,finally 块中的 return 语句具有最高优先级,永远是最后执行的返回操作。
函数的返回值由最后一个执行的 return 语句决定,因此 finally 中的 'from_finally' 覆盖了 try 中的 'from_try'。
9. 将 True 赋值为 False
True = False
if True == False:
print("I've lost faith in truth!")
代码输出如下:
I've lost faith in truth!
原因分析:
在 Python 早期版本中,并未定义 bool 类型(使用 0 表示 False,非 0 表示 True)。后来引入 True、False 和 bool 类型时,为了向后兼容性,True 和 False 并未被设为常量,而是作为内置变量存在,因此可以被重新赋值修改。在 Python 3 中,由于不再需要向后兼容旧版行为,这种情况不再发生。
10. 一步操作导致数据丢失
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})
代码输出如下:
>>> print(some_list) None >>> print(some_dict) None
原因分析:
许多修改序列或映射对象的方法(例如 list.append、dict.update、list.sort 等)都是原地修改对象并返回 None。因此,在涉及直接修改的操作时,应避免将返回值赋值给原变量。
11. Python for 循环的迭代机制
for i in range(4): print(i) i = 10
代码输出如下:
0 1 2 3
原因分析:
Python 的 for 循环机制是每次迭代到下一项时,都会对项进行解包并赋值。即 range(4) 中的四个值在每次迭代时都会被解包并赋值给变量 i;因此,循环体内对 i = 10 的赋值不会影响下一次迭代的取值。
总结
以上介绍了 wtfPython 项目中关于 Python 有趣且微妙的代码现象,希望能为读者提供帮助。如有任何疑问,欢迎留言交流,小编会及时回复。感谢大家对本站的支持!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









