التعداد السريع

د


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

لماذا هناك حاجة للتعداد


(إذا كنت تعرف كل شيء - فانتقل إلى قسم "التعدادات في المكتبة القياسية")

تخيل أنك بحاجة إلى وصف مجموعة من جميع حالات الكيانات الممكنة في نموذج قاعدة البيانات الخاصة بك. على الأرجح ، ستأخذ مجموعة من الثوابت المعرفة مباشرة في مساحة اسم الوحدة النمطية:
 # /path/to/package/static.py: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 ... 

... أو كسمات فئة ثابتة:
 class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 

سيساعد هذا النهج على الإشارة إلى هذه الحالات بأسماء ذاكري ، بينما في مستودعك ستكون أعداد صحيحة عادية. وبالتالي ، تتخلص في الوقت نفسه من الأرقام السحرية المنتشرة في أجزاء مختلفة من الشفرة ، في نفس الوقت مما يجعلها أكثر قابلية للقراءة ومفيدة.

ومع ذلك ، فإن كل من ثابت الوحدة النمطية والفئة ذات السمات الثابتة تعاني من الطبيعة الجوهرية لكائنات Python: فهي كلها قابلة للتغيير (قابلة للتغيير). يمكنك تعيين قيمة لثابتك بطريق الخطأ في وقت التشغيل ، وتصحيح الأخطاء واستعادة الكائنات المكسورة هي مغامرة منفصلة. لذلك قد ترغب في جعل حزمة الثوابت دون تغيير ، بمعنى أن عدد الثوابت المعلنة وقيمها التي تم تعيينها لها لن يتغير أثناء تنفيذ البرنامج.

للقيام بذلك ، يمكنك محاولة تنظيمها في 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') 

أخيرًا ، تتحول الطبقة المسماة إلى:
 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 

كل شيء واضح بما فيه الكفاية ، بالمعلومات وقابلة للتوسيع. هذا هو ما نستخدمه التعدادات ل.

كيف يمكننا أن نجعلها أسرع؟


يعد التعداد من المكتبة القياسية بطيئًا إلى حد ما ، لذلك سألنا أنفسنا - هل يمكننا تسريع الأمر؟ كما اتضح ، يمكننا ، أي ، تنفيذ تعدادنا:

  • ثلاث مرات أسرع على الوصول إلى تعداد الأعضاء ؛
  • ~ 8.5 بشكل أسرع عند الوصول إلى السمة ( name ، value ) لأحد الأعضاء ؛
  • 3 مرات أسرع عند الوصول إلى عضو من حيث القيمة (استدعاء مُنشئ التعداد MyEnum(value)) ؛
  • 1.5 مرة أسرع عند الوصول إلى عضو بالاسم (كما هو الحال في MyEnum[name] ).

الأنواع والكائنات في بيثون ديناميكية. ولكن هناك أدوات للحد من الطبيعة الديناميكية للكائنات. يمكنك الحصول على زيادة كبيرة في الأداء باستخدام __slots__ . هناك أيضًا إمكانية لتحقيق مكاسب في السرعة إذا تجنبت استخدام واصفات البيانات حيثما أمكن ذلك - لكن يجب مراعاة إمكانية حدوث زيادة كبيرة في تعقيد التطبيق.

فتحات


على سبيل المثال ، يمكنك استخدام تعريف فئة باستخدام __slots__ - في هذه الحالة ، سيكون لكافة مثيلات الفئات مجموعة محدودة فقط من الخصائص المعلنة في __slots__ وجميع __slots__ الفئات الأصل.

واصفات


بشكل افتراضي ، يقوم مترجم Python بإرجاع قيمة سمة الكائن مباشرة (ومع ذلك ، نلاحظ أن القيمة في هذه الحالة هي أيضًا كائن Python ، على سبيل المثال ، ليست طويلة موقعة طويلة من حيث C):
value = my_obj.attribute # , .

وفقًا لنموذج بيانات Python ، إذا كانت قيمة السمة هي كائن يقوم بتنفيذ بروتوكول الواصف ، ثم عند محاولة الحصول على قيمة هذه السمة ، سيجد المترجم أولاً مرجعًا إلى الكائن الذي تشير إليه الخاصية ثم يتم استدعاء الأسلوب __get__ الخاص __get__ ، والذي سيتم تمريره إلى __get__ الأصلي باعتباره حجة:
 obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj) 

التعدادات في المكتبة القياسية


يتم التصريح عن name value خصائص أعضاء تطبيق التعداد القياسي على types.DynamicClassAttribute . types.DynamicClassAttribute . هذا يعني أنه عند محاولة الحصول على قيم name value ، سيحدث ما يلي:

 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_ 

وبالتالي ، يمكن تمثيل تسلسل المكالمات بالكامل بالرمز الزائف التالي:
 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') 

لقد كتبنا نصًا بسيطًا يوضح المخرجات الموضحة أعلاه:
 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 value أعضاء التعداد من المكتبة القياسية ، يتم استدعاء مؤشر. ينتهي هذا الواصف بدوره باستدعاء من فئة Enum من المكتبة القياسية لطريقة def name(self) ، والمزينة بالواصف.

قارن مع 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() 

ما يمكن رؤيته في الصورة التالية:


كل هذا يحدث بالفعل داخل تطبيق التعداد القياسي في كل مرة تقوم فيها بالوصول إلى خصائص name value لأعضائها. هذا هو السبب أيضًا في أن تنفيذنا أسرع.

يستخدم تطبيق التعدادات في مكتبة Python القياسية الكثير من المكالمات إلى الكائنات التي تنفذ بروتوكول واصف البيانات. عندما حاولنا استخدام تطبيق التعداد القياسي في مشاريعنا ، لاحظنا على الفور عدد واصفات البيانات التي تم استدعاؤها name value .
ونظرًا لاستخدام التعدادات على نطاق واسع في جميع أنحاء الشفرة ، كان الأداء الناتج منخفضًا.

بالإضافة إلى ذلك ، تحتوي فئة التعداد القياسية على العديد من السمات "المحمية" الإضافية:
  • _member_names_ - قائمة تحتوي على جميع أسماء أعضاء التعداد ؛
  • _member_map_ - OrderedDict ، الذي يعين اسم عضو التعداد إلى قيمتها ؛
  • _value2member_map_ - قاموس يحتوي على مطابقة في الاتجاه المعاكس: قيم أعضاء التعداد للأعضاء _value2member_map_ للتعداد.

يكون البحث في القاموس بطيئًا ، نظرًا لأن كل مكالمة تؤدي إلى حساب دالة التجزئة (ما لم يكن ، بالطبع ، يتم التخزين المؤقت للنتيجة بشكل منفصل ، وهو أمر غير ممكن دائمًا للرمز غير المُدار) وإلى بحث جدول التجزئة ، مما يجعل هذه القواميس لا تشكل أساسًا مثاليًا للتعدادات. حتى البحث عن أعضاء التعداد (كما في StdEnum.MEMBER ) نفسه هو بحث القاموس.

نهجنا


أنشأنا تطبيقنا للتعدادات مع مراعاة التعدادات الأنيقة في C والأعداد القابلة للتوسيع الجميلة في Java. المهام الرئيسية التي أردنا تنفيذها في المنزل هي كما يلي:

  • يجب أن يكون التعداد ثابتًا قدر الإمكان ؛ تعني كلمة "ثابت" هنا ما يلي - إذا كان يمكن حساب شيء ما مرة واحدة فقط وأثناء الإعلان ، فيجب حسابه في هذه اللحظة (وفي هذه اللحظة فقط) ؛
  • من المستحيل أن ترث من التعداد (يجب أن تكون فئة "نهائية") إذا كانت الفئة الموروثة تحدد أعضاء جدد من التعداد - وهذا صحيح للتنفيذ في المكتبة القياسية ، باستثناء أن الميراث محظور هناك ، حتى لو لم يحدد الميراث أعضاء جدد ؛
  • يجب أن يكون للتعداد مجال واسع للتوسع (سمات إضافية ، طرق ، إلخ.)

نحن نستخدم بحث القاموس في الحالة الوحيدة - وهذا هو التعيين العكسي لقيمة value لعضو التعداد. يتم تنفيذ جميع العمليات الحسابية الأخرى مرة واحدة فقط خلال إعلان الفصل (حيث يتم استخدام metaclasses لتكوين إنشاء الكتابة).
على عكس المكتبة القياسية ، نقوم بمعالجة القيمة الأولى فقط بعد علامة = تسجيل الدخول إلى فئة الإعلان كقيمة عضو:
A = 1, 'One' في المكتبة القياسية ، تعتبر المجموعة بأكملها 1, "One" قيمة value ؛
A: 'MyEnum' = 1, 'One' في تطبيقنا ، 1 فقط يعتبر قيمة value .

يتم تحقيق تسارع إضافي من خلال استخدام __slots__ حيثما أمكن ذلك. في فئات Python المعلنة باستخدام __slots__ ، لا يتم إنشاء سمة __dict__ التي تحتوي على تعيين أسماء السمات إلى قيمها (وبالتالي ، لا يمكنك إعلان أي خاصية لمثيل غير مذكور في __slots__ ). بالإضافة إلى ذلك ، يتم الوصول إلى قيم السمات المعرفة في __slots__ عند إزاحة ثابتة في مؤشر مثيل الكائن. هذا هو الوصول عالي السرعة إلى الخصائص لأنه يتجنب حسابات التجزئة وعمليات مسح جدول التجزئة.

ما هي رقائق اضافية؟


FastEnum غير متوافق مع أي إصدار من Python قبل 3.6 لأنه يستخدم تعليقات توضيحية للكتابة المطبقة في Python 3.6. يمكن افتراض أن تثبيت وحدة typing من PyPi سيساعد. الجواب القصير هو لا. يستخدم التطبيق PEP-484 للوسائط الخاصة ببعض الوظائف والأساليب والمؤشرات إلى نوع الإرجاع ، لذلك لا يتم دعم أي إصدار قبل Python 3.5 بسبب عدم توافق بناء الجملة. ولكن ، مرة أخرى ، يستخدم السطر الأول من التعليمات البرمجية في __new__ metaclass بناء جملة PEP-526 للإشارة إلى نوع المتغير. لذلك بيثون 3.5 لن تعمل أيضا. يمكنك نقل التطبيق إلى الإصدارات القديمة ، على الرغم من أننا في Qrator Labs نميل إلى استخدام التعليقات التوضيحية للكتابة كلما كان ذلك ممكنًا ، حيث يساعد ذلك كثيرًا في تطوير المشاريع المعقدة. حسنا ، في النهاية! لا تريد أن تتعثر في Python قبل الإصدار 3.6 ، لأنه في الإصدارات الأحدث لا يوجد عدم توافق مع التعليمات البرمجية الموجودة لديك (شريطة ألا تستخدم Python 2) ، وقد تم القيام بالكثير من العمل في تنفيذ asyncio مقارنة بـ 3.5 ، في وجهة نظرنا ، يستحق التحديث الفوري.

هذا ، بدوره ، يجعل الاستيراد الخاص auto غير ضروري ، على عكس المكتبة القياسية. تشير ببساطة إلى أن عضو التعداد سيكون مثالًا لهذا التعداد دون تقديم قيمة على الإطلاق - وسيتم إنشاء القيمة تلقائيًا لك. على الرغم من أن Python 3.6 كافٍ للعمل مع FastEnum ، ضع في اعتبارك أن الحفاظ على ترتيب المفاتيح في القواميس تم تقديمه فقط في Python 3.7 (ولم نستخدم OrderedDict بشكل منفصل للحالة 3.6). لا نعرف أي أمثلة على أهمية ترتيب القيم الذي تم إنشاؤه تلقائيًا ، حيث أننا نفترض أنه إذا قام المطور بتزويد البيئة بمهمة إنشاء وتعيين قيمة لأحد أعضاء التعداد ، فإن القيمة نفسها ليست مهمة جدًا بالنسبة إليها. ومع ذلك ، إذا لم تتحول بعد إلى Python 3.7 ، فقد حذرناك.

يمكن لأولئك الذين يحتاجون إلى التعدادات الخاصة بهم للبدء من 0 (صفر) بدلاً من القيمة الافتراضية (1) القيام بذلك باستخدام سمة خاصة عند إعلان التعداد _ZERO_VALUED ، والذي لن يتم تخزينه في الفئة الناتجة.

ومع ذلك ، هناك بعض القيود: يجب كتابة جميع أسماء أعضاء التعداد بأحرف كابيتال ، وإلا لن تتم معالجتها بواسطة علامة التعريف.

أخيرًا ، يمكنك إعلان فئة أساسية لتعداداتك (ضع في اعتبارك أن الفئة الأساسية يمكن أن تستخدم الفئة الأولية نفسها ، لذلك لا تحتاج إلى تقديم العلامة الأولية لجميع الفئات الفرعية) - فقط حدد المنطق العام (السمات والأساليب) في هذه الفئة ولا تحدد أعضاء التعداد (لذلك لن يتم "إنهاء" الفصل). بعد أن تتمكن من إعلان أكبر عدد تريده من الطبقات الموروثة من هذه الفئة ، سيكون للورثة أنفسهم نفس المنطق.

الأسماء المستعارة وكيف يمكنهم المساعدة


افترض أن لديك رمز باستخدام:
 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 إهمالها وسيتم إزالتها من الكود في المستقبل. على سبيل المثال ، في الإصدار التالي. افترض أن التعليمات البرمجية الخاصة بك بتخزين الكائنات الخاصة بك مع سمات تحتوي على أعضاء التعداد باستخدام pickle . في هذه المرحلة ، تستخدم MyMovedEnum في التعليمات البرمجية الخاصة بك ، ولكن داخليًا ، لا يزال جميع أعضاء التعداد مثيلات لـ MyEnum . خطوتك التالية هي تبديل تصريحات MyEnum و MyMovedEnum بحيث لا MyMovedEnum فئة فرعية من MyEnum جميع أعضائها بنفسها ؛ MyEnum ، من ناحية أخرى ، لا تعلن الآن أي أعضاء ، ولكنها تصبح مجرد اسم مستعار (فئة فرعية) من MyMovedEnum .

هذا كل شيء. عند إعادة تشغيل التطبيقات الخاصة بك في مرحلة unpickle سيتم إعادة الإعلان عن جميع أعضاء التعداد MyMovedEnum على MyMovedEnum وسيصبحون مرتبطين بهذه الفئة الجديدة. في اللحظة التي تكون متأكدًا فيها من إعادة تخزين جميع الكائنات المخزنة ، على سبيل المثال ، في قاعدة البيانات ، (وربما إعادة تسلسلها مرة أخرى وتخزينها في المستودع) - يمكنك إصدار إصدار جديد ، تم وضع علامة عليه مسبقًا كفئة قديمة قد يتم إعلان MyEnum على أنه غير ضروري وإزالته من قاعدة الكود.

جرب ذلك بنفسك: github.com/QratorLabs/fastenum ، pypi.org/project/fast-enum .
الايجابيات في الكرمة تذهب إلى المؤلف FastEnum - santjagocorkez .

UPD: في الإصدار 1.3.0 ، أصبح من الممكن أن ترث من الفئات الموجودة ، على سبيل المثال ، int ، float ، str . يجتاز أعضاء هذه التعدادات بنجاح اختبار المساواة إلى كائن نظيف بنفس القيمة ( IntEnum.MEMBER == int(value_given_to_member) ) وبطبيعة الحال ، فهم مثيلات لهذه الفئات الموروثة. هذا ، بدوره ، يسمح لعضو التعداد الموروث من int أن يكون وسيطة مباشرة إلى sys.exit() كرمز إرجاع مترجم python.

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


All Articles