Pertanyaan Apa itu metaclasses dengan Python?


Apa itu metaclasses dan untuk apa kita menggunakannya?


4572
2017-09-19 06:10


asal


Jawaban:


Metaclass adalah kelas dari sebuah kelas. Seperti kelas mendefinisikan bagaimana suatu instance dari kelas berperilaku, sebuah metaclass mendefinisikan bagaimana suatu kelas berperilaku. Kelas adalah instance dari metaclass.

metaclass diagram

Sementara di Python Anda dapat menggunakan panggilan acak untuk metaclasses (seperti Jerub menunjukkan), pendekatan yang lebih berguna sebenarnya adalah menjadikannya kelas yang sebenarnya. type adalah metaclass biasa dengan Python. Jika Anda bertanya-tanya, ya, type itu sendiri kelas, dan itu adalah jenisnya sendiri. Anda tidak akan dapat membuat ulang sesuatu seperti type murni dengan Python, tetapi Python menipu sedikit. Untuk membuat metaclass Anda sendiri dengan Python Anda benar-benar hanya ingin subclass type.

Metaclass paling sering digunakan sebagai kelas pabrik. Seperti Anda membuat turunan kelas dengan memanggil kelas, Python membuat kelas baru (saat mengeksekusi pernyataan 'kelas') dengan memanggil metaclass. Dikombinasikan dengan yang normal __init__ dan __new__ metode, metaclasses karena itu memungkinkan Anda untuk melakukan 'hal-hal tambahan' saat membuat kelas, seperti mendaftarkan kelas baru dengan beberapa registri, atau bahkan mengganti kelas dengan sesuatu yang lain sama sekali.

Ketika class pernyataan dijalankan, Python pertama mengeksekusi tubuh class pernyataan sebagai blok kode normal. Namespace yang dihasilkan (a dikt) memegang atribut dari kelas-to-be. Metaclass ditentukan dengan melihat baseclasses dari kelas-to-be (metaclasses diwariskan), di __metaclass__ atribut dari class-to-be (jika ada) atau __metaclass__ variabel global. Metaclass kemudian dipanggil dengan nama, basis dan atribut kelas untuk instantiate itu.

Namun, metaclasses sebenarnya mendefinisikan mengetik kelas, bukan hanya pabrik untuk itu, jadi Anda dapat melakukan lebih banyak lagi dengan mereka. Anda dapat, misalnya, mendefinisikan metode normal pada metaclass. Metaclass-metode ini seperti classmethods, di mana mereka dapat dipanggil di kelas tanpa contoh, tetapi mereka juga tidak seperti classmethods di bahwa mereka tidak dapat dipanggil pada sebuah instance dari kelas. type.__subclasses__() adalah contoh metode di type metaclass. Anda juga bisa mendefinisikan metode 'sihir' biasa, seperti __add__, __iter__ dan __getattr__, untuk mengimplementasikan atau mengubah cara kelas berperilaku.

Berikut ini contoh gabungan dari bit dan potongan:

def make_hook(f):
    """Decorator to turn 'foo' method into '__foo__'"""
    f.is_hook = 1
    return f

class MyType(type):
    def __new__(mcls, name, bases, attrs):

        if name.startswith('None'):
            return None

        # Go over attributes and see if they should be renamed.
        newattrs = {}
        for attrname, attrvalue in attrs.iteritems():
            if getattr(attrvalue, 'is_hook', 0):
                newattrs['__%s__' % attrname] = attrvalue
            else:
                newattrs[attrname] = attrvalue

        return super(MyType, mcls).__new__(mcls, name, bases, newattrs)

    def __init__(self, name, bases, attrs):
        super(MyType, self).__init__(name, bases, attrs)

        # classregistry.register(self, self.interfaces)
        print "Would register class %s now." % self

    def __add__(self, other):
        class AutoClass(self, other):
            pass
        return AutoClass
        # Alternatively, to autogenerate the classname as well as the class:
        # return type(self.__name__ + other.__name__, (self, other), {})

    def unregister(self):
        # classregistry.unregister(self)
        print "Would unregister class %s now." % self

class MyObject:
    __metaclass__ = MyType


class NoneSample(MyObject):
    pass

# Will print "NoneType None"
print type(NoneSample), repr(NoneSample)

class Example(MyObject):
    def __init__(self, value):
        self.value = value
    @make_hook
    def add(self, other):
        return self.__class__(self.value + other.value)

# Will unregister the class
Example.unregister()

inst = Example(10)
# Will fail with an AttributeError
#inst.unregister()

print inst + inst
class Sibling(MyObject):
    pass

ExampleSibling = Example + Sibling
# ExampleSibling is now a subclass of both Example and Sibling (with no
# content of its own) although it will believe it's called 'AutoClass'
print ExampleSibling
print ExampleSibling.__mro__

2079
2017-09-19 07:01



Kelas sebagai objek

Sebelum memahami metaclasses, Anda perlu menguasai kelas dengan Python. Dan Python memiliki gagasan yang sangat aneh tentang kelas apa, yang dipinjam dari bahasa Smalltalk.

Di sebagian besar bahasa, kelas hanyalah potongan kode yang menggambarkan cara menghasilkan objek. Itu agak benar di Python juga:

>>> class ObjectCreator(object):
...       pass
...

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

Tetapi kelas lebih dari itu dengan Python. Kelas adalah benda juga.

Ya, benda.

Segera setelah Anda menggunakan kata kunci class, Python mengeksekusinya dan menciptakan Sebuah Objek. Intruksi

>>> class ObjectCreator(object):
...       pass
...

menciptakan dalam memori objek dengan nama "ObjectCreator".

Objek ini (kelas) itu sendiri mampu menciptakan objek (contoh), dan inilah mengapa itu kelas.

Tapi tetap saja, itu adalah objek, dan karena itu:

  • Anda dapat menetapkannya ke variabel
  • Anda dapat menyalinnya
  • Anda dapat menambahkan atribut ke sana
  • Anda dapat meneruskannya sebagai parameter fungsi

misalnya.:

>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

Membuat kelas secara dinamis

Karena kelas adalah objek, Anda dapat membuatnya dengan cepat, seperti objek apa pun.

Pertama, Anda dapat membuat kelas dalam fungsi menggunakan class:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # return the class, not an instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>

Tetapi itu tidak begitu dinamis, karena Anda masih harus menulis seluruh kelas sendiri.

Karena kelas adalah objek, mereka harus dihasilkan oleh sesuatu.

Saat Anda menggunakan class kata kunci, Python menciptakan objek ini secara otomatis. Tetapi sebagai dengan banyak hal di Python, ini memberi Anda cara untuk melakukannya secara manual.

Ingat fungsinya type? Fungsi lama yang baik yang memungkinkan Anda tahu apa ketik objek adalah:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

Baik, type memiliki kemampuan yang sama sekali berbeda, itu juga dapat membuat kelas dengan cepat. type dapat mengambil deskripsi kelas sebagai parameter, dan kembalikan kelas.

(Saya tahu, itu konyol bahwa fungsi yang sama dapat memiliki dua kegunaan yang berbeda sesuai dengan parameter yang Anda lewati. Ini adalah masalah karena mundur kompatibilitas dengan Python)

type bekerja seperti ini:

type(name of the class,
     tuple of the parent class (for inheritance, can be empty),
     dictionary containing attributes names and values)

misalnya.:

>>> class MyShinyClass(object):
...       pass

dapat dibuat secara manual dengan cara ini:

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # create an instance with the class
<__main__.MyShinyClass object at 0x8997cec>

Anda akan melihat bahwa kami menggunakan "MyShinyClass" sebagai nama kelas dan sebagai variabel untuk menahan referensi kelas. Mereka bisa berbeda, tetapi tidak ada alasan untuk mempersulit hal-hal.

type menerima kamus untuk menentukan atribut kelas. Begitu:

>>> class Foo(object):
...       bar = True

Dapat diterjemahkan ke:

>>> Foo = type('Foo', (), {'bar':True})

Dan digunakan sebagai kelas normal:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True

Dan tentu saja, Anda dapat mewarisi darinya, jadi:

>>>   class FooChild(Foo):
...         pass

akan menjadi:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar is inherited from Foo
True

Akhirnya Anda akan ingin menambahkan metode ke kelas Anda. Hanya mendefinisikan suatu fungsi dengan tanda tangan yang tepat dan menetapkannya sebagai atribut.

>>> def echo_bar(self):
...       print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

Dan Anda dapat menambahkan lebih banyak metode setelah Anda secara dinamis membuat kelas, seperti menambahkan metode ke objek kelas yang biasanya dibuat.

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

Anda melihat ke mana kita akan pergi: dengan Python, kelas adalah objek, dan Anda dapat membuat kelas dengan cepat, secara dinamis.

Ini adalah apa yang dilakukan Python ketika Anda menggunakan kata kunci class, dan melakukannya dengan menggunakan metaclass.

Apa itu metaclasses (akhirnya)

Metaclasses adalah 'barang' yang menciptakan kelas.

Anda mendefinisikan kelas untuk membuat objek, bukan?

Tapi kami belajar bahwa kelas Python adalah objek.

Nah, metaclasses adalah apa yang membuat benda-benda ini. Mereka adalah kelas-kelas kelas, Anda dapat menggambarkannya dengan cara ini:

MyClass = MetaClass()
my_object = MyClass()

Anda telah melihat itu type memungkinkan Anda melakukan sesuatu seperti ini:

MyClass = type('MyClass', (), {})

Itu karena fungsinya type sebenarnya adalah metaclass. type adalah metaclass Python digunakan untuk membuat semua kelas di belakang layar.

Sekarang Anda bertanya-tanya mengapa sih itu ditulis dalam huruf kecil, dan tidak Type?

Yah, saya kira itu masalah konsistensi dengan str, kelas yang menciptakan string objek, dan intkelas yang menciptakan objek bilangan bulat. type aku s hanya kelas yang menciptakan objek kelas.

Anda melihatnya dengan memeriksa __class__ atribut.

Semuanya, dan yang saya maksud semuanya, adalah objek dengan Python. Itu termasuk int, string, fungsi, dan kelas. Semuanya adalah benda. Dan mereka semua punya telah dibuat dari kelas:

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

Sekarang, apa itu __class__ apapun __class__ ?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

Jadi, metaclass hanyalah barang yang menciptakan objek kelas.

Anda bisa menyebutnya 'pabrik kelas' jika Anda mau.

type adalah metaclass metaclass yang digunakan Python, tetapi tentu saja, Anda dapat membuat metaclass sendiri.

Itu __metaclass__ atribut

Anda dapat menambahkan __metaclass__ atribut saat Anda menulis kelas:

class Foo(object):
    __metaclass__ = something...
    [...]

Jika Anda melakukannya, Python akan menggunakan metaclass untuk membuat kelas Foo.

Hati-hati, itu rumit.

Anda menulis class Foo(object) pertama, tetapi objek kelas Foo tidak dibuat dalam memori belum.

Python akan mencari __metaclass__ dalam definisi kelas. Jika menemukannya, itu akan menggunakannya untuk membuat kelas objek Foo. Jika tidak, itu akan digunakan type untuk membuat kelas.

Baca itu beberapa kali.

Saat kamu melakukan:

class Foo(Bar):
    pass

Python melakukan hal berikut:

Apakah ada __metaclass__ atribut dalam Foo?

Jika ya, buat dalam memori objek kelas (saya katakan objek kelas, tinggal bersama saya di sini), dengan nama Foo dengan menggunakan apa yang ada di dalamnya __metaclass__.

Jika Python tidak dapat menemukannya __metaclass__, itu akan mencari a __metaclass__ pada tingkat MODUL, dan coba lakukan hal yang sama (tetapi hanya untuk kelas yang tidak mewarisi apa pun, kelas yang pada dasarnya bergaya lama).

Lalu jika tidak ditemukan __metaclass__ sama sekali, ia akan menggunakan Bar(orang tua pertama) memiliki metaclass (yang mungkin merupakan default type) untuk membuat objek kelas.

Hati-hati di sini bahwa __metaclass__ atribut tidak akan diwarisi, metaclass dari induknya (Bar.__class__) akan. Jika Bar digunakan a __metaclass__ atribut yang dibuat Bar dengan type() (dan tidak type.__new__()), subclass tidak akan mewarisi perilaku itu.

Sekarang pertanyaan besarnya adalah, apa yang bisa Anda masukkan __metaclass__ ?

Jawabannya adalah: sesuatu yang bisa membuat kelas.

Dan apa yang bisa membuat kelas? type, atau apa pun yang subclass atau menggunakannya.

Metaclasses khusus

Tujuan utama dari metaclass adalah untuk mengubah kelas secara otomatis, ketika itu dibuat.

Anda biasanya melakukan ini untuk API, di mana Anda ingin membuat kelas yang cocok dengan konteks saat ini.

Bayangkan contoh yang bodoh, di mana Anda memutuskan bahwa semua kelas dalam modul Anda harus atribut mereka ditulis dalam huruf besar. Ada beberapa cara untuk melakukannya lakukan ini, tapi satu cara adalah mengaturnya __metaclass__ di level modul.

Dengan cara ini, semua kelas dari modul ini akan dibuat menggunakan metaclass ini, dan kita hanya perlu memberi tahu metaclass untuk mengubah semua atribut menjadi huruf besar.

Untunglah, __metaclass__ sebenarnya bisa menjadi callable, tidak perlu menjadi kelas formal (saya tahu, sesuatu dengan 'kelas' dalam namanya tidak perlu kelas, tokoh ... tapi itu membantu).

Jadi kita akan mulai dengan contoh sederhana, dengan menggunakan fungsi.

# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attr):
    """
      Return a class object, with the list of its attribute turned
      into uppercase.
    """

    # pick up any attribute that doesn't start with '__' and uppercase it
    uppercase_attr = {}
    for name, val in future_class_attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

    # let `type` do the class creation
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr # this will affect all classes in the module

class Foo(): # global __metaclass__ won't work with "object" though
    # but we can define __metaclass__ here instead to affect only this class
    # and this will work with "object" children
    bar = 'bip'

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: 'bip'

Sekarang, mari kita lakukan hal yang sama, tetapi menggunakan kelas nyata untuk metaclass:

# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
    # __new__ is the method called before __init__
    # it's the method that creates the object and returns it
    # while __init__ just initializes the object passed as parameter
    # you rarely use __new__, except when you want to control how the object
    # is created.
    # here the created object is the class, and we want to customize it
    # so we override __new__
    # you can do some stuff in __init__ too if you wish
    # some advanced use involves overriding __call__ as well, but we won't
    # see this
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type(future_class_name, future_class_parents, uppercase_attr)

Tapi ini bukan OOP. Kami memanggil type langsung dan kami tidak menimpa atau hubungi orang tua __new__. Ayo lakukan:

class UpperAttrMetaclass(type):

    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # reuse the type.__new__ method
        # this is basic OOP, nothing magic in there
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

Anda mungkin telah memperhatikan argumen ekstra upperattr_metaclass. Ada tidak ada yang istimewa tentang itu: __new__ selalu menerima kelas yang ditentukan, sebagai parameter pertama. Sama seperti yang Anda miliki self untuk metode biasa yang menerima instance sebagai parameter pertama, atau kelas pendefinisian untuk metode kelas.

Tentu saja, nama-nama yang saya gunakan di sini panjang untuk kejelasan, tetapi suka untuk self, semua argumen memiliki nama-nama konvensional. Jadi, produksi nyata metaclass akan terlihat seperti ini:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type.__new__(cls, clsname, bases, uppercase_attr)

Kita bisa membuatnya lebih bersih dengan menggunakan super, yang akan memudahkan pewarisan (karena ya, Anda dapat memiliki metaclasses, mewarisi dari metaclasses, mewarisi dari jenis):

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)

Itu dia. Tidak ada yang lebih tentang metaclasses.

Alasan di balik kerumitan kode menggunakan metaclasses bukan karena metaclasses, itu karena Anda biasanya menggunakan metaclasses untuk melakukan hal-hal yang bengkok mengandalkan introspeksi, memanipulasi warisan, vars seperti __dict__, dll.

Memang, metaclasses sangat berguna untuk melakukan sihir hitam, dan karenanya hal yang rumit. Tetapi dengan sendirinya, mereka sederhana:

  • memotong ciptaan kelas
  • memodifikasi kelas
  • kembalikan kelas yang dimodifikasi

Mengapa Anda menggunakan kelas metaclasses alih-alih fungsi?

Sejak __metaclass__ dapat menerima apa pun yang dapat dipanggil, mengapa Anda menggunakan kelas karena ini jelas lebih rumit?

Ada beberapa alasan untuk melakukannya:

  • Niatnya jelas. Ketika kamu membaca UpperAttrMetaclass(type), kamu tahu apa yang akan diikuti
  • Anda dapat menggunakan OOP. Metaclass dapat mewarisi dari metaclass, mengesampingkan metode induk. Metaclasses bahkan dapat menggunakan metaclasses.
  • Subclass dari kelas akan menjadi contoh metaclass jika Anda menetapkan kelas metaclass, tetapi tidak dengan fungsi metaclass.
  • Anda dapat menyusun kode Anda lebih baik. Anda tidak pernah menggunakan metaclasses untuk sesuatu seperti sepele seperti contoh di atas. Biasanya untuk sesuatu yang rumit. Memiliki kemampuan membuat beberapa metode dan mengelompokkannya dalam satu kelas sangat bermanfaat untuk membuat kode lebih mudah dibaca.
  • Anda bisa mengaitkannya __new__, __init__ dan __call__. Yang memungkinkan kamu melakukan hal yang berbeda. Bahkan jika biasanya Anda bisa melakukan semuanya __new__, beberapa orang lebih nyaman menggunakannya __init__.
  • Ini disebut metaclasses, sialan! Itu pasti berarti sesuatu!

Mengapa Anda menggunakan metaclasses?

Sekarang pertanyaan besar. Mengapa Anda menggunakan beberapa fitur rawan kesalahan yang tidak jelas?

Yah, biasanya Anda tidak:

Metaclasses adalah sihir yang lebih dalam itu   99% pengguna tidak perlu khawatir.   Jika Anda bertanya-tanya apakah Anda membutuhkannya,   Anda tidak (orang-orang yang sebenarnya   perlu mereka tahu dengan pasti itu   mereka membutuhkannya, dan tidak membutuhkan   penjelasan tentang mengapa).

Python Guru Tim Peters

Kasus penggunaan utama untuk metaclass adalah membuat API. Contoh khas dari ini adalah ORM Django.

Ini memungkinkan Anda mendefinisikan sesuatu seperti ini:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

Tetapi jika Anda melakukan ini:

guy = Person(name='bob', age='35')
print(guy.age)

Itu tidak akan mengembalikan IntegerField obyek. Ini akan mengembalikan sebuah int, dan bahkan dapat mengambilnya langsung dari database.

Ini dimungkinkan karena models.Model mendefinisikan __metaclass__ dan ia menggunakan sihir yang akan mengubah Person Anda baru saja didefinisikan dengan pernyataan sederhana ke hook kompleks ke bidang database.

Django membuat sesuatu yang rumit terlihat sederhana dengan memaparkan API sederhana dan menggunakan metaclasses, membuat ulang kode dari API ini untuk melakukan pekerjaan nyata dibalik layar.

Kata terakhir

Pertama, Anda tahu bahwa kelas adalah objek yang dapat membuat instance.

Kenyataannya, kelas adalah contoh tersendiri. Dari metaclasses.

>>> class Foo(object): pass
>>> id(Foo)
142630324

Semuanya adalah objek dengan Python, dan semuanya merupakan contoh kelas atau contoh metaclasses.

Kecuali untuk type.

type sebenarnya metaclass sendiri. Ini bukan sesuatu yang kamu bisa berkembang biak dengan Python murni, dan dilakukan dengan mencontek sedikit pada saat implementasi tingkat.

Kedua, metaclasses rumit. Anda mungkin tidak ingin menggunakannya untuk perubahan kelas yang sangat sederhana. Anda dapat mengubah kelas dengan menggunakan dua teknik berbeda:

99% dari waktu yang Anda butuhkan perubahan kelas, Anda lebih baik menggunakan ini.

Tapi 98% dari waktu, Anda tidak perlu mengubah kelas sama sekali.


5755
2017-09-19 06:26



Catatan, jawaban ini untuk Python 2.x seperti yang ditulis pada tahun 2008, metaclasses sedikit berbeda dalam 3.x, lihat komentarnya.

Metaclasses adalah saus rahasia yang membuat pekerjaan 'kelas'. Metaclass default untuk objek gaya baru disebut 'tipe'.

class type(object)
  |  type(object) -> the object's type
  |  type(name, bases, dict) -> a new type

Metaclasses mengambil 3 arg. 'nama','basa'dan'dict'

Di sinilah rahasia dimulai. Carilah di mana nama, pangkalan dan dict berasal dari dalam definisi kelas contoh ini.

class ThisIsTheName(Bases, Are, Here):
    All_the_code_here
    def doesIs(create, a):
        dict

Mari kita definisikan metaclass yang akan menunjukkan bagaimana 'kelas:'Meneleponnya.

def test_metaclass(name, bases, dict):
    print 'The Class Name is', name
    print 'The Class Bases are', bases
    print 'The dict has', len(dict), 'elems, the keys are', dict.keys()

    return "yellow"

class TestName(object, None, int, 1):
    __metaclass__ = test_metaclass
    foo = 1
    def baz(self, arr):
        pass

print 'TestName = ', repr(TestName)

# output => 
The Class Name is TestName
The Class Bases are (<type 'object'>, None, <type 'int'>, 1)
The dict has 4 elems, the keys are ['baz', '__module__', 'foo', '__metaclass__']
TestName =  'yellow'

Dan sekarang, sebuah contoh yang sebenarnya berarti sesuatu, ini secara otomatis akan membuat variabel dalam daftar "atribut" ditetapkan di kelas, dan diatur ke Tidak Ada.

def init_attributes(name, bases, dict):
    if 'attributes' in dict:
        for attr in dict['attributes']:
            dict[attr] = None

    return type(name, bases, dict)

class Initialised(object):
    __metaclass__ = init_attributes
    attributes = ['foo', 'bar', 'baz']

print 'foo =>', Initialised.foo
# output=>
foo => None

Perhatikan bahwa perilaku sihir yang 'Initalised' meningkat dengan memiliki metaclass init_attributes tidak diteruskan ke subkelas Initalised.

Berikut adalah contoh yang lebih konkret, yang menunjukkan bagaimana Anda dapat 'mengetik' subclass untuk membuat metaclass yang melakukan tindakan ketika kelas dibuat. Ini cukup sulit:

class MetaSingleton(type):
    instance = None
    def __call__(cls, *args, **kw):
        if cls.instance is None:
            cls.instance = super(MetaSingleton, cls).__call__(*args, **kw)
        return cls.instance

 class Foo(object):
     __metaclass__ = MetaSingleton

 a = Foo()
 b = Foo()
 assert a is b

311
2017-09-19 06:45



Satu penggunaan untuk metaclasses adalah menambahkan properti dan metode baru ke sebuah instance secara otomatis.

Misalnya, jika Anda melihat Model Django, definisi mereka terlihat agak membingungkan. Sepertinya Anda hanya mendefinisikan properti kelas:

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

Namun, pada saat runtime objek Person dipenuhi dengan segala macam metode yang berguna. Lihat sumber untuk beberapa metaclassery yang menakjubkan.


124
2018-06-21 16:30



Orang lain telah menjelaskan bagaimana cara kerja metaclasses dan bagaimana mereka masuk ke dalam sistem tipe Python. Inilah contoh dari apa yang dapat mereka gunakan. Dalam kerangka pengujian yang saya tulis, saya ingin melacak urutan di mana kelas didefinisikan, sehingga saya nantinya dapat memberi contoh kepada mereka dalam urutan ini. Saya merasa paling mudah melakukan ini menggunakan metaclass.

class MyMeta(type):

    counter = 0

    def __init__(cls, name, bases, dic):
        type.__init__(cls, name, bases, dic)
        cls._order = MyMeta.counter
        MyMeta.counter += 1

class MyType(object):              # Python 2
    __metaclass__ = MyMeta

class MyType(metaclass=MyMeta):    # Python 3
    pass

Apa pun yang merupakan subkelas MyType kemudian mendapat atribut class _order yang mencatat urutan kelas didefinisikan.


117
2017-09-19 06:32



Saya pikir pengenalan ONLamp untuk pemrograman metaclass ditulis dengan baik dan memberikan pengenalan yang sangat baik untuk topik meskipun sudah berusia beberapa tahun.

http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html

Singkatnya: Kelas adalah cetak biru untuk pembuatan sebuah instance, metaclass adalah cetak biru untuk pembuatan kelas. Dapat dengan mudah dilihat bahwa dalam kelas Python perlu juga objek kelas satu untuk mengaktifkan perilaku ini.

Saya tidak pernah menulisnya sendiri, tetapi saya pikir salah satu penggunaan metaclasses paling baik dapat dilihat di Kerangka Django. Kelas model menggunakan pendekatan metaclass untuk mengaktifkan gaya deklaratif penulisan model baru atau kelas formulir. Sementara metaclass menciptakan kelas, semua anggota mendapatkan kemungkinan untuk menyesuaikan kelas itu sendiri.

Hal yang tersisa untuk dikatakan adalah: Jika Anda tidak tahu metaclasses apa, probabilitas bahwa Anda tidak akan membutuhkannya 99%.


86
2017-08-10 23:28



Apa itu metaclasses? Anda menggunakannya untuk apa?

TLDR: A metaclass instantiate dan mendefinisikan perilaku untuk kelas seperti kelas instantiate dan mendefinisikan perilaku untuk sebuah instance.

Pseudocode:

>>> Class(...)
instance

Hal di atas seharusnya terlihat familiar. Nah, dimana Class berasal dari? Ini adalah instance dari metaclass (juga pseudocode):

>>> Metaclass(...)
Class

Dalam kode asli, kita dapat melewati metaclass default, type, semua yang kita butuhkan untuk instantiate kelas dan kita mendapatkan kelas:

>>> type('Foo', (object,), {}) # requires a name, bases, and a namespace
<class '__main__.Foo'>

Menempatkannya berbeda

  • Kelas adalah sebuah instance sebagai metaclass adalah kelas.

    Ketika kita instantiate objek, kita mendapatkan sebuah instance:

    >>> object()                          # instantiation of class
    <object object at 0x7f9069b4e0b0>     # instance
    

    Demikian juga, ketika kita mendefinisikan kelas secara eksplisit dengan metaclass default, type, kami memberi contoh:

    >>> type('Object', (object,), {})     # instantiation of metaclass
    <class '__main__.Object'>             # instance
    
  • Dengan kata lain, kelas adalah instance dari metaclass:

    >>> isinstance(object, type)
    True
    
  • Letakkan cara ketiga, metaclass adalah kelas kelas.

    >>> type(object) == type
    True
    >>> object.__class__
    <class 'type'>
    

Ketika Anda menulis definisi kelas dan Python mengeksekusinya, ia menggunakan metaclass untuk instantiate objek kelas (yang akan, pada gilirannya, digunakan untuk instantiate instance dari kelas itu).

Sama seperti kita dapat menggunakan definisi kelas untuk mengubah bagaimana perilaku objek kustom berperilaku, kita dapat menggunakan definisi kelas metaclass untuk mengubah cara perilaku objek kelas.

Untuk apa mereka bisa digunakan? Dari dokumen:

Penggunaan potensial untuk metaclasses tidak terbatas. Beberapa ide yang telah dieksplorasi termasuk penebangan, pemeriksaan antarmuka, delegasi otomatis, pembuatan properti otomatis, proksi, kerangka kerja, dan penguncian / sinkronisasi sumber daya otomatis.

Namun demikian, biasanya dianjurkan bagi pengguna untuk menghindari penggunaan metaclasses kecuali benar-benar diperlukan.

Anda menggunakan metaclass setiap kali Anda membuat kelas:

Ketika Anda menulis definisi kelas, misalnya, seperti ini,

class Foo(object): 
    'demo'

Anda instantiate objek kelas.

>>> Foo
<class '__main__.Foo'>
>>> isinstance(Foo, type), isinstance(Foo, object)
(True, True)

Ini sama dengan panggilan secara fungsional type dengan argumen yang tepat dan menetapkan hasilnya ke variabel dari nama itu:

name = 'Foo'
bases = (object,)
namespace = {'__doc__': 'demo'}
Foo = type(name, bases, namespace)

Perhatikan, beberapa hal secara otomatis ditambahkan ke __dict__, mis., ruangnama:

>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, 
'__module__': '__main__', '__weakref__': <attribute '__weakref__' 
of 'Foo' objects>, '__doc__': 'demo'})

Itu metaclass dari objek yang kami buat, dalam kedua kasus, adalah type.

(Catatan tambahan tentang isi kelas __dict__: __module__ apakah ada karena kelas harus tahu di mana mereka didefinisikan, dan __dict__ dan __weakref__ ada karena kita tidak mendefinisikan __slots__ - jika kita menetapkan __slots__ kami akan menghemat sedikit ruang dalam instance, karena kami dapat melarangnya __dict__ dan __weakref__ dengan mengecualikan mereka. Sebagai contoh:

>>> Baz = type('Bar', (object,), {'__doc__': 'demo', '__slots__': ()})
>>> Baz.__dict__
mappingproxy({'__doc__': 'demo', '__slots__': (), '__module__': '__main__'})

... tapi saya ngelantur.)

Kami dapat memperpanjang type sama seperti definisi kelas lainnya:

Ini standarnya __repr__ kelas:

>>> Foo
<class '__main__.Foo'>

Salah satu hal paling berharga yang dapat kita lakukan secara default dalam menulis objek Python adalah menyediakannya dengan baik __repr__. Saat kami menelepon help(repr) kita belajar bahwa ada tes yang bagus untuk __repr__ yang juga membutuhkan tes kesetaraan - obj == eval(repr(obj)). Implementasi sederhana berikut ini __repr__ dan __eq__ untuk instance kelas dari kelas jenis kami menyediakan demonstrasi yang dapat meningkatkan pada default __repr__ kelas:

class Type(type):
    def __repr__(cls):
        """
        >>> Baz
        Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
        >>> eval(repr(Baz))
        Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
        """
        metaname = type(cls).__name__
        name = cls.__name__
        parents = ', '.join(b.__name__ for b in cls.__bases__)
        if parents:
            parents += ','
        namespace = ', '.join(': '.join(
          (repr(k), repr(v) if not isinstance(v, type) else v.__name__))
               for k, v in cls.__dict__.items())
        return '{0}(\'{1}\', ({2}), {{{3}}})'.format(metaname, name, parents, namespace)
    def __eq__(cls, other):
        """
        >>> Baz == eval(repr(Baz))
        True            
        """
        return (cls.__name__, cls.__bases__, cls.__dict__) == (
                other.__name__, other.__bases__, other.__dict__)

Jadi sekarang ketika kita membuat objek dengan metaclass ini, yang __repr__ bergema pada baris perintah memberikan pemandangan yang jauh lebih buruk daripada default:

>>> class Bar(object): pass
>>> Baz = Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
>>> Baz
Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})

Dengan bagus __repr__ ditentukan untuk instance kelas, kami memiliki kemampuan yang lebih kuat untuk melakukan debug pada kode kami. Namun, banyak pengecekan lebih lanjut dengan eval(repr(Class)) tidak mungkin (karena fungsi akan agak mustahil untuk divalidasi dari defaultnya __repr__'s).

Penggunaan yang diharapkan: __prepare__ sebuah namespace

Jika, misalnya, kami ingin tahu dalam urutan apa metode kelas dibuat, kami dapat memberikan perintah dict sebagai namespace kelas. Kami akan melakukan ini dengan __prepare__ yang mengembalikan nilai namespace untuk kelas jika diimplementasikan dengan Python 3:

from collections import OrderedDict

class OrderedType(Type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwargs):
        return OrderedDict()
    def __new__(cls, name, bases, namespace, **kwargs):
        result = Type.__new__(cls, name, bases, dict(namespace))
        result.members = tuple(namespace)
        return result

Dan penggunaan:

class OrderedMethodsObject(object, metaclass=OrderedType):
    def method1(self): pass
    def method2(self): pass
    def method3(self): pass
    def method4(self): pass

Dan sekarang kami memiliki catatan urutan di mana metode ini (dan atribut kelas lainnya) dibuat:

>>> OrderedMethodsObject.members
('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4')

Perhatikan, contoh ini diadaptasi dari dokumentasi - yang baru enum di perpustakaan standar Melakukan hal ini.

Jadi apa yang kami lakukan adalah instantiate metaclass dengan membuat kelas. Kita juga bisa memperlakukan metaclass seperti yang kita lakukan di kelas lain. Ini memiliki urutan resolusi metode:

>>> inspect.getmro(OrderedType)
(<class '__main__.OrderedType'>, <class '__main__.Type'>, <class 'type'>, <class 'object'>)

Dan itu kira-kira yang benar repr (yang kita tidak bisa lagi eval kecuali kita dapat menemukan cara untuk mewakili fungsi kita.):

>>> OrderedMethodsObject
OrderedType('OrderedMethodsObject', (object,), {'method1': <function OrderedMethodsObject.method1 at 0x0000000002DB01E0>, 'members': ('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4'), 'method3': <function OrderedMet
hodsObject.method3 at 0x0000000002DB02F0>, 'method2': <function OrderedMethodsObject.method2 at 0x0000000002DB0268>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'OrderedMethodsObject' objects>, '__doc__': None, '__d
ict__': <attribute '__dict__' of 'OrderedMethodsObject' objects>, 'method4': <function OrderedMethodsObject.method4 at 0x0000000002DB0378>})

74
2018-03-01 19:48



Pembaruan Python 3

Ada (pada titik ini) dua metode utama dalam metaclass:

  • __prepare__, dan
  • __new__

__prepare__ memungkinkan Anda menyediakan pemetaan khusus (seperti OrderedDict) untuk digunakan sebagai namespace ketika kelas sedang dibuat. Anda harus mengembalikan instance dari namespace apa pun yang Anda pilih. Jika Anda tidak mengimplementasikan __prepare__normal dict digunakan.

__new__ bertanggung jawab atas ciptaan / modifikasi aktual dari kelas terakhir.

Sebuah tulang telanjang, metaclass do-nothing-extra ingin:

class Meta(type):

    def __prepare__(metaclass, cls, bases):
        return dict()

    def __new__(metacls, cls, bases, clsdict):
        return super().__new__(metacls, cls, bases, clsdict)

Contoh sederhana:

Katakanlah Anda ingin beberapa kode validasi sederhana untuk berjalan di atribut Anda - seperti itu harus selalu menjadi int atau a str. Tanpa metaclass, kelas Anda akan terlihat seperti:

class Person:
    weight = ValidateType('weight', int)
    age = ValidateType('age', int)
    name = ValidateType('name', str)

Seperti yang Anda lihat, Anda harus mengulangi nama atribut dua kali. Ini membuat kesalahan ketik mungkin bersama dengan bug yang menjengkelkan.

Metaclass sederhana dapat mengatasi masalah itu:

class Person(metaclass=Validator):
    weight = ValidateType(int)
    age = ValidateType(int)
    name = ValidateType(str)

Seperti inilah metaclass (tidak digunakan) __prepare__ karena tidak diperlukan):

class Validator(type):
    def __new__(metacls, cls, bases, clsdict):
        # search clsdict looking for ValidateType descriptors
        for name, attr in clsdict.items():
            if isinstance(attr, ValidateType):
                attr.name = name
                attr.attr = '_' + name
        # create final class and return it
        return super().__new__(metacls, cls, bases, clsdict)

Contoh run dari:

p = Person()
p.weight = 9
print(p.weight)
p.weight = '9'

menghasilkan:

9
Traceback (most recent call last):
  File "simple_meta.py", line 36, in <module>
    p.weight = '9'
  File "simple_meta.py", line 24, in __set__
    (self.name, self.type, value))
TypeError: weight must be of type(s) <class 'int'> (got '9')

Catatan: Contoh ini cukup sederhana, bisa juga dilakukan dengan dekorator kelas, tapi mungkin metaclass yang sebenarnya akan melakukan lebih banyak lagi.

Kelas 'ValidateType' untuk referensi:

class ValidateType:
    def __init__(self, type):
        self.name = None  # will be set by metaclass
        self.attr = None  # will be set by metaclass
        self.type = type
    def __get__(self, inst, cls):
        if inst is None:
            return self
        else:
            return inst.__dict__[self.attr]
    def __set__(self, inst, value):
        if not isinstance(value, self.type):
            raise TypeError('%s must be of type(s) %s (got %r)' %
                    (self.name, self.type, value))
        else:
            inst.__dict__[self.attr] = value

55
2017-10-13 09:21



Metaclass adalah kelas yang menceritakan bagaimana (beberapa) kelas lain harus dibuat.

Ini adalah kasus di mana saya melihat metaclass sebagai solusi untuk masalah saya: Saya memiliki masalah yang sangat rumit, yang mungkin bisa diselesaikan secara berbeda, tetapi saya memilih untuk menyelesaikannya menggunakan metaclass. Karena kerumitannya, ini adalah salah satu dari beberapa modul yang saya tulis di mana komentar di modul melampaui jumlah kode yang telah ditulis. Ini dia...

#!/usr/bin/env python

# Copyright (C) 2013-2014 Craig Phillips.  All rights reserved.

# This requires some explaining.  The point of this metaclass excercise is to
# create a static abstract class that is in one way or another, dormant until
# queried.  I experimented with creating a singlton on import, but that did
# not quite behave how I wanted it to.  See now here, we are creating a class
# called GsyncOptions, that on import, will do nothing except state that its
# class creator is GsyncOptionsType.  This means, docopt doesn't parse any
# of the help document, nor does it start processing command line options.
# So importing this module becomes really efficient.  The complicated bit
# comes from requiring the GsyncOptions class to be static.  By that, I mean
# any property on it, may or may not exist, since they are not statically
# defined; so I can't simply just define the class with a whole bunch of
# properties that are @property @staticmethods.
#
# So here's how it works:
#
# Executing 'from libgsync.options import GsyncOptions' does nothing more
# than load up this module, define the Type and the Class and import them
# into the callers namespace.  Simple.
#
# Invoking 'GsyncOptions.debug' for the first time, or any other property
# causes the __metaclass__ __getattr__ method to be called, since the class
# is not instantiated as a class instance yet.  The __getattr__ method on
# the type then initialises the class (GsyncOptions) via the __initialiseClass
# method.  This is the first and only time the class will actually have its
# dictionary statically populated.  The docopt module is invoked to parse the
# usage document and generate command line options from it.  These are then
# paired with their defaults and what's in sys.argv.  After all that, we
# setup some dynamic properties that could not be defined by their name in
# the usage, before everything is then transplanted onto the actual class
# object (or static class GsyncOptions).
#
# Another piece of magic, is to allow command line options to be set in
# in their native form and be translated into argparse style properties.
#
# Finally, the GsyncListOptions class is actually where the options are
# stored.  This only acts as a mechanism for storing options as lists, to
# allow aggregation of duplicate options or options that can be specified
# multiple times.  The __getattr__ call hides this by default, returning the
# last item in a property's list.  However, if the entire list is required,
# calling the 'list()' method on the GsyncOptions class, returns a reference
# to the GsyncListOptions class, which contains all of the same properties
# but as lists and without the duplication of having them as both lists and
# static singlton values.
#
# So this actually means that GsyncOptions is actually a static proxy class...
#
# ...And all this is neatly hidden within a closure for safe keeping.
def GetGsyncOptionsType():
    class GsyncListOptions(object):
        __initialised = False

    class GsyncOptionsType(type):
        def __initialiseClass(cls):
            if GsyncListOptions._GsyncListOptions__initialised: return

            from docopt import docopt
            from libgsync.options import doc
            from libgsync import __version__

            options = docopt(
                doc.__doc__ % __version__,
                version = __version__,
                options_first = True
            )

            paths = options.pop('<path>', None)
            setattr(cls, "destination_path", paths.pop() if paths else None)
            setattr(cls, "source_paths", paths)
            setattr(cls, "options", options)

            for k, v in options.iteritems():
                setattr(cls, k, v)

            GsyncListOptions._GsyncListOptions__initialised = True

        def list(cls):
            return GsyncListOptions

        def __getattr__(cls, name):
            cls.__initialiseClass()
            return getattr(GsyncListOptions, name)[-1]

        def __setattr__(cls, name, value):
            # Substitut option names: --an-option-name for an_option_name
            import re
            name = re.sub(r'^__', "", re.sub(r'-', "_", name))
            listvalue = []

            # Ensure value is converted to a list type for GsyncListOptions
            if isinstance(value, list):
                if value:
                    listvalue = [] + value
                else:
                    listvalue = [ None ]
            else:
                listvalue = [ value ]

            type.__setattr__(GsyncListOptions, name, listvalue)

    # Cleanup this module to prevent tinkering.
    import sys
    module = sys.modules[__name__]
    del module.__dict__['GetGsyncOptionsType']

    return GsyncOptionsType

# Our singlton abstract proxy class.
class GsyncOptions(object):
    __metaclass__ = GetGsyncOptionsType()

41
2017-08-09 18:49