Banyak orang berpikir bahwa metaprogramming di Python tidak perlu mempersulit kode, tetapi jika Anda menggunakannya dengan benar, Anda dapat dengan cepat dan elegan menerapkan pola desain yang kompleks. Selain itu, kerangka kerja Python yang terkenal seperti Django, DRF, dan SQLAlchemy menggunakan metaclasses untuk memberikan ekstensibilitas yang mudah dan penggunaan kembali kode yang mudah.

Dalam artikel ini saya akan memberi tahu Anda mengapa Anda tidak perlu takut menggunakan metaprogramming dalam proyek Anda dan menunjukkan tugas apa yang paling cocok untuk Anda. Anda dapat mempelajari lebih lanjut tentang opsi metaprogramming di kursus Advanced Python .
Untuk memulai, mari kita ingat kembali dasar-dasar metaprogramming dengan Python. Tidak akan berlebihan untuk menambahkan bahwa semua yang ditulis di bawah ini berlaku untuk Python versi 3.5 dan lebih tinggi.
Tur singkat model data Python
Jadi, kita semua tahu bahwa segala sesuatu di Python adalah objek, dan bukan rahasia lagi bahwa untuk setiap objek ada kelas tertentu yang digunakannya, misalnya:
>>> def f(): pass >>> type(f) <class 'function'>
Jenis objek atau kelas di mana objek itu dihasilkan dapat ditentukan menggunakan fungsi tipe built-in, yang memiliki tanda tangan panggilan yang agak menarik (kita akan membicarakannya sedikit kemudian). Efek yang sama dapat dicapai dengan menurunkan atribut __class__
pada objek apa pun.
Jadi, untuk membuat fungsi, function
kelas function
. Mari kita lihat apa yang bisa kita lakukan dengannya. Untuk melakukan ini, ambil yang kosong dari modul tipe bawaan:
>>> from types import FunctionType >>> FunctionType <class 'function'> >>> help(FunctionType) class function(object) | function(code, globals[, name[, argdefs[, closure]]]) | | Create a function object from a code object and a dictionary. | The optional name string overrides the name from the code object. | The optional argdefs tuple specifies the default argument values. | The optional closure tuple supplies the bindings for free variables.
Seperti yang bisa kita lihat, fungsi apa pun dalam Python adalah turunan dari kelas yang dijelaskan di atas. Sekarang mari kita mencoba membuat fungsi baru tanpa menggunakan deklarasi melalui def
. Untuk melakukan ini, kita perlu belajar cara membuat objek kode menggunakan fungsi kompilasi yang dibangun ke dalam interpreter:
Hebat! Dengan bantuan meta-tools, kami belajar cara membuat fungsi dengan cepat, tetapi dalam praktiknya pengetahuan seperti itu jarang digunakan. Sekarang mari kita lihat bagaimana objek kelas dan objek contoh dari kelas-kelas ini dibuat:
>>> class User: pass >>> user = User() >>> type(user) <class '__main__.User'> >>> type(User) <class 'type'>
Cukup jelas bahwa kelas User
digunakan untuk membuat instance dari user
, jauh lebih menarik untuk melihat kelas type
, yang digunakan untuk membuat kelas User
itu sendiri. Di sini kita akan beralih ke opsi kedua memanggil fungsi type
built-in, yang dalam kombinasi adalah metaclass untuk semua kelas di Python. Metaclass adalah, menurut definisi, kelas yang turunannya adalah kelas lain. Metaclasses memungkinkan kita untuk menyesuaikan proses pembuatan kelas dan mengontrol sebagian proses pembuatan instance kelas.
Menurut dokumentasi, type(name, bases, attrs)
tanda tangan kedua type(name, bases, attrs)
- mengembalikan tipe data baru atau, jika dengan cara sederhana - kelas baru, dan atribut name
menjadi atribut __name__
dari kelas yang dikembalikan, bases
- daftar kelas induk akan tersedia sebagai __bases__
, Nah, attrs
- objek seperti dict yang berisi semua atribut dan metode kelas, akan masuk ke __dict__
. Prinsip fungsi dapat digambarkan sebagai kode semu sederhana dengan Python:
type(name, bases, attrs) ~ class name(bases): attrs
Mari kita lihat bagaimana Anda bisa, hanya menggunakan panggilan type
, membangun kelas yang sama sekali baru:
>>> User = type('User', (), {}) >>> User <class '__main__.User'>
Seperti yang Anda lihat, kita tidak perlu menggunakan kata kunci class
untuk membuat kelas baru, fungsi type
tidak tanpanya, sekarang mari kita lihat contoh yang lebih rumit:
class User: def __init__(self, name): self.name = name class SuperUser(User): """Encapsulate domain logic to work with super users""" group_name = 'admin' @property def login(self): return f'{self.group_name}/{self.name}'.lower()
Seperti yang dapat Anda lihat dari contoh di atas, deskripsi kelas dan fungsi menggunakan class
kata kunci dan def
hanyalah gula sintaksis dan semua jenis objek dapat dibuat dengan panggilan biasa ke fungsi bawaan. Dan sekarang, akhirnya, mari kita bicara tentang bagaimana Anda dapat menggunakan penciptaan dinamis kelas dalam proyek nyata.
Terkadang kita perlu memvalidasi informasi dari pengguna atau dari sumber eksternal lain sesuai dengan skema data yang diketahui sebelumnya. Misalnya, kami ingin mengubah formulir login pengguna dari panel admin - hapus dan tambahkan bidang, ubah strategi validasinya, dll.
Untuk mengilustrasikannya, mari kita coba secara dinamis membuat formulir Django , deskripsi skema yang disimpan dalam format json
berikut:
{ "fist_name": { "type": "str", "max_length": 25 }, "last_name": { "type": "str", "max_length": 30 }, "age": { "type": "int", "min_value": 18, "max_value": 99 } }
Sekarang, berdasarkan uraian di atas, buat satu set bidang dan formulir baru menggunakan fungsi type
sudah kita ketahui:
import json from django import forms fields_type_map = { 'str': forms.CharField, 'int': forms.IntegerField, }
Hebat! Sekarang Anda dapat mentransfer formulir yang dibuat ke templat dan merendernya untuk pengguna. Pendekatan yang sama dapat digunakan dengan kerangka kerja lain untuk validasi data dan presentasi ( DRF Serializers , marshmallow dan lain-lain).
Di atas, kami melihat type
metaclass yang sudah “selesai”, tetapi paling sering dalam kode Anda akan membuat metaclasses Anda sendiri dan menggunakannya untuk mengonfigurasi pembuatan kelas baru dan instance mereka. Dalam kasus umum, "kosong" dari metaclass terlihat seperti ini:
class MetaClass(type): """ : mcs – , <__main__.MetaClass> name – , , , "User" bases – -, (SomeMixin, AbstractUser) attrs – dict-like , cls – , <__main__.User> extra_kwargs – keyword- args kwargs – """ def __new__(mcs, name, bases, attrs, **extra_kwargs): return super().__new__(mcs, name, bases, attrs) def __init__(cls, name, bases, attrs, **extra_kwargs): super().__init__(cls) @classmethod def __prepare__(mcs, cls, bases, **extra_kwargs): return super().__prepare__(mcs, cls, bases, **kwargs) def __call__(cls, *args, **kwargs): return super().__call__(*args, **kwargs)
Untuk menggunakan metaclass ini untuk mengonfigurasi kelas User
, sintaksis berikut digunakan:
class User(metaclass=MetaClass): def __new__(cls, name): return super().__new__(cls) def __init__(self, name): self.name = name
Yang paling menarik adalah urutan interpreter Python memanggil metaclass metamethod pada saat kelas itu sendiri dibuat:
- Interpreter menentukan dan menemukan kelas induk untuk kelas saat ini (jika ada).
- Penerjemah mendefinisikan metaclass (
MetaClass
dalam kasus kami). - Metode
MetaClass.__prepare__
- ia harus mengembalikan objek seperti dict di mana atribut dan metode kelas akan ditulis. Setelah itu, objek akan diteruskan ke metode MetaClass.__new__
melalui argumen attrs
. Kita akan berbicara tentang penggunaan praktis metode ini sedikit kemudian dalam contoh. - Interpreter membaca tubuh kelas
User
dan menghasilkan parameter untuk meneruskannya ke metaclass MetaClass
. - Metode
MetaClass.__new__
- metode MetaClass.__new__
, mengembalikan objek kelas yang dibuat. Kami sudah bertemu dengan name
argumen, bases
dan attrs
ketika kami meneruskannya ke fungsi type
, dan kami akan berbicara tentang parameter **extra_kwargs
sedikit kemudian. Jika jenis argumen __prepare__
diubah menggunakan __prepare__
, maka harus dikonversi ke dict
sebelum meneruskannya ke pemanggilan metode super()
. - Metode
MetaClass.__init__
- metode initializer yang dengannya Anda dapat menambahkan atribut dan metode tambahan ke objek kelas di kelas. Dalam praktiknya, ini digunakan dalam kasus di mana metaclasses diwarisi dari metaclasses lain, jika tidak semua yang dapat dilakukan dalam __init__
lebih baik dilakukan dalam __new__
. Sebagai contoh, parameter __slots__
hanya dapat diatur dalam metode __new__
dengan menulisnya ke objek attrs
. - Pada langkah ini, kelas dianggap dibuat.
Sekarang buat instance kelas User
kami dan lihat rantai panggilan:
user = User(name='Alyosha')
- Pada saat memanggil
User(...)
interpreter memanggil metode MetaClass.__call__(name='Alyosha')
, di mana ia melewati objek kelas dan argumen berlalu. MetaClass.__call__
memanggil User.__new__(name='Alyosha')
- metode konstruktor yang membuat dan mengembalikan turunan dari kelas User
- Selanjutnya,
MetaClass.__call__
memanggil User.__init__(name='Alyosha')
- metode inisialisasi yang menambahkan atribut baru ke instance yang dibuat. MetaClass.__call__
mengembalikan instance kelas User
yang dibuat dan diinisialisasi.- Pada titik ini, turunan dari kelas dianggap dibuat.
Deskripsi ini, tentu saja, tidak mencakup semua nuansa menggunakan metaclasses, tetapi cukup untuk mulai menggunakan metaprogramming untuk menerapkan beberapa pola arsitektur. Teruskan ke contoh!
Kelas abstrak
Dan contoh pertama dapat ditemukan di perpustakaan standar: ABCMeta - metaclass memungkinkan Anda untuk mendeklarasikan abstrak kelas kami dan memaksa semua turunannya untuk menerapkan metode, properti, dan atribut yang telah ditentukan, jadi lihat:
from abc import ABCMeta, abstractmethod class BasePlugin(metaclass=ABCMeta): """ supported_formats run """ @property @abstractmethod def supported_formats(self) -> list: pass @abstractmethod def run(self, input_data: dict): pass
Jika semua metode dan atribut abstrak tidak diterapkan di ahli waris, maka ketika kami mencoba membuat turunan dari kelas pewaris, kami mendapatkan TypeError
:
class VideoPlugin(BasePlugin): def run(self): print('Processing video...') plugin = VideoPlugin()
Menggunakan kelas abstrak membantu untuk segera memperbaiki antarmuka kelas dasar dan menghindari kesalahan pewarisan di masa mendatang, misalnya kesalahan ketik atas nama metode yang diganti.
Sistem plugin pendaftaran otomatis
Cukup sering, metaprogramming digunakan untuk mengimplementasikan berbagai pola desain. Hampir semua kerangka kerja terkenal menggunakan metaclasses untuk membuat objek registri . Objek tersebut menyimpan tautan ke objek lain dan memungkinkannya diterima dengan cepat di mana saja dalam program. Pertimbangkan contoh sederhana pendaftaran otomatis plugin untuk memutar file media dari berbagai format.
Implementasi metaclass:
class RegistryMeta(ABCMeta): """ , . " " -> " " """ _registry_formats = {} def __new__(mcs, name, bases, attrs): cls: 'BasePlugin' = super().__new__(mcs, name, bases, attrs)
Dan di sini adalah plugin itu sendiri, kami akan mengambil implementasi BasePlugin
dari contoh sebelumnya:
class BasePlugin(metaclass=RegistryMeta): ... class VideoPlugin(BasePlugin): supported_formats = ['mpg', 'mov'] def run(self): ... class AudioPlugin(BasePlugin): supported_formats = ['mp3', 'flac'] def run(self): ...
Setelah mengeksekusi kode ini, penerjemah akan mendaftarkan 4 format dan 2 plugin di registri kami yang dapat memproses format ini:
>>> RegistryMeta.show_registry() {'flac': <class '__main__.AudioPlugin'>, 'mov': <class '__main__.VideoPlugin'>, 'mp3': <class '__main__.AudioPlugin'>, 'mpg': <class '__main__.VideoPlugin'>} >>> plugin_class = RegistryMeta.get_plugin('mov') >>> plugin_class <class '__main__.VideoPlugin'> >>> plugin_class().run() Processing video...
Di sini perlu diperhatikan satu lagi nuansa menarik dari bekerja dengan metaclasses, berkat urutan resolusi metode yang tidak jelas, kita dapat memanggil metode show_registry
tidak hanya untuk kelas RegistyMeta
, tetapi untuk kelas lain apa pun metaclass itu:
>>> AudioPlugin.get_plugin('avi')
Menggunakan metaclasses, Anda bisa menggunakan nama atribut kelas sebagai metadata untuk objek lain. Tidak ada yang jelas? Tapi saya yakin Anda telah melihat pendekatan ini berkali-kali, misalnya, deklarasi deklarasi bidang model di Django:
class Book(models.Model): title = models.Charfield(max_length=250)
Pada contoh di atas, title
adalah nama pengidentifikasi Python, juga digunakan untuk memberi nama kolom pada tabel book
, meskipun kami tidak secara eksplisit menunjukkan ini di mana pun. Ya, "keajaiban" seperti itu dapat diwujudkan dengan bantuan metaprogramming. Mari, misalnya, menerapkan sistem untuk mengirimkan kesalahan aplikasi ke front-end, sehingga setiap pesan memiliki kode yang dapat dibaca yang dapat digunakan untuk menerjemahkan pesan ke bahasa lain. Jadi, kami memiliki objek pesan yang dapat dikonversi ke json
:
class Message: def __init__(self, text, code=None): self.text = text self.code = code def to_json(self): return json.dumps({'text': self.text, 'code': self.code})
Semua pesan kesalahan kami akan disimpan di "namespace" yang terpisah:
class Messages: not_found = Message('Resource not found') bad_request = Message('Request body is invalid') ... >>> Messages.not_found.to_json() {"text": "Resource not found", "code": null}
Sekarang kita ingin code
menjadi bukan null
, tetapi not_found
, untuk ini kita menulis metaclass berikut:
class MetaMessage(type): def __new__(mcs, name, bases, attrs): for attr, value in attrs.items():
Mari kita lihat bagaimana postingan kita sekarang terlihat:
>>> Messages.not_found.to_json() {"text": "Resource not found", "code": "not_found"} >>> Messages.bad_request.to_json() {"text": "Request body is invalid", "code": "bad_request"}
Apa yang kamu butuhkan! Sekarang Anda tahu apa yang harus dilakukan sehingga dengan format data Anda dapat dengan mudah menemukan kode yang memprosesnya.
Kasus umum lainnya adalah caching setiap data statis pada tahap pembuatan kelas, agar tidak membuang waktu untuk menghitungnya saat aplikasi sedang berjalan. Selain itu, beberapa data dapat diperbarui saat membuat instance kelas baru, misalnya, penghitung jumlah objek yang dibuat.
Bagaimana ini bisa digunakan? Misalkan Anda sedang mengembangkan kerangka kerja untuk membuat laporan dan tabel dan Anda memiliki objek seperti itu:
class Row(metaclass=MetaRow): name: str age: int ... def __init__(self, **kwargs): self.counter = None for attr, value in kwargs.items(): setattr(self, attr, value) def __str__(self): out = [self.counter]
Kami ingin menyimpan dan menambah penghitung saat membuat baris baru, dan juga ingin membuat tajuk dari tabel yang dihasilkan sebelumnya. Metaclass untuk menyelamatkan!
class MetaRow(type):
Dua hal yang perlu diklarifikasi di sini:
- Kelas
Row
tidak memiliki atribut kelas dengan nama name
dan age
- ini adalah tipe anotasi , sehingga tidak ada dalam kunci kamus attrs
, dan untuk mendapatkan daftar bidang, kami menggunakan __annotations__
kelas __annotations__
. - Operasi
cls.row_count += 1
seharusnya menyesatkan Anda: bagaimana bisa begitu? Lagipula, cls
adalah kelas Row
, itu tidak memiliki atribut row_count
. Semuanya benar, tetapi seperti yang saya jelaskan di atas - jika kelas yang dibuat tidak memiliki atribut atau metode yang mereka coba panggil, maka penerjemah melangkah lebih jauh di sepanjang rantai kelas dasar - jika tidak ada di dalamnya, pencarian dilakukan di metaclass. Dalam kasus seperti itu, agar tidak membingungkan siapa pun, lebih baik menggunakan catatan lain: MetaRow.row_count += 1
.
Lihat betapa elegannya Anda sekarang dapat menampilkan seluruh tabel:
rows = [ Row(name='Valentin', age=25), Row(name='Sergey', age=33), Row(name='Gosha'), ] print(' | '.join(Row.__header__)) for row in rows: print(row)
№ | age | name 1 | 25 | Valentin 2 | 33 | Sergey 3 | N/A | Gosha
Omong-omong, menampilkan dan bekerja dengan tabel dapat dienkapsulasi dalam kelas Sheet
terpisah.
Dilanjutkan ...
Pada bagian selanjutnya dari artikel ini, saya akan menjelaskan cara menggunakan metaclasses untuk men-debug kode aplikasi Anda, cara membuat parameter pembuatan metaclass, dan menunjukkan contoh dasar menggunakan metode __prepare__
. Tetap disini!
Secara lebih rinci tentang metaclasses dan deskriptor dalam Python saya akan menceritakan dalam kerangka Advanced Python .