Проверьте, определяет ли производный класс конкретную переменную экземпляра c, выдает ошибку из метакласса, если нет - PullRequest
0 голосов
/ 04 августа 2020

Итак, я знаю, что метаклассы предоставляют нам возможность подключиться к инициализации объектов класса в 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 не определяет метод, который ожидает базовый класс .

Я пытаюсь выяснить, смогу ли я реализовать аналогичную проверку для переменных экземпляра класса Derived. IE, могу ли я использовать метакласс, чтобы проверить, определена ли переменная self.path в классе Derived? И, если нет, выдать явную ошибку, говоря что-то вроде "self.path" was not defined in Derived class as a file path.

1 Ответ

1 голос
/ 08 августа 2020

«Нормальные» переменные экземпляра, такие как те, которые преподавались с тех пор, как 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>

...