资讯专栏INFORMATION COLUMN

神坑·Python 装饰类无限递归

spacewander / 1344人阅读

摘要:如今查找结果有误,说明继承链是错误的,因而极有可能是出错。真相一切都源于装饰器语法糖。核心思路就是不要更改被装饰名称的引用。

本文首发于我的博客,转载请注明出处

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

简化版问题

现有两个 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),通常可极大地减少定义相似类时的工作量:

class Mixin(object):
    pass

def register(cls):

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

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

完整的代码如下:

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 神秘地失效了呢?

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

>

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

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

super 的签名为 super(cls, instance),宏观效果为 遍历 cls 的继承链查找父类方法,并以 instance 作为 self 进行调用。如今查找结果有误,说明 继承链是错误的,因而极有可能是 cls 出错。

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

原来,作用域中的 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 上的新方法,缺点是较为繁琐。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/38085.html

相关文章

  • Python中的动态属性和特性

    摘要:一利用动态属性处理数据源属性在中,数据的属性和处理数据的方法统称属性。处理无效属性名在中,由于关键字被保留,名称为关键字的属性是无效的。内置函数列出对象的大多数属性。点号和内置函数会触发这个方法。 导语:本文章记录了本人在学习Python基础之元编程篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。 本文重点: 1、了解如何利用动态属性处理数据;2、掌握Pyth...

    scola666 评论0 收藏0
  • [译] 属性访问、特性和描述符 2

    摘要:不像其他属性,描述符在类级别上创建。当所有者类被定义时,每个描述符对象都是被绑定到一个不同的类级别属性的描述符类实例。这必须返回描述符的值。此外,描述符对有一个方便的响应和请求格式。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python __getattribute__()方法 __getattribute__()方法是...

    CloudwiseAPM 评论0 收藏0
  • Python中的函数装饰器和闭包

    摘要:变量查找规则在中一个变量的查找顺序是局部环境,闭包,全局,内建闭包引用了自由变量的函数。闭包的作用闭包的最大特点是可以将父函数的变量与内部函数绑定,并返回绑定变量后的函数,此时即便生成闭包的环境父函数已经释放,闭包仍然存在。 导语:本文章记录了本人在学习Python基础之函数篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。 本文重点: 1、掌握装饰器的本质、功...

    caozhijian 评论0 收藏0
  • Python】一文弄懂python装饰器(附源码例子)

    摘要:装饰器的使用符合了面向对象编程的开放封闭原则。三简单的装饰器基于上面的函数执行时间的需求,我们就手写一个简单的装饰器进行实现。函数体就是要实现装饰器的内容。类装饰器的实现是调用了类里面的函数。类装饰器的写法比我们装饰器函数的写法更加简单。 目录 前言 一、什么是装饰器 二、为什么要用装饰器 ...

    liuchengxu 评论0 收藏0
  • Python开启尾递归优化!

    摘要:尾递归优化一般递归与尾递归一般递归执行可以看到一般递归每一级递归都产生了新的局部变量必须创建新的调用栈随着递归深度的增加创建的栈越来越多造成爆栈尾递归尾递归基于函数的尾调用每一级调用直接返回递归函数更新调用栈没有新局部变量的产生类似迭代的 Python尾递归优化 一般递归与尾递归 一般递归: def normal_recursion(n): if n == 1: ...

    junnplus 评论0 收藏0

发表评论

0条评论

spacewander

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<