واحدة من الميزات الجديدة المقدمة في Python 3.7 هي فئات البيانات. وهي مصممة لأتمتة عملية إنشاء التعليمات البرمجية للفئات التي يتم استخدامها لتخزين البيانات. على الرغم من حقيقة أنهم يستخدمون آليات عمل أخرى ، إلا أنه يمكن مقارنتهم بـ "صفوف اسمية قابلة للتغيير مع قيم افتراضية".
مقدمة
تتطلب كل الأمثلة المذكورة أعلاه Python 3.7 أو أعلى لتشغيلها.
يجب على معظم مطوري الثعبان كتابة هذه الفصول بانتظام:
class RegularBook: def __init__(self, title, author): self.title = title self.author = author
بالفعل في هذا المثال ، التكرار مرئي. يتم استخدام معرفات المؤلف والعنوان عدة مرات. سيحتوي الفصل الحقيقي أيضًا على الطرق __eq__
و __repr__
.
تحتوي وحدة dataclasses
على مصمم @dataclass
. باستخدامه ، سيبدو رمز مشابه كما يلي:
from dataclasses import dataclass @dataclass class Book: title: str author: str
من المهم ملاحظة أن أنواع التعليقات التوضيحية مطلوبة . سيتم تجاهل جميع الحقول التي ليس لها علامات نوع. بالطبع ، إذا كنت لا ترغب في استخدام نوع معين ، يمكنك تحديد Any
من وحدة typing
.
ما الذي تحصل عليه نتيجة لذلك؟ ستحصل تلقائيًا على فصل __repr__
، باستخدام الأساليب التي تم تنفيذها __init__
و __repr__
و __str__
و __eq__
. بالإضافة إلى ذلك ، سيكون فصلًا عاديًا ويمكنك أن ترثه أو تضيف أساليب تعسفية.
>>> book = Book(title="Fahrenheit 451", author="Bradbury") >>> book Book(title='Fahrenheit 451', author='Bradbury') >>> book.author 'Bradbury' >>> other = Book("Fahrenheit 451", "Bradbury") >>> book == other True
البدائل
Tuple أو القاموس
بالطبع ، إذا كانت البنية بسيطة للغاية ، يمكنك حفظ البيانات في قاموس أو مجموعة:
book = ("Fahrenheit 451", "Bradbury") other = {'title': 'Fahrenheit 451', 'author': 'Bradbury'}
ومع ذلك ، فإن هذا النهج له عيوب:
- يجب أن نتذكر أن المتغير يحتوي على بيانات تتعلق بهذه البنية.
- في حالة القاموس ، يجب أن تتبع أسماء المفاتيح. هذا التهيئة للقاموس
{'name': 'Fahrenheit 451', 'author': 'Bradbury'}
سيكون صحيحًا أيضًا. - في حالة وجود مجموعة ، يجب عليك تتبع ترتيب القيم ، حيث ليس لديهم أسماء.
هناك خيار أفضل:
مسمى
from collections import namedtuple NamedTupleBook = namedtuple("NamedTupleBook", ["title", "author"])
إذا استخدمنا الفئة التي تم إنشاؤها بهذه الطريقة ، نحصل عمليا على نفس الشيء مثل استخدام فئة البيانات.
>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury") >>> book.author 'Bradbury' >>> book NamedTupleBook(title='Fahrenheit 451', author='Bradbury') >>> book == NamedTupleBook("Fahrenheit 451", "Bradbury")) True
ولكن على الرغم من التشابه العام ، إلا أن الصفوف المسماة لها حدودها. إنهم يأتون من حقيقة أن الصفوف المسماة لا تزال الصف.
أولاً ، لا يزال بإمكانك مقارنة مثيلات الفئات المختلفة.
>>> Car = namedtuple("Car", ["model", "owner"]) >>> book = NamedTupleBook("Fahrenheit 451", "Bradbury")) >>> book == Car("Fahrenheit 451", "Bradbury") True
ثانيًا ، الصفوف المسماة غير قابلة للتغيير. هذا مفيد في بعض الحالات ، لكني أرغب في مزيد من المرونة.
أخيرًا ، يمكنك العمل على مجموعة محددة بالإضافة إلى واحدة عادية. على سبيل المثال ، كرر الأمر.
مشاريع أخرى
إذا لم تقتصر على المكتبة القياسية ، يمكنك العثور على حلول أخرى لهذه المشكلة. على وجه الخصوص ، attrs المشروع. يمكن أن يفعل أكثر من dataclass ويعمل على الإصدارات الأقدم من الثعبان مثل 2.7 و 3.4. ومع ذلك ، فإن حقيقة أنها ليست جزءًا من المكتبة القياسية قد تكون غير ملائمة
الخلق
يمكنك استخدام الديكور @dataclass
لإنشاء فئة بيانات. في هذه الحالة ، سيتم استخدام جميع حقول الفئة المحددة مع التعليق التوضيحي للنوع في الأساليب المقابلة للفئة الناتجة.
كبديل ، هناك وظيفة make_dataclass
، والتي تعمل بشكل مشابه لإنشاء tuples مسمى.
from dataclasses import make_dataclass Book = make_dataclass("Book", ["title", "author"]) book = Book("Fahrenheit 451", "Bradbury")
القيم الافتراضية
ميزة مفيدة واحدة هي سهولة إضافة القيم الافتراضية إلى الحقول. لا تزال هناك حاجة لإعادة تعريف طريقة __init__
، فقط حدد القيم مباشرة في الفصل.
@dataclass class Book: title: str = "Unknown" author: str = "Unknown author"
سيتم أخذها في الاعتبار في طريقة __init__
ولدت
>>> Book() Book(title='Unknown', author='Unknown author') >>> Book("Farenheit 451") Book(title='Farenheit 451', author='Unknown author')
ولكن كما هو الحال مع الفصول والأساليب العادية ، يجب أن تكون حذرًا عند استخدام الإعدادات الافتراضية القابلة للتغيير. إذا كنت ، على سبيل المثال ، تحتاج إلى استخدام القائمة كقيمة افتراضية ، فهناك طريقة أخرى ، ولكن أكثر من ذلك أدناه.
بالإضافة إلى ذلك ، من المهم مراقبة الترتيب الذي يتم فيه تحديد الحقول ذات القيم الافتراضية ، حيث إنها تتطابق تمامًا مع ترتيبها في طريقة __init__
فئات البيانات الثابتة
مثيلات tuples المسماة ثابتة. في العديد من المواقف ، هذه فكرة جيدة. بالنسبة لفئات البيانات ، يمكنك القيام بذلك أيضًا. ما عليك FrozenInstanceError
تحديد المعلمة frozen=True
عند إنشاء الفئة ، وإذا حاولت تغيير حقولها ، فسيتم FrozenInstanceError
استثناء FrozenInstanceError
@dataclass(frozen=True) class Book: title: str author: str
>>> book = Book("Fahrenheit 451", "Bradbury") >>> book.title = "1984" dataclasses.FrozenInstanceError: cannot assign to field 'title'
إعداد فئة البيانات
بالإضافة إلى المعلمة frozen
، فإن @dataclass
decorator لديه معلمات أخرى:
init
: إذا كان True
(افتراضي) ، يتم إنشاء طريقة __init__
. إذا كان للفصل بالفعل طريقة __init__
محددة ، فسيتم تجاهل المعلمة.repr
: يمكّن (افتراضيًا) إنشاء طريقة __repr__
. تحتوي السلسلة التي تم إنشاؤها على اسم الفئة واسم وتمثيل جميع الحقول المحددة في الفئة. في هذه الحالة ، يمكن استبعاد الحقول الفردية (انظر أدناه)eq
: يتيح (افتراضيًا) إنشاء طريقة __eq__
. تتم مقارنة الكائنات بنفس الطريقة كما لو كانت مجموعات تحتوي على قيم الحقول المقابلة. بالإضافة إلى ذلك ، يتم تحديد نوع المطابقة.- يمكّن
order
( __lt__
الافتراضي هو إيقاف) إنشاء __ge__
و __ge__
و __ge__
و __ge__
. تتم مقارنة الكائنات بالطريقة نفسها التي تتم بها مقارنة قيم tuples لقيم المجال. في نفس الوقت ، يتم فحص نوع الأشياء أيضًا. إذا order
تحديد الطلب ، ولكن لم order
تحديد eq
، فسيتم طرح استثناء ValueError
. أيضا ، لا يجب أن يحتوي الفصل على طرق مقارنة محددة بالفعل. - يؤثر
unsafe_hash
في __hash__
طريقة __hash__
. يعتمد السلوك أيضًا على قيم المعلمات eq
frozen
تخصيص الحقول الفردية
في معظم الحالات القياسية ، هذا غير مطلوب ، ولكن من الممكن تخصيص سلوك فئة البيانات بما يصل إلى الحقول الفردية باستخدام وظيفة الحقل.
افتراضيات قابلة للتعديل
الحالة النموذجية المذكورة أعلاه هي استخدام القوائم أو القيم الافتراضية الأخرى القابلة للتغيير. قد ترغب في فصل "رف كتب" يحتوي على قائمة بالكتب. إذا قمت بتشغيل التعليمات البرمجية التالية:
@dataclass class Bookshelf: books: List[Book] = []
سيبلغ المترجم عن خطأ:
ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory
ومع ذلك ، بالنسبة للقيم الأخرى القابلة للتغيير ، لن يعمل هذا التحذير وسيؤدي إلى سلوك برنامج غير صحيح.
لتجنب المشاكل ، يقترح استخدام المعلمة default_factory
للدالة field
. يمكن أن تكون قيمته أي كائن أو وظيفة تسمى بدون معلمات.
تبدو النسخة الصحيحة من الفصل كما يلي:
@dataclass class Bookshelf: books: List[Book] = field(default_factory=list)
خيارات أخرى
بالإضافة إلى default_factory
المحدد ، فإن الدالة الميدانية لها المعلمات التالية:
default
: القيمة default
. هذه المعلمة مطلوبة لأن الاستدعاء field
يحل محل قيمة الحقل الافتراضية.init
: يمكّن (افتراضيًا) استخدام حقل في طريقة __init__
repr
: يمكّن (افتراضيًا) استخدام حقل في طريقة __repr__
- تشمل
compare
(افتراضيًا) استخدام الحقل في طرق المقارنة ( __eq__
و __le__
وغيرها) hash
: قد تكون قيمة منطقية أو None
. إذا كان True
، يتم استخدام الحقل لحساب التجزئة. إذا None
تحديد بلا (افتراضيًا) ، يتم استخدام قيمة معلمة compare
.
أحد أسباب تحديد hash=False
compare=True
معينة compare=True
قد يكون صعوبة حساب التجزئة المجال أثناء الضرورة للمقارنة.metadata
: قاموس مخصص أو بلا. يتم تغليف القيمة في MappingProxyType
بحيث تصبح ثابتة. لا يتم استخدام هذه المعلمة من قبل فئات البيانات نفسها وهي مخصصة لملحقات الجهات الخارجية.
المعالجة بعد التهيئة
تستدعي طريقة __init__
إنشاؤها __post_init__
طريقة __post_init__
، إذا تم تعريفها في الفصل. كقاعدة ، يتم استدعاؤها في النموذج self.__post_init__()
، ومع ذلك ، إذا تم تحديد متغيرات من النوع InitVar
في الفئة ، فسيتم تمريرها كمعلمات الأسلوب.
إذا لم يتم إنشاء طريقة __init__
، فلن يتم استدعاء __post_init__
.
على سبيل المثال ، قم بإضافة وصف كتاب تم إنشاؤه
@dataclass class Book: title: str author: str desc: str = None def __post_init__(self): self.desc = self.desc or "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury')
معلمات التهيئة فقط
أحد الاحتمالات المرتبطة بطريقة __post_init__
هي المعلمات المستخدمة __post_init__
فقط. إذا ، عند تعريف حقل ، حدد InitVar
، سيتم تمرير قيمته كمعلمة لطريقة __post_init__
. لا يتم استخدام هذه الحقول في فئة البيانات بأي حال من الأحوال.
@dataclass class Book: title: str author: str gen_desc: InitVar[bool] = True desc: str = None def __post_init__(self, gen_desc: str): if gen_desc and self.desc is None: self.desc = "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury') >>> Book("Fareneheit 481", "Bradbury", gen_desc=False) Book(title='Fareneheit 481', author='Bradbury', desc=None)
الوراثة
عند استخدام الديكور @dataclass
، فإنه يمر عبر جميع الفئات الرئيسية بدءًا بالكائن ولكل فئة بيانات وجدت أنها تحفظ الحقول في قاموس منظم ، ثم تضيف خصائص الفصل الجاري معالجته. تستخدم جميع الأساليب التي تم إنشاؤها حقولًا من القاموس المرتبط الناتج.
ونتيجة لذلك ، إذا كان الفصل الأصل يحدد القيم الافتراضية ، فسيتعين عليك تحديد الحقول ذات القيم الافتراضية.
نظرًا لأن القاموس المنظم يقوم بتخزين القيم في ترتيب الإدراج للفئات التالية
@dataclass class BaseBook: title: Any = None author: str = None @dataclass class Book(BaseBook): desc: str = None title: str = "Unknown"
سيتم إنشاء طريقة __init__
مع هذا التوقيع:
def __init__(self, title: str="Unknown", author: str=None, desc: str=None)