الوحدة enum في بايثون

من موسوعة حسوب

enumeration هو مجموعة من الأسماء الرمزية (العناصر) المرتبطة بقيم ثابتة وفريدة. يمكن مقارنة عناصر enumeration عن طريق هويتها، ويمكن المرور على عناصر enumeration بواسطة حلقة تكرارية.

وحدة enum

تقدّم وحدة enum أربعة أصناف enumeration يمكن استخدامها لتعريف مجموعة فريدة من الأسماء والقيم، وهذه الأصناف هي: Enum و IntEnum و Flag و IntFlag. وإلى جانب ما سبق تقدّم الوحدة مزخرفًا واحدًا هو unique()‎، وصنفًا مساعدًا واحدًا هو auto.

الصنف enum.Enum

هو الصنف الأساسي والذي يستخدم لإنشاء ثوابت معدّدة enumerated constants. راجع قسم الواجهة البرمجية الوظيفية للاطلاع على الصيغة البديلة.

الصنف enum.IntEnum

الصنف الأساسي الذي يستخدم لإنشاء ثوابت معدّدة تكون كذلك أصنافًا فرعية للصنف int.

الصنف enum.IntFlag

الصنف الأساسي الذي يستخدم لإنشاء ثوابت معدّدة يمكن دمجها مع بعضها البعض باستخدم عامل bitwise دون التأثير على كونها عناصر للصنف IntFlag. وتكون عناصر IntFlag أصنافًا فرعية من الصنف int أيضًا. 

الصنف enum.Flag

الصنف الرئيسي الذي يستخدم لإنشاء ثوابت معدّدة يمكن دمجها مع بعضها البعض دون التأثير على كونها من نوع Flag.

المزخرف enum.unique()‎

مزخرف الصنف Enum والذي يضمن ارتباط اسم واحد بقيمة واحدة فقط.

الصنف المساعد enum.auto

عند استخدام هذا الصنف تُستبدل نسخ الصنف Enum بقيم ملائمة.

ملاحظة: الأصناف Flag و IntFlag و auto جديدة في الإصدار 3.6 من بايثون.

إنشاء Enum

يمكن إنشاء Enumeration باستخدام الصيغة الخاصّة بإنشاء الأصناف في بايثون، الأمر الذي يسهّل قراءة وكتابة هذا النوع من البيانات، ويوضح المثال التالي طريقة تعريف enumeration:

>>> from enum import Enum
>>> class Color(Enum):
...     RED = 1
...     GREEN = 2
...     BLUE = 3

ملاحظة: قيم عناصر Enumeration

يمكن استخدام أي نوع من البيانات (الأعداد الصحيحة، والسلاسل النصية) كقيمة لعناصر enumeration. إن كانت القيمة المضبوطة غير مهمّة فيمكن استخدام نسخ auto وستختار اللغة القيمة الملائمة، ولكن يجب الانتباه عند استخدام auto مع قيم أخرى.

ملاحظة: التسمية

  • الصنف Color هو enumeration (أو enum)
  • الخاصيات Color.RED، Color.GREEN وغيرها هي عناصر enumeration (أو عناصر enum) وهي قيم ثابتة.
  • تمتلك عناصر enum أسماء وقيم (اسم Color.RED هو RED، وقيمة Color.BLUE هي 3).

ملاحظة:

صحيح أن تعريف Enumerations يكون باستخدام صيغة تعريف الأصناف، إلا أنّ Enums لا تعدّ أصنافًا اعتيادية في بايثون. 

تمتلك عناصر Enumeration تمثيلًا نصّيًا قابلًا للقراءة:

>>> print(Color.RED)
Color.RED

في حين تعطي الدالة repr() معلومات أكثر:

>>> print(repr(Color.RED))
<Color.RED: 1>

أما نوع العنصر فهو الـ enumeration الذي ينتمي إليه:

>>> type(Color.RED)
<enum 'Color'>
>>> isinstance(Color.GREEN, Color)
True
>>>

تمتلك عناصر Enum خاصّية تحمل اسم العنصر فقط:

>>> print(Color.RED.name)
RED

تدعم Enumeration المرور على العناصر بحسب ترتيب تعريفها:

>>> class Shake(Enum):
...     VANILLA = 7
...     CHOCOLATE = 4
...     COOKIES = 9
...     MINT = 3
...
>>> for shake in Shake:
...     print(shake)
...
Shake.VANILLA
Shake.CHOCOLATE
Shake.COOKIES
Shake.MINT

عناصر Enumeration قابلة للتقطيع (hashable)؛ لذا يمكن استخدامها في القواميس والمجموعات:

>>> apples = {}
>>> apples[Color.RED] = 'red delicious'
>>> apples[Color.GREEN] = 'granny smith'
>>> apples == {Color.RED: 'red delicious', Color.GREEN: 'granny smith'}
True

الوصول إلى عناصر Enumeration وخواصّها برمجيًا

من المفيد في بعض الأحيان الوصول إلى عناصر Enumeration برمجيًّا (بمعنى أنّه لا يمكن استخدام أسماء محدّدة مثل Color.RED لأنّه اللون المطلوب قد يكون غير معروف أثناء كتابة البرنامج) ويمكن القيام بذلك كما يلي:

>>> Color(1)
<Color.RED: 1>
>>> Color(3)
<Color.BLUE: 3>

ويمكن الوصول إلى عناصر enum بواسطة أسمائها كما يلي:

>>> Color['RED']
<Color.RED: 1>
>>> Color['GREEN']
<Color.GREEN: 2>

للحصول على اسم أو قيمة عنصر من عناصر enum:

>>> member = Color.RED
>>> member.name
'RED'
>>> member.value
1

مضاعفة عناصر وقيم enum

لا تسمح اللغة بوجود عنصري enum يحملان نفس الاسم:

>>> class Shape(Enum):
...     SQUARE = 2
...     SQUARE = 3
...
Traceback (most recent call last):
...
TypeError: Attempted to reuse key: 'SQUARE'

ولكن يمكن لعنصري enum أن يمتلكا القيمة ذاتها. لو فرضنا وجود عنصرين A و B يحملان القيمة ذاتها (وكان A معرّفًا قبل B) فإن العنصر B سيكون اختصارًا (alias، أو اسمًا بديلًا) للعنصر A. إن جرى الاستعلام عن قيمة A و B فإنّ النتيجة ستكون A، وإن جرى الاستعلام عن اسم B فإن النتيجة ستكون A أيضًا:

>>> class Shape(Enum):
...     SQUARE = 2
...     DIAMOND = 1
...     CIRCLE = 3
...     ALIAS_FOR_SQUARE = 2
...
>>> Shape.SQUARE
<Shape.SQUARE: 2>
>>> Shape.ALIAS_FOR_SQUARE
<Shape.SQUARE: 2>
>>> Shape(2)
<Shape.SQUARE: 2>

ملاحظة: لا يمكن إنشاء عنصر يحمل اسم خاصّية معرّفة مسبقًا (عنصر آخر، أو تابع ...إلخ.) كما لا يمكن إنشاء خاصّية تحمل نفس اسم عنصر معرّف مسبقًا.

ضمان فرادة قيم enumeration

تسمح enumerations - افتراضيًّا - باستخدام أسماء متعددة كمختصرات للقيمة نفسها. إن كان هذا الأسلوب غير مرغوب به فيمكن استخدام المزخرف التالي لضمان استخدام كلّ قيمة في الـ enumeration لمرة واحدة فقط:

@enum.unique

يوضح المثال التالي كيفية استخدام هذا المزخرف (decorator):

>>> from enum import Enum, unique
>>> @unique
... class Mistake(Enum):
...     ONE = 1
...     TWO = 2
...     THREE = 3
...     FOUR = 3
...
Traceback (most recent call last):
...
ValueError: duplicate values found in <enum 'Mistake'>: FOUR -> THREE

استخدام القيم التلقائية

إن كانت القيمة الدقيقة غير مهمّة فيمكن استخدام auto محلّها:

>>> from enum import Enum, auto
>>> class Color(Enum):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

تختار اللغة القيم عن طريق التابع ‎_generate_next_value_()‎ والذي يمكن إعادة تعريفه (override):

>>> class AutoName(Enum):
...     def _generate_next_value_(name, start, count, last_values):
...         return name
...
>>> class Ordinal(AutoName):
...     NORTH = auto()
...     SOUTH = auto()
...     EAST = auto()
...     WEST = auto()
...
>>> list(Ordinal)
[<Ordinal.NORTH: 'NORTH'>, <Ordinal.SOUTH: 'SOUTH'>, <Ordinal.EAST: 'EAST'>, <Ordinal.WEST: 'WEST'>]

المرور على عناصر enumeration

لا تقدم عملية المرور على عناصر enumeration الاختصارات الموجودة فيه:

>>> list(Shape)
[<Shape.SQUARE: 2>, <Shape.DIAMOND: 1>, <Shape.CIRCLE: 3>]

الخاصّية الخاصة __members__ هي قاموس مرتّب يربط الأسماء بالعناصر، ويتضمن جميع الأسماء المعرّفة في enumeration وبضمنها الاختصارات:

>>> for name, member in Shape.__members__.items():
...     name, member
...
('SQUARE', <Shape.SQUARE: 2>)
('DIAMOND', <Shape.DIAMOND: 1>)
('CIRCLE', <Shape.CIRCLE: 3>)
('ALIAS_FOR_SQUARE', <Shape.SQUARE: 2>)

يمكن الاستفادة من الخاصية __members__ للوصول إلى عناصر enumeration برمجيًّا. فعلى سبيل المثال يمكن العثور على جميع الاختصارات بالطريقة التالية:

>>> [name for name, member in Shape.__members__.items() if member.name != name]
['ALIAS_FOR_SQUARE']

مقارنة العناصر

يمكن عقد المقارنات بين عناصر enumeration باستخدام هوية العنصر:

>>> Color.RED is Color.RED
True
>>> Color.RED is Color.BLUE
False
>>> Color.RED is not Color.BLUE
True

لا تدعم اللغة المقارنات المرتّبة بين قيم enumeration، إلى جانب أنّ عناصر Enum ليست أعدادًا صحيحة.

>>> Color.RED < Color.BLUE
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Color' and 'Color'

ولكن يمكن إجراء اختبارات المساواة:

>>> Color.BLUE == Color.RED
False
>>> Color.BLUE != Color.RED
True
>>> Color.BLUE == Color.BLUE
True

عقد المقارنة مع قيم ليست ضمن enumeration تكون نتيجتها دومًا عدم المساواة:

>>> Color.BLUE == 2
False

العناصر والخصائص المسموح بها في enumeration

في الأمثلة السابقة كانت قيم enumeration أعدادًا صحيحة. بالرغم من أن استخدام الأعداد الصحيحة مختصر وسريع، ولكنّه ليس إلزاميًا. في أغلب الحالات ليس من المهمّ معرفة القيمة الحقيقية للـ enumeration، ولكن إن كانت القيمة مهمّة، فيمكن للـ enumerations أن تمتلك أيّ قيمة ممكنة.

Enumerations هي أصناف بايثون؛ لذا يمكن أن تمتلك توابع وتوابع خاصّة كما هو الحال مع الأصناف، فلو كان لدينا الـ enumeration التالي:

>>> class Mood(Enum):
...     FUNKY = 1
...     HAPPY = 3
...
...     def describe(self):
...         # self is the member here
...         return self.name, self.value
...
...     def __str__(self):
...         return 'my custom str! {0}'.format(self.value)
...
...     @classmethod
...     def favorite_mood(cls):
...         # cls here is the enumeration
...         return cls.HAPPY
...

فيمكن حينئذ كتابة الشيفرة التالية:

>>> Mood.favorite_mood()
<Mood.HAPPY: 3>
>>> Mood.HAPPY.describe()
('HAPPY', 3)
>>> str(Mood.FUNKY)
'my custom str! 1'

تتبع عملية التسمية القواعد التالية:

  • تكون الأسماء التي تبدأ وتنتهي بشرطة سفلية واحدة محجوزة من طرف enum ولا يمكن استخدامها. 
  • تصبح جميع الخاصيات الأخرى المعرّفة ضمن enumeration عناصر له؛ باستثناء التوابع الخاصة (‎__str__()، __add__()‎، وغيرها) والواصفات (descriptors) (التوابع واصفات أيضًا).

التفريع المقيد لأصناف enum

تسمح اللغة فقط بإنشاء أصناف فرعية للـ enumeration التي لا تعرّف أي عناصر، ولهذا تعدّ الشيفرة التالية خاطئة:

>>> class MoreColor(Color):
...     PINK = 17
...
Traceback (most recent call last):
...
TypeError: Cannot extend enumerations

أما الشيفرة التالية فصحيحة:

>>> class Foo(Enum):
...     def some_behavior(self):
...         pass
...
>>> class Bar(Foo):
...     HAPPY = 1
...     SAD = 2

يؤدي السماح بإنشاء أصناف فرعية للصنف enum الذي يمتلك عددًا من العناصر إلى مخالفة بعض الثوابت المهمّة في الأنواع ونسخ الأصناف، ولكن من جانب آخر من المعقول أن يُسمح بمشاركة بعض الأمور المشتركة بين مجموعة من الـ enumeration.

(راجع OrderedEnum للاطلاع على الأمثلة).

عملية Pickling

يمكن إجراء عمليتي pickling و unpickling على enumerations:

>>> from test.test_enum import Fruit
>>> from pickle import dumps, loads
>>> Fruit.TOMATO is loads(dumps(Fruit.TOMATO))
True

وتنطبق هنا جميع القيود المعروفة لعملية pickling: حيث يجب تعريف الـ enums في المستوى الأعلى من الوحدة؛ وذلك لأنّ عملية unpickling تتطلب كون هذه الـ enums قابلة للاستيراد من تلك الوحدة.

ملاحظة: من الممكن بالاعتماد على الإصدار الرابع من بروتوكول عملية pickling إجراء هذه العملية على enums متداخلة في أصناف أخرى.

من الممكن تعديل طريقة إجراء عمليتي pickling و unpickling على عناصر الصنف Enum وذلك بتعريف التابع ‎__reduce_ex__()‎ في صنف enumeration.

الواجهة البرمجية الوظيفية Functional API

الصنف Enum قابل للاستدعاء، ويقدّم الواجهة البرمجية الوظيفية التالية:

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG')
>>> Animal
<enum 'Animal'>
>>> Animal.ANT
<Animal.ANT: 1>
>>> Animal.ANT.value
1
>>> list(Animal)
[<Animal.ANT: 1>, <Animal.BEE: 2>, <Animal.CAT: 3>, <Animal.DOG: 4>]

هذه الواجهة البرمجية مشابهة للصفوف المسمّاة، ويكون المعامل الأول في عملية استدعاء الصنف Enum هو اسم enumeration.

أما المعامل الثاني source فهو مصدر أسماء عناصر enumeration. يمكن لهذا المعامل أن يكون سلسلة نصية تتضمن أسماء مفصولة عن بعضها البعض بمسافات بيضاء، أو تسلسل من الأسماء، أو تسلسل من صفوف مزدوجة تتضمن زوج مفتاح/قيمة، أو ربط mapping (مثل: القاموس) أسماء بقيم. 

يتيح الخياران الأخيران إسناد قيم متنوعة إلى enumerations، في حين تسند الخيارات الأخرى وبصورة تلقائية أعدادًا صحيحة تبدأ من الرقم 1 (يمكن استخدام المعامل start لتحديد قيمة ابتدائية مختلفة)، ويعاد صنف جديد مشتّق من الصنف Enum.

وبعبارة أخرى فإنّ عملية الإسناد في المثال السابقة إلى الصنف Animal تكون مكافئة لما يلي:

>>> class Animal(Enum):
...     ANT = 1
...     BEE = 2
...     CAT = 3
...     DOG = 4
...

إن السبب الذي يدعو إلى البدء بالعدد 1 وليس 0 هو أنّ العدد 0 يمثّل القيمة False في السياقات البوليانية، لكنّ جميع عناصر enum تُعالج إلى القيمة True.

إجراء عملية Pickling على enums أنشئت بواسطة الواجهة البرمجية الوظيفية قد يكون صعبًا في بعض الأحيان، وذلك لأنّ frame stack implementation details تستخدم لمعرفة الوحدة التي أنشئ فيها enumeration (على سبيل المثال ستفشل العملية عند استخدام دالة مساعدة في وحدة أخرى، وقد لا تتم العملية أيضًا على IronPython أو Jython).

وحل هذه المشكلة هو تحديد اسم الوحدة وعلى النحو التالي:

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG', module=__name__)

تحذير:

إن لم تُعرّف الوحدة المستخدمة، ولم يتمكن الصنف Enum من تحديدها، فبالإمكان إجراء عملية unpickling على عناصر Enum، ولكن لتقريب الأخطاء إلى مصادرها، فإنّ عملية pickling هي التي ستعطل.

كذلك يعتمد البروتوكول الرابع الجديد الخاص بعملية pickling -في بعض الحالات- على التابع ‎__qualname__‎ والذي يُعيّن الموقع الذي تكون فيه عملية pickling قادرة على إيجاد الصنف. فمثلًا، إن كان الصنف موجودًا في صنف SomeData ضمن النطاق العام:

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG', qualname='SomeData.Animal')

والتوقيع الكامل هو:

Enum(value='NewEnumName', names=<...>, *, module='...', qualname='...', type=<mixed-in class>, start=1)
value القيمة التي سيسجّلها الصنف Enum كاسم له.
names عناصر الصنف Enum. يمكن أن يكون سلسلة نصية تتضمن أسماء مفصولة بمسافات بيضاء أو بفواصل (القيم تبدأ بالرقم 1 إلا إذا حُدّد عدد آخر كعدد ابتدائي):

'RED GREEN BLUE' | 'RED,GREEN,BLUE' | 'RED, GREEN, BLUE'

أو كقائمة من الأسماء:

['RED', 'GREEN', 'BLUE']

أو كقائمة من أزواج (اسم، قيمة):

[('CYAN', 4), ('MAGENTA', 5), ('YELLOW', 6)]

أو كقاموس:

{'CHARTREUSE': 7, 'SEA_GREEN': 11, 'ROSEMARY': 42}

module اسم الوحدة التي يمكن إيجاد الصنف Enum الجديد فيها.
qualname المكان الذي يمكن إيجاد الصنف Enum الجديد فيه ضمن الوحدة.
type النوع المطلوب دمجه مع صنف Enum الجديد.
start العدد الذي يبدأ منه العدّ عند تمرير الأسماء فقط.

ملاحظة: أضيف المعامل start في الإصدار 3.5 من بايثون.

الـ Enumerations المشتقّة

الصنف IntEnum

أول صنف مشتّق من Enum وهو صنف مشتّق كذلك من int. يمكن مقارنة عناصر الصنف IntEnum مع الأعداد الصحيحة، ويمكن كذلك مقارنة الـ enumerations العددية ذات الأنواع المختلفة مع بعضها بعضًا:

>>> from enum import IntEnum
>>> class Shape(IntEnum):
...     CIRCLE = 1
...     SQUARE = 2
...
>>> class Request(IntEnum):
...     POST = 1
...     GET = 2
...
>>> Shape == 1
False
>>> Shape.CIRCLE == 1
True
>>> Shape.CIRCLE == Request.POST
True

ولكن لا يمكن مقارنتها مع enumerations من نوع Enum الاعتيادية:

>>> class Shape(IntEnum):
...     CIRCLE = 1
...     SQUARE = 2
...
>>> class Color(Enum):
...     RED = 1
...     GREEN = 2
...
>>> Shape.CIRCLE == Color.RED
False

تسلك قيم IntEnum سلوك الأعداد الصحيحة وحسب ما هو متوقع:

>>> int(Shape.CIRCLE)
1
>>> ['a', 'b', 'c'][Shape.CIRCLE]
'b'
>>> [i for i in range(Shape.SQUARE)]
[0, 1]

الصنف IntFlag

يستند هذا النوع من أصناف Enum على الصنف int أيضًا، ويمتاز هذا الصنف بقدرة عناصره على الاندماج باستخدام عوامل bitwise (& و | و ^ و ~) وتبقى النتيجة عنصر IntFlag. ولكن، وكما يوحي إليه الاسم، فإنّ عناصر IntFlag هي أصناف فرعية من int أيضًا، ويمكن استخدامها في أي مكان يمكن استخدام الأعداد الصحيحة فيه، ولكن يؤدي إجراء أي عملية على عناصر IntFlag باستثناء عمليات bitwise إلى خروج العنصر من IntFlag.

ملاحظة: هذا الصنف جديد في الإصدار 3.6 من بايثون.

يعرض المثال التالي نموذجًا لصنف IntFlag:

>>> from enum import IntFlag
>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...
>>> Perm.R | Perm.W
<Perm.R|W: 6>
>>> Perm.R + Perm.W
6
>>> RW = Perm.R | Perm.W
>>> Perm.R in RW
True

يمكن كذلك تسمية التركيبات combinations:

>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...     RWX = 7
>>> Perm.RWX
<Perm.RWX: 7>
>>> ~Perm.RWX
<Perm.-8: -8>

هناك فارق آخر بين الصنفين IntFlag و Enum، وهو أنّه في حال عدم تعيين أية رايات (أي كانت القيمة هي 0) فإنّ IntFlag يعالج في السياقات البوليانية إلى القيمة False:

>>> Perm.R & Perm.X
<Perm.0: 0>
>>> bool(Perm.R & Perm.X)
False

لمّا كانت عناصر IntFlag أصنافًا فرعية من الصنف int، فمن الممكن إذًا دمج الصنفين بعضهما ببعض:

>>> Perm.X | 8
<Perm.8|X: 9>

الصنف Flag

النوع الأخير من أصناف Enum هو الصنف Flag. كما هو الحال مع الصنف IntFlag، فبالإمكان دمج عناصر الصنف Flag باستخدام معاملات bitwise (& و | و ^ و ~). ولكن بخلاف IntFlag، لا يمكن دمج عناصر الصنف Flag أو مقارنتها مع أي صنف Flag أو int

على الرغم من إمكانية تحديد القيم بصورة مباشرة، إلا أنّه ينصح باستخدام auto كقيمة والسماح للصنف Flag باختيار القيمة المناسبة.

ملاحظة: هذا الصنف جديد في الإصدار 3.6.

كما هو الحال مع الصنف IntFlag، إن لم تعين مجموعة من عناصر Flag أيّة راية، فإنّ الصنف يعالج إلى القيمة False في السياقات البوليانية:

>>> from enum import Flag, auto
>>> class Color(Flag):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.RED & Color.GREEN
<Color.0: 0>
>>> bool(Color.RED & Color.GREEN)
False

يجب أن تحمل الرايات المفردة قيمًا تكون قوىً للعدد 2 (1، 2، 4، 8، ...)، أما مجموعات الرايات فلا:

>>> class Color(Flag):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...     WHITE = RED | BLUE | GREEN
...
>>> Color.WHITE
<Color.WHITE: 7>

لا يؤدي إعطاء اسم إلى الشرط "no flags set" إلى تغيير قيمته البوليانية:

>>> class Color(Flag):
...     BLACK = 0
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.BLACK
<Color.BLACK: 0>
>>> bool(Color.BLACK)
False

ملاحظة:

ينصح بشدة استخدام الصنفين Enum و Flag في الشيفرات الجديدة، وذلك لأنّ الصنفين IntEnum و IntFlag يكسران بعض القواعد الخاصة بالـ enumeration (مثل إمكانية المقارنة مع الأعداد الصحيحة، الأمر الذي يؤدي إلى الانتقال إلى enumerations أخرى غير ذات علاقة).

يجب استخدام الصنفين IntEnum و IntFlags فقط في الحالات التي يكون فيها استخدام الصنفين Enum و Flag غير مجدٍ، كأن يجري استبدال ثوابت الأعداد الصحيحة بالـ enumerations، أو للحاجة إلى التوافق مع الأنظمة الأخرى.

أصناف أخرى

على الرغم من كون الصنف IntEnum جزءًا من الوحدة enum، إلا أنّه يمكن وبكل سهولة استخدامه على نحو منعزل:

class IntEnum(int, Enum):
    pass

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

ولكن هناك بعض القواعد:

  1. عند تفريع الصنف Enum، يجب أن يظهر النوع المراد دمجه قبل Enum في تسلسل الأصناف الأساسية، كما هو مبين في المثال السابق.
  2. يمكن للصنف Enum أن يمتلك عناصر ذات أنواع مختلفة، ولكن عند دمج هذا الصنف مع نوع آخر يصبح من الواجب أن تمتلك عناصر الصنف Enum قيمًا من ذلك النوع حصرًا (النوع int في المثال السابق). هذا القيد لا ينطبق على النوع الممزوج مع الصنف Enum، حيث تؤدي عملية المزج إلى إضافة توابع النوع الممزوج فقط، ولا تحدد أي نوع آخر من البيانات بخصوص الأنواع مثل int أو str.
  3. عند مزج نوع آخر من البيانات، لا تكون الخاصية value مطابقة لعنصر enum نفسه، على الرغم من أنّهما متساويان.
  4. التنسيق بنمط %: يستدعي الرمزان ‎%s و ‎%r التابعين ‎__str__()‎ و ‎__repr__()‎ على التوالي من الصنف Enum، أما الرموز الباقية (مثل ‎%i أو ‎%h لأجل IntNum) فتعامل عنصر enum معاملة النوع الممزوج مع الصنف Enum.
  5. تستخدم سلاسل التنسيق النصية والتابع str.format()‎ والدالة format()‎، التابع ‎__format()‎__‎ الخاصّ بالنوع الممزوج. إن كان المطلوب الوصول إلى التابع str()‎ أو repr()‎ في الصنف Enum فيمكن استخدام علامة التعجب ! (‎!s أو ‎!r).

بعض الأمثلة المفيدة

صحيح أنّ الأصناف Enum و IntEnum و IntFlag و Flag تغطي الغالبية العظمى من الحالات، ولكنّها لن تستطيع القيام بكلّ شيء. يتضمّن هذا القسم بعض الأنواع المختلفة من enumerations والتي يمكن استخدامها على نحو مباشرة، أو الاستفادة منها في إنشاء أنواع خاصّة بالمستخدم.

حذف القيم

في الكثير من الحالات لا تكون لقيمة enumeration أي أهمّية، وهناك الكثير من الطرق التي يمكن من خلالها تعريف هذا مثل هذا النوع من الـ enumeration البسيط:

  • استخدام نسخ من الصنف auto كقيمة.
  • استخدم نسخ من كائن كقيمة.
  • استخدم سلسلة نصية واصفة كقيمة.
  • استخدام صف كقيمة، وتابع ‎__new__()‎ خاصّ لاستبدال الصف بقيمة من نوع int.

استخدام أي طريقة من الطرق السابقة يعني أنّ هذه القيمة ليست مهمّة، إلى جانب أنّها تسمح بإضافة وحذف وإعادة ترتيب العناصر دون الحاجة إلى إعادة ترقيم العناصر الباقية.

ومهما كانت الطريقة المستخدمة، يجب توفير دالة repr()‎ والتي تخفي كذلك القيم (غير المهمّة):

>>> class NoValue(Enum):
...     def __repr__(self):
...         return '<%s.%s>' % (self.__class__.__name__, self.name)
...

استخدام auto

يمكن استخدام auto بالطريقة التالية:

>>> class Color(NoValue):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.GREEN
<Color.GREEN>

استخدام كائن

يمكن استخدام الكائن بالطريقة التالية:

>>> class Color(NoValue):
...     RED = object()
...     GREEN = object()
...     BLUE = object()
...
>>> Color.GREEN
<Color.GREEN>

استخدام سلسلة نصية واصفة

يمكن استخدام السلسلة النصية الواصفة بالطريقة التالية:

>>> class Color(NoValue):
...     RED = 'stop'
...     GREEN = 'go'
...     BLUE = 'too fast!'
...
>>> Color.GREEN
<Color.GREEN>
>>> Color.GREEN.value
'go'

استخدام تابع ‎__new__()‎ خاص

يمكن استخدام تابع ‎__new__()‎ لترقيم العناصر تلقائيًا وبالطريقة التالية:

>>> class AutoNumber(NoValue):
...     def __new__(cls):
...         value = len(cls.__members__) + 1
...         obj = object.__new__(cls)
...         obj._value_ = value
...         return obj
...
>>> class Color(AutoNumber):
...     RED = ()
...     GREEN = ()
...     BLUE = ()
...
>>> Color.GREEN
<Color.GREEN>
>>> Color.GREEN.value
2

ملاحظة:

يستخدم التابع ‎__new__‎()‎ (في حال تعريفه) أثناء عملية إنشاء عناصر Enum، ويُستبدل بعد ذلك بتابع ‎__new__()‎ الخاصّ بالصنف Enum والذي يستخدم بعد إنشاء الصنف للبحث عن العناصر الموجودة في الصنف Enum.

الـ Enumerations المرتبة OrderdEnum

الـ Enumeration المرتّب هو ذلك الذي لا يستند إلى الصنف IntEnum وبذلك يحافظ على خصائص Enum الاعتيادية (مثل عدم كونه قابلًا للمقارنة مع enumerations أخرى):

>>> class OrderedEnum(Enum):
...     def __ge__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value >= other.value
...         return NotImplemented
...     def __gt__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value > other.value
...         return NotImplemented
...     def __le__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value <= other.value
...         return NotImplemented
...     def __lt__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value < other.value
...         return NotImplemented
...
>>> class Grade(OrderedEnum):
...     A = 5
...     B = 4
...     C = 3
...     D = 2
...     F = 1
...
>>> Grade.C < Grade.A
True

الـ Enumeration المضاعف الحرّ DuplicateFreeEnum

يطلق هذا الـ enumeration خطأً في حال وجود عناصر ذات أسماء مكرّرة، عوضًا عن إنشاء مختصر لها:

>>> class DuplicateFreeEnum(Enum):
...     def __init__(self, *args):
...         cls = self.__class__
...         if any(self.value == e.value for e in cls):
...             a = self.name
...             e = cls(self.value).name
...             raise ValueError(
...                 "aliases not allowed in DuplicateFreeEnum:  %r --> %r"
...                 % (a, e))
...
>>> class Color(DuplicateFreeEnum):
...     RED = 1
...     GREEN = 2
...     BLUE = 3
...     GRENE = 2
...
Traceback (most recent call last):
...
ValueError: aliases not allowed in DuplicateFreeEnum:  'GRENE' --> 'GREEN'

ملاحظة:

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

مثال: صنف الكوكب Planet

إن جرى تعريف التابع ‎__new__()‎ أو ‎__init__()‎، فإن قيمة عنصر enum ستُمرّر إلى هذين التابعين:

>>> class Planet(Enum):
...     MERCURY = (3.303e+23, 2.4397e6)
...     VENUS   = (4.869e+24, 6.0518e6)
...     EARTH   = (5.976e+24, 6.37814e6)
...     MARS    = (6.421e+23, 3.3972e6)
...     JUPITER = (1.9e+27,   7.1492e7)
...     SATURN  = (5.688e+26, 6.0268e7)
...     URANUS  = (8.686e+25, 2.5559e7)
...     NEPTUNE = (1.024e+26, 2.4746e7)
...     def __init__(self, mass, radius):
...         self.mass = mass       # in kilograms
...         self.radius = radius   # in meters
...     @property
...     def surface_gravity(self):
...         # universal gravitational constant  (m3 kg-1 s-2)
...         G = 6.67300E-11
...         return G * self.mass / (self.radius * self.radius)
...
>>> Planet.EARTH.value
(5.976e+24, 6378140.0)
>>> Planet.EARTH.surface_gravity
9.802652743337129

مثال: صنف الفترة الزمنية TimePeriod

يعرض المثال التالي طريقة استخدام الخاصّية _ignore_:

>>> from datetime import timedelta
>>> class Period(timedelta, Enum):
...     "different lengths of time"
...     _ignore_ = 'Period i'
...     Period = vars()
...     for i in range(367):
...         Period['day_%d' % i] = i
...
>>> list(Period)[:2]
[<Period.day_0: datetime.timedelta(0)>, <Period.day_1: datetime.timedelta(days=1)>]
>>> list(Period)[-2:]
[<Period.day_365: datetime.timedelta(days=365)>, <Period.day_366: datetime.timedelta(days=366)>]

ما الذي يميّز Enums

تمتلك Enums صنفًا عاليًا خاصًّا metaclass له القدرة على التأثير على العديد من الجوانب المرتبطة بأصناف Enum المشتّقة وبنسخها كذلك (عناصرها).

أصناف Enum

يقدم الصنف العالي EnumMeta التوابع ‎__contains__()‎ و ‎__dir__()‎ و ‎__iter__()‎ وتوابع أخرى تسمح بأداء بعض الوظائف التي لا يمكن أداؤها مع الأصناف الاعتيادية، مثل الحصول على قائمة بالعناصر list(Color)‎ أو المرور على العناصر some_enumv_var in Color. إلى جانب ما سبق، يضمن الصنف EnumMeta صحّة توابع أخرى متعدّدة في صنف Enum النهائية (مثل ‎__new__()‎ و ‎__getnewargs__()‎ و ‎__str__()‎ و ‎__repr__()‎).

عناصر الصنف Enum (نُسخ الصنف)

إن أكثر ما يميّز عناصر الصنف Enum أنّها منفردة singletons، حيث يتولّى الصنف EnumMeta مهمّة إنشاء هذه العناصر أثناء إنشاء الصنف Enum، ثم يضعها في تابع ‎__new__()‎ الخاص وفي مكانها الصحيح لضمان عدم إنشاء نسخ من العناصر الجديدة باستخدام العناصر الموجودة فعلًا.

أسماء __dunder__ المدعومة

‎__members__‎ هو قاموس مرتّب OrderdDict من عناصر تأخذ الهيئة member_name:member، وهو متوفّر في الصنف فقط.

يجب على التابع ‎__new__()‎، في حال استخدامه، أن ينشئ ويعيد عناصر enum، ويُنصح كذلك استخدام قيم _value_ مناسبة لكل عنصر. لن يُستخدم التابع ‎__new__()‎ بعد إتمام علمية إنشاء جميع العناصر. 

أسماء _sunder_ المدعومة

_name_: اسم العنصر.

_value_: قيمة العنصر، يمكن تعيينها أو تعديلها في __new__

_missing_: دالة باحثة تستخدم عند عدم العثور على القيمة، ويمكن إعادة تعريفها override.

_ignore_: قائمة الأسماء، إما كقائمة list()‎ أو كسلسلة نصية str()‎، التي لن تحوّل إلى عناصر، والتي تُحذف من الصنف النهائي.

_order_: يستخدم في شيفرات الإصدارين 2 و 3 من بايثون لضمان تراتبية العناصر (خواصّ الصنف، يحذف أثناء عملية إنشاء الصنف).

_generate_next_value_: يُستخدم من طرف الواجهة البرمجية الوظيفية والصنف auto للحصول على القيمة المناسبة لعنصر enum، ويمكن إعادة تعريفه.

جديد في الإصدار 3.6: _missing_ و _order_ و _generate_next_value_

جديد في الإصدار 3.7: _ignore_

يمكن تعريف الترتيب المطلوب باستخدام _order_ وذلك لضمان توافق شيفرات بايثون في الإصدارين 2 و 3، حيث ستجري مقارنة الترتيب الفعلي للـ enumeration مع الترتيب المقدّم، ويطلق الخطأ في حال عدم وجود التطابق:

>>> class Color(Enum):
...     _order_ = 'RED GREEN BLUE'
...     RED = 1
...     BLUE = 3
...     GREEN = 2
...
Traceback (most recent call last):
...
TypeError: member order does not match _order_

ملاحظة:

في الإصدار الثاني من بايثون تكون الخاصية _order_ مطلوبة لتحديد ترتيب عناصر enum، وذلك لأنّ ترتيب العناصر يُفقد قبل تسجيله.

أنواع عناصر Enum

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

>>> class FieldTypes(Enum):
...     name = 0
...     value = 1
...     size = 2
...
>>> FieldTypes.value.size
<FieldTypes.size: 2>
>>> FieldTypes.size.value
2

القيم المنطقية لأصناف Enum وعناصرها

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

def __bool__(self):
    return bool(self.value)

أما أصناف Enum فتعالج دائمًا إلى القيمة True.

أصناف Enum مع التوابع

عند إضافة توابع جديدة إلى صنف Enum الفرعي (مثل الصنف Planet في أعلاه) فإنّ التوابع المضافة تظهر عند استخدام الدالة dir()‎ مع العناصر وليس مع الصنف:

>>> dir(Planet)
['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__members__', '__module__']
>>> dir(Planet.EARTH)
['__class__', '__doc__', '__module__', 'name', 'surface_gravity', 'value']

دمج عناصر الصنف Flag

في حال عدم تسمية مجموعة من عناصر الصنف Flag، فإنّ الدالة repr()‎ ستتضمن جميع الرايات وجميع مجموعات الرايات المسمّاة:

>>> class Color(Flag):
...     RED = auto()
...     GREEN = auto()
...     BLUE = auto()
...     MAGENTA = RED | BLUE
...     YELLOW = RED | GREEN
...     CYAN = GREEN | BLUE
...
>>> Color(3)  # مجموعة مسماة
<Color.YELLOW: 3>
>>> Color(7)      # مجموعة غير مسمّاة

انظر أيضًا

مصادر