无论是为面试做准备还是与代码片段玩耍,有时候即使像 Python 这样透明和良好结构化的语言也会带来真正的挑战。
尽管普遍了解 Python 之禅,如可迭代解包赋值或 += 和 + 操作符之间的差异等独特情况似乎对于经验丰富的工程师来说也是反直觉且令人困惑的。在这里,我汇编了 10 个最佳示例,展示了 Python 解释器的行为如何让你质疑语言结构的逻辑。
以下所有片段都在 Python 3.8.10 版本上测试过。
1. type == object
执行以下代码的结果是什么:
>>> isinstance(type, object)
>>> isinstance(object, type)
>>> isinstance(object, object)
>>> isinstance(type, type)
正确答案:True, True, True, True
在 Python 中,所有东西都是对象,因此对于对象的任何实例检查都将返回 True:isinstance(Anything, object) #=> True
。
Python 的 type 表示构建所有 Python 类型的元类。因此,所有类型,如 int、str、object 都是 type 类的实例,而 type 类本身也是一个对象,与 Python 中的一切一样。
type 是 Python 中唯一一个自身是自己实例的对象。
>>> type(5)
<class 'int'>
>>> type(int)
<class 'type'>
>>> type(type)
<class 'type'>
2. 空布尔值
执行以下代码的结果是什么:
>>> any([])
>>> all([])
答案:False, True
根据内置函数 any 的定义,我们知道它将:
如果 iterable 中的任何元素为 true,则返回 true。
Python 中的逻辑运算符是惰性的,算法是查找第一个 true 元素的出现情况,如果没有找到,则返回 False。由于序列为空,因此没有元素可以是 true,因此 any([])
返回 False。
all 的例子稍微有些复杂,因为它表示真空的真实性。与链式惰性逻辑运算符类似,算法是查找第一个 false 元素,如果没有找到,则返回 True。由于在空序列中没有 false 元素,因此 all([])
返回 True。
3. 绕路
执行以下代码的结果是什么:
>>> round(7 / 2)
>>> round(3 / 2)
>>> round(5 / 2)
答案:4, 2, 2
为什么 round(5 / 2)
返回 2 而不是 3?这里的问题在于 Python 的 round 方法实现了银行家舍入,其中所有半值都将四舍五入到最接近的偶数。
4. 实例优先!
这段代码将在控制台中打印什么?
class A:
answer = 42 def __init__(self):
self.answer = 21
self.__add__ = lambda x, y: x.answer + y def __add__(self, y):
return self.answer - yprint(A() + 5)
答案:16 (21 - 5)
为了解决属性名称,Python 首先会在实例级别上搜索它,然后在类级别上搜索,然后在父类中搜索。这对于 dunder 方法来说是正确的。在搜索它们时,Python 将跳过实例检查,直接在类中搜索。
5. 把它们加起来
执行以下代码的结果是什么:
>>> sum("")
>>> sum("", [])
>>> sum("", {})
答案:0, [], {}
为了理解这里发生了什么,我们需要检查 sum 函数的签名:
`sum`(iterable, /, start=0)
从左到右对 iterable 的 start 和项进行求和并返回总和。iterable 的项通常是数字,不允许将 start 值设置为字符串。
在所有上述情况下,空字符串都被视为空序列,因此 sum 将简单地将 start 参数作为总结果返回。在第一种情况中,它默认为零,对于第二种和第三种情况,它意味着传入空列表和字典作为 start 参数。
6. 意外的属性
执行以下代码的结果是什么:
>>> sum([
el.imag
for el in [
0, 5, 10e9, float('inf'), float('nan')
]
])
答案:0.0
这个片段不会引起 AttributeError。Python 中的所有数值类型(int、real、float)都继承自基本的 object 类。尽管如此,它们都支持实数和虚数属性,分别返回实部和虚部。这也包括 Infinity 和 NaN。
7. 懒惰的 Python
执行以下代码的结果是什么:
class A:
def function(self):
return A()a = A()
A = int
print(a.function())
答案:0
Python 函数内部的代码只有在调用时才会被执行,这意味着所有的 NameErrors 都会被引发,并且变量只有在实际调用方法时才会绑定。在上面的示例中,在方法定义期间,Python 允许引用尚未定义的类。然而,在执行期间,Python 将从外部作用域绑定名称 A,这意味着 function 方法将返回一个新创建的 int 实例。## 8. 我想要比-1还少
以下代码的执行结果是什么:
>>> "this is a very long string" * (-1)
答案:空字符串(empty string)
从 Python 文档 可知:
值小于 `0` 的 n 会被视为 `0`(这将产生与 s 相同类型的空序列)。
对于任何序列类型都适用。
9. 打破数学规则
以下代码的执行结果是什么:
>>> max(-0.0, 0.0)
答案:-0.0
为什么会这样?这是由于两个原因造成的。
- 在 Python 中,负零 和零被视为相等。
- Python 文档中的
max
函数说明如下:
如果多个项最大,则该函数返回首次遇到的项。
因此,max
函数返回第一个零出现的位置,这恰好是负数。问题解决了。
10. 再次打破数学规则
以下代码的执行结果是什么:
>>> x = (1 << 53) + 1
>>> x + 1.0 > x
答案:False
有三个因素导致这种反直觉的行为:长算术、浮点数精度限制和数字比较。Python 可以支持非常大的整数,如果隐式超过了限制,则切换计算模式(在 Python 2.* 中可以显式使用 long
类型),但 Python 中的浮点精度是有限的。所以,这个数字:
2⁵³ + 1 = 9007199254740993
是最小的不能完全表示为 Python 浮点数的整数。因此,为了执行 x + 1.0
,Python 将 x
转换为 float
,将其舍入为 9007199254740992.0
,这是 Python 可以表示的,然后将 1.0
加到它上面,但由于相同的表示限制,它将其设置回 9007199254740992.0
。
这里的另一个问题是比较规则。与其他语言不同,Python 和 Ruby 在 float
与 int
比较时不会引发错误,也不会尝试将两个操作数都强制转换为相同的类型。而是比较实际的数值。因为 9007199254740992.0
小于 9007199254740993
,Python 返回 False。
结论
尽管如此,Python 仍然保持着其清晰透明的编程语言的称号。在撰写本文时,我遇到了许多这样的反直觉的代码片段,它们在新版本的 Python 中被修复或得到了社区的解释。上述示例代表了 Python 用法的边界情况,而在真实的商业项目中遇到它们的机会相对较小。
然而,检查和理解这样的“陷阱”可以帮助你更好地理解内部语言结构,并避免使用案例和可疑做法,这可能会导致意外的错误和故障。
评论(0)