«Нормальные» переменные экземпляра, такие как те, которые преподавались с тех пор, как Python 2 первых дня не могут быть проверены во время создания класса - все переменные экземпляра динамически создаются при выполнении метода __init__
(или другого).
Однако, начиная с Python 3.6, можно «аннотировать» переменные в теле класса - они обычно служат только в качестве подсказки для инструментов проверки типов c stati, которые, в свою очередь, ничего не делают, когда программа фактически запущена.
Однако при аннотировании атрибута в теле класса без предоставления начального значения (которое затем создавало бы его как «атрибут класса») оно будет отображаться в пространстве имен внутри __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>
И затем, наряду с «абстрактными методами», AB C баз (которые реализованы с помощью метакласса, мало чем отличающегося от метакласса в вашем примере, хотя они имеют некоторую поддержку в ядре языка), можно объявить «абстрактные свойства» - они объявлены как класс атрибуты, и вызовет ошибку при создании экземпляра класса (как указано выше), если производный класс не переопределит атрибут. Основное отличие от подхода «аннотации», описанного выше, заключается в том, что на самом деле для этого требуется установить значение атрибута в теле класса, тогда как объявление 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>