【问题标题】:Can a decorator of an instance method access the class?实例方法的装饰器可以访问该类吗?
【发布时间】:2011-01-22 22:17:24
【问题描述】:

我的内容大致如下。基本上我需要从在其定义中的实例方法上使用的装饰器访问实例方法的类。

def decorator(view):
    # do something that requires view's class
    print view.im_class
    return view

class ModelA(object):
    @decorator
    def a_method(self):
        # do some stuff
        pass

代码原样给出:

AttributeError: 'function' 对象没有属性 'im_class'

我发现了类似的问题/答案 - Python decorator makes function forget that it belongs to a classGet class in Python decorator - 但这些都依赖于一种解决方法,即在运行时通过抢夺第一个参数来获取实例。就我而言,我将根据从其类中收集到的信息来调用该方法,所以我等不及来电了。

【问题讨论】:

    标签: python decorator


    【解决方案1】:

    您将可以访问在您的装饰器应返回的装饰方法中调用该方法的对象的类。像这样:

    def decorator(method):
        # do something that requires view's class
        def decorated(self, *args, **kwargs):
            print 'My class is %s' % self.__class__
            method(self, *args, **kwargs)
        return decorated
    

    使用您的 ModelA 类,它的作用如下:

    >>> obj = ModelA()
    >>> obj.a_method()
    My class is <class '__main__.ModelA'>
    

    【讨论】:

    • 谢谢,但这正是我在我的问题中引用的对我不起作用的解决方案。我正在尝试使用装饰器实现观察者模式,如果在将方法添加到观察调度程序时在某个时刻没有类,我将永远无法从我的观察调度程序在正确的上下文中调用该方法。在方法调用时获取类并不能帮助我首先正确调用方法。
    • 哇,抱歉我懒得看你的整个问题。
    【解决方案2】:

    问题是当装饰器被调用时,这个类还不存在。试试这个:

    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()
    

    这个程序会输出:

    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
    

    如您所见,您将不得不想出一种不同的方式来做您想做的事。

    【讨论】:

    • 当定义一个函数时,该函数还不存在,但可以从其自身内部递归调用该函数。我猜这是特定于函数的语言特性,类不可用。
    • DGGenuine:该函数只被调用,因此该函数只有在完全创建后才能访问自己。在这种情况下,调用装饰器时类不能完整,因为类必须等待装饰器的结果,该结果将作为类的属性之一存储。
    【解决方案3】:

    正如 Ants 所指出的,您无法从类中获取对该类的引用。但是,如果您有兴趣区分不同的类(而不是操作实际的类类型对象),您可以为每个类传递一个字符串。您还可以使用类风格的装饰器将您喜欢的任何其他参数传递给装饰器。

    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()
    

    打印:

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

    另外,see Bruce Eckel's page on decorators.

    【讨论】:

    • 感谢您确认我令人沮丧的结论,即这是不可能的。我还可以使用完全限定模块/类的字符串('module.Class'),存储字符串直到类全部加载完毕,然后自己通过导入检索类。这似乎是完成我的任务的一种非常糟糕的方式。
    • 你不需要为这种装饰器使用类:惯用的方法是在装饰器函数中使用一层额外的嵌套函数。但是,如果您确实使用类,最好不要在类名中使用大写来使装饰本身看起来“标准”,即 @decorator('Bar') 而不是 @Decorator('Bar')
    【解决方案4】:

    如果您使用的是 Python 2.6 或更高版本,则可以使用类装饰器,可能是这样的(警告:未经测试的代码)。

    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
    

    方法装饰器通过添加“use_class”属性将方法标记为感兴趣的方法 - 函数和方法也是对象,因此您可以将其他元数据附加到它们。

    创建类后,类装饰器会遍历所有方法,并对已标记的方法执行所需的操作。

    如果您希望所有方法都受到影响,那么您可以省略方法装饰器而只使用类装饰器。

    【讨论】:

    • 谢谢,我认为这是要走的路。对于我想使用这个装饰器的任何类,只需多写一行代码。也许我可以使用自定义元类并在 new...期间执行相同的检查?
    • 任何试图将它与 staticmethod 或 classmethod 一起使用的人都会想阅读这个 PEP:python.org/dev/peps/pep-0232 不确定它是否可能,因为你不能在类/静态方法上设置属性,我认为他们会狼吞虎咽将任何自定义函数属性应用于函数时。
    • 正是我正在寻找的基于 DBM 的 ORM...谢谢,伙计。
    • 你应该使用inspect.getmro(cls)来处理类装饰器中的所有基类以支持继承。
    • 哦,实际上它看起来像 inspect 救援 stackoverflow.com/a/1911287/202168
    【解决方案5】:

    正如其他人所指出的,在调用装饰器时尚未创建类。 但是,可以使用装饰器参数注释函数对象,然后在元类的__new__ 方法中重新装饰函数。您需要直接访问函数的 __dict__ 属性,至少对我而言,func.foo = 1 导致了 AttributeError。

    【讨论】:

    • setattr 应该使用而不是访问__dict__
    【解决方案6】:

    flask-classy 所做的是创建一个临时缓存,它存储在方法上,然后它使用其他东西(Flask 将使用register 类方法注册类的事实)来实际包装该方法。

    您可以重用此模式,这次使用元类,以便您可以在导入时包装方法。

    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
    

    在实际的类上(你可以使用元类做同样的事情):

    @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
    

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

    【讨论】:

    • 这是一个有用的模式,但这并没有解决方法装饰器能够引用它所应用的方法的父类的问题
    • 我更新了我的答案以更明确地说明这对于在导入时访问类(即使用元类 + 缓存方法上的装饰器参数)很有用。
    【解决方案7】:

    这是一个简单的例子:

    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"
    

    输出是:

    >>> import Test
    >>> a = Test()
    >>> a.bar()
    starting dec
    bar
    ending dec
    

    【讨论】:

      【解决方案8】:

      这是一个老问题,但遇到了 venusian。 http://venusian.readthedocs.org/en/latest/

      它似乎有能力装饰方法并让您在这样做的同时访问类和方法。 请注意,调用 setattr(ob, wrapped.__name__, decorated) 不是使用 venusian 的典型方式,并且在某种程度上违背了目的。

      不管怎样……下面的例子已经完成,应该可以运行了。

      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()
      

      【讨论】:

        【解决方案9】:

        正如马克建议的那样:

        1. 在构建类之前调用​​任何装饰器,因此装饰器不知道。
        2. 我们可以标记这些方法,并在以后进行任何必要的后期处理。
        3. 我们有两种后处理选项:在类定义结束时自动或在应用程序运行之前的某个地方自动进行。我更喜欢使用基类的第一种方法,但您也可以采用第二种方法。

        此代码显示了使用自动后处理如何工作:

        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__)
        

        输出产量:

        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}
        

        请注意,在此示例中:

        1. 我们可以使用任意参数注释任何函数。
        2. 每个类都有自己的公开方法。
        3. 我们也可以继承公开的方法。
        4. 随着公开功能的更新,方法可以被覆盖。

        希望对你有帮助

        【讨论】:

          【解决方案10】:

          当装饰器代码运行时,函数不知道它是否是定义点的方法。只有当它通过类/实例标识符访问时,它才可能知道它的类/实例。为了克服这个限制,您可以通过描述符对象进行装饰,以将实际的装饰代码延迟到访问/调用时间:

          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)
          

          这允许您装饰单个(静态|类)方法:

          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
          

          现在您可以使用装饰器代码进行自省...

          >>> 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
          

          ...以及改变函数行为:

          >>> 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'}
          

          【讨论】:

          • 遗憾的是,这种方法在功能上等同于Will McCutchenequally inapplicable answer。根据原始问题的要求,这个和那个答案都在方法 call 时间而不是方法 decoration 时间获得所需的类。在足够早的时间内获得此类的唯一合理方法是在类定义时内省所有方法(例如,通过类装饰器或元类)。 &lt;/sigh&gt;
          【解决方案11】:

          从 python 3.6 开始,您可以使用object.__set_name__ 以非常简单的方式完成此操作。该文档指出 __set_name__ 是“在创建拥有类 owner 时调用的”。 这是一个例子:

          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)
          

          注意它在类创建时被调用:

          >>> 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
          

          如果您想了解更多关于类是如何创建的,尤其是在何时调用__set_name__,您可以参考documentation on "Creating the class object"

          【讨论】:

          • 使用带参数的装饰器会是什么样子?例如。 @class_decorator('test', foo='bar')
          • @luckydonald 你可以像普通的decorators that take arguments 一样处理它。只要有def decorator(*args, **kwds): class Descriptor: ...; return Descriptor
          • 这种方法有一个缺点:静态检查器根本不理解这一点。 mypy 会认为hello 不是方法,而是class_decorator 类型的对象。
          • @kawing-chiu 如果没有其他方法,您可以使用if TYPE_CHECKINGclass_decorator 定义为返回正确类型的普通装饰器。
          • 我认为这不可靠,因为装饰器可能与其他装饰器组合在一起。因此,如果它被另一个装饰器包裹,那么它可能不会被调用。
          【解决方案12】:

          正如其他答案所指出的那样,装饰器是一种功能性的东西,由于尚未创建该类,因此您无法访问该方法所属的类。不过,完全可以使用装饰器“标记”函数,然后再使用元类技术处理该方法,因为在__new__ 阶段,该类已由其元类创建。

          这是一个简单的例子:

          我们使用@field将方法标记为特殊字段,并在元类中处理。

          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": ...}
          

          【讨论】:

          • 您在这一行中有错字:if inspect.isfunction(v) and getattr(k, "is_field", False): 应该是 getattr(v, "is_field", False)
          【解决方案13】:

          我只想添加我的示例,因为它包含我能想到的从装饰​​方法访问类的所有内容。它使用@tyrion 建议的描述符。装饰器可以接受参数并将它们传递给描述符。它既可以处理类中的方法,也可以处理没有类的函数。

          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")
              
          

          【讨论】:

            【解决方案14】:

            @asterio 冈萨雷斯

            我更喜欢你的方法,但是为了让 Python 3 符合新的元类处理,它必须稍作改动(另外,一些打印语句缺少括号):

            #!/usr/bin/env python3
            # -*- coding: utf-8 -*-
            """
            Created on Mon Aug  9 15:27:30 2021
            
            @author: yves
            """
            
            def expose(**kw):
                "Note that using **kw you can tag the function with any parameters"
                def wrap(func):
                    name = func.__name__
                    assert not name.startswith('_'), "Only public methods can be exposed"
            
                    meta = func.__meta__ = kw
                    meta['exposed'] = None
                    return func
            
                return wrap
            
            class ExposableMetaclass(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 Exposable(metaclass=ExposableMetaclass):
                "Base class to expose instance methods"
                _exposable_ = None  # Not necessary, just for pylint
            
            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_)
            

            满足我的需求!

            【讨论】:

              猜你喜欢
              • 2020-08-09
              • 2011-06-26
              • 2012-12-15
              • 2019-02-03
              相关资源
              最近更新 更多