装饰ABCMeta子类中的所有方法时,强制执行abstractmethod行为

时间:2018-07-05 06:53:54

标签: python python-3.x metaprogramming metaclass abstract-methods

我想实现一个用于包装方法的元类以记录其他信息。但是我还需要拥有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。但是,我期望它会失败,因为我没有在foo中实现FooImpl

我该如何解决?

1 个答案:

答案 0 :(得分:1)

问题是,当您装饰一个函数(或方法)并返回另一个对象时,您实际上用其他东西替换了该函数(方法)。在您的情况下,该方法不再是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