Wiem więc, że metaklasy zapewniają nam sposób na podłączenie do inicjalizacji obiektów klasy w Pythonie. Mogę to użyć, aby sprawdzić, czy klasa pochodna instancja tworzy oczekiwana metoda, tak jak:

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())

W tym przykładzie, metaklas podnosi typeError, jeśli klasa pochodna nie określa metody, którą oczekuje klasy podstawy.

Co próbuję dowiedzieć się, czy mogę wdrożyć podobny czek do zmiennych instancji klasy pochodnej. TO ZNACZY. Czy mogę użyć metaklasu, aby sprawdzić, czy zmienna self.path jest zdefiniowana w klasie pochodnej? A jeśli nie, rzuć wyraźny błąd mówiąc coś takiego jak "self.path" was not defined in Derived class as a file path.

0
Michael Green 4 sierpień 2020, 01:51

1 odpowiedź

Najlepsza odpowiedź

"Normalne" zmienne instancji, takie jak te, które nauczane od Pythona 2 nie można sprawdzić w czasie tworzenia klasy - wszystkie zmienne instancji są dynamicznie tworzone, gdy metoda {{x0} (lub inna) jest wykonywana.

Ponieważ jednak Python 3.6 Możliwe jest "adnotacja" zmiennych w korpusie klasy - one zwykle służą wyłącznie jako podpowiedź dla narzędzi do kontroli typu statycznego, co z kolei nic nie robią, gdy program jest faktycznie uruchomiony.

Jednak przy adnotacji atrybutu w korpusie klasy, bez dostarczania wartości początkowej (która wtedy utworzyłaby ją jako "atrybut klasy"), pokaże się w przestrzeni nazw wewnątrz klucza __annotations__ (a nie jako klucza sami).

Krótko mówiąc: możesz zaprojektować metaklas wymagający atrybutu, który ma być adnotowany w korpusie klasy, chociaż nie można upewnić się, że jest to faktycznie wypełnione o wartości wewnątrz {x0}}, zanim będzie faktycznie uruchomić. (Ale można go sprawdzić po nazywa się to pierwszy raz - sprawdź drugą część tej odpowiedzi).

W sumie - potrzebujesz czegoś takiego:

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

Jeśli bar: int nie jest obecny w korpusie klasy pochodnej, podnosi się metaklas. Jednakże, jeśli self.bar = 0 nie jest obecny wewnątrz __init__, Metaclas nie ma sposobu, aby "znać", nie bez uruchamiania kodu.

Zamknij rzeczy obecne w języku

Było od jakiegoś czasu w Pythonie "AbstractClasses" Zrób prawie dokładnie, jaki proponuje twój pierwszy przykład: można mandat To pochodne klasy wdrażają metody o określonej nazwie. Jednak, To sprawdzanie jest wykonane, gdy klasa jest pierwsza instancja , a nie kiedy jest tworzony. (Dzięki temu, co pozwala na więcej niż jeden poziom klas abstrakcyjnych dziedziczących jeden z innego, a to działa tak daleko, że żaden z nich jest instancji):


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>


A następnie wraz z "AbstractMethods", bazy ABC (które są zaimplementowane z metaklaską, nie w przeciwieństwie do tego w swoim przykładzie, chociaż mają one pewne wsparcie w rdzeniu językowym), możliwe jest zadeklarowanie "AbstractProperties" - to są Deklarowane jako atrybuty klasy i podniosą błąd podczas instancji klasy (tak jak powyżej), jeśli klasa pochodna nie zastępują atrybutu. Główną różnicą w powyższym podejściu "adnotacje" jest to, że faktycznie wymaga wartości do ustawiania na atrybucie na korpusie klasy, w którym jako deklaracja bar: int nie tworzy rzeczywistej atrybutu klasy:

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()                      

Rozumiem, że może nie być pożądane - ale zwracałem uwagę na naturalny "Time Time" w tych przypadkach, ponieważ możliwe jest, aby zrobić ..

# ... Sprawdź atrybut instancji po __init__ Uruchom po raz pierwszy.

W tym podejściu sprawdzenie jest wykonywane tylko wtedy, gdy klasa jest tworzona, a nie, gdy jest zadeklarowany - i polega na rzuceniu __init__ w dekoratorie, który sprawdzi wymagane atrybuty po uruchomieniu po raz pierwszy:

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)

I sprawdzanie tego w trybie interaktywnym:

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>

1
jsbueno 8 sierpień 2020, 04:19