د
github.com/QratorLabs/fastenumpip install fast-enum
ما هي التعدادات
(إذا كنت تعتقد أنك تعرف ذلك - فانتقل إلى قسم "التعدادات في المكتبة القياسية").
تخيل أنك بحاجة إلى وصف مجموعة من جميع الحالات الممكنة للكيانات في نموذج قاعدة البيانات الخاصة بك. ربما ستستخدم مجموعة من الثوابت المعرفة كسمات على مستوى الوحدة النمطية:
... أو كسمات على مستوى الفصل المعرفة في الفصل الخاص بهم:
class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4
يساعدك ذلك على الرجوع إلى هذه الحالات بأسمائها في الذاكرة الخاطئة ، بينما تستمر في التخزين كعدد صحيح بسيط. بهذا ، تتخلص من الأرقام السحرية المنتشرة في الشفرة وتجعلها أكثر قابلية للقراءة والوصف الذاتي.
لكن الثابت على مستوى الوحدة النمطية والفئة ذات السمات الثابتة تعاني من الطبيعة الكامنة لأجسام الثعبان: كلها قابلة للتغيير. يمكنك تعيين قيمة لثابتك بطريق الخطأ في وقت التشغيل ، وهذه هي فوضى لتصحيح واستعادة الكيانات المعطلة. لذلك ، قد ترغب في جعل مجموعة الثوابت الخاصة بك غير قابلة للتغيير ، مما يعني أن عدد الثوابت المعلنة والقيم التي تم تعيينها يجب ألا يتم تعديلها في وقت التشغيل.
لهذا الغرض ، يمكنك محاولة تنظيمها في tuples مسمى باسم
namedtuple()
، كمثال:
MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4)
ومع ذلك ، لا يبدو هذا أمرًا مفهومًا للغاية: بالإضافة إلى ذلك ، الكائنات
namedtuple
ليست قابلة للتوسع حقًا. لنفترض أن لديك واجهة مستخدم تعرض كل هذه الحالات. يمكنك بعد ذلك استخدام الثوابت المستندة إلى الوحدة النمطية ، أو الفصل الدراسي الخاص بك مع السمات ، أو tuples المسماة لعرضها (آخرهما أسهل في العرض ، بينما نحن في ذلك). لكن الكود لا يوفر أي فرص لإعطاء المستخدم وصفًا مناسبًا لكل ولاية حددتها. علاوة على ذلك ، إذا كنت تخطط لتنفيذ دعم متعدد اللغات و i18n في واجهة المستخدم الخاصة بك ، ستجد أن ملء جميع الترجمات لهذه الأوصاف يصبح مهمة شاقة بشكل لا يصدق. قد لا تحتوي قيم حالة المطابقة بالضرورة على أوصاف مطابقة ، مما يعني أنه لا يمكنك فقط تعيين جميع
INITIAL
على نفس الوصف في
gettext
. بدلاً من ذلك ، يصبح ثابتك هذا:
INITIAL = (0, 'My_MODEL_INITIAL_STATE')
فصلك ثم يصبح هذا:
class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE')
وأخيرًا ، يصبح هذا
namedtuple
الخاص بك
namedtuple
:
EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...)
حسنًا ، جيد بما فيه الكفاية ، فهو الآن يتأكد من تعيين كل من قيمة الحالة و كعب الترجمة للترجمة إلى اللغات التي يدعمها واجهة المستخدم الخاصة بك. لكن قد تلاحظ الآن أن الشفرة التي تستخدم هذه التعيينات قد تحولت إلى فوضى. عندما تحاول تعيين قيمة لكيانك ، لا تحتاج أيضًا إلى نسيان استخراج القيمة في الفهرس 0 من التعيين الذي تستخدمه:
my_entity.state = INITIAL[0]
أو
my_entity.state = MyModelStates.INITIAL[0]
أو
my_entity.state = EntityStates.INITIAL[0]
و هكذا. ضع في اعتبارك أن أول طريقتين باستخدام الثوابت والسمات الصفية ، على التوالي ، لا تزال تعاني من قابلية التغيير.
ثم تأتي التعدادات في المرحلة
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')
هذا كل شيء. الآن يمكنك تكرار التعداد بسهولة في العارض الخاص بك (بناء جملة Jinja2):
{% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %}
التعداد غير قابل للتغيير لكلتا المجموعتين العضوين (لا يمكنك تحديد عضو جديد في وقت التشغيل ، ولا يمكنك حذف عضو محدد بالفعل) وقيم الأعضاء التي يحتفظ بها (لا يمكنك إعادة تعيين أي قيم للسمة أو حذف سمة).
في الكود الخاص بك ، يمكنك فقط تعيين قيم لكياناتك مثل هذا:
my_entity.state = MyEntityStates.INITIAL.val
حسنا ، واضح بما فيه الكفاية. المتمتعة بالحكم الذاتي وصفي. الموسعة إلى حد ما. هذا ما نستخدمه Enums ل.
لماذا هو أسرع؟
لكن العدد الافتراضي لـ ENUM بطيء إلى حد ما ، لذا سألنا أنفسنا - هل يمكن أن نجعله أسرع؟
كما اتضح ، يمكننا. وهي ، من الممكن أن نجعلها:
- 3 مرات أسرع على وصول الأعضاء
- ~ 8.5 مرات أسرع على السمة (
name
value
) الوصول - 3 مرات أسرع عند الوصول إلى التعداد حسب القيمة (استدعاء على فئة التعداد
MyEnum(value)
) - 1.5 مرات أسرع عند الوصول إلى التعداد بالاسم (يشبه
MyEnum[name]
)
الأنواع والكائنات ديناميكية في بيثون. لكن بيثون لديها الأدوات اللازمة للحد من الطبيعة الديناميكية للكائنات. من خلال مساعدتهم ، يمكن للمرء الحصول على زيادة كبيرة في الأداء باستخدام
__slots__
وكذلك تجنب استخدام
__slots__
البيانات حيثما أمكن ذلك دون نمو تعقيد كبير أو إذا كنت تستطيع الاستفادة بسرعة.
فتحات
على سبيل المثال ، يمكن استخدام تعريف فئة مع
__slots__
- في هذه الحالة ، سيكون لمثيلات الفئة فقط مجموعة مقيدة من السمات: السمات المعلنة في
__slots__
وجميع
__slots__
من الفئات الأصل.
واصفات
بشكل افتراضي ، يقوم مترجم Python بإرجاع قيمة سمة لكائن مباشرةً:
value = my_obj.attribute
وفقًا لنموذج بيانات Python ، إذا كانت قيمة السمة لكائن هي في حد ذاتها كائن يقوم بتنفيذ بروتوكول واصف البيانات ، فهذا يعني أنه عندما تحاول الحصول على تلك القيمة ، تحصل أولاً على السمة ككائن ثم طريقة خاصة
__get__
هي استدعى كائن السمة هذا مرر كائن keeper نفسه كوسيطة:
obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj)
التعدادات في المكتبة القياسية
يتم التصريح عن سمات
name
value
الأقل لتطبيق Enum القياسي
types.DynamicClassAttribute
.
types.DynamicClassAttribute
. هذا يعني أنه عند محاولة الحصول على
name
العضو (أو
value
) ، فإن التدفق يتبع:
one_value = StdEnum.ONE.value
لذلك ، يمكن تمثيل التدفق الكامل باعتباره الكود الزائف التالي:
def get_func(enum_member, attrname):
لقد صنعنا نصًا بسيطًا يوضح الاستنتاج أعلاه:
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()
وبعد قيامنا بتشغيل السيناريو ، صنعت هذه الصورة لنا:

هذا يثبت أنه في كل مرة تقوم فيها بالوصول إلى
name
سمات stdlib enum
value
فإنه يستدعي واصفًا. وينتهي هذا الواصف بدوره باستدعاء خاصية
def name(self)
لـ stdlib enum المزينة بالواصف.
حسنًا ، يمكنك مقارنة هذا بـ FastEnum:
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()
الذي يخرج هذه الصورة:

هذا ما يتم فعلاً داخل تطبيق Enum القياسي في كل مرة تقوم فيها بالوصول إلى سمات
name
value
أعضاء Enum. وهذا هو السبب في أن تطبيقنا أسرع.
يستخدم تطبيق Python Standard Library لفئة Enum أطنانًا من مكالمات بروتوكول الواصف. عندما حاولنا استخدام التعداد القياسي في مشاريعنا ، لاحظنا عدد استدعاء بروتوكول الواصفات للسمات والسمات المميزة لأعضاء
Enum
الذين تم استدعاءهم. ونظرًا لاستخدام التعدادات بشكل مفرط خلال التعليمات البرمجية ، كان الأداء الناتج سيئًا.
علاوة على ذلك ، تحتوي فئة التعداد القياسية على عدد من الصفات المميزة "المحمية" المساعدة:
_member_names_
- قائمة تحتوي على جميع أسماء أعضاء التعداد ؛_member_map_
- OrderedDict الذي يعين اسم عضو التعداد إلى العضو نفسه ؛_value2member_map_
- قاموس عكسي يقوم بتعيين قيم الأعضاء _value2member_map_
أعضاء التعداد المناظر.
عمليات البحث في القاموس بطيئة لأن كل واحد يؤدي إلى حساب تجزئة وبحث جدول تجزئة ، مما يجعل تلك القواميس هياكل أساسية غير مثالية لفئة التعداد. حتى استرجاع الأعضاء نفسه (كما في
StdEnum.MEMBER
) هو بحث القاموس.
طريقنا
أثناء تطوير تطبيق Enum الخاص بنا ، وضعنا في الاعتبار تلك التعدادات بلغة C الجميلة وتعدادات Java القابلة للتوسيع الجميلة. الملامح الرئيسية التي أردناها في تطبيقنا:
- يجب أن يكون التعداد ثابتًا قدر الإمكان ؛ نعني بكلمة "ثابت": إذا كان من الممكن حساب شيء ما في وقت الإعلان ، فيجب أن ؛
- لا يمكن تصنيف التعداد الفرعي (يجب أن يكون فئة "نهائية") إذا كانت الفئة الفرعية تعرف أعضاء التعداد الجدد - وهذا صحيح بالنسبة للتطبيق القياسي للمكتبة ، باستثناء أن التصنيف الفرعي محظور حتى إذا لم يتم تعريف أعضاء جدد ؛
- يجب أن تحتوي التعدادات على إمكانيات هائلة للإضافات (سمات إضافية وأساليب وما إلى ذلك).
المرة الوحيدة التي نستخدم فيها عمليات البحث عن القاموس هي
value
تعيين عكسي لعضو Enum. تتم جميع العمليات الحسابية الأخرى مرة واحدة فقط خلال إعلان الفصل (حيث يتم استخدام ربطات metaclasses لتخصيص إنشاء الكتابة).
على عكس تطبيق المكتبة القياسي ، نتعامل مع القيمة الأولى بعد علامة
=
تسجيل الدخول في فئة الإعلان كقيمة عضو:
A = 1, 'One'
في المكتبة القياسية تحسب المجموعة بأكملها
1, "One"
يعامل
value
A: 'MyEnum' = 1, 'One'
يتم التعامل مع
A: 'MyEnum' = 1, 'One'
في تطبيقنا
1
فقط
value
يتم الحصول على
__slots__
باستخدام
__slots__
كلما كان ذلك ممكنًا. في فئات نموذج بيانات Python المعلنة بـ
__slots__
ليس لها
__dict__
سمة تحمل سمات المثيل (لذلك لا يمكنك تعيين أي سمة غير مذكورة في
__slots__
). بالإضافة إلى ذلك ،
__slots__
الوصول إلى السمات المعرفة في
__slots__
عند إزاحة ثابتة إلى مؤشر كائن المستوى C. هذا هو الوصول إلى السمة عالية السرعة لأنه يتجنب حسابات التجزئة وعمليات المسح بالهاش.
ما هي الامتيازات الإضافية؟
FastEnum غير متوافق مع أي إصدار من Python قبل 3.6 ، لأنه يستخدم بشكل مفرط وحدة
typing
التي تم تقديمها في Python 3.6 ؛ يمكن للمرء أن يفترض أن تثبيت وحدة
typing
backported من PyPI من شأنه أن يساعد. الجواب هو: لا. يستخدم التطبيق PEP-484 لبعض الوظائف والوسائط والأساليب وإشارة إلى نوع القيمة المرجعة ، وبالتالي فإن أي إصدار قبل Python 3.5 غير مدعوم بسبب عدم توافق بناء الجملة. ولكن بعد ذلك مرة أخرى ، يستخدم السطر الأول من التعليمات البرمجية في
__new__
من metaclass بناء جملة PEP-526 لتلميح نوع متغير. لذلك بيثون 3.5 لن تفعل أي منهما. من الممكن نقل التطبيق إلى الإصدارات الأقدم ، على الرغم من أننا في Qrator Labs نميل إلى استخدام تلميح الكتابة كلما أمكن ذلك لأنه يساعد في تطوير المشاريع المعقدة إلى حد كبير. واهلا! لا تريد التمسك بأي بيثون قبل 3.6 لأنه لا يوجد أي تعارض مع التعليمات البرمجية الموجودة لديك (على افتراض أنك لا تستخدم Python 2) على الرغم من أن الكثير من العمل تم في asyncio مقارنة بـ 3.5.
وهذا بدوره يجعل الواردات الخاصة مثل
auto
غير ضرورية ، على عكس المكتبة القياسية. قمت بكتابة تلميح لجميع أعضاء Enum الخاص بك مع اسم فئة Enum الخاص بك ، دون تقديم أي قيمة على الإطلاق - وسيتم إنشاء القيمة لك تلقائيًا. على الرغم من أن python 3.6 كافٍ للعمل مع FastEnum ، إلا أنه تم تحذيرك من أنه تم تقديم ترتيب القاموس القياسي لضمان الإعلان فقط في python 3.7. لا نعرف أي أجهزة مفيدة يكون فيها ترتيب القيمة الذي يتم إنشاؤه تلقائيًا مهمًا (نظرًا لأننا نفترض أن القيمة التي تم إنشاؤها بنفسها ليست هي القيمة التي يهتم بها المبرمج). ومع ذلك ، فكر في تحذير نفسك إذا كنت لا تزال متمسكًا بالبيثون 3.6 ؛
أولئك الذين يحتاجون إلى التعداد الخاص بهم يبدأون من 0 (صفر) بدلاً من الافتراضي 1 يمكنهم القيام بذلك باستخدام سمة إعلان التعداد الخاصة
_ZERO_VALUED
، يتم "محو" هذه السمة من فئة التعداد الناتجة ؛
هناك بعض القيود على الرغم من ذلك: يجب أن تكون جميع أسماء أعضاء التعداد مرفوعة أو لن يتم التقاطها بواسطة علامة التعريف ولن تتم معاملتها كأعضاء في التعداد ؛
ومع ذلك ، يمكنك التصريح عن فئة أساسية لتعداداتك (ضع في اعتبارك أن الفئة الأساسية يمكنها استخدام علامة التعداد الأولية نفسها ، لذلك لا تحتاج إلى توفير علامة وصفية لجميع الفئات الفرعية): يمكنك تحديد المنطق المشترك (السمات والأساليب) في هذا فئة ، ولكن قد لا تحدد أعضاء التعداد (لذلك لن يتم "الانتهاء" من هذه الفئة)). يمكنك حينئذٍ تصنيف هذه الفئة في أكبر عدد تريده من إعلانات التزويد والتي من شأنها أن توفر لك كل المنطق المشترك ؛
الأسماء المستعارة. سنشرح لهم في موضوع منفصل (نفذت في 1.2.5)
الأسماء المستعارة وكيف يمكنهم المساعدة
افترض أن لديك رمزًا يستخدم:
package_a.some_lib_enum.MyEnum
وأعلن MyEnum مثل هذا:
class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum'
الآن ، قررت إجراء بعض إعادة التسكين وتريد نقل التعداد الخاص بك إلى حزمة أخرى. يمكنك إنشاء شيء مثل هذا:
package_b.some_lib_enum.MyMovedEnum
حيث يتم إعلان MyMovedEnum مثل هذا:
class MyMovedEnum(MyEnum): pass
الآن. أنت جاهز لبدء مرحلة "الإهمال" لكافة التعليمات البرمجية التي تستخدم التعدادات الخاصة بك. تقوم بتحويل
MyEnum
استخدام
MyMovedEnum
لاستخدام
MyMovedEnum
(هذا الأخير لديه كل أعضائه يتم
MyEnum
إلى
MyEnum
). تذكر ضمن مستندات مشروعك أن
MyEnum
تم إهماله وسيتم إزالته من الكود في وقت ما في المستقبل. على سبيل المثال ، في الإصدار التالي. النظر في التعليمات البرمجية الخاصة بك يحفظ الكائنات الخاصة بك مع سمات التعداد باستخدام المخلل. في هذه المرحلة ، يمكنك استخدام
MyMovedEnum
في التعليمات البرمجية الخاصة بك ، ولكن داخليًا ، لا يزال جميع أعضاء التعداد هم مثيلات
MyEnum
. ستكون خطوتك التالية هي تبادل إقرارات
MyEnum
و
MyMovedEnum
بحيث لا
MyMovedEnum
الآن فئة فرعية من
MyEnum
وتعلن عن جميع أعضائها ؛
MyEnum
، من ناحية أخرى ، لن تعلن أي أعضاء ولكنها تصبح مجرد اسم مستعار (فئة فرعية) من
MyMovedEnum
.
وهذا يخلصها. عند إعادة تشغيل أوقات التشغيل الخاصة بك على خشبة المسرح ، سيتم إعادة توجيه جميع قيم التعداد إلى
MyMovedEnum
وتصبح مرتبطة بهذه الفئة الجديدة. في اللحظة التي تكون فيها متأكدًا من إلغاء إعادة (إعادة) كل الكائنات المخللة الخاصة بك مع بنية تنظيم الفصل هذه ، يكون لك
MyEnum
الحرية في إصدار إصدار جديد ، حيث يمكن اعتبار
MyEnum
الخاص بك
MyEnum
على أنه مهمل وتم طمسه من قاعدة الكود.
نحن نشجعك على تجربتها!
github.com/QratorLabs/fastenum ،
pypi.org/project/fast-enum . جميع الاعتمادات تذهب إلى مؤلف
FastEnum santjagocorkez .