检查派生类是否定义了特定的实例变量,如果没有,则从元类抛出错误



所以我知道元类为我们提供了一种挂接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())

在本例中,如果派生类没有定义基类所期望的方法,则元类将引发TypeError。

我想弄清楚的是,我是否可以对Derived类的实例变量实现类似的检查。I.E.我可以使用元类来检查self.path变量是否在Derived类中定义?如果没有,则抛出一个显式错误,表示类似"self.path" was not defined in Derived class as a file path的内容。

"正常";实例变量,例如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>

然后,连同";抽象方法";,ABC基(用元类实现,与您的示例中的元类没有什么不同,尽管它们在语言核心中确实有一些支持(;抽象性质"-这些被声明为classattributes,如果派生类没有覆盖该属性,则在类实例化时会引发错误(就像上面一样(。与";注释";上面的方法是,这实际上需要在类主体上的属性上设置一个值,其中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()                      

我不知道这可能是不可取的,但我呼吁人们注意自然的";实例化时间";在这些情况下,由于有可能进行a.,因此会引发错误

#。。。在首次运行__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>

最新更新