Enum lebih cepat

tl; dr


github.com/QratorLabs/fastenum
pip install fast-enum 

Apa itu enum?


(Jika Anda pikir Anda tahu itu - gulir ke bawah ke bagian "Enums in Standard Library").

Bayangkan Anda perlu mendeskripsikan sekumpulan keadaan yang memungkinkan untuk entitas dalam model database Anda. Anda mungkin akan menggunakan banyak konstanta yang didefinisikan sebagai atribut tingkat modul:
 # /path/to/package/static.py: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 ... 

... atau sebagai atribut level-kelas yang didefinisikan di kelas mereka sendiri:
 class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 

Itu membantu Anda merujuk negara-negara tersebut dengan nama mnemoniknya, sementara mereka tetap berada di penyimpanan Anda sebagai bilangan bulat sederhana. Dengan ini, Anda menyingkirkan angka ajaib yang tersebar melalui kode Anda dan membuatnya lebih mudah dibaca dan deskriptif diri.

Tapi, baik konstanta level modul dan kelas dengan atribut statis menderita dari sifat inheren objek python: mereka semua bisa berubah. Anda dapat secara tidak sengaja memberikan nilai pada konstanta saat runtime, dan itu adalah kekacauan untuk debug dan mengembalikan entitas yang rusak. Jadi, Anda mungkin ingin membuat rangkaian konstanta Anda tidak berubah, yang berarti jumlah konstanta yang dideklarasikan dan nilai-nilai yang dipetakan tidak boleh dimodifikasi saat runtime.

Untuk tujuan ini, Anda dapat mencoba mengaturnya menjadi nama tupel dengan namedtuple() , sebagai contoh:
 MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4) 

Namun, ini masih tidak terlihat terlalu dimengerti: di samping itu, objek namedtuple tidak benar-benar dapat diperpanjang. Katakanlah Anda memiliki UI yang menampilkan semua status ini. Anda kemudian dapat menggunakan konstanta berbasis modul, kelas Anda dengan atribut, atau menamai tuple untuk merendernya (dua yang terakhir lebih mudah untuk di-render, sementara kami melakukannya). Tetapi kode Anda tidak memberikan peluang apa pun untuk memberi pengguna deskripsi yang memadai untuk setiap negara yang telah Anda tetapkan. Selain itu, jika Anda berencana untuk mengimplementasikan dukungan multi-bahasa dan i18n di UI Anda, Anda akan menemukan bahwa mengisi semua terjemahan untuk deskripsi ini menjadi tugas yang sangat melelahkan. Nilai-nilai negara yang cocok mungkin tidak harus memiliki deskripsi yang cocok yang berarti bahwa Anda tidak bisa hanya memetakan semua status INITIAL Anda ke deskripsi yang sama di gettext . Alih-alih, konstanta Anda menjadi ini:
 INITIAL = (0, 'My_MODEL_INITIAL_STATE') 

Kelas Anda kemudian menjadi ini:
 class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE') 

Dan akhirnya, namamu yang namedtuple menjadi ini:
 EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...) 

Baiklah, cukup bagus, sekarang memastikan nilai status dan stub terjemahan dipetakan ke bahasa yang didukung oleh UI Anda. Tetapi sekarang Anda mungkin memperhatikan bahwa kode yang menggunakan pemetaan itu telah berubah menjadi berantakan. Setiap kali Anda mencoba untuk menetapkan nilai pada entitas Anda, Anda juga tidak perlu lupa untuk mengekstraksi nilai pada indeks 0 dari pemetaan yang Anda gunakan:

 my_entity.state = INITIAL[0] 
atau
 my_entity.state = MyModelStates.INITIAL[0] 
atau
 my_entity.state = EntityStates.INITIAL[0] 

Dan sebagainya. Perlu diingat bahwa dua pendekatan pertama yang menggunakan atribut konstanta dan kelas, masing-masing, masih mengalami mutabilitas.

Dan kemudian Enums muncul di panggung


 class MyEntityStates(Enum): def __init__(self, val, description): self.val = val self.description = description INITIAL = (0, 'MY_MODEL_INITIAL_STATE') PROCESSING = (1, 'MY_MODEL_BEING_PROCESSED_STATE') PROCESSED = (2, 'MY_MODEL_PROCESSED_STATE') DECLINED = (3, 'MY_MODEL_DECLINED_STATE') RETURNED = (4, 'MY_MODEL_RETURNED_STATE') 

Itu dia. Sekarang Anda dapat dengan mudah mengulangi enum di renderer Anda (sintaks Jinja2):
 {% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %} 

Enum tidak dapat diubah untuk kedua set anggota (Anda tidak dapat menentukan anggota baru saat runtime, Anda juga tidak dapat menghapus anggota yang sudah ditentukan) dan nilai anggota yang mereka pertahankan (Anda tidak dapat menetapkan kembali nilai atribut apa pun atau menghapus atribut).

Dalam kode Anda, Anda hanya menetapkan nilai untuk entitas Anda seperti ini:
 my_entity.state = MyEntityStates.INITIAL.val 

Cukup jelas. Cukup deskriptif. Cukup diperpanjang. Untuk itulah kami menggunakan Enums.

Kenapa lebih cepat?


Tetapi ENUM default agak lambat jadi kami bertanya pada diri sendiri - bisakah kami membuatnya lebih cepat?
Ternyata, kita bisa. Yaitu, dimungkinkan untuk membuatnya:

  • 3 kali lebih cepat pada akses anggota
  • ~ 8,5 kali lebih cepat pada akses atribut ( name , value )
  • 3 kali lebih cepat pada akses enum dengan nilai (panggilan pada kelas enum MyEnum(value) )
  • 1,5 kali lebih cepat pada akses enum dengan nama (seperti MyEnum[name] seperti dict)

Jenis dan objek bersifat dinamis dengan Python. Tapi Python memiliki alat untuk membatasi sifat dinamis objek. Dengan bantuan mereka, seseorang dapat memperoleh peningkatan kinerja yang signifikan menggunakan __slots__ serta menghindari menggunakan Deskriptor Data jika memungkinkan tanpa pertumbuhan kompleksitas yang signifikan atau jika Anda bisa mendapatkan keuntungan dalam kecepatan.

Slot


Sebagai contoh, seseorang dapat menggunakan deklarasi kelas dengan __slots__ - dalam kasus ini, instance kelas hanya akan memiliki sekumpulan atribut terbatas: atribut yang dideklarasikan dalam __slots__ dan semua __slots__ dari kelas induk.

Penjelas


Secara default, juru bahasa Python mengembalikan nilai atribut suatu objek secara langsung:
 value = my_obj.attribute # this is a direct access to the attribute value by the pointer that the object holds for that attribute 

Menurut model data Python, jika nilai atribut suatu objek itu sendiri merupakan objek yang mengimplementasikan Protokol Penjelasan Data, itu berarti bahwa ketika Anda mencoba untuk mendapatkan nilai itu, Anda pertama-tama mendapatkan atribut sebagai objek dan kemudian metode khusus __get__ adalah dipanggil pada atribut-objek yang melewati objek penjaga itu sendiri sebagai argumen:
 obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj) 

Enum di Perpustakaan Standar


Setidaknya name dan value atribut dari implementasi Enum standar dinyatakan sebagai types.DynamicClassAttribute . Itu berarti bahwa ketika Anda mencoba untuk mendapatkan name (atau value ) anggota, alurnya mengikuti:

 one_value = StdEnum.ONE.value # that is what you write in your code one_value_attribute = StdEnum.ONE.value one_value = one_value_attribute.__get__(StdEnum.ONE) 

 # and this is what really __get__ does (python 3.7 implementation): def __get__(self, instance, ownerclass=None): if instance is None: if self.__isabstractmethod__: return self raise AttributeError() elif self.fget is None: raise AttributeError("unreadable attribute") return self.fget(instance) 

 # since DynamicClassAttribute is a decorator on Enum methods `name` and `value` the final row of __get__() ends up with: @DynamicClassAttribute def name(self): """The name of the Enum member.""" return self._name_ @DynamicClassAttribute def value(self): """The value of the Enum member.""" return self._value_ 

Jadi, aliran lengkap dapat direpresentasikan sebagai pseudo-code berikut:
 def get_func(enum_member, attrname): # this is also a __dict__ lookup so hash + hashtable scan also occur return getattr(enum_member, f'_{attrnme}_') def get_name_value(enum_member): name_descriptor = get_descriptor(enum_member, 'name') if enum_member is None: if name_descriptor.__isabstractmethod__: return name_descriptor raise AttributeError() elif name_descriptor.fget is None: raise AttributeError("unreadable attribute") return get_func(enum_member, 'name') 

Kami telah membuat skrip sederhana yang menunjukkan kesimpulan di atas:
 from enum import Enum class StdEnum(Enum): def __init__(self, value, description): self.v = value self.description = description A = 1, 'One' B = 2, 'Two' def get_name(): return StdEnum.A.name from pycallgraph import PyCallGraph from pycallgraph.output import GraphvizOutput graphviz = GraphvizOutput(output_file='stdenum.png') with PyCallGraph(output=graphviz): v = get_name() 

Dan setelah kita menjalankan skrip, ia menciptakan gambar ini untuk kita:


Ini membuktikan bahwa setiap kali Anda mengakses name atribut stdlib enum dan value itu panggilan descriptor. Deskriptor itu pada akhirnya berakhir dengan panggilan ke properti def name(self) stdlib enum def name(self) didekorasi dengan deskriptor.

Nah, Anda dapat membandingkan ini dengan FastEnum kami:
 from fast_enum import FastEnum class MyNewEnum(metaclass=FastEnum): A = 1 B = 2 def get_name(): return MyNewEnum.A.name from pycallgraph import PyCallGraph from pycallgraph.output import GraphvizOutput graphviz = GraphvizOutput(output_file='fastenum.png') with PyCallGraph(output=graphviz): v = get_name() 

Yang menampilkan gambar ini:


Itulah yang benar-benar dilakukan di dalam implementasi Enum standar setiap kali Anda mengakses atribut name dan value anggota Enum Anda. Dan itulah mengapa implementasi kami lebih cepat.

Implementasi kelas Enum oleh Python Standard Library menggunakan banyak panggilan protokol descriptor. Ketika kami mencoba menggunakan enum standar dalam proyek kami, kami telah memperhatikan berapa banyak protokol deskriptor yang memanggil atribut name dan value dari anggota Enum yang dipanggil. Dan karena pencacahan digunakan secara berlebihan di seluruh kode, kinerja yang dihasilkan buruk.

Selain itu, kelas enum standar berisi beberapa atribut pembantu "terlindungi":
  • _member_names_ - daftar yang berisi semua nama anggota enum;
  • _member_map_ - sebuah OrderedDict yang memetakan nama anggota enum untuk anggota itu sendiri;
  • _value2member_map_ - kamus terbalik yang memetakan nilai anggota enum ke anggota enum yang sesuai.

Kamus pencarian lambat karena masing-masing mengarah ke perhitungan hash dan pencarian tabel hash, membuat kamus-kamus itu struktur dasar tidak optimal untuk kelas enum. Bahkan pengambilan anggota itu sendiri (seperti dalam StdEnum.MEMBER ) adalah pencarian kamus.

Jalan kita


Saat mengembangkan implementasi Enum kami, kami mengingat enumerasi C-language yang cantik dan Java Enum yang dapat diperluas. Fitur utama yang kami inginkan dalam implementasi kami:

  • sebuah Enum harus se-statis mungkin; yang kami maksud dengan "statis" adalah: Jika sesuatu dapat dihitung sekali dan pada waktu deklarasi, itu harus;
  • sebuah Enum tidak dapat disubklasifikasi (harus menjadi kelas "final") jika sebuah subkelas mendefinisikan anggota enum baru - ini berlaku untuk implementasi perpustakaan standar, dengan pengecualian bahwa subklasifikasi dilarang bahkan jika tidak ada anggota baru yang ditetapkan;
  • sebuah Enum harus memiliki kemungkinan luas untuk ekstensi (atribut tambahan, metode, dan sebagainya).

Satu-satunya waktu kami menggunakan pencarian kamus adalah dalam value pemetaan terbalik untuk anggota Enum. Semua perhitungan lain dilakukan hanya sekali selama deklarasi kelas (di mana kait metaclasses digunakan untuk mengkustomisasi pembuatan tipe).
Berbeda dengan implementasi perpustakaan standar, kami memperlakukan nilai pertama setelah = tanda dalam deklarasi kelas sebagai nilai anggota:
A = 1, 'One' di pustaka standar dengan seluruh tuple 1, "One" diperlakukan sebagai value
A: 'MyEnum' = 1, 'One' dalam implementasi kami, hanya 1 yang dianggap sebagai value

__slots__ lebih lanjut diperoleh dengan menggunakan __slots__ kapan pun memungkinkan. Dalam model data Python, kelas yang dideklarasikan dengan __slots__ tidak memiliki atribut __dict__ yang menyimpan atribut instance (jadi Anda tidak dapat menetapkan atribut apa pun yang tidak disebutkan dalam __slots__ ). Selain itu, atribut yang didefinisikan dalam __slots__ diakses pada offset konstan ke pointer objek level-C. Itu adalah akses atribut kecepatan tinggi karena ia menghindari perhitungan hash dan pemindaian hashtable.

Apa fasilitas tambahannya?


FastEnum tidak kompatibel dengan versi Python sebelum 3.6, karena terlalu banyak menggunakan modul typing yang diperkenalkan di Python 3.6; Orang bisa berasumsi bahwa menginstal modul typing backport dari PyPI akan membantu. Jawabannya adalah: tidak. Implementasi menggunakan PEP-484 untuk beberapa argumen fungsi dan metode dan mengisyaratkan tipe nilai kembali, sehingga versi apa pun sebelum Python 3.5 tidak didukung karena ketidakcocokan sintaksis. Tapi sekali lagi, baris pertama kode di __new__ dari metaclass menggunakan sintaks PEP-526 untuk mengisyaratkan tipe variabel. Jadi Python 3.5 tidak akan melakukan keduanya. Dimungkinkan untuk port implementasi ke versi yang lebih lama, meskipun kami di Qrator Labs cenderung menggunakan petunjuk tipe bila memungkinkan karena sangat membantu mengembangkan proyek yang kompleks. Dan hei! Anda tidak ingin tetap menggunakan python sebelum 3.6 karena tidak ada yang tidak kompatibel dengan kode Anda yang ada (dengan asumsi Anda tidak menggunakan Python 2) meskipun banyak pekerjaan yang dilakukan di asyncio dibandingkan dengan 3,5.

Itu, pada gilirannya, membuat impor khusus seperti auto tidak perlu, tidak seperti di perpustakaan standar. Anda mengetik-isyarat semua anggota Enum Anda dengan nama kelas Enum Anda, tidak memberikan nilai sama sekali - dan nilai akan dihasilkan untuk Anda secara otomatis. Meskipun python 3.6 sudah cukup untuk bekerja dengan FastEnum, perlu diingat bahwa urutan kamus standar dari pernyataan deklarasi diperkenalkan hanya dalam python 3.7. Kami tidak tahu peralatan apa pun yang bermanfaat di mana urutan nilai yang dihasilkan secara otomatis penting (karena kami menganggap nilai yang dihasilkan itu sendiri bukanlah nilai yang dipedulikan oleh programmer). Meskipun demikian, anggap diri Anda diperingatkan jika Anda masih menggunakan python 3.6;

Mereka yang membutuhkan enum mereka mulai dari 0 (nol) daripada default 1 dapat melakukan ini dengan atribut deklarasi enum khusus _ZERO_VALUED , atribut itu "dihapus" dari kelas Enum yang dihasilkan;

Namun ada beberapa batasan: semua nama anggota enum harus dikapitalisasi atau mereka tidak akan diambil oleh metaclass dan tidak akan diperlakukan sebagai anggota enum;

Namun, Anda bisa mendeklarasikan kelas dasar untuk enum Anda (perlu diingat bahwa kelas dasar dapat menggunakan enum metaclass itu sendiri, sehingga Anda tidak perlu memberikan metaclass ke semua subclass): Anda dapat menentukan logika umum (atribut dan metode) dalam hal ini kelas, tetapi mungkin tidak mendefinisikan anggota enum (sehingga kelas tidak akan "diselesaikan"). Anda kemudian dapat subkelas kelas itu dalam deklarasi enum sebanyak yang Anda inginkan dan itu akan memberi Anda semua logika umum;

Alias Kami akan menjelaskannya dalam topik terpisah (diterapkan pada 1.2.5)

Alias ​​dan bagaimana mereka bisa membantu


Misalkan Anda memiliki kode yang menggunakan:
 package_a.some_lib_enum.MyEnum 

Dan MyEnum dinyatakan seperti ini:
 class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum' 

Sekarang, Anda memutuskan untuk membuat refactoring dan ingin memindahkan enum Anda ke paket lain. Anda membuat sesuatu seperti ini:
 package_b.some_lib_enum.MyMovedEnum 

Di mana MyMovedEnum dinyatakan seperti ini:
 class MyMovedEnum(MyEnum): pass 

Sekarang Anda siap untuk memulai tahap "penghentian" untuk semua kode yang menggunakan enum Anda. Anda mengalihkan penggunaan langsung MyEnum untuk menggunakan MyMovedEnum (yang terakhir memiliki semua anggotanya diproksi ke dalam MyEnum ). Anda menyatakan dalam dokumen proyek Anda bahwa MyEnum sudah usang dan akan dihapus dari kode di beberapa titik di masa depan. Misalnya, dalam rilis berikutnya. Pertimbangkan kode Anda menyimpan objek Anda dengan atribut enum menggunakan acar. Pada titik ini, Anda menggunakan MyMovedEnum dalam kode Anda, tetapi secara internal semua anggota enum Anda masih merupakan instance MyEnum . Langkah Anda berikutnya adalah dengan menukar deklarasi MyEnum dan MyMovedEnum sehingga MyMovedEnum sekarang tidak akan menjadi subkelas dari MyEnum dan mendeklarasikan semua anggotanya sendiri; MyEnum , di sisi lain, tidak akan mendeklarasikan anggota tetapi menjadi hanya alias (subkelas) dari MyMovedEnum .

Dan itu menyimpulkannya. Saat memulai kembali runtime Anda pada tahap unpickle, semua nilai enum Anda akan dialihkan ke MyMovedEnum dan menjadi terikat kembali ke kelas baru itu. Saat Anda yakin semua objek acar Anda telah tidak (kembali) diasapi dengan struktur organisasi kelas ini, Anda bebas untuk membuat rilis baru, di mana sebelumnya ditandai sebagai MyEnum Anda yang sudah usang dapat dinyatakan usang dan dilenyapkan dari basis kode Anda.

Kami mendorong Anda untuk mencobanya! github.com/QratorLabs/fastenum , pypi.org/project/fast-enum . Semua kredit dikirim ke penulis FastEnum, santjagocorkez .

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


All Articles