wtfPython:14个反直觉的Python代码陷阱与原理解析

Alice
Alice
发布时间:2026-10-13 | 5人浏览过
原创

本文基于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 有趣且微妙的代码现象,希望能为读者提供帮助。如有任何疑问,欢迎留言交流,小编会及时回复。感谢大家对本站的支持!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表