Jakie są (konkretne) przypadki użycia metaklas?

118

Mam znajomego, który lubi używać metaklas i regularnie oferuje je jako rozwiązanie.

Jestem zdania, że ​​prawie nigdy nie musisz używać metaklas. Czemu? ponieważ myślę, że jeśli robisz coś takiego z klasą, prawdopodobnie powinieneś robić to z obiektem. Konieczne jest małe przeprojektowanie / refaktoryzacja.

Możliwość korzystania z metaklas spowodowała, że ​​wiele osób w wielu miejscach używa klas jako pewnego rodzaju obiektu drugorzędnego, co po prostu wydaje mi się katastrofalne. Czy programowanie ma zostać zastąpione przez metaprogramowanie? Dodanie dekoratorów klasowych niestety uczyniło go jeszcze bardziej akceptowalnym.

Więc proszę, jestem zdesperowany, aby poznać twoje prawidłowe (konkretne) przypadki użycia metaklas w Pythonie. Albo aby dowiedzieć się, dlaczego czasami mutowanie klas jest lepsze niż mutowanie obiektów.

Zacznę:

Czasami podczas korzystania z biblioteki innej firmy przydatna jest możliwość zmodyfikowania klasy w określony sposób.

(To jedyny przypadek, jaki przychodzi mi do głowy i nie jest konkretny)

Ali Afshar
źródło
3
To świetne pytanie. Sądząc po odpowiedziach poniżej, jest całkiem jasne, że nie ma czegoś takiego jak konkretne zastosowanie metaklas.
Marcus Ottosson,

Odpowiedzi:

25

Mam klasę, która obsługuje nieinteraktywne kreślenie, jako nakładka na Matplotlib. Jednak czasami chce się wykonać interaktywne kreślenie. Dzięki kilku funkcjom stwierdziłem, że byłem w stanie zwiększyć liczbę figur, ręcznie wywołać rysowanie itp., Ale musiałem to zrobić przed i po każdym wywołaniu kreślenia. Tak więc, aby utworzyć zarówno interaktywne opakowanie do drukowania, jak i opakowanie do drukowania poza ekranem, okazało się, że bardziej wydajne jest zrobienie tego za pomocą metaklasy, zawijając odpowiednie metody, niż zrobić coś takiego:

class PlottingInteractive:
    add_slice = wrap_pylab_newplot(add_slice)

Ta metoda nie nadąża za zmianami API i tak dalej, ale taka, która iteruje po atrybutach klasy __init__przed ponownym ustawieniem atrybutów klasy, jest bardziej wydajna i zapewnia aktualność:

class _Interactify(type):
    def __init__(cls, name, bases, d):
        super(_Interactify, cls).__init__(name, bases, d)
        for base in bases:
            for attrname in dir(base):
                if attrname in d: continue # If overridden, don't reset
                attr = getattr(cls, attrname)
                if type(attr) == types.MethodType:
                    if attrname.startswith("add_"):
                        setattr(cls, attrname, wrap_pylab_newplot(attr))
                    elif attrname.startswith("set_"):
                        setattr(cls, attrname, wrap_pylab_show(attr))

Oczywiście mogą być lepsze sposoby, aby to zrobić, ale okazało się, że jest to skuteczne. Oczywiście można to również zrobić w programie __new__lub __init__, ale było to rozwiązanie, które uznałem za najprostsze.

Matt
źródło
103

Ostatnio zadano mi to samo pytanie i otrzymałem kilka odpowiedzi. Mam nadzieję, że mogę ożywić ten wątek, ponieważ chciałem rozwinąć kilka z wymienionych przypadków użycia i dodać kilka nowych.

Większość metaklas, które widziałem, robi jedną z dwóch rzeczy:

  1. Rejestracja (dodanie klasy do struktury danych):

    models = {}
    
    class ModelMetaclass(type):
        def __new__(meta, name, bases, attrs):
            models[name] = cls = type.__new__(meta, name, bases, attrs)
            return cls
    
    class Model(object):
        __metaclass__ = ModelMetaclass
    

    Za każdym razem, gdy tworzysz podklasę Model, Twoja klasa jest rejestrowana w modelssłowniku:

    >>> class A(Model):
    ...     pass
    ...
    >>> class B(A):
    ...     pass
    ...
    >>> models
    {'A': <__main__.A class at 0x...>,
     'B': <__main__.B class at 0x...>}
    

    Można to również zrobić za pomocą dekoratorów klas:

    models = {}
    
    def model(cls):
        models[cls.__name__] = cls
        return cls
    
    @model
    class A(object):
        pass
    

    Lub z wyraźną funkcją rejestracji:

    models = {}
    
    def register_model(cls):
        models[cls.__name__] = cls
    
    class A(object):
        pass
    
    register_model(A)
    

    Właściwie jest to prawie to samo: niepomyślnie wspominasz o dekoratorach klas, ale tak naprawdę jest to nic innego jak cukier syntaktyczny dla wywołania funkcji w klasie, więc nie ma w tym żadnej magii.

    W każdym razie zaletą metaklas w tym przypadku jest dziedziczenie, ponieważ działają one dla dowolnych podklas, podczas gdy inne rozwiązania działają tylko dla podklas jawnie ozdobionych lub zarejestrowanych.

    >>> class B(A):
    ...     pass
    ...
    >>> models
    {'A': <__main__.A class at 0x...> # No B :(
    
  2. Refaktoryzacja (modyfikacja atrybutów klas lub dodanie nowych):

    class ModelMetaclass(type):
        def __new__(meta, name, bases, attrs):
            fields = {}
            for key, value in attrs.items():
                if isinstance(value, Field):
                    value.name = '%s.%s' % (name, key)
                    fields[key] = value
            for base in bases:
                if hasattr(base, '_fields'):
                    fields.update(base._fields)
            attrs['_fields'] = fields
            return type.__new__(meta, name, bases, attrs)
    
    class Model(object):
        __metaclass__ = ModelMetaclass
    

    Za każdym razem, gdy podklasujesz Modeli definiujesz niektóre Fieldatrybuty, są one wstrzykiwane wraz z ich nazwami (na przykład w celu uzyskania bardziej szczegółowych komunikatów o błędach) i grupowane w _fieldssłowniku (dla łatwej iteracji, bez konieczności przeglądania wszystkich atrybutów klasy i wszystkich jej klas podstawowych '' atrybuty za każdym razem):

    >>> class A(Model):
    ...     foo = Integer()
    ...
    >>> class B(A):
    ...     bar = String()
    ...
    >>> B._fields
    {'foo': Integer('A.foo'), 'bar': String('B.bar')}
    

    Ponownie, można to zrobić (bez dziedziczenia) za pomocą dekoratora klas:

    def model(cls):
        fields = {}
        for key, value in vars(cls).items():
            if isinstance(value, Field):
                value.name = '%s.%s' % (cls.__name__, key)
                fields[key] = value
        for base in cls.__bases__:
            if hasattr(base, '_fields'):
                fields.update(base._fields)
        cls._fields = fields
        return cls
    
    @model
    class A(object):
        foo = Integer()
    
    class B(A):
        bar = String()
    
    # B.bar has no name :(
    # B._fields is {'foo': Integer('A.foo')} :(
    

    Lub wyraźnie:

    class A(object):
        foo = Integer('A.foo')
        _fields = {'foo': foo} # Don't forget all the base classes' fields, too!
    

    Chociaż, w przeciwieństwie do twojego poparcia dla czytelnego i łatwego w utrzymaniu programowania niemetatowego, jest to znacznie bardziej uciążliwe, nadmiarowe i podatne na błędy:

    class B(A):
        bar = String()
    
    # vs.
    
    class B(A):
        bar = String('bar')
        _fields = {'B.bar': bar, 'A.foo': A.foo}
    

Biorąc pod uwagę najbardziej powszechne i konkretne przypadki użycia, jedynymi przypadkami, w których absolutnie MUSISZ używać metaklas, są sytuacje, gdy chcesz zmodyfikować nazwę klasy lub listę klas bazowych, ponieważ po zdefiniowaniu parametry te są wstawiane do klasy i nie ma dekoratora lub funkcja może je rozpalić.

class Metaclass(type):
    def __new__(meta, name, bases, attrs):
        return type.__new__(meta, 'foo', (int,), attrs)

class Baseclass(object):
    __metaclass__ = Metaclass

class A(Baseclass):
    pass

class B(A):
    pass

print A.__name__ # foo
print B.__name__ # foo
print issubclass(B, A)   # False
print issubclass(B, int) # True

Może to być przydatne w frameworkach do wydawania ostrzeżeń, gdy zdefiniowane są klasy o podobnych nazwach lub niepełnych drzewach dziedziczenia, ale nie mogę wymyślić powodu poza trollowaniem, aby faktycznie zmienić te wartości. Może David Beazley może.

W każdym razie w Pythonie 3 metaklasy mają również __prepare__metodę, która pozwala oszacować treść klasy na odwzorowanie inne niż a dict, obsługując w ten sposób uporządkowane atrybuty, przeciążone atrybuty i inne niesamowite fajne rzeczy:

import collections

class Metaclass(type):

    @classmethod
    def __prepare__(meta, name, bases, **kwds):
        return collections.OrderedDict()

    def __new__(meta, name, bases, attrs, **kwds):
        print(list(attrs))
        # Do more stuff...

class A(metaclass=Metaclass):
    x = 1
    y = 2

# prints ['x', 'y'] rather than ['y', 'x']

 

class ListDict(dict):
    def __setitem__(self, key, value):
        self.setdefault(key, []).append(value)

class Metaclass(type):

    @classmethod
    def __prepare__(meta, name, bases, **kwds):
        return ListDict()

    def __new__(meta, name, bases, attrs, **kwds):
        print(attrs['foo'])
        # Do more stuff...

class A(metaclass=Metaclass):

    def foo(self):
        pass

    def foo(self, x):
        pass

# prints [<function foo at 0x...>, <function foo at 0x...>] rather than <function foo at 0x...>

Możesz argumentować, że uporządkowane atrybuty można osiągnąć za pomocą liczników tworzenia, a przeciążenie można symulować za pomocą domyślnych argumentów:

import itertools

class Attribute(object):
    _counter = itertools.count()
    def __init__(self):
        self._count = Attribute._counter.next()

class A(object):
    x = Attribute()
    y = Attribute()

A._order = sorted([(k, v) for k, v in vars(A).items() if isinstance(v, Attribute)],
                  key = lambda (k, v): v._count)

 

class A(object):

    def _foo0(self):
        pass

    def _foo1(self, x):
        pass

    def foo(self, x=None):
        if x is None:
            return self._foo0()
        else:
            return self._foo1(x)

Poza tym, że jest dużo bardziej brzydki, jest również mniej elastyczny: co jeśli chcesz uporządkować atrybuty dosłowne, takie jak liczby całkowite i łańcuchy? A co, jeśli Nonejest to prawidłowa wartość x?

Oto kreatywny sposób rozwiązania pierwszego problemu:

import sys

class Builder(object):
    def __call__(self, cls):
        cls._order = self.frame.f_code.co_names
        return cls

def ordered():
    builder = Builder()
    def trace(frame, event, arg):
        builder.frame = frame
        sys.settrace(None)
    sys.settrace(trace)
    return builder

@ordered()
class A(object):
    x = 1
    y = 'foo'

print A._order # ['x', 'y']

A oto kreatywny sposób na rozwiązanie drugiego:

_undefined = object()

class A(object):

    def _foo0(self):
        pass

    def _foo1(self, x):
        pass

    def foo(self, x=_undefined):
        if x is _undefined:
            return self._foo0()
        else:
            return self._foo1(x)

Ale to dużo, DUŻO voodoo-er niż zwykła metaklasa (zwłaszcza pierwsza, która naprawdę topi twój mózg). Chodzi mi o to, że patrzysz na metaklasy jako nieznane i sprzeczne z intuicją, ale możesz też spojrzeć na nie jako na kolejny krok ewolucji języków programowania: po prostu musisz dostosować swój sposób myślenia. W końcu mógłbyś prawdopodobnie zrobić wszystko w C, w tym zdefiniować strukturę ze wskaźnikami do funkcji i przekazać ją jako pierwszy argument do jej funkcji. Osoba, która zobaczy C ++ po raz pierwszy, może powiedzieć: „co to za magia? Dlaczego kompilator niejawnie przechodzithisdo metod, ale nie do funkcji regularnych i statycznych? Lepiej jest mówić wprost i rozwlekle w swoich argumentach. ”Ale z drugiej strony programowanie zorientowane obiektowo jest znacznie potężniejsze, kiedy już je zrozumiesz; i tak samo jest z tym, uh ... programowaniem quasi-aspektowym, jak sądzę. rozumiesz metaklasy, w rzeczywistości są one bardzo proste, więc dlaczego nie używać ich, gdy jest to wygodne?

I wreszcie metaklasy są świetne, a programowanie powinno być zabawne. Korzystanie ze standardowych konstrukcji programistycznych i wzorców projektowych przez cały czas jest nudne i mało inspirujące oraz ogranicza wyobraźnię. Żyć trochę! Oto metametaklasa specjalnie dla Ciebie.

class MetaMetaclass(type):
    def __new__(meta, name, bases, attrs):
        def __new__(meta, name, bases, attrs):
            cls = type.__new__(meta, name, bases, attrs)
            cls._label = 'Made in %s' % meta.__name__
            return cls 
        attrs['__new__'] = __new__
        return type.__new__(meta, name, bases, attrs)

class China(type):
    __metaclass__ = MetaMetaclass

class Taiwan(type):
    __metaclass__ = MetaMetaclass

class A(object):
    __metaclass__ = China

class B(object):
    __metaclass__ = Taiwan

print A._label # Made in China
print B._label # Made in Taiwan

Edytować

To dość stare pytanie, ale wciąż otrzymuję głosy za, więc pomyślałem, że dodam link do bardziej wyczerpującej odpowiedzi. Jeśli chcesz przeczytać więcej o metaklasach i ich zastosowaniach, właśnie opublikowałem tutaj artykuł na ten temat .

Dan Gittik
źródło
5
To świetna odpowiedź, dziękuję za czas poświęcony na jej napisanie i podanie wielu przykładów
Chen A.
„... zaletą metaklas w tym przypadku jest dziedziczenie, ponieważ działają one z dowolnymi podklasami” - chyba nie w Pythonie 3? Myślę, że działa w Pythonie 2 tylko dlatego, że każda klasa potomna dziedziczy __metaclass__atrybut, ale ten atrybut nie jest już specjalny w Pythonie 3. Czy istnieje sposób, aby to "klasy potomne były również konstruowane przez metaklasę rodzica" działały w Pythonie 3 ?
ForceBru
2
Dotyczy to również Pythona 3, ponieważ klasa B, dziedzicząca po A, której metaklasa to M, jest również typem M.W związku z tym, gdy wartość B jest oceniana, wywoływana jest M, aby ją utworzyć, co skutecznie pozwala „pracować na dowolnych podklasach” (z A). Powiedziawszy to, Python 3.6 wprowadził znacznie prostsze init_subclass, więc teraz możesz manipulować podklasami w klasie bazowej i nie potrzebujesz już do tego celu metaklasy.
Dan Gittik
To jest genialne, przeczytałem tak wiele postów na blogu na temat metaklas, tylko ten jeden zna zalety i wady oraz alternatywy dla metaklasy.
ospider
36

Celem metaklas nie jest zastępowanie rozróżnienia klasy / obiektu metaklasą / klasą - ma to na celu zmianę zachowania definicji klas (a tym samym ich instancji) w jakiś sposób. W rzeczywistości ma to na celu zmianę zachowania instrukcji class w sposób, który może być bardziej przydatny dla Twojej konkretnej domeny niż domyślny. Użyłem ich do:

  • Podklasy śledzące, zwykle do rejestrowania programów obsługi. Jest to przydatne, gdy używasz konfiguracji w stylu wtyczki, gdzie chcesz zarejestrować procedurę obsługi dla określonej rzeczy, po prostu przez podklasę i ustawienie kilku atrybutów klas. na przykład. załóżmy, że piszesz program obsługi dla różnych formatów muzycznych, gdzie każda klasa implementuje odpowiednie metody (tagi odtwarzania / pobierania itp.) dla swojego typu. Dodanie procedury obsługi dla nowego typu staje się:

    class Mp3File(MusicFile):
        extensions = ['.mp3']  # Register this type as a handler for mp3 files
        ...
        # Implementation of mp3 methods go here

    Następnie metaklasa utrzymuje słownik {'.mp3' : MP3File, ... }itp. I konstruuje obiekt odpowiedniego typu, gdy żądasz programu obsługi za pośrednictwem funkcji fabryki.

  • Zmiana zachowania. Możesz chcieć nadać specjalne znaczenie pewnym atrybutom, powodując zmianę zachowania, gdy są obecne. Na przykład możesz chcieć poszukać metod o nazwie _get_fooi _set_fooprzekonwertować je w sposób przezroczysty na właściwości. Jako przykład ze świata rzeczywistego, oto przepis, który napisałem, aby podać więcej definicji struktur podobnych do C. Metaklasa jest używana do konwersji zadeklarowanych elementów na łańcuch formatu struct, obsługi dziedziczenia itp. Oraz do stworzenia klasy, która może sobie z tym poradzić.

    Aby zapoznać się z innymi przykładami ze świata rzeczywistego, przyjrzyj się różnym ORMom, takim jak ORM sqlalchemy lub sqlobject . Ponownie, celem jest interpretacja definicji (tutaj definicji kolumn SQL) o szczególnym znaczeniu.

Brian
źródło
3
Cóż, tak, śledzenie podklas. Ale dlaczego miałbyś kiedykolwiek tego chcieć? Twój przykład jest niejawny dla register_music_file (Mp3File, ['.mp3']), a jawny sposób jest bardziej czytelny i łatwiejszy w utrzymaniu. To jest przykład złych przypadków, o których mówię.
Ali Afshar,
Jeśli chodzi o przypadek ORM, czy mówisz o opartym na klasach sposobie definiowania tabel lub metaklas na mapowanych obiektach. Ponieważ SQLAlchemy może (słusznie) mapować do dowolnej klasy (i zakładam, że nie używa metaklasy do tego działania).
Ali Afshar,
10
Wolę bardziej deklaratywny styl, zamiast wymagać dodatkowych metod rejestracji dla każdej podklasy - lepiej, jeśli wszystko jest opakowane w jednym miejscu.
Brian
W przypadku sqlalchemy myślę głównie o warstwie deklaratywnej, więc być może sqlobject jest lepszym przykładem. Jednak metaklasy używane wewnętrznie są również przykładami podobnej reinterpretacji poszczególnych atrybutów w celu zadeklarowania znaczenia.
Brian
2
Przepraszam, jedno z moich przekonań zgubiło się w scenariuszu przekroczenia limitu czasu SO. Uważam, że zajęcia deklaratywne są niemal obrzydliwością. Wiem, że ludzie to uwielbiają i jest to akceptowane zachowanie. Ale (z doświadczenia) wiem, że jest to bezużyteczne w sytuacji, gdy chcesz coś zadeklarować ONZ. Wyrejestrowanie się z zajęć jest trudne .
Ali Afshar,
17

Zacznijmy od klasycznego cytatu Tima Petera:

Metaklasy to głębsza magia, niż 99% użytkowników powinno się kiedykolwiek martwić. Jeśli zastanawiasz się, czy ich potrzebujesz, to nie (ludzie, którzy ich faktycznie potrzebują, wiedzą z całą pewnością, że ich potrzebują i nie potrzebują wyjaśnienia, dlaczego). Tim Peters (clp po 22.12.2002)

Powiedziawszy to, natknąłem się (okresowo) na prawdziwe zastosowania metaklas. Przychodzi mi na myśl w Django, gdzie wszystkie modele dziedziczą po modelach. Model z kolei wykonuje poważną magię, aby otoczyć modele DB dobrocią ORM Django. Ta magia dzieje się za pośrednictwem metaklas. Tworzy wszelkiego rodzaju klasy wyjątków, klasy menedżerów itp.

Zobacz django / db / models / base.py, klasa ModelBase () na początek historii.

Peter Rowell
źródło
Cóż, tak, rozumiem. Nie zastanawiam się „jak” lub „dlaczego” używać metaklas, zastanawiam się „kto” i „co”. Widzę tutaj, że ORMy są częstym przypadkiem. Niestety ORM Django jest dość kiepski w porównaniu do SQLAlchemy, który ma mniej magii. Magia jest zła, a metaklasy naprawdę nie są do tego potrzebne.
Ali Afshar,
9
Po przeczytaniu cytatu Tima Petersa w przeszłości czas pokazał, że jego stwierdzenie jest raczej niepomocne. Dopiero po zbadaniu metaklasy Pythona w StackOverflow stało się jasne, jak je w ogóle zaimplementować. Po tym, jak zmusiłem się do nauki pisania i używania metaklas, ich umiejętności zaskoczyły mnie i pozwoliły mi lepiej zrozumieć, jak naprawdę działa Python. Klasy mogą zapewniać kod wielokrotnego użytku, a metaklasy mogą zapewniać ulepszenia wielokrotnego użytku dla tych klas.
Noctis Skytower
6

Metaklasy mogą być przydatne do tworzenia języków specyficznych dla domeny w Pythonie. Konkretnymi przykładami są Django, deklaratywna składnia schematów bazy danych SQLObject.

Podstawowy przykład z A Conservative Metaclass autorstwa Iana Bickinga :

Metaklasy, których używałem, służyły przede wszystkim do obsługi pewnego rodzaju deklaratywnego stylu programowania. Weźmy na przykład pod uwagę schemat walidacji:

class Registration(schema.Schema):
    first_name = validators.String(notEmpty=True)
    last_name = validators.String(notEmpty=True)
    mi = validators.MaxLength(1)
    class Numbers(foreach.ForEach):
        class Number(schema.Schema):
            type = validators.OneOf(['home', 'work'])
            phone_number = validators.PhoneNumber()

Inne techniki: Składniki do budowania DSL w Pythonie (pdf).

Edytuj (autor: Ali): Wolałbym, aby to zrobić przy użyciu kolekcji i instancji. Ważnym faktem są instancje, które dają więcej mocy i eliminują powód do używania metaklas. Ponadto warto zauważyć, że Twój przykład używa mieszanki klas i instancji, co z pewnością wskazuje, że nie możesz tego wszystkiego zrobić za pomocą metaklas. I tworzy naprawdę niejednolity sposób na zrobienie tego.

number_validator = [
    v.OneOf('type', ['home', 'work']),
    v.PhoneNumber('phone_number'),
]

validators = [
    v.String('first_name', notEmpty=True),
    v.String('last_name', notEmpty=True),
    v.MaxLength('mi', 1),
    v.ForEach([number_validator,])
]

Nie jest doskonały, ale już prawie nie ma magii, nie ma potrzeby stosowania metaklas i poprawiono jednolitość.

jfs
źródło
Dzięki za to. To jest bardzo dobry przykład przypadku użycia, który moim zdaniem jest niepotrzebny, brzydki i niemożliwy do zarządzania, co byłoby prostsze w oparciu o prostą instancję kolekcji (z zagnieżdżonymi kolekcjami zgodnie z wymaganiami).
Ali Afshar
1
@Ali A: zapraszamy do przedstawienia konkretnego przykładu porównania równoległego między deklaratywną składnią za pomocą metaklas a podejściem opartym na prostej instancji kolekcji.
jfs
@ Ali A: możesz edytować moją odpowiedź w miejscu, aby dodać przykład stylu kolekcji.
jfs
Ok, zrobiłem to. Przepraszamy, spieszy mi się dzisiaj, ale spróbuję odpowiedzieć na wszelkie pytania później / jutro. Wesołych Świąt!
Ali Afshar,
2
Drugi przykład jest brzydki, ponieważ trzeba było powiązać instancję walidatora z ich nazwą. Nieco lepszym sposobem na zrobienie tego jest użycie słownika zamiast listy, ale z drugiej strony, w klasach Pythona są po prostu cukier składniowy dla słownika, więc dlaczego nie użyć klas? Otrzymujesz również bezpłatne sprawdzanie poprawności nazwy, ponieważ babes Pythona nie może zawierać spacji ani znaków specjalnych, które mógłby zawierać ciąg.
Lie Ryan
6

Rozsądnym wzorcem użycia metaklasy jest zrobienie czegoś raz, gdy klasa jest zdefiniowana, a nie powtarzanie się, gdy tworzona jest instancja tej samej klasy.

Gdy wiele klas ma to samo specjalne zachowanie, powtarzanie __metaclass__=Xjest oczywiście lepsze niż powtarzanie kodu specjalnego przeznaczenia i / lub wprowadzanie współdzielonych klas nadrzędnych ad hoc.

Ale nawet z tylko jedną klasą specjalną i bez przewidywalnego rozszerzenia __new__oraz __init__metaklasy są czystszym sposobem inicjowania zmiennych klas lub innych danych globalnych niż mieszanie kodu specjalnego przeznaczenia i normalnych defi classinstrukcji w treści definicji klasy.

user412090
źródło
5

Jedyny raz, kiedy użyłem metaklas w Pythonie, to podczas pisania wrappera dla Flickr API.

Moim celem było zeskrobanie strony API Flickr i dynamiczne wygenerowanie pełnej hierarchii klas, aby umożliwić dostęp do API za pomocą obiektów Pythona:

# Both the photo type and the flickr.photos.search API method 
# are generated at "run-time"
for photo in flickr.photos.search(text=balloons):
    print photo.description

W tym przykładzie, ponieważ wygenerowałem całe API Python Flickr ze strony internetowej, tak naprawdę nie znam definicji klas w czasie wykonywania. Możliwość dynamicznego generowania typów była bardzo przydatna.

Tryptyk
źródło
2
Możesz dynamicznie generować typy bez używania metaklas. >>> pomoc (wpisz)
Ali Afshar
8
Nawet jeśli nie jesteś tego świadomy, to za pomocą metaclasses wtedy. type to metaklasa, w rzeczywistości najbardziej powszechna. :-)
Veky
5

Myślałem o tym samym wczoraj i całkowicie się z tym zgadzam. Komplikacje w kodzie spowodowane próbami uczynienia go bardziej deklaratywnym generalnie sprawiają, że baza kodu jest trudniejsza w utrzymaniu, trudniejsza do odczytania i moim zdaniem mniej pytoniczna. Zwykle wymaga również dużo kopiowania.copy () ing (aby zachować dziedziczenie i kopiować z klasy do instancji) i oznacza, że ​​musisz szukać w wielu miejscach, aby zobaczyć, co się dzieje (zawsze patrząc od metaklasy w górę), co jest sprzeczne z ziarno pytona również. Przeglądałem kod formencode i sqlalchemy, aby zobaczyć, czy taki deklaratywny styl był tego wart, a wyraźnie nie. Taki styl należy pozostawić deskryptorom (takim jak właściwość i metody) oraz niezmiennym danym. Ruby ma lepsze wsparcie dla takich deklaratywnych stylów i cieszę się, że podstawowy język Pythona nie idzie tą drogą.

Widzę ich zastosowanie do debugowania, dodaję metaklasę do wszystkich klas podstawowych, aby uzyskać bogatsze informacje. Widzę również ich użycie tylko w (bardzo) dużych projektach, aby pozbyć się pewnego standardowego kodu (ale z utratą przejrzystości). Na przykład sqlalchemy używa ich gdzie indziej, aby dodać określoną niestandardową metodę do wszystkich podklas w oparciu o wartość atrybutu w ich definicji klasy, np. przykład zabawki

class test(baseclass_with_metaclass):
    method_maker_value = "hello"

mogłaby mieć metaklasę, która generowała metodę w tej klasie ze specjalnymi właściwościami opartymi na „hello” (powiedzmy metodę, która dodała „hello” na końcu łańcucha). Dla łatwości utrzymania może być dobre upewnienie się, że nie musisz pisać metody w każdej tworzonej podklasie, a zamiast tego wystarczy zdefiniować wartość method_maker_value.

Potrzeba tego jest jednak tak rzadka i ogranicza się tylko do odrobiny pisania, że ​​nie jest to warte rozważenia, chyba że masz wystarczająco dużą bazę kodów.

David Raznick
źródło
5

Nigdy absolutnie nie musisz używać metaklasy, ponieważ zawsze możesz skonstruować klasę, która robi to, co chcesz, używając dziedziczenia lub agregacji klasy, którą chcesz zmodyfikować.

To powiedziawszy, w Smalltalk i Ruby może być bardzo przydatna możliwość modyfikowania istniejącej klasy, ale Python nie lubi tego robić bezpośrednio.

Jest doskonały artykuł DeveloperWorks na temat metaklas w Pythonie, który może pomóc. Artykuł w Wikipedii też jest całkiem niezły.

Charlie Martin
źródło
1
Nie potrzebujesz również obiektów do programowania zorientowanego obiektowo - możesz to zrobić za pomocą funkcji pierwszej klasy. Nie musisz więc używać obiektów. Ale są tam dla wygody. Nie jestem więc pewien, o co chodzi w pierwszym akapicie.
Tyler Crompton
1
Spójrz na pytanie.
Charlie Martin
4

Niektóre biblioteki GUI mają problemy, gdy wiele wątków próbuje z nimi współdziałać. tkinterjest jednym z takich przykładów; i chociaż można bezpośrednio poradzić sobie z problemem ze zdarzeniami i kolejkami, znacznie prostsze może być użycie biblioteki w sposób, który całkowicie ignoruje problem. Spójrz - magia metaklas.

Możliwość bezproblemowego dynamicznego przepisywania całej biblioteki, tak aby działała zgodnie z oczekiwaniami w aplikacji wielowątkowej, może być niezwykle pomocna w niektórych okolicznościach. Safetkinter moduł robi to z pomocą metaklasą dostarczonych przez threadbox modułu - nie potrzebnej imprezy i kolejek.

Jednym z fajnych aspektów threadboxjest to, że nie obchodzi go, jaką klasę klonuje. Zawiera przykład tego, jak metaklasa może w razie potrzeby dotknąć wszystkich klas podstawowych. Kolejną zaletą metaklas jest to, że działają one również na klasach dziedziczących. Programy, które same się piszą - czemu nie?

Noctis Skytower
źródło
4

Jedynym uzasadnionym przypadkiem użycia metaklasy jest powstrzymanie innych wścibskich programistów przed dotykaniem twojego kodu. Gdy wścibski programista opanuje metaklasy i zacznie szperać w twoich, wrzuć kolejny poziom lub dwa, aby ich nie dopuścić. Jeśli to nie zadziała, zacznij używać type.__new__lub być może jakiegoś schematu przy użyciu rekurencyjnej metaklasy.

(napisany z przymrużeniem oka, ale widziałem, jak robiono tego rodzaju zaciemnianie. Django to doskonały przykład)

Mike A.
źródło
7
Nie jestem pewien, czy motywacja była taka sama w Django.
Ali Afshar
3

Metaklasy nie zastępują programowania! To tylko sztuczka, która może zautomatyzować lub uczynić niektóre zadania bardziej eleganckimi. Dobrym tego przykładem jest biblioteka podświetlania składni Pygments . Posiada klasę o nazwie, RegexLexerktóra pozwala użytkownikowi zdefiniować zestaw reguł leksykalnych jako wyrażenia regularne w klasie. Metaklasa służy do przekształcania definicji w użyteczny parser.

Są jak sól; jest zbyt łatwy w użyciu.

Benjamin Peterson
źródło
Cóż, moim zdaniem, ta sprawa Pygments jest po prostu niepotrzebna. Dlaczego po prostu nie mieć zwykłej kolekcji, takiej jak dykt, po co zmuszać klasę do tego?
Ali Afshar,
4
Ponieważ klasa nice zawiera ideę Lexera i ma inne przydatne metody, takie jak guess_filename () itp.
Benjamin Peterson,
3

Sposób, w jaki użyłem metaklas, polegał na nadaniu klasom atrybutów. Weź na przykład:

class NameClass(type):
    def __init__(cls, *args, **kwargs):
       type.__init__(cls, *args, **kwargs)
       cls.name = cls.__name__

umieści atrybut name w każdej klasie, dla której metaklasa będzie wskazywała na NameClass.

hiperborejski
źródło
5
Tak, to działa. Możesz również użyć superklasy, która jest przynajmniej jawna i możliwa do naśladowania w kodzie. Z ciekawości, do czego to wykorzystałeś?
Ali Afshar,
2

Jest to niewielkie zastosowanie, ale ... jedną rzeczą, do której przydały się metaklasy, jest wywołanie funkcji za każdym razem, gdy tworzona jest podklasa. Zakodowałem to w metaklasie, która szuka __initsubclass__atrybutu: za każdym razem, gdy tworzona jest podklasa, wywoływane są wszystkie klasy nadrzędne, które definiują tę metodę __initsubclass__(cls, subcls). Pozwala to na utworzenie klasy nadrzędnej, która następnie rejestruje wszystkie podklasy w jakimś rejestrze globalnym, przeprowadza niezmienne kontrole podklas, gdy są one zdefiniowane, wykonuje operacje późnego wiązania itp ... wszystko bez konieczności ręcznego wywoływania funkcji lub tworzenia niestandardowych metaklas, które wykonywać każdy z tych odrębnych obowiązków.

Pamiętaj, że powoli zdałem sobie sprawę, że ukryta magia tego zachowania jest nieco niepożądana, ponieważ jest to nieoczekiwane, gdy patrzy się na definicję klasy poza kontekstem ... więc odeszłam od używania tego rozwiązania do czegokolwiek poważnego poza inicjowanie __superatrybutu dla każdej klasy i instancji.

Eli Collins
źródło
1

Niedawno musiałem użyć metaklasy, aby pomóc w deklaratywnym zdefiniowaniu modelu SQLAlchemy wokół tabeli bazy danych wypełnionej danymi ze spisu ludności USA z http://census.ire.org/data/bulkdata.html

IRE zapewnia powłoki baz danych dla tabel danych spisowych, które tworzą kolumny z liczbami całkowitymi zgodnie z konwencją nazewnictwa opracowaną przez Census Bureau p012015, p012016, p012017 itp.

Chciałem a) móc uzyskać dostęp do tych kolumn za pomocą model_instance.p012017składni, b) jasno określić, co robię oraz c) nie musieć jawnie definiować dziesiątek pól w modelu, więc podklasowałem SQLAlchemy, DeclarativeMetaaby iterować przez zakres kolumny i automatycznie tworzą pola modelu odpowiadające kolumnom:

from sqlalchemy.ext.declarative.api import DeclarativeMeta

class CensusTableMeta(DeclarativeMeta):
    def __init__(cls, classname, bases, dict_):
        table = 'p012'
        for i in range(1, 49):
            fname = "%s%03d" % (table, i)
            dict_[fname] = Column(Integer)
            setattr(cls, fname, dict_[fname])

        super(CensusTableMeta, cls).__init__(classname, bases, dict_)

Następnie mógłbym użyć tej metaklasy do mojej definicji modelu i uzyskać dostęp do automatycznie wyliczanych pól w modelu:

CensusTableBase = declarative_base(metaclass=CensusTableMeta)

class P12Tract(CensusTableBase):
    __tablename__ = 'ire_p12'

    geoid = Column(String(12), primary_key=True)

    @property
    def male_under_5(self):
        return self.p012003

    ...
Geoffrey Hing
źródło
1

Wydaje się, że opisane tutaj uzasadnione użycie - przepisywanie dokumentów w języku Python za pomocą metaklasy.

mistermarko
źródło
0

Musiałem ich użyć raz dla parsera binarnego, aby był łatwiejszy w użyciu. Definiujesz klasę wiadomości z atrybutami pól obecnych w łączu. Należało je uporządkować w sposób, w jaki zadeklarowano, aby zbudować z niego ostateczny format drutu. Możesz to zrobić za pomocą metaklas, jeśli używasz uporządkowanej dyktatury przestrzeni nazw. W rzeczywistości jest to w przykładach dla metaklas:

https://docs.python.org/3/reference/datamodel.html#metaclass-example

Ale ogólnie: bardzo dokładnie oceń, czy naprawdę potrzebujesz dodatkowej złożoności metaklas.

GeeF
źródło
0

odpowiedź od @Dan Gittik jest fajna

przykłady na końcu mogłyby wyjaśnić wiele rzeczy, zmieniłem go na Pythona 3 i podam kilka wyjaśnień:

class MetaMetaclass(type):
    def __new__(meta, name, bases, attrs):
        def __new__(meta, name, bases, attrs):
            cls = type.__new__(meta, name, bases, attrs)
            cls._label = 'Made in %s' % meta.__name__
            return cls

        attrs['__new__'] = __new__
        return type.__new__(meta, name, bases, attrs)

#China is metaclass and it's __new__ method would be changed by MetaMetaclass(metaclass)
class China(MetaMetaclass, metaclass=MetaMetaclass):
    __metaclass__ = MetaMetaclass

#Taiwan is metaclass and it's __new__ method would be changed by MetaMetaclass(metaclass)
class Taiwan(MetaMetaclass, metaclass=MetaMetaclass):
    __metaclass__ = MetaMetaclass

#A is a normal class and it's __new__ method would be changed by China(metaclass)
class A(metaclass=China):
    __metaclass__ = China

#B is a normal class and it's __new__ method would be changed by Taiwan(metaclass)
class B(metaclass=Taiwan):
    __metaclass__ = Taiwan


print(A._label)  # Made in China
print(B._label)  # Made in Taiwan
  • wszystko jest przedmiotem, więc klasa jest przedmiotem
  • obiekt klasy jest tworzony przez metaklasę
  • cała klasa dziedziczona po typie to metaklasa
  • metaklasa może sterować tworzeniem klas
  • metaklasa może również kontrolować tworzenie metaklasy (więc może zapętlić się na zawsze)
  • to jest metaprogramowanie ... możesz kontrolować system typów w czasie wykonywania
  • znowu wszystko jest obiektem, to jest jednolity system, wpisz typ tworzenia i typ tworzenia instancji
foolcage
źródło
0

Innym przypadkiem użycia jest możliwość modyfikowania atrybutów na poziomie klasy i upewnienia się, że ma to wpływ tylko na dany obiekt. W praktyce oznacza to „scalanie” faz metaklas i instancji klas, co prowadzi do zajmowania się tylko instancjami klas ich własnego (unikatowego) rodzaju.

Musiałem to również zrobić, gdy (ze względu na czytelność i polimorfizm ) chcieliśmy dynamicznie zdefiniować property wartości zwracane (mogą) wynikać z obliczeń opartych na (często zmieniających się) atrybutach na poziomie instancji, co można zrobić tylko na poziomie klasy , tj. po instancji metaklasy i przed instancją klasy.

utrzymać przy życiu
źródło