tl; dr
github.com/QratorLabs/fastenumpip 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:
... 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
Dengan demikian, seluruh urutan panggilan dapat diwakili oleh pseudo-code berikut:
def get_func(enum_member, attrname):
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.