检查 derived class 是否定义了特定的实例变量,如果没有则从 metaclass 中抛出错误

Check to see if derived class defines a specific instance variable, throw error from metaclass if not

所以我知道 metaclasses 为我们提供了一种挂钩 class 对象在 Python 中初始化的方法。我可以使用它来检查派生的 class 是否实例化了预期的方法,如下所示:

class BaseMeta(type):
    def __new__(cls, name, bases, body):
        print(cls, name, bases, body)
        if name != 'Base' and 'bar' not in body:
            raise TypeError("bar not defined in derived class")
        return super().__new__(cls, name, bases, body)

class Base(metaclass=BaseMeta):
    def foo(self):
        return self.bar()

class Derived(Base):
    def __init__(self):
        self.path = '/path/to/locality'

    def bar(self):
        return 'bar'

if __name__ == "__main__":
    print(Derived().foo())

在此示例中,如果 Derived class 未定义 Base class 期望的方法,则 metaclass 会引发 TypeError。

我想弄清楚的是,我是否可以对 Derived class 的实例变量实施类似的检查。 IE。我可以使用 metaclass 检查 Derived class 中是否定义了 self.path 变量吗?并且,如果不是,则抛出一个明确的错误,如 "self.path" was not defined in Derived class as a file path.

“正常”实例变量,例如自 Python 2 早期以来教授的那些,无法在 class 创建时检查 - 所有实例变量都是在 __init__(或其他)方法被执行。

然而,自 Python 3.6 以来,可以在 class 主体中“注释”变量 - 这些通常仅用作静态类型检查工具的提示,而静态类型检查工具反过来会执行当程序实际上是 运行.

时什么也没有

但是,当在class body 中注释一个属性时,如果不提供初始值(然后将其创建为“class attribute”),它将显示在命名空间里面__annotations__ 键(而不是键本身)。

简而言之:你可以设计一个 metaclass 要求在 class 主体中注释一个属性,尽管你不能确保它实际上是 filled-in 具有值在 __init__ 之前实际上是 运行。 (但是可以在第一次调用之后检查它-检查这个答案的第二部分)。

总而言之 - 你需要这样的东西:

class BaseMeta(type):
    def __new__(cls, name, bases, namespace):
        print(cls, name, bases, namespace)
        if name != 'Base' and (
            '__annotations__' not in namespace or 
            'bar' not in namespace['__annotations__']
        ):
            raise TypeError("bar not annotated in derived class body")
        return super().__new__(cls, name, bases, namespace)

class Base(metaclass=BaseMeta):
    def foo(self):
        return self.bar

class Derived(Base):
    bar: int
    def __init__(self):
        self.path = '/path/to/locality'
        self.bar = 0

如果 bar: int 不存在于派生的 class 主体中,元 class 将提高。但是,如果 __init__ 中不存在 self.bar = 0,则元 class 无法“知道”它 - 如果没有 运行 代码。

关闭语言中存在的内容

Python“摘要classes”已经有一段时间了——他们 几乎完全按照您的第一个示例所建议的去做:一个人可以授权 派生的 classes 实现具有特定名称的方法。然而, 此检查是在 class 首次 实例化 时进行的,而不是在 它被创建。 (因此允许多于一层的抽象 classes 从另一层继承,并且在那些 none 被实例化):


In [68]: from abc import ABC, abstractmethod                                                                  

In [69]: class Base(ABC): 
    ...:     def foo(self): 
    ...:         ... 
    ...:     @abstractmethod 
    ...:     def bar(self): pass 
    ...:                                                                                                      

In [70]: class D1(Base): pass                                                                                 

In [71]: D1()                                                                                                 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-71-1689c9d98c94> in <module>
----> 1 D1()

TypeError: Can't instantiate abstract class D1 with abstract methods bar

In [72]: class D2(Base): 
    ...:     def bar(self): 
    ...:         ... 
    ...:                                                                                                      

In [73]: D2()                                                                                                 
Out[73]: <__main__.D2 at 0x7ff64270a850>


然后,与“抽象方法”一起,ABC 基础(使用元 class 实现,与您的示例中的不同,尽管它们确实在语言核心中有一些支持),它可以声明“abstractproperties” - 这些被声明为 class attributes,并且会在 class 实例化时引发错误(就像上面一样),如果派生class 不覆盖该属性。与上述“注释”方法的主要区别在于,这实际上需要在 class 主体的属性上设置一个值,而 bar: int 声明不会创建实际的 class属性:

In [75]: import abc                                                                                           

In [76]: class Base(ABC): 
    ...:     def foo(self): 
    ...:         ... 
    ...:     bar = abc.abstractproperty() 
    ...:      
    ...:  
    ...:                                                                                                      

In [77]: class D1(Base): pass                                                                                 

In [78]: D1()                                                                                                 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-78-1689c9d98c94> in <module>
----> 1 D1()

TypeError: Can't instantiate abstract class D1 with abstract methods bar

In [79]: class D2(Base): 
    ...:     bar = 0 
    ...:                                                                                                      

In [80]: D2()                      

我明白这可能不是我们想要的 - 但我提请注意自然的“实例化时间”error-raising,在这些情况下,因为可以做一个..

#...检查实例属性 after __init__ 运行 第一次.

在这种方法中,检查仅在实例化 class 时执行,而不是在声明时执行 - 并且包括将 __init__ 包装在装饰器中,该装饰器将在之后检查所需的属性这是第一次运行:

from functools import wraps

class BaseMeta(type):
    def __init__(cls, name, bases, namespace):
        # Overides __init__ instead of __new__: 
        # we process "cls" after it was created.
        wrapped = cls.__init__
        sentinel = object()
        @wraps(wrapped)
        def _init_wrapper(self, *args, **kw):
            wrapped(self, *args, **kw)
            errored = []
            for attr in cls._required:
                if getattr(self, attr, sentinel) is sentinel:
                    errored.append(attr)
            if errored:
                raise TypeError(f"Class {cls.__name__} did not set attribute{'s' if len(errored) > 1 else ''} {errored} when instantiated")
            # optionally "unwraps" __init__ after the first instance is created:
            cls.__init__ = wrapped
        if cls.__name__ != "Base":
            cls.__init__ = _init_wrapper
        super().__init__(name, bases, namespace)

并在交互模式下检查:

In [84]: class Base(metaclass=BaseMeta): 
    ...:     _required = ["bar"] 
    ...:     def __init__(self): 
    ...:         pass 
    ...:                                                                                                      

In [85]: class Derived(Base): 
    ...:     def __init__(self): 
    ...:         pass 
    ...:                                                                                                      

In [86]: Derived()                                                                                            
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-87-8da841e1a3d5> in <module>
----> 1 Derived()

<ipython-input-83-8bf317642bf5> in _init_wrapper(self, *args, **kw)
     13                     errored.append(attr)
     14             if errored:
---> 15                 raise TypeError(f"Class {cls.__name__} did not set attribute{'s' if len(errored) > 1 else ''} {errored} when instantiated")
     16             # optionally "unwraps" __init__ after the first instance is created:
     17             cls.__init__ = wrapped

TypeError: Class Derived did not set attribute ['bar'] when instantiated

In [87]: class D2(Base): 
    ...:     def __init__(self): 
    ...:         self.bar = 0 
    ...:                                                                                                      

In [88]: D2()                                                                                                 
Out[88]: <__main__.D2 at 0x7ff6418e9a10>