2013-08-08 14 views
60

Czy istnieje sposób wywołania kodu, gdy moja klasa jest podklasy?Python: Uruchom kod, gdy klasa jest podklasowana

class SuperClass: 
    def triggered_routine(subclass): 
     print("was subclassed by " + subclass.__name__) 

magically_register_triggered_routine() 

print("foo") 

class SubClass0(SuperClass): 
    pass 

print("bar") 

class SubClass1(SuperClass): 
    print("test") 

powinien wypisać

foo 
was subclassed by SubClass0 
bar 
test 
was subclassed by SubClass1 
+1

Użyj metaclass; metaclasses są wywoływane podczas tworzenia klas, tak jak klasy są wywoływane podczas tworzenia instancji. –

+2

Nie można dodać odpowiedzi, ale dzisiaj python3.6 ma '__init_subclass__' - sprawdź to! –

+1

@ Orduan: dziękuję, brzmi użytecznie. Może nawet wystarczający powód, aby odznaczać to pytanie jako duplikat, ponieważ istnieje teraz dedykowane rozwiązanie mojego problemu zamiast "używać metaclass". –

Odpowiedz

39

zajęciach (domyślnie) są instancjami type. Tak jak instancja klasy Foo jest tworzona przez foo = Foo(...), instancja type (tj. Klasa) jest tworzona przez myclass = type(name, bases, clsdict).

Jeśli chcesz, aby coś szczególnego wydarzyło się w momencie tworzenia klasy, musisz zmodyfikować rzecz tworząc klasę - tj. type. Sposobem na to jest zdefiniowanie podklasy type - tj. Metaklasy.

Metaclass jest do swojej klasy, jako klasa do swojej instancji.

W python2 byś zdefiniował metaklasą o klasie

class SuperClass: 
    __metaclass__ = Watcher 

gdzie Watcher jest podklasą type.

W Python3 składnia została zmieniona na

class SuperClass(metaclass=Watcher) 

Oba są równoważne

Superclass = Watcher(name, bases, clsdict) 

gdzie w tym przypadku name równy ciąg 'Superclass' i bases jest krotka (object,). clsdict to słownik atrybutów klas zdefiniowanych w treści definicji klasy. Podaj podobieństwo do myclass = type(name, bases, clsdict).

Więc, tak jak byłoby użyć klasy na __init__ kontrolować wydarzenia w momencie tworzenia instancji, można kontrolować wydarzenia w momencie utworzenia klasy jest z metaklasa na __init__:


class Watcher(type): 
    def __init__(cls, name, bases, clsdict): 
     if len(cls.mro()) > 2: 
      print("was subclassed by " + name) 
     super(Watcher, cls).__init__(name, bases, clsdict) 

class SuperClass: 
    __metaclass__ = Watcher 


print("foo") 

class SubClass0(SuperClass): 
    pass 

print("bar") 

class SubClass1(SuperClass): 
    print("test") 

drukuje

foo 
was subclassed by SubClass0 
bar 
test 
was subclassed by SubClass1 
+2

Dzięki. Jednak twój kod działa tylko w python2. W python3, SuperClass musiałby być "class SuperClass (metaclass = Watcher): pass' –

+0

Dzięki. Zmieniłem post, by uwzględnić różnicę w składni Python2 i Python3. – unutbu

+1

Dwie kluczowe rzeczy, które pominąłem, gdy próbowałem tego na własną rękę: 1 - Podklasy "Watcher'' type', a nie 'object'. 2 - 'SuperClass' nie ma w ogóle żadnej nadklasy, a nie' obiektu'. Jestem po prostu przyzwyczajony do zapisywania 'obiektu' jako superklasy dla każdej klasy, którą zdefiniowałem, że jakoś tęskniłem za tym, że ten kod używał czegoś innego. Chociaż nie wydaje się, aby istniała jakakolwiek szkoda w tworzeniu 'SuperClass', dziedzicząc po' obiekcie' ... konieczne jest, aby 'Watcher' dziedziczył po' type', a nie 'object'. – ArtOfWarfare

6

Edytuj: Mój stary wpis w rzeczywistości nie działał. Podklasy z classmethod nie działają zgodnie z oczekiwaniami.

Po pierwsze, chcielibyśmy mieć jakiś sposób, aby powiedzieć metaclassowi, że ta konkretna metoda ma mieć specjalne wywoływane zachowanie podklasy, po prostu ustawimy atrybut na funkcję, którą chcemy wywołać. Dla wygody możemy nawet zamienić tę funkcję na classmethod, dzięki czemu można odkryć prawdziwą klasę podstawową, w której się znajduje. Zwrócimy classmethod, aby mógł być użyty jako dekorator, co jest najwygodniejsze.

import types 
import inspect 

def subclass_hook(func): 
    func.is_subclass_hook = True 
    return classmethod(func) 

Mamy również zamiar chcą w wygodny sposób, aby zobaczyć, że subclass_hook dekorator użyto. Wiemy, że użyto classmethod, więc sprawdzimy to i dopiero wtedy poszukujemy atrybutu is_subclass_hook.

def test_subclass_hook(thing): 
    x = (isinstance(thing, types.MethodType) and 
     getattr(thing.im_func, 'is_subclass_hook', False)) 
    return x 

Wreszcie, musimy metaklasa który działa na informacjach: W większości przypadków najbardziej interesującą rzeczą do zrobienia jest po prostu sprawdzić każdy z dostarczonych baz dla haków. W ten sposób super działa w najmniej zaskakujący sposób.

class MyMetaclass(type): 
    def __init__(cls, name, bases, attrs): 
     super(MyMetaclass, cls).__init__(name, bases, attrs) 

     for base in bases: 
      if base is object: 
       continue 
      for name, hook in inspect.getmembers(base, test_subclass_hook): 
       hook(cls) 

i to powinno wystarczyć.

>>> class SuperClass: 
...  __metaclass__ = MyMetaclass 
...  @subclass_hook 
...  def triggered_routine(cls, subclass): 
...   print(cls.__name__ + " was subclassed by " + subclass.__name__) 

>>> class SubClass0(SuperClass): 
...  pass 
SuperClass was subclassed by SubClass0 

>>> class SubClass1(SuperClass): 
...  print("test") 
test 
SuperClass was subclassed by SubClass1 
Powiązane problemy