python - 检查派生类是否定义了特定的实例变量,如果没有则从元类中抛出错误
问题描述
所以我知道元类为我们提供了一种挂钩 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
.
解决方案
“普通”实例变量,例如自 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>
推荐阅读
- crystal-reports - 可以在水晶报表中增加带有缩进的文本字段
- c# - 如何解析存储过程?
- javascript - 如何使用 Safari 禁用网页上的默认捏合行为?
- java - BufferedReader.readLine() 与 FileReader.read(charArray) 性能
- node.js - FeathersJs:无法设置标题
- c - Projecteuler 3 - 最大素数
- .net - 使用 Visual Studio 2017 附带的 MSBuild 15 构建正常的 .Net 项目
- webpack - React Loadable 如何处理多个包中使用的代码?
- cassandra-2.0 - 如何为 cassandra 客户端程序添加/注册和生成指标
- javascript - 如何在 JS 中决定图片上传质量