Saya ditanyai pertanyaan yang sama baru-baru ini, dan mendapatkan beberapa jawaban. Saya harap tidak apa-apa untuk menghidupkan kembali utas ini, karena saya ingin menguraikan beberapa kasus penggunaan yang disebutkan, dan menambahkan beberapa yang baru.
Kebanyakan metaclasses yang pernah saya lihat melakukan salah satu dari dua hal berikut:
Pendaftaran (menambahkan kelas ke struktur data):
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
Setiap kali Anda membuat subkelas Model
, kelas Anda terdaftar di models
kamus:
>>> class A(Model):
... pass
...
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...>,
'B': <__main__.B class at 0x...>}
Ini juga dapat dilakukan dengan dekorator kelas:
models = {}
def model(cls):
models[cls.__name__] = cls
return cls
@model
class A(object):
pass
Atau dengan fungsi registrasi eksplisit:
models = {}
def register_model(cls):
models[cls.__name__] = cls
class A(object):
pass
register_model(A)
Sebenarnya, ini kurang lebih sama: Anda menyebut dekorator kelas dengan tidak baik, tapi sebenarnya itu tidak lebih dari gula sintaksis untuk pemanggilan fungsi di kelas, jadi tidak ada keajaiban tentang itu.
Bagaimanapun, keuntungan dari metaclass dalam hal ini adalah inheritance, karena metaclass bekerja untuk setiap subclass, sedangkan solusi lain hanya bekerja untuk subclass yang didekorasi atau didaftarkan secara eksplisit.
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...> # No B :(
Refactoring (memodifikasi atribut kelas atau menambahkan yang baru):
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
Setiap kali Anda membuat subkelas Model
dan menentukan beberapa Field
atribut, atribut tersebut akan dimasukkan dengan namanya (untuk pesan kesalahan yang lebih informatif, misalnya), dan dikelompokkan ke dalam _fields
kamus (untuk iterasi yang mudah, tanpa harus melihat semua atribut kelas dan semua kelas dasarnya. atribut setiap waktu):
>>> class A(Model):
... foo = Integer()
...
>>> class B(A):
... bar = String()
...
>>> B._fields
{'foo': Integer('A.foo'), 'bar': String('B.bar')}
Sekali lagi, ini dapat dilakukan (tanpa warisan) dengan dekorator kelas:
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')} :(
Atau secara eksplisit:
class A(object):
foo = Integer('A.foo')
_fields = {'foo': foo} # Don't forget all the base classes' fields, too!
Meskipun, bertentangan dengan anjuran Anda untuk pemrograman non-meta yang dapat dibaca dan dipelihara, ini jauh lebih rumit, berlebihan, dan rawan kesalahan:
class B(A):
bar = String()
# vs.
class B(A):
bar = String('bar')
_fields = {'B.bar': bar, 'A.foo': A.foo}
Setelah mempertimbangkan kasus penggunaan yang paling umum dan konkret, satu-satunya kasus di mana Anda benar-benar HARUS menggunakan metaclass adalah saat Anda ingin mengubah nama kelas atau daftar kelas dasar, karena setelah ditentukan, parameter ini dimasukkan ke dalam kelas, dan tidak ada dekorator atau fungsi dapat membatalkannya.
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
Ini mungkin berguna dalam kerangka kerja untuk mengeluarkan peringatan setiap kali kelas dengan nama yang mirip atau pohon warisan yang tidak lengkap didefinisikan, tapi saya tidak bisa memikirkan alasan selain trolling untuk benar-benar mengubah nilai-nilai ini. Mungkin David Beazley bisa.
Bagaimanapun, di Python 3, metaclasses juga memiliki __prepare__
metode, yang memungkinkan Anda mengevaluasi badan kelas ke dalam pemetaan selain a dict
, sehingga mendukung atribut yang dipesan, atribut yang kelebihan beban, dan hal-hal keren lainnya yang jahat:
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...>
Anda mungkin berpendapat bahwa atribut yang dipesan dapat dicapai dengan penghitung pembuatan, dan kelebihan beban dapat disimulasikan dengan argumen default:
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)
Selain jauh lebih jelek, ini juga kurang fleksibel: bagaimana jika Anda ingin atribut literal yang diurutkan, seperti integer dan string? Bagaimana jika None
nilai yang valid untuk x
?
Inilah cara kreatif untuk menyelesaikan masalah pertama:
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']
Dan inilah cara kreatif untuk menyelesaikan yang kedua:
_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)
Tapi ini jauh, JAUH voodoo-er dari metaclass sederhana (terutama yang pertama, yang benar-benar melelehkan otak Anda). Maksud saya adalah, Anda melihat metaclass sebagai sesuatu yang asing dan kontra-intuitif, tetapi Anda juga dapat melihatnya sebagai langkah evolusi berikutnya dalam bahasa pemrograman: Anda hanya perlu menyesuaikan pola pikir Anda. Lagi pula, Anda mungkin bisa melakukan semuanya di C, termasuk mendefinisikan struct dengan pointer fungsi dan meneruskannya sebagai argumen pertama ke fungsinya. Seseorang yang melihat C ++ untuk pertama kalinya mungkin berkata, "sihir apa ini? Mengapa compiler secara implisit meneruskanthis
metode, tetapi tidak untuk fungsi biasa dan statis? Lebih baik bersikap eksplisit dan bertele-tele tentang argumen Anda ". Tapi kemudian, pemrograman berorientasi objek jauh lebih kuat setelah Anda mendapatkannya; dan begitu juga ini, uh ... pemrograman berorientasi aspek kuasi, saya rasa. Dan begitu Anda memahami metaclasses, sebenarnya sangat sederhana, jadi mengapa tidak menggunakannya bila nyaman?
Dan terakhir, metaclass adalah rad, dan pemrograman harus menyenangkan. Menggunakan konstruksi pemrograman standar dan pola desain sepanjang waktu itu membosankan dan tidak menginspirasi, serta menghalangi imajinasi Anda. Hidup sedikit! Ini metametaclass, khusus untuk Anda.
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
Sunting
Ini adalah pertanyaan yang cukup lama, tetapi masih mendapatkan suara positif, jadi saya pikir saya akan menambahkan tautan ke jawaban yang lebih komprehensif. Jika Anda ingin membaca lebih lanjut tentang metaclasses dan penggunaannya, saya baru saja menerbitkan artikel tentangnya di sini .