Hacks on Computer Vision

[译] Python装饰器和元类的高级使用

2014.10.22

Advanced Use of Python Decorators and Metaclasses

摘要

当向人们介绍Python元类时,我意识到有时大多数强有力的Python特性的大问题在于,程序员没有觉察到他们可以如何简化日常工作。因此,类似元类的特性被认为是一个幻想,在标准OOP语言中是一个无用的添加,而不是改变规则的方法。

本文想要展示如何使用元类和装饰器来创造一个强大的类,可以通过简单的添加装饰方法来继承和定制化。

Metaclasses and decorators: a match made in space

元类是一个复杂的话题,通常即使是高级程序员在实际中也不经常大范围的使用。机遇在于这是Python的一部分(或者其他支持元类的语言,像Smalltalk和Ruby),它基本满足了可以在C++和Java中找到的“标准”面向对象模式或方法,仅仅是提下这两个巨头。

确实,通常元类的用武之地在于编写高级库或者框架,其中需要提供大量的自动操作。比如,Django Forms表单系统非常依赖元类来提供所有的魔法特性。

然而,我们也要注意到,我们常常将不熟悉的技术称作“magic”或者“tricks”,结果在Python中很多东西都这么称呼,和其他语言相比倒挺奇特的。

是时间给你的程序来点香料了:让我们练习一些Python魔法,探索下该语言的力量!

在本文中,我想展示一个有趣的联合使用装饰器和元类的例子。我将展示怎么使用装饰器来标记方法,使得它们在给定操作时,可以自动的被类使用。

更详细的说,我将实现一个可以被用来“处理”字符串的类,并展示如何通过简单的装饰器方法实现不同的“滤波器”。我想得到的是类似于下面的东西:

class MyStringProcessor(StringProcessor):
    @stringfilter
    def capitalize(self, str):
        [...]

    @stringfilter
    def remove_double_spaces(self, str):
        [...]

msp = MyStringProcessor()
"A test string" == msp("a test  string")

这个模块定义了一个StringProcessor类,我可以继承并定制添加有标准签名(self, str)的方法,并用@stringfilter来装饰。这个类一会会被实例化,实例可以直接用来处理字符串并返回结果。在内部,这个类自动按顺序执行了所有被装饰的方法。我也希望这个类可以遵守我定义过滤器的顺序:第一个定义的,第一个执行。

The Hitchhiker’s Guide To Metaclasses

元类是如何帮助完成这个目标的?

简单插一句,元类是实例化得到类的类。这意味着,无论何时我要使用一个类,比如实例化它,Python首先通过元类和我们写的类定义来__构建__那个类。例如,你知道你可以在__dict__属性中找到类成员:这个属性就是被标准元类创建,即type元类。

有了这些知识,一个元类对于我们来说是个很好的出发点,通过插入一些代码来确认类的定义中的一个函数子集。换句话说,我们希望元类(即类)的输出和构建标准样式一样,但有额外的信息:一个用@stringfilter装饰的所有方法的列表。

你知道一个类有一个__namespace__,这个是类内定义内容的一个字典。因此,当标准的type元类被用来创建一个类时,这个类的主体被解析,一个dict()主体被用来收集命名空间。

然而我们对保留定义的顺序感兴趣,Python字典是一个无序的结构,所以我们可以利用Python 3中类创建进程的__prepare__钩子。这个函数,如果在元类中出现,是用来预处理类,并返回用来托管命名空间的结构体。因此,下面的例子是在官方文档中找到的,我们可以开始定义一个元类:

class FilterClass(type):
    def __prepare__(name, bases, **kwds):
        return collections.OrderedDict()

这样,当类被创建时,一个orderedDict将被用来托管命名空间,允许我们保持定义的顺序。请注意,签名__prepare__(name, bases, **kwds)是语言强制的。如果你想要该方法得到的第一个参数是元类(因为方法的代码需要它),你需要改变签名为__prepare(metacls, name, bases, **kwds),并以@classmethod装饰它。

我们想要在元类中定义的第二个函数是__new__。正如类的实例化一样,这个方法被Python唤醒,得到元类的一个新的实例,在__init__之前运行。它的签名必须是__new__(metacls, name, bases, namespace, **kwds),结果应该是元类的一个实例。至于它正常的类副本(毕竟一个元类也是类),__new__()经常覆盖父类的相同方法,这时的type,会添加自己的自定义方法。

我们需要的自定义方法是创造方法的一个列表,并以某种方法标记(装饰的过滤器)。为了简洁,装饰方法有一个属性_filter

完整的元类如下:

class FilterClass(type):
    @classmethod
    def __prepare__(name, bases, **kwds):
         return collections.OrderedDict()

    def __new__(metacls, name, bases, namespace, **kwds):
        result = type.__new__(metacls, name, bases, dict(namespace))
        result._filters = [
            value for value in namespace.values() if hasattr(value, '_filter')]
        return result

现在我们需要找到一个方法来用_filter属性来标记滤波器方法。

The Anatomy of Purple Decorators

decorate在物体或地方添加一些东西,尤其是为了让它更加吸引人(剑桥字典)

装饰器,意如其名,是扩展函数或方法的最好方式。记住装饰器基本就是个可以接受另一个调用、处理并返回的一个可调用方法。

和元类一起使用,装饰器是在我们代码中实现高级特性的一个非常强大且有表现力的方式。在这个情况下,我们可以轻松使用它们对被装饰的方法添加一个属性,这是装饰器最基本的任务之一。

我决定实现@stringfilter装饰器为一个函数,即使我通常选择把它们实现为类。原因在于,当实现没有属性的装饰器而不是有属性的装饰器时,装饰器类的表现是不同的。而这本例中,这种不同会强制我们写更多复杂的代码以及解释,这有些过于浪费了。在将来的文章中,你会得到所有血淋淋的细节,但同时你可以看看参考部分列出的三篇Bruce Eckel博文。

装饰器非常简单:

def stringfilter(func):
    func._filter = True
    return func

如你所见,装饰器仅仅在函数中创建了一个_filter属性(记住那个函数是对象)。这个属性的真实值在本例中并不重要,因为我们仅仅对包含它的不同类成员感兴趣。

The Dynamics of a Callable Object

我们常常认为函数是特殊的语言部分,可以被“调用”或执行。在Python中,函数是对象,和其他一样,允许它们被执行的特性来自于__call__()方法。Python通过设计和基于代理实现多态,所以代码中发生的(几乎)所有情况都依赖于目标对象的某些特性。

这个泛化的结果就是每个包含__call__()方法的对象都能像函数一样执行,并得到__callable object__的名字。

StringProcessor类因此需要包含这个方法,执行所有包含滤波器的字符串处理。代码如下:

class StringProcessor(metaclass=FilterClass):
    
    def __call__(self, string):
        _string = string
        for _filter in self._filters:
            _string = _filter(self, _string)

        return _string

快速回顾下这个简单的函数,它接收一个字符串作为参数,存到一个局部变量中,在滤波器上循环,在局部字符串上执行每个滤波器,基于前个滤波器的结果。

滤波器函数是从self._filters列表中提取的,被我们已经讨论过的FilterClass元类编译。

现在我们要做的是从StringProcessor继承,来得到元类机制和__call__()方法,并定义需要数量的方法,用@stringfilter装饰器来装饰它们。

注意,由于装饰器和元类,你可以在类中有其他不与字符串处理相关的方法,只要它们不被装饰器装饰就行。

下面就是一个可能的派生类的例子:

class MyStringProcessor(StringProcessor):

    @stringfilter
    def capitalize(self, string):
        return string.capitalize()

    @stringfilter
    def remove_double_spaces(self, string):
        return string.replace('  ', ' ')

这两个capitalize()remove_double_spaces()方法已经被装饰了,所以当调用类时为了处理传入的字符串,它们将被调用。 本节课最后一个例子是:

>>> import strproc
>>> msp = strproc.MyStringProcessor()
>>> input_string = "a test  string"
>>> output_string = msp(input_string)
>>> print("INPUT STRING:", input_string)
INPUT STRING: a test  string
>>> print("OUTPUT STRING:", output_string)
OUTPUT STRING: A test string
>>> 

就是这样!

Final words

显然有其他方法来完成这个任务,本文仅想举出一个具体的例子,来说明元类的适用范围,以及我认为它们应该是Python程序员“军火库”的一部分的原因。

[更新]在Reddit和Linkedin上的一些开发者对本文内容有异议,大概是因为这个例子可能不用元类也能完美实现,以及元类的危险本质。因为我试着向每个人学习,很感谢它们的建议。

更有趣的是有些开发者认为元类的使用是一件危险的事情,因为它们隐藏了很多类的结构和潜在机制。确实没错,因此(你对于其他技术也应该这门做),考虑清楚你使用元类的原因,确保你非常了解它们。

Book Trivia

标题来源于下面这些书: A Match Made in Space - George McFly, The Hitchhiker’s Guide To the Galaxy - Various Authors, The Anatomy of Purple Dragons - Unknown, The Dynamics of an Asteroid - James Moriarty

Source code

strproc.py包含了本文中的所有源代码。

Online resources

下面这些资源会有用。

Metaclasses
Decorators
Callable objects

Rafe Kettler提供了关于Python“魔法”方法的详细指导。

Updates

2014-10-17: Matthew DillonDamon McDougall发现了两处拼写错误。多谢!

2014-10-17: [ionelmc]建议修改这里这里。 两个都是正确的,所以我照做了。第二个更多的是关于风格的,但和本文的介绍目的是一致的。多谢!

Feedback

欢迎使用blog Google+ 来评论。GitHub issues 页面是提交修改的最好地方。

PS BY LEY:在工作间隙,仓促翻译完了。

__EOF__

本文作者HackCV
版权声明本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
本文链接https://hackcv.com/posts/%E8%AF%91-python%E8%A3%85%E9%A5%B0%E5%99%A8%E5%92%8C%E5%85%83%E7%B1%BB%E7%9A%84%E9%AB%98%E7%BA%A7%E4%BD%BF%E7%94%A8/

发表评论