检查 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>
所以我知道 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>