【问题标题】:How to Retrieve a Metaclass Method如何检索元类方法
【发布时间】:2016-05-10 03:39:48
【问题描述】:

在 Python 中工作,如何通过它实例化的类来检索元类(元方法)拥有的方法?在以下场景中,这很简单——只需使用getattr 或点表示法:

* 所有示例都使用版本安全的with_metaclass

class A(type):
        class A(type):
    """a metaclass"""

    def method(cls):
        m = "this is a metamethod of '%s'"
        print(m % cls.__name__)

class B(with_metaclass(A, object)):
    """a class"""
    pass

B.method()
# prints: "this is a metamethod of 'B'"

但这很奇怪,因为在dir(B) 的任何地方都找不到'method'。由于这个事实,动态覆盖这样的方法变得很困难,因为元类不在super 的查找链中:

class A(type):
    """a metaclass"""

    def method(cls):
        m = "this is a metamethod of '%s'"
        print(m % cls.__name__)

class B(with_metaclass(A, object)):
    """a class"""

    @classmethod
    def method(cls):
        super(B, cls).method()

B.method()

# raises: "AttributeError: 'super' object has no attribute 'method'"

那么,正确覆盖元方法的最简单方法是什么?我已经对这个问题提出了自己的答案,但期待任何建议或替代答案。提前感谢您的回复。

【问题讨论】:

    标签: python metaclass


    【解决方案1】:

    正如您所说,并在实践中发现,A.method 不是 B 的查找链上的 - 类和元类的关系不是继承之一 - 它是“实例”之一一个类是元类的一个实例。

    Python 是一种很好的语言,它以预期的方式运行,几乎没有什么意外——在这种情况下也是如此:如果我们正在处理“普通”对象,你的情况将与拥有一个 instance BA。并且B.method 将出现在B.__dict__ 中——在为此类实例定义的方法上放置的“超级”调用永远无法到达A.method——它实际上会产生错误。因为B 是一个类对象,所以在B 的__dict__ 的方法中的super 是有意义的——但它会搜索B 的__mro__ 类链(在本例中为(object,))——这就是你命中。

    这种情况不应该经常发生,我什至认为根本不应该发生;从语义上讲,很难在一个方法中存在任何意义,既可以作为元类方法,也可以作为类本身的方法。此外,如果method 没有在B 中重新定义,请注意它甚至不会从B 的实例中可见(也不能调用)。

    也许你的设计应该:

    一个。有一个基类Base,使用你的元类A,它定义method,而不是在A中定义它——然后定义class B(Base):

    b.或者让元类A 而不是在它创建的每个class 中注入method,并在它的__init____new__ 方法中添加代码 - 沿:

    def method(cls):
        m = "this is an injected method of '%s'"
        print(m % cls.__name__)
    
    class A(type):
        def __init__(cls, name, bases, dct):
            dct['method'] = classmethod(method)
    

    这将是我的首选方法 - 但它不允许 在使用此元类的类中覆盖此method - 如果没有一些额外的逻辑,上面的方法宁愿覆盖正文中的任何此类method 显式。 更简单的方法是像上面一样拥有一个基类Base,或者注入一个具有不同名称的方法,例如在最终类上的base_method,并在任何覆盖method 中对其进行硬编码调用:

    class B(metaclass=A):
       @classmethod
       def method(cls):
            cls.base_method()
            ...
    

    (在元类的__init__ 上使用额外的if,以便默认method 别名为base_method

    你真正要求的从这里开始

    现在,如果您确实有一个从类调用元类中的方法的用例,那么“一个显而易见的”方法是简单地对调用进行硬编码,就像在 @987654356 存在之前所做的那样@

    您可以:

    class B(metaclass=A):
       @classmethod
       def method(cls):
            A.method(cls)
            ...
    

    动态性更小,但魔力更小,可读性更强 - 或者:

    class B(metaclass=A):
       @classmethod
       def method(cls):
            cls.__class__.method(cls)
            ...
    

    哪个更动态(__class__ 属性适用于cls,就像它在B 只是A 的一些实例的情况下起作用,就像我在第二段中的示例:B.__class__ is A

    在这两种情况下,您都可以通过简单的if hasattr(cls.__class__, "method"): ... 来防止在元类中调用不存在的method

    【讨论】:

    • 这更像是一种智力上的追求,而不是一种实际的追求。基本上我有一个用例,我在元类的__init__ 中调用方法setup_classsetup_class 有一个基本功能,因此在元类上定义它不会完全不直观,尽管在遇到这个问题后我意识到它可以很容易地在基类上定义。我将很快发布我的解决方案尝试。感谢您的明确答复
    • 如果您有一个对元类有意义的 setup_class,并且可以补充对类有意义的东西,您可以记录一个命名标准或装饰器,以便在该类上调用方法通过执行 setup_class 本身。 (但是,有一个技巧 - 在元类 __init__ 返回之前调用的类方法,不能使用 super - 请参阅 stackoverflow.com/questions/13126727/…
    【解决方案2】:

    覆盖method 不是问题;正确使用super 是。 method 不是从任何基类继承的,并且您不能保证 MRO 中的每个类都使用相同的元类来提供方法,因此您会遇到与没有根类的任何方法相同的问题来处理它打电话给super

    仅仅因为元类可以注入类方法并不意味着您可以使用该方法进行协作继承。

    【讨论】:

      【解决方案3】:

      这是我第一次尝试解决方案:

      def metamethod(cls, name):
          """Get a method owned by a metaclass
      
          The metamethod is retrieved from the first metaclass that is found
          to have instantiated the given class, or one of its parent classes
          provided the method doesn't exist in the instantiated lookup chain.
      
          Parameters
          ----------
          cls: class
              The class whose mro will be searched for the metamethod
          """
          for c in cls.mro()[1:]:
              if name not in c.__dict__ and hasattr(c, name):
                  found = getattr(c, name)
                  if isinstance(found, types.MethodType):
                      new = classmethod(found.__func__)
                      return new.__get__(None, cls)
          else:
              m = "No metaclass attribute '%s' found"
              raise AttributeError(m % name)
      

      但是,这有一个问题——它不适用于six.with_metaclass。解决方法是重新创建它,并使用它在从它继承的类的 mro 中创建一个新的基础。因为这个基础不是临时的,所以实现其实很简单:

      def with_metaclass(meta, *bases):
          """Create a base class with a metaclass."""
          return meta("BaseWithMeta", bases, {})
      

      瞧,这种方法确实解决了我提出的情况:

      class A(type):
          """a metaclass"""
      
          def method(cls):
              m = "this is a metamethod of '%s'"
              print(m % cls.__name__)
      
      class B(six.with_metaclass(A, object)):
          """a class"""
      
          @classmethod
          def method(cls):
              metamethod(cls, 'method')()
      
      B.method()
      # prints: "this is a metamethod of 'B'"
      

      但是,它会失败,并且会以同样的方式失败,原因与此相同:

      class A(type):
          """a metaclass"""
      
          def method(cls):
              m = "this is a metamethod of '%s'"
              print(m % cls.__name__)
      
      class C(object):
          """a class"""
      
          @classmethod
          def method(cls):
              m = "this is a classmethod of '%s'"
              print(m % cls.__name__)
      
      class B(six.with_metaclass(A, C)):
          """a class"""
          pass
      
      B.method()
      # prints: "this is a classmethod of 'B'"
      

      【讨论】:

      • 您对 with_metaclass 的重新实现不会将 method 从元类转移到它返回的基类 - 它与纯 Python(或原始的 with_metaclass)基本相同,其中存在 methodB.__class__ 中,但不在B 中。增强它以将所需的方法从metaclass 复制到动态基类,然后再返回它(从而使method 存在于正常的mro 查找中)。但是他们,你可以在元类本身上进行这种方法注入(这就是元类的用途——毕竟在类创建时注入和改变东西)
      • 方法注入是有道理的,但出于某种原因,我从未研究过类的__class__ 属性。我的印象是您无法访问元类。在这种情况下,您只需在 B.__class__ 中搜索 method,而不是像我现在那样遍历 mro。我想一个更好的方式来表达我的问题是问你如何检索一个类的元类。
      【解决方案4】:

      我认为这最能解决检索元方法的初始问题。

      def metamethod(cls, name):
          """Get an unbound method owned by a metaclass
      
          The method is retrieved from the instantiating metaclass.
      
          Parameters
          ----------
          cls: class
              The class whose instantiating metaclass will be searched.
          """
          if hasattr(cls.__class__, name):
              return getattr(cls.__class__, name)
      

      但是,这并没有真正解决正确覆盖在类被完全实例化为@jsbueno explained 之前调用的方法的问题,以响应我的特定用例。

      【讨论】:

        猜你喜欢
        • 2017-07-07
        • 1970-01-01
        • 2021-02-26
        • 1970-01-01
        • 1970-01-01
        • 2014-06-07
        • 1970-01-01
        • 2011-03-11
        • 1970-01-01
        相关资源
        最近更新 更多