首页 > 解决方案 > 检查派生类是否定义了特定的实例变量,如果没有则从元类中抛出错误

问题描述

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

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 类未定义 Base 类期望的方法,则元类将引发 TypeError。

我想弄清楚的是我是否可以对 Derived 类的实例变量进行类似的检查。IE 我可以使用元类来检查self.path变量是否在派生类中定义?如果没有,抛出一个明确的错误,比如"self.path" was not defined in Derived class as a file path.

标签: pythonoopmetaclass

解决方案


“普通”实例变量,例如自 Python 2 早期以来教授的实例变量,无法在类创建时检查 - 所有实例变量都是在执行__init__(或其他)方法时动态创建的。

但是,从 Python 3.6 开始,可以在类主体中“注释”变量——这些通常仅作为静态类型检查工具的提示,而这些工具在程序实际运行时什么也不做。

但是,当在类主体中注释属性时,不提供初始值(然后将其创建为“类属性”),它将显示在__annotations__键内的命名空间中(而不是作为键本身)。

简而言之:您可以设计一个需要在类主体中注释属性的元类,尽管您不能确保它在__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派生类主体中不存在,则元类将引发。但是,如果self.bar = 0内部不存在__init__,则元类无法“知道”它 - 除非运行代码。

关闭语言中存在的东西

Python 中的“抽象类”已经有一段时间了——它们几乎完全按照您的第一个示例的建议进行:可以强制派生类实现具有特定名称的方法。但是,此检查是在类首次实例化时进行的,而不是在创建时进行。(因此允许多个抽象类从另一个继承一个,并且只要没有一个实例化就可以工作):


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>


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

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()                      

我不明白这可能是不可取的 - 但我提请注意自然的“实例化时间”错误引发,在这些情况下,因为有可能做一个..

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

在这种方法中,检查仅在类被实例化时执行,而不是在它被声明时执行 - 并且包含在__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>


推荐阅读