神坑·Python 装饰类无限递归

本文首发于个人博客转载请注明出处python

《神坑》系列将会不按期更新一些可遇而不可求的坑
防止他人入坑,也防止本身再次入坑django

简化版问题

现有两个 View 类:函数

class View(object):

    def method(self):
        # Do something...
        pass

class ChildView(View):

    def method(self):
        # Do something else ...
        super(ChildView, self).method()

以及一个用于修饰该类的装饰器函数 register——用于装饰类的装饰器很常见(如 django.contrib.adminregister),一般可极大地减小定义类似类时的工做量:code

class Mixin(object):
    pass

def register(cls):

    return type(
        'DecoratedView',
        (Mixin, cls),
        {}
    )

这个装饰器为被装饰类附加上一个额外的父类 Mixin,以增添自定义的功能。blog

完整的代码以下:继承

class Mixin(object):
    pass

def register(cls):

    return type(
        'DecoratedView',
        (Mixin, cls),
        {}
    )

class View(object):

    def method(self):
        # Do something...
        pass

@register
class ChildView(View):

    def method(self):
        # Do something else ...
        super(ChildView, self).method()

看上去彷佛没什么问题。然而一旦调用 ChildView().method(),却会报出诡异的 无限递归 错误:递归

# ...
File "test.py", line 23, in method
  super(ChildView, self).method()
File "test.py", line 23, in method
  super(ChildView, self).method()
File "test.py", line 23, in method
  super(ChildView, self).method()
RuntimeError: maximum recursion depth exceeded while calling a Python object

【一脸懵逼】作用域

猜测 & 验证

从 Traceback 中能够发现:是 super(ChildView, self).method() 在不停地调用本身——这着实让我吃了一惊,由于 按理说 super 应该沿着继承链查找父类,可为何在这里 super 神秘地失效了呢?get

为了验证 super(...).method 的指向,能够尝试将该语句改成 print(super(ChildView, self).method),并观察结果:博客

<bound method ChildView.method of <__main__.ChildView object at 0xb70fec6c>>

输出代表: method 的指向确实有误,此处本应为 View.method

super 是 python 内置方法,确定不会出错。那,会不会是 super 的参数有误呢?

super 的签名为 super(cls, instance),宏观效果为 遍历 cls 的继承链查找父类方法,并以 instance 做为 self 进行调用。现在查找结果有误,说明 继承链是错误的,于是极有多是 cls 出错。

所以,有必要探测一下 ChildView 的指向。在 method 中加上一句: print(ChildView)

<class '__main__.DecoratedView'>

原来,做用域中的 ChildView 已经被改变了。

真相

一切都源于装饰器语法糖。咱们回忆一下装饰器的等价语法:

@decorator
class Class:
    pass

等价于

class Class:
    pass

Class = decorator(Class)

这说明:装饰器会更改该做用域内被装饰名称的指向

这原本没什么,但和 super 一块儿使用时却会出问题。一般状况下咱们会将本类的名称传给 super(在这里为 ChildView),而本类名称和装饰器语法存在于同一做用域中,从而在装饰时被一同修改了(在本例中指向了子类 DecoratedView),进而使 super(...).method 指向了 DecoratedView 的最近祖先也就是 ChildView 自身的 method 方法,致使递归调用。

解决方案

找到了病因,就不难想到解决方法了。核心思路就是:不要更改被装饰名称的引用

若是你只是想在内部使用装饰后的新类,能够在装饰器方法中使用 DecoratedView,而在装饰器返回时 return cls,以保持引用不变:

def register(cls):

    decorated = type(
        'DecoratedView',
        (Mixin, cls),
        {}
    )

    # Do something with decorated

    return cls

这种方法的缺点是:从外部没法使用 ChildView.another_method 调用 Mixin 上的方法。可若是真的有这样的需求,能够采用另外一个解决方案:

def register(cls):

    cls.another_method = Mixin.another_method
    return cls

即经过赋值的方式为 cls 添加 Mixin 上的新方法,缺点是较为繁琐。

两种方法各有利弊,要根据实际场景权衡使用。

相关文章
相关标签/搜索