← 返回文章流

从 Python 的魔法方法说开去

一天我在群里看到这样一个有意思的 Python 现象: <!-- more --> >>> import os >>> r=os.popen('ls') >>> r.__next__() '0B4581EB10DBC182A83D85B0024F1E70.jpg\n' >>> next(r) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: '_wrap_close' object is not an iterator >>> 如果你对 Python 的魔法方法有所了解,就能发现这里的奇怪之处:popen的对象有__next__()方法,但却不能被next()调用,也就不是个迭代器。还有这种事吗?于是我们来看源码,看看popen()到底返回了个什么对象(省略了无关代码): def popen(cmd, mode="r", buffering=-1): ... return _wrap_close(io.TextIOWrapper(proc.stdin), proc) # Helper for popen() -- a proxy for a file whose close waits for the process class _wrap_close: def __init__(self, stream, proc): self._stream = stream self._proc = proc def __getattr__(self, name): return getattr(self._stream, name) def __iter__(self): return iter(self._stream) popen()返回了一个_wrap_close对象,而后者仅仅是一个 Iterable,而不是 Iterator(没有定义__next__())。然而,_wrap_close却定义了__getattr__()魔法方法,这样所有其他找不到的属性、方法就会传递给self._stream对象,而这个对象有__next__()方法。这就解释了为什么r.__next__()能调用成功。 所以,Python 对于魔法方法的调用是基于这个类有没有定义此方法吗? 答案是肯定的,查看 Python 源码中next()内建函数的实现,可以看到下面的代码: #define PyIter_Check(obj) \ (Py_TYPE(obj)->tp_iternext != NULL && \ Py_TYPE(obj)->tp_iternext != &_PyObject_NextNotImplemented) 判断一个obj是不是迭代器,是基于Py_TYPE(obj)是否有__next__()方法,而不是obj本身。__next__()如此,其他魔法方法也是一样。 问题解决了,我们可以得到下面的推论: 动态修改(或者叫 monkey patch)一个实例的魔法方法,是不生效的。 看下面的例子: >>> class Foo: pass ... foo = Foo() >>> foo.__str__ = lambda: '42' # <= 企图修改foo的__str__方法 >>> print(foo) <__main__.Foo object at 0x1024f7fd0> foo的字符串依然是原来的默认值,没有改变。要想改变,必须修改Foo.__str__方法。 下面这段是额外的思考,可能比较绕: 再回头去看最开始的例子,这个问题之所以奇怪,是因为它用了__getattr__()让实例获得了并不存在于类中的属性。也就是说,原来的类并没有获得这些额外的属性。而魔法行为的判断是基于类中是否有这个魔法方法。这两件事合起来看,那我是不是可以通过元类中的__getattr__()方法让类获得本不属于它的魔法方法,继而使得实例具有某些行为呢?说干就干: class IterMeta(type): def __getattr__(self, name): if name == '__next__': return lambda x: 42 return super().__getattr__(name) class Foo(metaclass=IterMeta): pass foo = Foo() next(foo) # TypeError: 'Foo' object is not an iterator foo.__next__() # AttributeError: 'Foo' object has no attribute '__next__' Foo.__next__(foo) # 42 Foo.__next__ = lambda x: 42 next(foo) # 42 不能!明明Foo能获取到__next__()属性,看来(Py_TYPE(obj)->tp_iternext并不会触发__getattr__。 我用 Python 的时间不可谓短,也自认对 Python 的语言特性比较了解了,但 Python 却总能时不时让我意外一下,这是什么情况?

来自 Frost's Blog作者主页
在原博客阅读全文 (新标签页打开)

以上内容来自博客订阅源,版权归原作者。关注作者后可以在 关注列表 第一时间查看作者文章。

一起聊聊 0

尊重不同观点

还没有评论。欢迎分享第一个想法。

RSS SUBSCRIPTIONS

选择 RSS 订阅

把博客汇的文章添加到你常用的阅读器。

关注文章 RSS 订阅本订阅需要登录后使用