Python: metaprogramming dalam produksi. Bagian satu

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:


 #   ,    "Hello, world!" >>> code = compile('print("Hello, world!")', '<repl>', 'eval') >>> code <code object <module> at 0xdeadbeef, file "<repl>", line 1> #  ,     , #      >>> func = FunctionType(code, globals(), 'greetings') >>> func <function <module> at 0xcafefeed> >>> func.__name__ 'greetings' >>> func() Hello, world! 

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() #     SuperUser "" CustomSuperUser = type( #   'SuperUser', #  ,      (User, ), #         { '__doc__': 'Encapsulate domain logic to work with super users', 'group_name': 'admin', 'login': property(lambda self: f'{self.group_name}/{self.name}'.lower()), } ) assert SuperUser.__doc__ == CustomSuperUser.__doc__ assert SuperUser('Vladimir').login == CustomSuperUser('Vladimir').login 

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.


Secara dinamis membuat formulir dan validator


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, } # form_description –  json    deserialized_form_description: dict = json.loads(form_description) form_attrs = {} #            for field_name, field_description in deserialized_form_description.items(): field_class = fields_type_map[field_description.pop('type')] form_attrs[field_name] = field_class(**field_description) user_form_class = type('DynamicForm', (forms.Form, ), form_attrs) >>> form = user_form_class({'age': 101}) >>> form <DynamicForm bound=True, valid=Unknown, fields=(fist_name;last_name;age)> >>> form.is_valid() False >>> form.errors {'fist_name': ['This field is required.'], 'last_name': ['This field is required.'], 'age': ['Ensure this value is less than or equal to 99.']} 

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).


Mengkonfigurasi pembuatan kelas baru melalui metaclass


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:


  1. Interpreter menentukan dan menemukan kelas induk untuk kelas saat ini (jika ada).
  2. Penerjemah mendefinisikan metaclass ( MetaClass dalam kasus kami).
  3. 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.
  4. Interpreter membaca tubuh kelas User dan menghasilkan parameter untuk meneruskannya ke metaclass MetaClass .
  5. 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() .
  6. 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 .
  7. Pada langkah ini, kelas dianggap dibuat.

Sekarang buat instance kelas User kami dan lihat rantai panggilan:


 user = User(name='Alyosha') 

  1. Pada saat memanggil User(...) interpreter memanggil metode MetaClass.__call__(name='Alyosha') , di mana ia melewati objek kelas dan argumen berlalu.
  2. MetaClass.__call__ memanggil User.__new__(name='Alyosha') - metode konstruktor yang membuat dan mengembalikan turunan dari kelas User
  3. Selanjutnya, MetaClass.__call__ memanggil User.__init__(name='Alyosha') - metode inisialisasi yang menambahkan atribut baru ke instance yang dibuat.
  4. MetaClass.__call__ mengembalikan instance kelas User yang dibuat dan diinisialisasi.
  5. 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() # TypeError: Can't instantiate abstract class VideoPlugin # with abstract methods supported_formats 

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) #     (BasePlugin) if inspect.isabstract(cls): return cls for media_format in cls.supported_formats: if media_format in mcs._registry_formats: raise ValueError(f'Format {media_format} is already registered') #       mcs._registry_formats[media_format] = cls return cls @classmethod def get_plugin(mcs, media_format: str): try: return mcs._registry_formats[media_format] except KeyError: raise RuntimeError(f'Plugin is not defined for {media_format}') @classmethod def show_registry(mcs): from pprint import pprint pprint(mcs._registry_formats) 

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') # RuntimeError: Plugin is not found for avi 

Menggunakan nama atribut sebagai metadata


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(): #          Message #    code    # ( code   ) if isinstance(value, Message) and value.code is None: value.code = attr return super().__new__(mcs, name, bases, attrs) class Messages(metaclass=MetaMessage): ... 

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.


Caching metadata tentang kelas dan turunannya


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] #  __header__      for name in self.__header__[1:]: out.append(getattr(self, name, 'N/A')) return ' | '.join(map(str, out)) 

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): #      row_count = 0 def __new__(mcs, name, bases, attrs): cls = super().__new__(mcs, name, bases, attrs) #          cls.__header__ = ['№'] + sorted(attrs['__annotations__'].keys()) return cls def __call__(cls, *args, **kwargs): #      row: 'Row' = super().__call__(*args, **kwargs) #    cls.row_count += 1 #     row.counter = cls.row_count return row 

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 .

Source: https://habr.com/ru/post/id422409/


All Articles