如何检索元类方法

4
在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'"

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

4个回答

8
正如你所说并在实践中发现的那样,A.method不在B的查找链上 - 类和元类之间的关系不是继承关系,而是“实例”的关系,因为类是元类的一个实例。
Python是一种表现符合预期、几乎没有什么意外情况的优秀语言 - 在这种情况下也是如此:如果我们处理的是“普通”对象,那么你的情况就相当于有一个类A的实例B。而B.method将出现在B.__dict__中 - 在为这样的实例定义的方法上放置“super”调用永远无法到达A.method - 实际上会产生错误。由于B是一个类对象,在B的__dict__中的方法中使用super是有意义的 - 但它会在B的__mro__类链上搜索(在这种情况下,(object,))- 这就是你遇到的问题。
这种情况不应该经常发生,甚至我认为根本不应该发生;从语义上讲,很难存在一种既作为元类方法又作为类本身方法有意义的方法。此外,如果method没有在B中重新定义,则它甚至不会从B的实例中可见(也不能调用)。
也许你的设计应该:
a. 有一个基类Base,使用你的元类A,定义method而不是在A中定义它 - 然后定义class B(Base): b. 或者让元类Amethod注入到它创建的每个class中,在它的__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):
        setattr(cls, '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。如果你真的有使用元类中的方法从类中调用的用例,“一个显而易见”的方法是直接硬编码调用,就像在 super 存在之前所做的那样。你可以这样做:
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_class。有一个基本的setup_class功能,因此在元类上定义它并不完全不合适,但在遇到这个问题后,我意识到它同样可以在基类上定义。我很快会发布我的解决方案。感谢您清晰的回复。 - rmorshea
1
如果您有一个在元类上有意义的setup_class,并且可以补充该类的内容也有意义,那么您可以记录命名标准或装饰器,以便通过执行setup_class本身来调用类本身的方法。 (但是,有一个技巧-在元类__init__返回之前调用的classmethod不能使用super-请参见https://dev59.com/EGcs5IYBdhLWcg3wHwRH#28605694) - jsbueno

1
覆盖方法并不是问题,正确使用 super 才是。该方法没有从任何基类继承,并且您无法保证 MRO 中的每个类都使用相同的元类来提供该方法,因此您会遇到与任何没有根类处理它而不调用 super 的方法相同的问题。
仅仅因为元类可以注入类方法,并不意味着您可以将该方法用于协作继承。

0

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

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)相同,在其中 method 存在于 B.__class__ 中,但不存在于 B 中。增强它以从元类复制所需的方法到动态基类,然后返回它(从而使 method 存在于正常的 mro 查找中)。但是,你可以在元类本身上进行这种方法注入(毕竟这就是元类的作用 - 在类创建时注入和更改内容)。 - jsbueno
方法注入是有意义的,但出于某种原因,我从未调查过类的__class__属性。我一直认为您无法访问元类。既然如此,您只需搜索B.__class__以获取method,而不是像我现在这样迭代mro。我想更好地表达我的问题应该是询问如何检索类的元类。 - rmorshea

0

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

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在回应我的特定用例时所解释的那样


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接