Can a decorator of an instance method access the class?

后端 未结 13 899
没有蜡笔的小新
没有蜡笔的小新 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 12:57

    Function doesn't know whether it's a method at definition point, when the decorator code runs. Only when it's accessed via class/instance identifier it may know its class/instance. To overcome this limitation, you may decorate by descriptor object to delay actual decorating code until access/call time:

    class decorated(object):
        def __init__(self, func, type_=None):
            self.func = func
            self.type = type_
    
        def __get__(self, obj, type_=None):
            func = self.func.__get__(obj, type_)
            print('accessed %s.%s' % (type_.__name__, func.__name__))
            return self.__class__(func, type_)
    
        def __call__(self, *args, **kwargs):
            name = '%s.%s' % (self.type.__name__, self.func.__name__)
            print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
            return self.func(*args, **kwargs)
    

    This allows you to decorate individual (static|class) methods:

    class Foo(object):
        @decorated
        def foo(self, a, b):
            pass
    
        @decorated
        @staticmethod
        def bar(a, b):
            pass
    
        @decorated
        @classmethod
        def baz(cls, a, b):
            pass
    
    class Bar(Foo):
        pass
    

    Now you can use decorator code for introspection...

    >>> Foo.foo
    accessed Foo.foo
    >>> Foo.bar
    accessed Foo.bar
    >>> Foo.baz
    accessed Foo.baz
    >>> Bar.foo
    accessed Bar.foo
    >>> Bar.bar
    accessed Bar.bar
    >>> Bar.baz
    accessed Bar.baz
    

    ...and for changing function behavior:

    >>> Foo().foo(1, 2)
    accessed Foo.foo
    called Foo.foo with args=(1, 2) kwargs={}
    >>> Foo.bar(1, b='bcd')
    accessed Foo.bar
    called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
    >>> Bar.baz(a='abc', b='bcd')
    accessed Bar.baz
    called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}
    
    0 讨论(0)
  • 2020-11-27 12:57

    I just want to add my example since it has all the things I could think of for accessing the class from the decorated method. It uses a descriptor as @tyrion suggests. The decorator can take arguments and passes them to the descriptor. It can deal with both a method in a class or a function without a class.

    import datetime as dt
    import functools
    
    def dec(arg1):
        class Timed(object):
            local_arg = arg1
            def __init__(self, f):
                functools.update_wrapper(self, f)
                self.func = f
    
            def __set_name__(self, owner, name):
                # doing something fancy with owner and name
                print('owner type', owner.my_type())
                print('my arg', self.local_arg)
    
            def __call__(self, *args, **kwargs):
                start = dt.datetime.now()
                ret = self.func(*args, **kwargs)
                time = dt.datetime.now() - start
                ret["time"] = time
                return ret
            
            def __get__(self, instance, owner):
                from functools import partial
                return partial(self.__call__, instance)
        return Timed
    
    class Test(object):
        def __init__(self):
            super(Test, self).__init__()
    
        @classmethod
        def my_type(cls):
            return 'owner'
    
        @dec(arg1='a')
        def decorated(self, *args, **kwargs):
            print(self)
            print(args)
            print(kwargs)
            return dict()
    
        def call_deco(self):
            self.decorated("Hello", world="World")
    
    @dec(arg1='a function')
    def another(*args, **kwargs):
        print(args)
        print(kwargs)
        return dict()
    
    if __name__ == "__main__":
        t = Test()
        ret = t.call_deco()
        another('Ni hao', world="shi jie")
        
    
    0 讨论(0)
  • 2020-11-27 12:59

    Here's a simple example:

    def mod_bar(cls):
        # returns modified class
    
        def decorate(fcn):
            # returns decorated function
    
            def new_fcn(self):
                print self.start_str
                print fcn(self)
                print self.end_str
    
            return new_fcn
    
        cls.bar = decorate(cls.bar)
        return cls
    
    @mod_bar
    class Test(object):
        def __init__(self):
            self.start_str = "starting dec"
            self.end_str = "ending dec" 
    
        def bar(self):
            return "bar"
    

    The output is:

    >>> import Test
    >>> a = Test()
    >>> a.bar()
    starting dec
    bar
    ending dec
    
    0 讨论(0)
  • 2020-11-27 13:00

    As Mark suggests:

    1. Any decorator is called BEFORE class is built, so is unknown to the decorator.
    2. We can tag these methods and make any necessary post-process later.
    3. We have two options for post-processing: automatically at the end of the class definition or somewhere before the application will run. I prefer the 1st option using a base class, but you can follow the 2nd approach as well.

    This code shows how this may works using automatic post-processing:

    def expose(**kw):
        "Note that using **kw you can tag the function with any parameters"
        def wrap(func):
            name = func.func_name
            assert not name.startswith('_'), "Only public methods can be exposed"
    
            meta = func.__meta__ = kw
            meta['exposed'] = True
            return func
    
        return wrap
    
    class Exposable(object):
        "Base class to expose instance methods"
        _exposable_ = None  # Not necessary, just for pylint
    
        class __metaclass__(type):
            def __new__(cls, name, bases, state):
                methods = state['_exposed_'] = dict()
    
                # inherit bases exposed methods
                for base in bases:
                    methods.update(getattr(base, '_exposed_', {}))
    
                for name, member in state.items():
                    meta = getattr(member, '__meta__', None)
                    if meta is not None:
                        print "Found", name, meta
                        methods[name] = member
                return type.__new__(cls, name, bases, state)
    
    class Foo(Exposable):
        @expose(any='parameter will go', inside='__meta__ func attribute')
        def foo(self):
            pass
    
    class Bar(Exposable):
        @expose(hide=True, help='the great bar function')
        def bar(self):
            pass
    
    class Buzz(Bar):
        @expose(hello=False, msg='overriding bar function')
        def bar(self):
            pass
    
    class Fizz(Foo):
        @expose(msg='adding a bar function')
        def bar(self):
            pass
    
    print('-' * 20)
    print("showing exposed methods")
    print("Foo: %s" % Foo._exposed_)
    print("Bar: %s" % Bar._exposed_)
    print("Buzz: %s" % Buzz._exposed_)
    print("Fizz: %s" % Fizz._exposed_)
    
    print('-' * 20)
    print('examine bar functions')
    print("Bar.bar: %s" % Bar.bar.__meta__)
    print("Buzz.bar: %s" % Buzz.bar.__meta__)
    print("Fizz.bar: %s" % Fizz.bar.__meta__)
    

    The output yields:

    Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
    Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
    Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
    Found bar {'msg': 'adding a bar function', 'exposed': True}
    --------------------
    showing exposed methods
    Foo: {'foo': <function foo at 0x7f7da3abb398>}
    Bar: {'bar': <function bar at 0x7f7da3abb140>}
    Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
    Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
    --------------------
    examine bar functions
    Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
    Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
    Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}
    

    Note that in this example:

    1. We can annotate any function with any arbitrary parameters.
    2. Each class has its own exposed methods.
    3. We can inherit exposed methods as well.
    4. methods can be overriding as exposing feature is updated.

    Hope this helps

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

    As Ants indicated, you can't get a reference to the class from within the class. However, if you're interested in distinguishing between different classes ( not manipulating the actual class type object), you can pass a string for each class. You can also pass whatever other parameters you like to the decorator using class-style decorators.

    class Decorator(object):
        def __init__(self,decoratee_enclosing_class):
            self.decoratee_enclosing_class = decoratee_enclosing_class
        def __call__(self,original_func):
            def new_function(*args,**kwargs):
                print 'decorating function in ',self.decoratee_enclosing_class
                original_func(*args,**kwargs)
            return new_function
    
    
    class Bar(object):
        @Decorator('Bar')
        def foo(self):
            print 'in foo'
    
    class Baz(object):
        @Decorator('Baz')
        def foo(self):
            print 'in foo'
    
    print 'before instantiating Bar()'
    b = Bar()
    print 'calling b.foo()'
    b.foo()
    

    Prints:

    before instantiating Bar()
    calling b.foo()
    decorating function in  Bar
    in foo
    

    Also, see Bruce Eckel's page on decorators.

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

    The problem is that when the decorator is called the class doesn't exist yet. Try this:

    def loud_decorator(func):
        print("Now decorating %s" % func)
        def decorated(*args, **kwargs):
            print("Now calling %s with %s,%s" % (func, args, kwargs))
            return func(*args, **kwargs)
        return decorated
    
    class Foo(object):
        class __metaclass__(type):
            def __new__(cls, name, bases, dict_):
                print("Creating class %s%s with attributes %s" % (name, bases, dict_))
                return type.__new__(cls, name, bases, dict_)
    
        @loud_decorator
        def hello(self, msg):
            print("Hello %s" % msg)
    
    Foo().hello()
    

    This program will output:

    Now decorating <function hello at 0xb74d35dc>
    Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
    Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
    Hello World
    

    As you see, you are going to have to figure out a different way to do what you want.

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