ENUM cepat

tl; dr


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

Mengapa pencacahan dibutuhkan


(jika Anda tahu segalanya - buka bagian "Enumerasi di perpustakaan standar")

Bayangkan bahwa Anda perlu mendeskripsikan satu set semua status entitas yang mungkin dalam model database Anda sendiri. Kemungkinan besar, Anda akan mengambil banyak konstanta yang didefinisikan langsung di namespace modul:
 # /path/to/package/static.py: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 ... 

... atau sebagai atribut kelas statis:
 class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 

Pendekatan ini akan membantu merujuk negara-negara ini dengan nama mnemonik, sedangkan di repositori Anda, mereka akan menjadi bilangan bulat biasa. Dengan demikian, Anda secara simultan menyingkirkan angka ajaib yang tersebar di berbagai bagian kode, sekaligus membuatnya lebih mudah dibaca dan informatif.

Namun, baik konstanta modul dan kelas dengan atribut statis menderita dari sifat intrinsik objek Python: mereka semua bisa berubah (bisa berubah). Anda dapat secara tidak sengaja memberikan nilai pada konstanta saat run time, dan debugging dan memutar kembali objek yang rusak adalah petualangan yang terpisah. Jadi, Anda mungkin ingin membuat bundel konstanta tidak berubah dalam arti bahwa jumlah konstanta yang dideklarasikan dan nilai-nilai mereka yang dipetakan tidak akan berubah selama eksekusi program.

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

Tapi ini tidak terlihat sangat rapi dan mudah dibaca, dan objek yang namedtuple , pada gilirannya, tidak terlalu dapat diperluas. Misalkan Anda memiliki UI yang menampilkan semua status ini. Anda dapat menggunakan konstanta dalam modul, kelas dengan atribut, atau menamai tuple untuk merendernya (dua yang terakhir lebih mudah di-render karena kita membicarakan ini). Tetapi kode seperti itu tidak memungkinkan untuk memberikan kepada pengguna deskripsi yang memadai untuk setiap negara yang Anda tetapkan. Selain itu, jika Anda berencana untuk menerapkan multibahasa dan dukungan i18n di UI Anda, Anda akan menyadari betapa cepatnya menyelesaikan semua terjemahan untuk deskripsi ini menjadi tugas yang sangat membosankan. Mencocokkan nama negara tidak harus berarti mencocokkan deskripsi, yang berarti Anda tidak bisa hanya memetakan semua status INITIAL Anda ke deskripsi yang sama di gettext . Alih-alih, konstanta Anda mengambil bentuk berikut:
 INITIAL = (0, 'My_MODEL_INITIAL_STATE') 

Atau kelas Anda menjadi seperti ini:
 class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE') 

Akhirnya, tuple bernama berubah menjadi:
 EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...) 

Sudah tidak buruk - sekarang menjamin bahwa nilai status dan tulisan rintisan ditampilkan dalam bahasa yang didukung oleh UI. Tetapi Anda mungkin memperhatikan bahwa kode yang menggunakan pemetaan ini telah menjadi berantakan. Setiap kali, mencoba untuk menetapkan nilai entitas, Anda harus mengekstraksi nilai dengan indeks 0 dari tampilan yang Anda gunakan:

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

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

Dan transfer datang untuk membantu kami


 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 saja. Sekarang Anda dapat dengan mudah beralih pada daftar di render Anda (sintaks Jinja2):
 {% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %} 

Pencacahan tidak dapat diubah untuk sekumpulan elemen - Anda tidak dapat menentukan anggota baru pencacahan saat runtime dan Anda tidak dapat menghapus anggota yang sudah ditentukan, atau untuk nilai-nilai elemen yang disimpannya - Anda tidak dapat [kembali] menetapkan nilai atribut apa pun atau menghapus atribut.

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

Semuanya cukup jelas, informatif, dan dapat diperluas. Untuk inilah kami menggunakan enumerasi.

Bagaimana kita bisa membuatnya lebih cepat?


Enumerasi dari perpustakaan standar agak lambat, jadi kami bertanya pada diri sendiri - dapatkah kami mempercepatnya? Ternyata, kita bisa, yaitu, pelaksanaan enumerasi kami:

  • Tiga kali lebih cepat pada akses ke pencacahan anggota;
  • ~ 8,5 lebih cepat ketika mengakses atribut ( name , value ) anggota;
  • 3 kali lebih cepat ketika mengakses anggota berdasarkan nilai (panggil konstruktor enumerasi MyEnum(value)) ;
  • 1,5 kali lebih cepat ketika mengakses anggota dengan nama (seperti dalam kamus MyEnum[name] ).

Jenis dan objek dalam Python bersifat dinamis. Tetapi ada alat untuk membatasi sifat dinamis dari objek. Anda bisa mendapatkan peningkatan kinerja yang signifikan dengan __slots__ . Ada juga potensi peningkatan kecepatan jika Anda menghindari penggunaan deskriptor data jika memungkinkan - tetapi Anda harus mempertimbangkan kemungkinan peningkatan signifikan dalam kompleksitas aplikasi.

Slot


Misalnya, Anda dapat menggunakan deklarasi kelas menggunakan __slots__ - dalam kasus ini, semua instance kelas hanya akan memiliki seperangkat properti terbatas yang dideklarasikan dalam __slots__ dan semua __slots__ kelas induk.

Penjelas


Secara default, interpreter Python mengembalikan nilai atribut objek secara langsung (pada saat yang sama, kami menetapkan bahwa dalam hal ini nilai juga merupakan objek Python, dan tidak, misalnya, tidak ditandai lama dalam hal bahasa C):
value = my_obj.attribute # , .

Menurut model data Python, jika nilai atribut adalah objek yang mengimplementasikan protokol deskriptor, maka ketika mencoba untuk mendapatkan nilai atribut ini, interpreter pertama-tama akan menemukan referensi ke objek yang dirujuk oleh properti dan kemudian memanggil metode __get__ khusus untuk __get__ , yang akan diteruskan ke objek asli kita sebagai, argumen:
 obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj) 

Pencacahan di Perpustakaan Standar


Setidaknya properti name dan value anggota dari implementasi enumerasi standar dinyatakan sebagai types.DynamicClassAttribute . Ini berarti bahwa ketika Anda mencoba untuk mendapatkan nilai name dan value , hal berikut akan terjadi:

 one_value = StdEnum.ONE.value #        #   ,      one_value_attribute = StdEnum.ONE.value one_value = one_value_attribute.__get__(StdEnum.ONE) 

 #   ,  __get__     (  python3.7): 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) 

 #   DynamicClassAttribute     `name`  `value`   __get__()  : @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_ 

Dengan demikian, seluruh urutan panggilan dapat diwakili oleh pseudo-code berikut:
 def get_func(enum_member, attrname): #        __dict__,        -     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 menulis skrip sederhana yang menunjukkan output yang dijelaskan 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 eksekusi, skrip memberi kami gambar berikut:


Ini menunjukkan bahwa setiap kali Anda mengakses atribut name dan value anggota enumerasi dari perpustakaan standar, sebuah pegangan dipanggil. Deskriptor ini, pada gilirannya, berakhir dengan panggilan dari kelas Enum dari perpustakaan standar metode def name(self) , didekorasi dengan deskriptor.

Bandingkan 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() 

Apa yang bisa dilihat pada gambar berikut:


Semua ini benar-benar terjadi di dalam implementasi enumerasi standar setiap kali Anda mengakses properti name dan value anggota mereka. Ini juga alasan mengapa implementasi kami lebih cepat.

Menerapkan enumerasi di pustaka standar Python menggunakan banyak panggilan ke objek yang mengimplementasikan protokol deskriptor data. Ketika kami mencoba menggunakan implementasi enumerasi standar dalam proyek kami, kami segera memperhatikan berapa banyak deskriptor data yang dipanggil untuk name dan value .
Dan karena enumerasi digunakan cukup luas di seluruh kode, kinerja yang dihasilkan rendah.

Selain itu, kelas Enum standar berisi beberapa atribut "dilindungi" tambahan:
  • _member_names_ - daftar yang berisi semua nama anggota enumerasi;
  • _member_map_ - OrderedDict , yang memetakan nama anggota enumerasi ke nilainya;
  • _value2member_map_ - kamus yang berisi kecocokan dalam arah yang berlawanan: nilai anggota enumerasi dengan anggota enumerasi yang sesuai.

Pencarian kamus lambat, karena setiap panggilan mengarah ke perhitungan fungsi hash (kecuali, tentu saja, hasilnya di-cache secara terpisah, yang tidak selalu memungkinkan untuk kode yang tidak dikelola) dan pencarian di tabel hash, yang menjadikan kamus ini bukan dasar yang optimal untuk enumerasi. Bahkan pencarian anggota enumerasi (seperti dalam StdEnum.MEMBER ) itu sendiri adalah pencarian kamus.

Pendekatan kami


Kami menciptakan implementasi enumerasi kami dengan memperhatikan enumerasi elegan di C dan enumerasi yang dapat diperluas yang indah di Jawa. Fungsi utama yang ingin kami terapkan di rumah adalah sebagai berikut:

  • pencacahan harus se-statis mungkin; "Statis" di sini berarti yang berikut - jika sesuatu dapat dihitung hanya sekali dan selama pengumuman, maka itu harus dihitung pada saat ini (dan hanya pada saat ini);
  • tidak mungkin untuk mewarisi dari enumerasi (harus menjadi kelas "final") jika kelas pewaris mendefinisikan anggota baru enumerasi - ini berlaku untuk implementasi di perpustakaan standar, dengan pengecualian bahwa pewarisan dilarang di sana, bahkan jika kelas pewaris tidak mendefinisikan anggota baru;
  • enumerasi harus memiliki cakupan yang luas untuk ekspansi (atribut tambahan, metode, dll.)

Kami menggunakan pencarian kamus dalam satu-satunya kasus - ini adalah pemetaan terbalik dari nilai value ke anggota enumerasi. Semua perhitungan lain dilakukan hanya sekali selama deklarasi kelas (di mana metaclasses digunakan untuk mengkonfigurasi pembuatan tipe).
Tidak seperti perpustakaan standar, kami hanya memproses nilai pertama setelah tanda = dalam deklarasi kelas sebagai nilai anggota:
A = 1, 'One' di perpustakaan standar, seluruh tuple 1, "One" dianggap sebagai nilai value ;
A: 'MyEnum' = 1, 'One' dalam implementasi kami, hanya 1 dianggap sebagai nilai value .

Akselerasi lebih lanjut dicapai melalui penggunaan __slots__ jika memungkinkan. Dalam kelas Python dideklarasikan menggunakan __slots__ , atribut __dict__ tidak dibuat untuk __dict__ , yang berisi pemetaan nama atribut dengan nilai-nilai mereka (karena itu, Anda tidak dapat mendeklarasikan properti dari instance yang tidak disebutkan dalam __slots__ ). Selain itu, nilai atribut yang didefinisikan dalam __slots__ diakses pada offset konstan pada pointer instance objek. Ini adalah akses berkecepatan tinggi ke properti karena menghindari perhitungan hash dan pemindaian tabel hash.

Apa chip tambahannya?


FastEnum tidak kompatibel dengan versi Python sebelum 3.6 karena ia secara universal menggunakan tipe anotasi yang diimplementasikan dalam Python 3.6. Dapat diasumsikan bahwa menginstal modul typing dari PyPi akan membantu. Jawaban singkatnya adalah tidak. Implementasi menggunakan PEP-484 untuk argumen beberapa fungsi, metode, dan pointer ke tipe kembali, sehingga versi apa pun sebelum Python 3.5 tidak didukung karena ketidakcocokan sintaksis. Tapi, sekali lagi, baris kode pertama dalam metaclass __new__ menggunakan sintaks PEP-526 untuk menunjukkan jenis variabel. Jadi Python 3.5 tidak akan berfungsi juga. Anda dapat port implementasi ke versi yang lebih lama, meskipun kami di Qrator Labs cenderung menggunakan anotasi jenis bila memungkinkan, karena ini banyak membantu dalam mengembangkan proyek yang kompleks. Nah, pada akhirnya! Anda tidak ingin terjebak dalam Python sebelum versi 3.6, karena dalam versi yang lebih baru tidak ada ketidaksesuaian mundur dengan kode yang ada (asalkan Anda tidak menggunakan Python 2), dan banyak pekerjaan telah dilakukan dalam implementasi asyncio dibandingkan dengan 3,5, pada pandangan kami, layak pembaruan segera.

Ini, pada gilirannya, membuat impor khusus auto tidak diperlukan, tidak seperti perpustakaan standar. Anda cukup menunjukkan bahwa anggota enumerasi akan menjadi contoh enumerasi ini tanpa memberikan nilai sama sekali - dan nilai akan dihasilkan secara otomatis untuk Anda. Meskipun Python 3.6 cukup untuk bekerja dengan FastEnum, perlu diingat bahwa menjaga urutan kunci dalam kamus hanya diperkenalkan di Python 3.7 (dan kami tidak menggunakan OrderedDict secara terpisah untuk kasus 3.6). Kami tidak tahu contoh di mana urutan nilai yang dihasilkan secara otomatis adalah penting, karena kami menganggap bahwa jika pengembang menyediakan lingkungan dengan tugas menghasilkan dan menetapkan nilai kepada anggota enumerasi, maka nilai itu sendiri tidak begitu penting untuk itu. Namun, jika Anda masih belum beralih ke Python 3.7, kami memperingatkan Anda.

Mereka yang membutuhkan enumerasi mereka mulai dari 0 (nol) dan bukan nilai default (1) dapat melakukan ini menggunakan atribut khusus ketika mendeklarasikan enumerasi _ZERO_VALUED , yang tidak akan disimpan di kelas yang dihasilkan.

Namun, ada beberapa batasan: semua nama anggota enumerasi harus ditulis dalam huruf MODAL, jika tidak mereka tidak akan diproses oleh metaclass.

Akhirnya, Anda dapat mendeklarasikan kelas dasar untuk enumerasi Anda (perlu diingat bahwa kelas dasar dapat menggunakan metaclass itu sendiri, sehingga Anda tidak perlu memberikan metaclass ke semua subclass) - cukup tentukan logika umum (atribut dan metode) di kelas ini dan tidak mendefinisikan anggota enumerasi (sehingga kelas tidak akan "diselesaikan"). Setelah Anda dapat mendeklarasikan sebanyak mungkin kelas pewarisan dari kelas ini seperti yang Anda inginkan, dan pewarisnya sendiri akan memiliki logika yang sama.

Alias ​​dan bagaimana mereka bisa membantu


Misalkan Anda memiliki kode menggunakan:
 package_a.some_lib_enum.MyEnum 

Dan bahwa kelas MyEnum dinyatakan sebagai berikut:
 class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum' 

Sekarang, Anda telah memutuskan bahwa Anda ingin melakukan refactoring dan mentransfer daftar 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 tahap di mana transfer yang terletak di alamat lama dianggap usang. Anda menulis ulang impor dan panggilan enumerasi ini sehingga nama baru enumerasi ini (aliasnya) sekarang digunakan - Anda dapat yakin bahwa semua anggota enumerasi alias ini benar-benar dideklarasikan di kelas dengan nama lama. Dalam dokumentasi proyek Anda, Anda menyatakan bahwa MyEnum usang dan akan dihapus dari kode di masa mendatang. Misalnya, dalam rilis berikutnya. Misalkan kode Anda menyimpan objek Anda dengan atribut yang berisi anggota enumerasi menggunakan pickle . Pada titik ini, Anda menggunakan MyMovedEnum dalam kode Anda, tetapi secara internal, semua anggota enumerasi masih merupakan contoh dari MyEnum . Langkah Anda selanjutnya adalah menukar deklarasi MyEnum dan MyMovedEnum sehingga MyMovedEnum bukan subkelas dari MyEnum dan MyEnum semua anggotanya sendiri; MyEnum , di sisi lain, sekarang tidak mendeklarasikan anggota, tetapi menjadi hanya alias (subkelas) dari MyMovedEnum .

Itu saja. Saat Anda me-restart aplikasi Anda pada tahap unpickle semua anggota enumerasi akan dideklarasikan kembali sebagai instance dari MyMovedEnum dan menjadi terkait dengan kelas baru ini. Saat Anda yakin bahwa semua objek Anda disimpan, misalnya, dalam database, telah deserialized (dan mungkin serial lagi dan disimpan dalam repositori) - Anda dapat merilis rilis baru, di mana sebelumnya ditandai sebagai kelas usang MyEnum dapat dinyatakan lebih tidak perlu dan dihapus dari basis kode.

Cobalah sendiri: github.com/QratorLabs/fastenum , pypi.org/project/fast-enum .
Pro dalam karma pergi ke penulis FastEnum - santjagocorkez .

UPD: Dalam versi 1.3.0, menjadi mungkin untuk mewarisi dari kelas yang ada, misalnya, int , float , str . Anggota enumerasi tersebut berhasil lulus ujian untuk kesetaraan ke objek bersih dengan nilai yang sama ( IntEnum.MEMBER == int(value_given_to_member) ) dan, tentu saja, itu adalah contoh dari kelas-kelas yang diwarisi ini. Ini, pada gilirannya, memungkinkan anggota enum yang diwarisi dari int untuk menjadi argumen langsung ke sys.exit() sebagai kode pengembalian juru bahasa python.

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


All Articles