Can a decorator of an instance method access the class?

后端 未结 13 901
没有蜡笔的小新
没有蜡笔的小新 2020-11-27 12:33

I have something roughly like the following. Basically I need to access the class of an instance method from a decorator used upon the instance method in its definition.

相关标签:
13条回答
  • 2020-11-27 13:10

    Since python 3.6 you can use object.__set_name__ to accomplish this in a very simple way. The doc states that __set_name__ is "called at the time the owning class owner is created". Here is an example:

    class class_decorator:
        def __init__(self, fn):
            self.fn = fn
    
        def __set_name__(self, owner, name):
            # do something with owner, i.e.
            print(f"decorating {self.fn} and using {owner}")
            self.fn.class_name = owner.__name__
    
            # then replace ourself with the original method
            setattr(owner, name, self.fn)
    

    Notice that it gets called at class creation time:

    >>> class A:
    ...     @class_decorator
    ...     def hello(self, x=42):
    ...         return x
    ...
    decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
    >>> A.hello
    <function __main__.A.hello(self, x=42)>
    >>> A.hello.class_name
    'A'
    >>> a = A()
    >>> a.hello()
    42
    

    If you want to know more about how classes are created and in particular exactly when __set_name__ is called, you can refer to the documentation on "Creating the class object".

    0 讨论(0)
  • 2020-11-27 13:10

    As other answers have pointed out, decorator is an function-ish thing, you can not access the class which this method belongs to since the class has not been created yet. However, it's totally ok to use a decorator to "mark" the function and then use metaclass techniques to deal with the method later, because at the __new__ stage, the class has been created by its metaclass.

    Here is a simple example:

    We use @field to mark the method as a special field and deal with it in metaclass.

    def field(fn):
        """Mark the method as an extra field"""
        fn.is_field = True
        return fn
    
    class MetaEndpoint(type):
        def __new__(cls, name, bases, attrs):
            fields = {}
            for k, v in attrs.items():
                if inspect.isfunction(v) and getattr(k, "is_field", False):
                    fields[k] = v
            for base in bases:
                if hasattr(base, "_fields"):
                    fields.update(base._fields)
            attrs["_fields"] = fields
    
            return type.__new__(cls, name, bases, attrs)
    
    class EndPoint(metaclass=MetaEndpoint):
        pass
    
    
    # Usage
    
    class MyEndPoint(EndPoint):
        @field
        def foo(self):
            return "bar"
    
    e = MyEndPoint()
    e._fields  # {"foo": ...}
    
    0 讨论(0)
  • 2020-11-27 13:10

    This is an old question but came across venusian. http://venusian.readthedocs.org/en/latest/

    It seems to have the ability to decorate methods and give you access to both the class and the method while doing so. Note tht calling setattr(ob, wrapped.__name__, decorated) is not the typical way of using venusian and somewhat defeats the purpose.

    Either way... the example below is complete and should run.

    import sys
    from functools import wraps
    import venusian
    
    def logged(wrapped):
        def callback(scanner, name, ob):
            @wraps(wrapped)
            def decorated(self, *args, **kwargs):
                print 'you called method', wrapped.__name__, 'on class', ob.__name__
                return wrapped(self, *args, **kwargs)
            print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
            setattr(ob, wrapped.__name__, decorated)
        venusian.attach(wrapped, callback)
        return wrapped
    
    class Foo(object):
        @logged
        def bar(self):
            print 'bar'
    
    scanner = venusian.Scanner()
    scanner.scan(sys.modules[__name__])
    
    if __name__ == '__main__':
        t = Foo()
        t.bar()
    
    0 讨论(0)
  • 2020-11-27 13:11

    As others have pointed out, the class hasn't been created at the time the decorator is called. However, it's possible to annotate the function object with the decorator parameters, then re-decorate the function in the metaclass's __new__ method. You'll need to access the function's __dict__ attribute directly, as at least for me, func.foo = 1 resulted in an AttributeError.

    0 讨论(0)
  • 2020-11-27 13:15

    If you are using Python 2.6 or later you could use a class decorator, perhaps something like this (warning: untested code).

    def class_decorator(cls):
       for name, method in cls.__dict__.iteritems():
            if hasattr(method, "use_class"):
                # do something with the method and class
                print name, cls
       return cls
    
    def method_decorator(view):
        # mark the method as something that requires view's class
        view.use_class = True
        return view
    
    @class_decorator
    class ModelA(object):
        @method_decorator
        def a_method(self):
            # do some stuff
            pass
    

    The method decorator marks the method as one that is of interest by adding a "use_class" attribute - functions and methods are also objects, so you can attach additional metadata to them.

    After the class has been created the class decorator then goes through all the methods and does whatever is needed on the methods that have been marked.

    If you want all the methods to be affected then you could leave out the method decorator and just use the class decorator.

    0 讨论(0)
  • 2020-11-27 13:15

    What flask-classy does is create a temporary cache that it stores on the method, then it uses something else (the fact that Flask will register the classes using a register class method) to actually wraps the method.

    You can reuse this pattern, this time using a metaclass so that you can wrap the method at import time.

    def route(rule, **options):
        """A decorator that is used to define custom routes for methods in
        FlaskView subclasses. The format is exactly the same as Flask's
        `@app.route` decorator.
        """
    
        def decorator(f):
            # Put the rule cache on the method itself instead of globally
            if not hasattr(f, '_rule_cache') or f._rule_cache is None:
                f._rule_cache = {f.__name__: [(rule, options)]}
            elif not f.__name__ in f._rule_cache:
                f._rule_cache[f.__name__] = [(rule, options)]
            else:
                f._rule_cache[f.__name__].append((rule, options))
    
            return f
    
        return decorator
    

    On the actual class (you could do the same using a metaclass):

    @classmethod
    def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
                 trailing_slash=None):
    
        for name, value in members:
            proxy = cls.make_proxy_method(name)
            route_name = cls.build_route_name(name)
            try:
                if hasattr(value, "_rule_cache") and name in value._rule_cache:
                    for idx, cached_rule in enumerate(value._rule_cache[name]):
                        # wrap the method here
    

    Source: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py

    0 讨论(0)
提交回复
热议问题