如何使用参数作为类来实现Python装饰器? [英] How to implement Python decorator with arguments as a class?

查看:74
本文介绍了如何使用参数作为类来实现Python装饰器?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试实现一个接受一些参数的装饰器。通常,带有参数的装饰器被实现为双嵌套闭包,例如:

I'm trying to implement a decorator which accepts some arguments. Usually decorators with arguments are implemented as double-nested closures, like this:

def mydecorator(param1, param2):
    # do something with params
    def wrapper(fn):
        def actual_decorator(actual_func_arg1, actual_func_arg2):
            print("I'm decorated!")

            return fn(actual_func_arg1, actual_func_arg2)

        return actual_decorator

    return wrapper

但是我个人不喜欢这种方法,因为它非常难以理解且难以理解。

But personally I don't like such approach because it is very unreadable and difficult to understand.

所以我最终得到了这样的结果:

So I ended up with this:

class jsonschema_validate(object):
    def __init__(self, schema):
        self._schema = schema

    def __call__(self, fn):
        self._fn = fn

        return self._decorator

    def _decorator(self, req, resp, *args, **kwargs):
        try:
            jsonschema.validate(req.media, self._schema, format_checker=jsonschema.FormatChecker())
        except jsonschema.ValidationError as e:
            _log.exception('Validation failed: %r', e)

            raise errors.HTTPBadRequest('Bad request')

        return self._fn(req, resp, *args, **kwargs)

这个想法很简单:在实例化时,我们只捕获装饰器args,在调用时,我们捕获装饰器函数并返回绑定的装饰器实例的方法。绑定是很重要的,因为在装饰器调用时,我们要访问 self 并存储所有信息。

The idea is very simple: at instantiation time we just captures decorator args, and at call time we capture decorated function and return decorator instance's method, which is bound. It is important it to be bound because at decorator's invocation time we want to access self with all information stored in it.

然后我们在某些类上使用它:

Then we use it on some class:

class MyResource(object):
    @jsonschema_validate(my_resource_schema)
    def on_post(self, req, resp):
        pass

不幸的是,这种方法不起作用。问题是在装饰器调用时,由于在装饰时(定义类时)未绑定装饰方法,因此我们释放了装饰实例的上下文。绑定发生在属性访问时。但是目前,我们已经有了装饰器的绑定方法( jsonschema_validate._decorator )和 self 被隐式传递,并且它的值不是 MyResource 实例,而是 jsonschema_validate 实例。而且,我们不想丢失这个 self 值,因为我们想在装饰器调用时访问它的属性。最后,调用 self._fn(req,resp,* args,** kwargs)时,会导致 TypeError 抱怨缺少所需的位置参数'resp'因为传入 req arg会成为 MyResource.on_post " self

Unfortunately, this approach doesn't work. The problem is that at decorator invocation time we looses context of decorated instance because at decoration time (when defining class) decorated method is not bound. Binding occurs later at attribute access time. But at this moment we already have decorator's bound method (jsonschema_validate._decorator) and self is passed implicitly, and it's value isn't MyResource instance, rather jsonschema_validate instance. And we don't want to loose this self value because we want to access it's attributes at decorator invocation time. In the end it results in TypeError when calling self._fn(req, resp, *args, **kwargs) with complains that "required positional argument 'resp' is missing" because passed in req arg becomes MyResource.on_post "self" and all arguments effectively "shifts".

那么,有没有一种方法可以将装饰器实现为类而不是嵌套函数呢?

So, is there a way implement decorator as a class rather than as a bunch of nested functions?

由于我第一次尝试将装饰器实现为简单类失败,因此很快,我立即恢复为嵌套函数。似乎正确实现的类方法似乎更加难以理解和纠结,但我还是想找到解决问题的方法。

As my first attempt of implementing decorator as simple class was failed rather quickly, I immediately reverted to nested functions. It seems like properly implemented class approach is even more unreadable and tangled, but I want to find solution anyway for the fun of the thing.

最终找到了解决方案,请参阅我自己的答案。

Finally found solution, see my own answer.

推荐答案

最后得到了!

如我所写,一个方法不能有两个 self 的问题,因此我们需要以某种方式捕获这两个值。 描述符闭包进行救援!

As I wrote, the problem that a method can't have two self, so we need to capture both values in some way. Descriptors and closures to the rescue!

以下是完整的示例:

class decorator_with_args(object):
    def __init__(self, arg):
        self._arg = arg

    def __call__(self, fn):
        self._fn = fn

        return self

    def __get__(self, instance, owner):
        if instance is None:
            return self

        def _decorator(self_, *args, **kwargs):
            print(f'decorated! arg: {self._arg}')

            return self._fn(self_, *args, **kwargs)

        return _decorator.__get__(instance, owner)

让我们分解成碎片!

它的开始与我之前的尝试完全相同。在 __ init __ 中,我们仅捕获其私有属性的装饰器参数。

It starts exactly as my previous attempt. In __init__ we just capture decorator arguments to it's private attribute(s).

下一部分内容将引起更多兴趣:a __ call __ 方法。

Things get more interested in next part: a __call__ method.

def __call__(self, fn):
    self._fn = fn

    return self

和以前一样,我们将装饰方法捕获到装饰器的private属性中。但是,我们没有返回实际的装饰器方法(在上一个示例中为 def _decorator ),而是返回了 self 。因此,装饰方法成为装饰器的实例。这是允许它充当 descriptor 。根据文档:

As before, we capture decorated method to decorator's private attribute. But then, instead of returning actual decorator method (def _decorator in previous example), we return self. So decorated method becomes instance of decorator. This is required to allow it to act as descriptor. According to docs:


描述符是具有绑定行为的对象属性。

a descriptor is an object attribute with "binding behavior"

令人困惑,嗯?实际上,它比看起来容易。描述符仅仅是具有魔术属性的对象。 (笨拙)分配给另一个对象属性的方法。当您尝试访问此属性时,将使用某些调用约定来调用那些dunder方法。我们将回到绑定行为

Confusing, uh? Actually, it is easier than it looks. Descriptor is just an object with "magic" (dunder) methods which is assigned to another's object attribute. When you try to access this attribute, those dunder methods will be invoked with some calling convention. And we'll return to "binding behavior" a little bit later.

让我们看一下细节。

def __get__(self, instance, owner):

描述符必须至少实现 __ get __ dunder(和 __ set __ __ delete __ (可选)。这被称为描述符协议。 (类似于上下文管理器协议,收集协议等)。

Descriptor must implement at least __get__ dunder (and __set__ & __delete__ optionally). This is called "descriptor protocol" (similar to "context manager protocol", "collection protocol" an so on).

    if instance is None:
        return self

这是按照惯例。当在类而不是实例上访问描述符时,它应该返回自身。

This is by convention. When descriptor accessed on class rather than instance, it should return itself.

下一部分是最有趣的。

        def _decorator(self_, *args, **kwargs):
            print(f'decorated! arg: {self._arg}')

            return self._fn(self_, *args, **kwargs)

        return _decorator.__get__(instance, owner)

以某种方式捕获装饰者的 self 以及装饰实例的 self 。由于我们不能用两个 self 定义函数(即使我们可以,Python也无法理解我们),因此我们封闭装饰器的 self closure -一个内部函数。在此关闭中,我们实际上改变了修饰方法的行为( print('decorated!arg:{self._arg}')),然后调用原始方法。同样,由于已经有一个名为 self 的参数,我们需要为实例的 self 选择另一个名称-在本例中,我将其命名为 self _ ,但实际上它是 self'- self prime。 (有点数学幽默)。

We need to capture decorator's self as well as decorated instance's self in some way. As we can't define function with two self (even if we can, Python couldn't understand us), so we enclose decorator's self with closure - an inner function. In this closure, we actually do alter behavior of decorated method (print('decorated! arg: {self._arg}')) and then call original one. Again, as there is already argument named self, we need to choose another name for instance's self - in this example I named it self_, but actually it is self' - "self prime" (kinda math humor).

        return _decorator.__get__(instance, owner)

最后,通常,当我们定义闭包时,我们只返回它: def inner():pass;返回内部。但是在这里我们不能这样做。由于绑定行为。我们需要返回的闭包必须绑定到装饰实例,以便其正常工作。让我用一个例子来解释。

And finally, usually, when we define closures, we just return it: def inner(): pass; return inner. But here we can't do that. Because of "binding behavior". What we need is returned closure to be bound to decorated instance in order it to work properly. Let me explain with an example.

class Foo(object):
    def foo(self):
        print(self)

Foo.foo
# <function Foo.foo at 0x7f5b1f56dcb0>
Foo().foo
# <bound method Foo.foo of <__main__.Foo object at 0x7f5b1f586910>>

访问类中的方法时,它只是一个普通Python函数。使它成为方法的是 binding 。绑定是将对象的方法与实例链接的一种行为,该实例作为firs参数隐式传递。通过惯例,它称为 self ,但大致来说这不是必需的。您甚至可以将方法存储在其他变量中并对其进行调用,并且仍将引用实例:

When you access method on class, it is just a plain Python function. What makes it a method instead is binding. Binding is an act of linking object's methods with instance which is passed implicitly as firs argument. By convention, it is called self, but roughly speaking this is not required. You can even store method in other variable and call it, and will still have reference to instance:

f = Foo()
f.foo()
# <__main__.Foo object at 0x7f5b1f5868d0>
other_foo = f.foo
other_foo()
# <__main__.Foo object at 0x7f5b1f5868d0>

因此,我们需要将返回的闭包绑定到经过修饰的实例。怎么做?还记得我们在看方法的时候吗?可能是这样的:

So, we need to bind our returned closure to decorated instance. How to do that? Remember when we was looking at method? It could be something like that:

# <bound method Foo.foo of <__main__.Foo object at 0x7f5b1f586910>>

让我们看一下它的类型:

Let's look at it's type:

type(f.foo)
# <class 'method'>

哇!它实际上甚至是一堂课!

Wow! It actually even a class! Let's create it!

method()
# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
# NameError: name 'method' is not defined

很遗憾,我们不能直接这样做。但是,有 types.MethodType

Unfortunately, we can't do that directly. But, there is types.MethodType:

types.MethodType
# <class 'method'>

似乎我们终于找到了想要的东西!但是,实际上,我们不需要手动创建方法!。我们需要做的就是委托标准方法创建机制。这是 Python中方法的实际工作方式-它们只是描述符,当作为实例的属性访问时,它们将自身绑定到实例!

Seems like we finally found what we wanted! But, actually, we don't need to create method manually!. All we need to do is to delegate to standard mechanics of method creation. And this is how actually methods work in Python - they are just descriptors which bind themselves to an instances when accessed as instance's attribute!


为支持方法调用,函数包括 __ get __()方法用于属性访问期间的绑定方法。

To support method calls, functions include the __get__() method for binding methods during attribute access.

因此,我们只需要委派绑定自动运行的机器:

So, we need to just delegate binding machinery to function itself:

_decorator.__get__(instance, owner)

并获取具有正确绑定的方法!

and get method with right binding!

这篇关于如何使用参数作为类来实现Python装饰器?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆