返回介绍

类错误之一:装饰类方法

发布于 2024-01-29 22:24:14 字数 3567 浏览 0 评论 0 收藏 0

当我编写上面的第一个tracer函数的时候,我幼稚地假设它也应该适用于任何方法——装饰的方法应该同样地工作,但是,自动的self实例参数应该直接包含在*args的前面。遗憾的是,我错了:当应用于类方法的时候,tracer的第一个版本失效了,因为self是装饰器类的实例,并且装饰的主体类的实例没有包含在*args中。在Python 3.0和Python 2.6中都是如此。

我在本章前面介绍了这一现象,但是现在,我们可以在真实的工作代码环境中看到它。假设基于类的跟踪装饰器如下:

简单函数的装饰与前面介绍的一样:

然而,类方法的装饰失效了(更明白的读者可能会认识到,这是我们在第27章面向对象教程中的Person类的再现):

这里问题的根源在于,tracer类的__call__方法的self——它是一个tracer实例,还是一个Person实例?我们真的需要将其编写为两者都是:tracer用于装饰器状态,Person用于指向最初的方法。实际上,self必须是tracer对象,以提供对tracer的状态信息的访问;不管装饰一个简单的函数还是一个方法,都是如此。

遗憾的是,当我们用__call__把装饰方法名重绑定到一个类实例对象的时候,Python只向self传递了tracer实例;它根本没有在参数列表中传递Person主体。此外,由于tracer不知道我们要用方法调用处理的Person实例的任何信息,没有办法创建一个带有一个实例的绑定的方法,因此,没有办法正确地分配调用。

实际上,前面的列表最终传递了太少的参数给装饰的方法,并且导致了一个错误。在装饰器的__call__方法添加一行,以打印所有的参数来验证这一点。正如你所看到的,self是一个tracer,并且Person实例完全缺失:

正如前面提到的,出现这种情况是因为:当一个方法名绑定只是绑定到一个简单的函数,Python向self传递了隐含的主体实例;当它是一个可调用类的实例的时候,就传递这个类的实例。从技术上讲,当方法是一个简单函数的时候,Python只是创建了一个绑定的方法对象,其中包含了主体实例。

使用嵌套函数来装饰方法

如果想要函数装饰器在简单函数和类方法上都能工作,最直接的解决方法在于使用前面介绍的状态保持方法之一——把自己的函数装饰器编写为嵌套的def,以便对于包装器类实例和主体类实例都不需要依赖于单个的self实例参数。

如下的替代方案使用Python 3.0的nonlocal。由于装饰的方法重新绑定到简单的函数而不是实例对象,所以Python正确地传递了Person对象作为第一个参数,并且装饰器将其从*args中的第一项传递给真正的、装饰的方法的self参数:

这个版本在函数和方法上都有效:

使用描述符装饰方法

尽管前一小节介绍的嵌套函数的解决方案是支持应用于函数和类方法的装饰器的最直接方法,其他的方法也是可能的。例如,我们在上一章中介绍的描述符功能,在这里也能派上用场。

还记得我们在前一章的讨论中,描述符可能是分配给对象的一个类属性,该对象带有一个__get__方法,当引用或获取该属性的时候自动运行该方法(在Python 2.6中需要对象派生,但在Python 3.0中不需要):

描述符也能够拥有__set__和__del__访问方法,但是,我们在这里不需要它们。现在,由于描述符的__get__方法在调用的时候接收描述符类和主体类实例,因此当我们需要装饰器的状态以及最初的类实例来分派调用的时候,它很适合于装饰方法。考虑如下的替代的跟踪装饰器,它也是一个描述符:

这和前面的嵌套的函数代码一样有效。装饰的函数只调用其__call__,而装饰的方法首先调用其__get__来解析方法名获取(在instance.method上);__get__返回的对象保持主体类实例并且随后调用以完成调用表达式,由此触发__call__。例如,要测试代码的调用:

首先运行tracer.__get__,因为Person类的giveRaise属性已经通过函数装饰器重新绑定到了一个描述符。然后,调用表达式触发返回的包装器对象的__call__方法,它返回来调用tracer.__call__。

包装器对象同时保持描述符和主体实例,因此,它可以将控制指回到最初的装饰器/描述符类实例。实际上,在方法属性获取过程中,包装的对象保持了主体类实例可用,并且将其添加到了随后调用的参数列表,该参数列表会传递给__call__。在这个应用程序中,用这种方法把调用路由到描述符类实例是需要的,由此对包装方法的所有调用都使用描述符实例对象中的同样的调用计数器状态信息。

此外,我们也可以使用一个嵌套的函数和封闭的作用域引用来实现同样的效果——如下的版本和前面的版本一样的有效,通过为一个嵌套函数和作用域引用交换类和对象属性,但是,它所需的代码显著减少:

为这些替代方法添加print语句是为了自己跟踪获取/调用过程的两个步骤,用前面嵌套函数替代方法中同样的测试代码来运行它们。在两种编码中,基于描述符的方法也比嵌套函数的选项要细致得多,因此,它可能是这里的又一种选择。在其他的环境中,它也可能是一种有用的编码模式。

在本章剩余的内容中,我们将相当随意地使用类或函数来编写函数装饰器,只要它们都只适用于函数。一些装饰器可能并不需要最初的类的实例,并且如果编写为一个类,它将在函数和方法上都有效——例如Python自己的静态方法装饰器,就不需要主体类的一个实例(实际上,它主要是从调用中删除实例)。

然而,这里的叙述的教训是,如果你想要装饰器在简单函数和类方法上都有效,最好使用基于嵌套函数的编码模式,而不是带有调用拦截的类。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文