在装饰 ABCMeta 子类中的所有方法时强制执行抽象方法行为

Enforcing abstractmethod behavior when decorating all methods in an ABCMeta subclass

我想实现一个元类来包装方法以记录附加信息。但我还需要 abstractmethods。我试图扩展 ABCMeta 但它似乎没有强制执行 @abstractmethod 装饰器:

import types
import abc

def logfunc(fn, *args, **kwargs):
    def fncomposite(*args, **kwargs):
        rt = fn(*args, **kwargs)
        print("Executed %s" % fn.__name__)
        return rt
    return fncomposite

class LoggerMeta(abc.ABCMeta):
    def __new__(cls, clsname, bases, dct):
        for name, value in dct.items():
            if type(value) is types.FunctionType or type(value) is types.MethodType:
                dct[name] = logfunc(value)
        return super(LoggerMeta, cls).__new__(cls, clsname, bases, dct)

    def __init__(cls, *args, **kwargs):
        super(LoggerMeta, cls).__init__(*args, **kwargs)
        if cls.__abstractmethods__:
            raise TypeError("{} has not implemented abstract methods {}".format(
                cls.__name__, ", ".join(cls.__abstractmethods__)))


class Foo(metaclass=LoggerMeta):
    @abc.abstractmethod
    def foo(self):
        pass

class FooImpl(Foo):
    def a(self):
        pass

v = FooImpl()
v.foo() 

当我 运行 它打印 Executed foo。但是我预计它会失败,因为我没有在 FooImpl.

中实现 foo

我该如何解决这个问题?

问题在于,当您装饰一个函数(或方法)和 return 一个不同的对象时,您实际上用其他东西替换了函数(方法)。在您的情况下,该方法不再是 abstractmethod 。它是一个包装 abstractmethod 的函数,ABCMeta.

不认为它是抽象的

在这种情况下修复相对容易:functools.wraps:

import functools  # <--- This is new

def logfunc(fn, *args, **kwargs):
    @functools.wraps(fn)   # <--- This is new
    def fncomposite(*args, **kwargs):
        rt = fn(*args, **kwargs)
        print("Executed %s" % fn.__name__)
        return rt
    return fncomposite

这就是您需要更改的全部内容。

随着这一变化,它正确地引发了:

TypeError: Foo has not implemented abstract methods foo

但是您不再需要 LoggerMeta.__init__。当存在未实现的抽象方法时,您可以简单地让 ABCMeta 处理这种情况。如果没有 LoggerMeta.__init__ 方法,这将引发另一个异常:

TypeError: Can't instantiate abstract class FooImpl with abstract methods foo

functools.wraps 不仅可以正确处理抽象方法。它还保留了装饰函数的签名和文档(以及其他一些不错的东西)。如果您使用装饰器来简单地包装函数,您几乎总是想使用 functools.wraps!