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