OrderedDict و dict در پایتون

امیرحسین بیگدلو 5 ماه قبل

 

 #  معرفی

گاهی به یک دیکشنری پایتون نیاز دارید که ترتیب آیتم‌ها را به خاطر بسپارد. در گذشته، تنها یک ابزار برای حل این مشکل وجود داشت: OrderedDict؛ دیکشنری زیرکلاسی که مخصوصا برای به‌خاطر نگه‌داشتن ترتیب آیتم‌ها طراحی شده است و بر اساس ترتیب ورود کلیدها تعریف می‌شود.

 

این در پایتون 3.6 تغییر کرد. کلاس داخلی dict نیز اکنون آیتم‌ها را به‌ترتیب نگه‌داری می‌کند. به همین دلیل است که در حال حاضر افراد زیادی شک دارند که آیا OrderedDict همچنان کارایی دارد یا خیر. بررسی دقیق‌تر OrderedDict، این که این کلاس همچنان امکانات ارزشمندی را به ما ارائه می‌دهد، آشکار خواهد ساخت.

 

با کسب این اطلاعات، قادر خواهید بود در صورت تمایل به حفظ ترتیب آیتم‌ها، کلاس دیکشنری متناسب با نیاز خود را انتخاب کنید. در انتهای این درسنامه، مثالی از پیاده‌سازی یک صف مبتنی بر دیکشنری با استفاده از OrderedDict را خواهید داد، که در صورت استفاده از شیء  dict، بسیار پرچالش‌تر انجام می‌شد.

 

دوره پیشنهادی: دوره آموزش پایتون (python)

 

 #  انتخاب بین dict و orderedDict

برای سال‌ها، دیکشنری‌های پایتون ساختارهای داده بدون ترتیب بودند. توسعه‌دهندگان پایتون نیز به این واقعیت عادت کرده و از list یا سایر ساختارهای توالی دار برای نگه‌داری ترتیب‌دار داده‌ها استفاده می‌کردند. با گذر زمان، توسعه‌دهندگان به نوع جدیدی از دیکشنری‌ها نیاز پیدا کردند؛ نوعی که باید می‌توانست آیتم‌ها را با ترتیب نگه‌داری کند.  

 

در سال 2008، PEP 372 ایده اضافه کردن یک کلاس دیکشنری جدید به collections را معرفی کرد. هدف اصلی آن، نگه‌داری ترتیب آیتم‌ها به ترتیب ورود کلید آنها بود. این، ریشه OrderedDict به حساب می‌آید.

 

توسعه‌دهندگان هسته پایتون قصد داشتند فضاهای خالی را پر کرده و دیکشنری را ارائه کنند که بتوانند ترتیب کلیدهای وارد شده را نگه‌داری کنند. این خود سبب پیاده‌سازی صریح‌تر الگوریتم‌هایی شد که مشخصا بر این ویژگی تکیه داشتند.

OrderedDict

در پایتون‌ 3.1 به کتابخانه‌های استاندارد اضافه شد. این کلاس API مشابهی با dict دارد. با این حال، OrderedDict کلیدها و مقادیر را با ترتیبی می‌پیماید که کلیدها وارد شده‌بودند. اگر یک ورودی جدید، ورودی قبلی را بازنویسی کند، ترتیب آیتم‌ها تغییری نخواهد کرد. اگر ورودی حذف شده و مجددا وارد شود، به انتهای دیکشنری منتقل خواهد شد.  

 

پایتون 3.6، پیاده‌سازی جدیدی از dict را ارائه کرد. این پیاده‌سازی جدید، از نظر مصرف حافظه و کارایی پیمایش‌ها، یک برد به حساب می‌آمد. به علاوه، این پیاده‌سازی ویژگی جدید و غیرمنتظره‌ای را به همراه داشت؛ اشیاء dict اکنون آیتم‌ها را با همان ترتیب ورودشان، نگه‌داری می‌کردند. در ابتدا، این ویژگی از جزییات پیاده‌سازی به شمار می‌رفت، و مستندات تکیه بر آن را پیشنهاد نمی‌کردند.

 

نکته: در این درسنامه، بر پیاده‌سازی dict و OrderedDict که توسط CPython ارائه می‌شود، تمرکز می‌کنیم.

 

طبق گفته ریموند هِتینگر، برنامه‌نویس هسته پایتون و از طراحان OrderedDict، این کلاس مخصوصا برای نگه‌داری ترتیب‌دار آیتم‌ها نگهداری شده‌اند، در حالی که پیاده‌سازی جدید dict برای فشرده و سریع‌تر شدن پیمایش‌ها معرفی شده است:

 

"دیکشنری معمولی فعلی مبتنی بر طراحیست که من سال‌ها پیش معرفی کردم. هدف اصلی آن طراحی، پیمایش فشرده و سریع آرایه‌هایی متراکم از کلیدها و مقادیر بود. حفظ ترتیب‌ها بیشتر یک محصول فرعی بود و نه هدف از طراحی. این طراحی می‌تواند ترتیب‌ها را نگه‌داری کند، اما تخصصش این کار نیست.

 

در مقابل، برای collections.OrderedDict طراحی متفاوتی ارائه کردم (که بعدا توسط اِریک اسنو به زبان C پیاده‌سازی شد). هدف اصلی این طراحی، حفظ مناسب ترتیب حجم‌های کاری بسیار بالا بود - مانند lru_cache که ترتیب آن دائما و بدون تغییر dict، تغییر می‌یابد. در ابتدا، OrderedDictطراحی داشت که توانایی‌های حفظ ترتیب را، به قیمت افزایش استفاده از حافظه و معیار ثابتی بدتر از زمان ورود، در اولویت قرار می‌داد.  

 

هدف من همچنان این است که collections.OrderedDict طراحی و عملکرد متفاوتی از کلاس معمولی dict داشته باشد. البته OrderedDict متدهای مختص ترتیبی دارد که dict آنها را ارائه نمی‌دهد (مانند move_to_end() و popitem() که می‌تواند آخرین آیتم‌ را از هر دو انتها بیرون بیاورد). OrderedDict باید در این عملیات‌ها قوی باشد، زیرا آنها این کلاس را از dict متمایز می‌کنند".

 

در پایتون 3.7، ویژگی ترتیب آیتم‌های dict به عنوان جزئی رسمی از شیوه‌های اعلان پایتون اعلام شد. در نتیجه، از آن زمان به بعد، توسعه‌دهندگان پایتون می‌توانستند در صورت نیاز به دیکشنری که آیتم‌ها را به ترتیب نگه‌داری کند، بر dict تکیه کنند.  

 

در اینجا بود که این پرسش مطرح شد: آیا پس از پیاده‌سازی جدید dict، همچنان به وجود OrderedDict نیاز است؟ پاسخ این سوال بسته به کاربرد و دقت موردنظر شما در نوشتن برنامه است.

 

در زمان نوشتن این درسنامه، برخی از ویژگی‌های OrderedDict، همچنان آن را ارزشمند و متمایز از OrderedDict می‌کنند.

 

1. سیگنال‌دهی دقیق: اگر به جای dict از OrderedDict استفاده کنید، کد برنامه شما مشخص خواهد کرد که حفظ ترتیب آیتم‌های دیکشنری شما بسیار بااهمیت است. در این صورت واضحا این پیام را منتقل خواهید کرد که برنامه شما، به ترتیب آیتم‌های دیکشنری نیاز دارد یا بر آن تکیه می‌کند.

 

2. داشتن کنترل بر ترتیب آیتم‌ها: اگر نیاز به تغییر ترتیب آیتم‌ها داشته باشید، می‌توانید از .move_to_end() یا نسخه‌های بهبودیافته .popitem() استفاده کنید.

 

3. تست تشابه رفتاری: اگر برنامه شما دیکشنری‌ها را با هم مقایسه کند، و ترتیب آیتم‌ها در این مقایسه مهم باشد، OrderedDict انتخاب مناسب شما خواهد بود. 

 

حداقل یک دلیل دیگر برای استفاده از OrderedDict نیز وجود دارد: تطابق با نسخه‌های قبلی. استفاده از شیء dict برای حفظ ترتیب آیتم‌ها سبب تخریب برنامه شما در محیط‌هایی که نسخه پایتون آنها قدیمی‌تر از 3.6 است می‌شود.

 

پیش‌بینی اینکه آیا dict کاملا جای OrderedDict را خواهد گرفت یا خیر، دشوار است. امروزه، OrderedDict امکانات جالب و ارزشمندی را ارائه می‎‌کند که در حین انتخاب یک ابزار مناسب، حائز توجه است.

 

ویدیو پیشنهادی: ویدیو آموزش orderedDict در پایتون

 

 #  شروع به کار با orderedDict در پایتون

OrderedDict در پایتون یک زیرکلاس از dict است که ترتیب ورود جفت‌های کلید-مقدار، که با نام آیتم نیز شناخته می‌شوند، به دیکشنری را حفظ می‌کند. زمانی که یک شیء OrderedDict را پیمایش می‌کنید، آیتم‌ها در ترتیب اصلی خود پیموده می‌شوند. اگر مقدار یک کلید موجود در شیء را به‌روزرسانی کنید، ترتیب تغییری نمی‌کند. در صورت حذف یک آیتم و ورود مجدد آن، این آیتم به انتهای دیکشنری اضافه می‌شود.

 

زیرکلاس dict بودن به این معناست که این کلاس، تمامی متدهای یک دیکشنری معمولی را به ارث برده است. OrderedDict ویژگی‌های اضافی نیز دارد که در این درسنامه درباره آنها خواهید آموخت. اما در بخش بعدی، آموزش‌های مقدماتی ایجاد و استفاده از اشیاء OrderedDict در کدهای خود را خواهید آموخت.

 

 

 +  ایجاد اشیا OrderedDict در پایتون

برخلاف dict، OrderedDict داخلی نیست، در نتیجه اولین قدم برای ایجاد یک شیء از آن، آوردن (import کردن) کلاس collections است.

راه‌های زیادی برای ایجاد دیکشنری‌های ترتیب‌دار وجود دارد، که اکثر آنها مشابه روش‌های ایجاد یک شیء dict عادی هستند. برای مثال، می‌توانید یک شیء OrderedDict خالی را با استفاده از ایجاد یک نمونه از کلاس آن بدون آرگومان، ایجاد کنید:

>>> from collections import OrderedDict

>>> numbers = OrderedDict()

>>> numbers["one"] = 1
>>> numbers["two"] = 2
>>> numbers["three"] = 3

>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

 

در اینجا، ابتدا OrderedDict و dict به داخل برنامه آورده می‌شوند. سپس با ایجاد یک نمونه از OrderedDict بدون ارائه آرگومان، یک دیکشنری ترتیب‌دار خالی ایجاد می‌کنید.

 

می‌توانید با قرار دادن کلید در براکت‌های مستطیلی ([]) و اختصاص یک مقدار به آن، یک جفت کلید-مقدار تعریف کنید. وقتی به numbers رجوع می‌کنید، مجموعه‌ای از آیتم‌های قابل شمارش، شامل جفت‌های کلید-مقدار می‌یابید که ترتیب آیتم‌ها را همانطور که وارد شده بودند، حفظ می‌کنند.  

 

می‌توانید آرگومانی از آیتم‌های قابل شمارش را به constructor در OrderedDict انتقال دهید:

>>> from collections import OrderedDict

>>> numbers = OrderedDict([("one", 1), ("two", 2), ("three", 3)])
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

>>> letters = OrderedDict({("a", 1), ("b", 2), ("c", 3)})
>>> letters
OrderedDict([('c', 3), ('a', 1), ('b', 2)])

 

زمانی که از ساختارهای ترتیبی مانند list یا tuple استفاده می‌کنید، ترتیب آیتم‌ها در دیکشنری نهایی مانند ترتیب ورودی‌هاست. اگر از set استفاده می‌کنید، مانند مثال دوم بالا، ترتیب نهایی تا زمان ایجاد OrderedDict مشخص نمی‌شود.

 

اگر از یک دیکشنری عادی به عنوان آغازگر شیء OrderedDict استفاده کنید و نسخه پایتون شما 3.6 یا جدید‌تر باشد، نتیجه زیر را مشاهده خواهید کرد:

Python 3.9.0 (default, Oct  5 2020, 17:52:02)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import OrderedDict

>>> numbers = OrderedDict({"one": 1, "two": 2, "three": 3})
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

 

ترتیب آیتم‌ها در شیء OrderedDict مشابه ترتیب آنها در دیکشنری اولیه خواهد بود. از طرف دیگر، اگر نسخه پایتون شما از 3.6 قدیمی‌تر باشد، ترتیب آیتم‌ها مشخص نخواهد شد:

Python 3.5.10 (default, Jan 25 2021, 13:22:52)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import OrderedDict

>>> numbers = OrderedDict({"one": 1, "two": 2, "three": 3})
>>> numbers
OrderedDict([('one', 1), ('three', 3), ('two', 2)])

 

از آنجا که دیکشنری‌ها در پایتون 3.5 ترتیب آیتم‌ها را به خاطر نمی‌سپارند، تا زمان ایجاد شیء از ترتیب موجود در دیکشنری باخبر نخواهید شد. سپس، ترتیب پس از ایجاد شیء، شکل خواهد گرفت.

 

می‌توانید یک دیکشنری ترتیب‌دار را با فرستادن آرگومان‌های کلیدی به constructor در کلاس، ایجاد کنید:

>>> from collections import OrderedDict

>>> numbers = OrderedDict(one=1, two=2, three=3)
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

 

از پایتون 3.6، توابع ترتیب آرگومان‌های کلیدی را حفظ می‌کنند، در نتیجه ترتیب آیتم‌ها در OrderedDict بالا مشابه ترتیب انتقال آرگومان‌ها به constructor خواهد بود. در نسخه‌های قدیمی‌تر پایتون، این ترتیب نامعلوم است.

 

در آخر، OrderedDict دارای متد .fromkeys() است که یک دیکشنری جدید از مجموعه قابل پیمایشی از کلیدها ساخته و مقادیر کلیدها را برابر یک مقدار اولیه قرار می‌دهد.

>>> from collections import OrderedDict

>>> keys = ["one", "two", "three"]
>>> OrderedDict.fromkeys(keys, 0)
OrderedDict([('one', 0), ('two', 0), ('three', 0)])

 

در این صورت، می‌توانید از لیستی از کلیدها به عنوان شروع، یک دیکشنری ترتیب‌دار ایجاد کنید. دومین آرگومان ورودی .fromkeys() مقدار اولیه تمام کلیدها را مشخص می‌کند.

 

ویدیو پیشنهادی: توضیح dictionary در پایتون

 

 +  مدیریت آیتم‌ها در OrderedDict

از آنجایی که OrderedDict یک ساختار داده تغییرپذیر است، می‌توان بر روی نمونه‌های آن عملیات تغییر انجام داد؛ از جمله اضافه کردن آیتم‌های جدید، حذف یا به‌روزرسانی آنها. اگر آیتمی جدید به یک دیکشنری ترتیب‌دار اضافه کنید، آیتم در انتهای آن قرار خواهد گرفت.

>>> from collections import OrderedDict

>>> numbers = OrderedDict(one=1, two=2, three=3)
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

>>> numbers["four"] = 4
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3), ('four', 4)])

 

آیتم جدید، یعنی  ('four', 4) به انتهای دیکشنری اضافه شده است تا ترتیب سایر آیتم‌ها به هم نخورد و دیکشنری ترتیب ورود آنها را حفظ کند.

 

اگر یک آیتم را از دیکشنری حذف کرده و سپس آن را مجددا وارد کنید، آیتم در انتهای دیکشنری جای خواهد گرفت:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> del numbers["one"]
>>> numbers
OrderedDict([('two', 2), ('three', 3)])

>>> numbers["one"] = 1
>>> numbers
OrderedDict([('two', 2), ('three', 3), ('one', 1)])

 

اگر آیتم ('one', 1) را حذف کرده و یک نمونه جدید از آن را مجددا اضافه کنید، آیتم جدید در انتهای دیکشنری موجود قرار می‌گیرد.  

 

اگر مقدار یک جفت کلید-مقدار را به‌روزرسانی کنید، کلید جایگاه خود را در دیکشنری حفظ کرده، اما مقدارش تغییر خواهد کرد:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> numbers["one"] = 1.0
>>> numbers
OrderedDict([('one', 1.0), ('two', 2), ('three', 3)])

>>> numbers.update(two=2.0)
>>> numbers
OrderedDict([('one', 1.0), ('two', 2.0), ('three', 3)])

 

به طور مشابه، اگر از متد .update()  برای به‌روزرسانی مقدار یک جفت کلید-مقدار استفاده کنید، دیکشنری جایگاه کلید را به خاطر سپرده و مقدار آن را به‌روزرسانی می‌کند.

 

مقاله پیشنهادی: راهنمایی کامل پایتون و rest api

 

 +  پیمایش آیتم‌ها در OrderedDict

مشابه دیکشنری عادی، می‌توانید با استفاده از روش‌های مختلف یک شیء OrderedDict را پیمایش کنید. می‌توانید مستقیما کلیدها را بپیمایید، یا از متدهای دیکشنری مانند .items()، .keys() و .values() استفاده کنید.

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> # Iterate over the keys directly
>>> for key in numbers:
...     print(key, "->", numbers[key])
...
one -> 1
two -> 2
three -> 3

>>> # Iterate over the items using .items()
>>> for key, value in numbers.items():
...     print(key, "->", value)
...
one -> 1
two -> 2
three -> 3

>>> # Iterate over the keys using .keys()
>>> for key in numbers.keys():
...     print(key, "->", numbers[key])
...
one -> 1
two -> 2
three -> 3

>>> # Iterate over the values using .values()
>>> for value in numbers.values():
...     print(value)
...
1
2
3

 

اولین حلقه for مستقیما کلیدهای numbers را پیمایش می‌کند. سه حلقه بعدی، متدهای دیکشنری برای پیمایش آیتم‌ها، کلیدها و مقادیر را نشان می‌دهد.

 

مقاله پیشنهادی: مستند سازی کد پایتون

 

 +  پیمایش برعکس آیتم‌ها در OrderedDict با reversed

ویژگی دیگری که OrderedDict از پایتون 3.5 به بعد ارائه کرده است این است که آیتم‌ها، کلیدها و مقادیر با استفاده از متد reversed()، از پیمایش برعکس پشتیبانی می‌کنند. این ویژگی در پایتون 3.8 به دیکشنری‌های عادی نیز اضافه شد، در نتیجه اگر برنامه شما از پیمایش برعکس استفاده می‌کند، استفاده از دیکشنری عادی آن را با محدودیت بسیار زیادتری روبه‌رو خواهد کرد.  

 

می‌توانید reversed() را برای کلیدها، آیتم‌ها و مقدارهای شیء OrderedDict استفاده کنید:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> # Iterate over the keys directly in reverse order
>>> for key in reversed(numbers):
...     print(key, "->", numbers[key])
...
three -> 3
two -> 2
one -> 1

>>> # Iterate over the items in reverse order
>>> for key, value in reversed(numbers.items()):
...     print(key, "->", value)
...
three -> 3
two -> 2
one -> 1

>>> # Iterate over the keys in reverse order
>>> for key in reversed(numbers.keys()):
...     print(key, "->", numbers[key])
...
three -> 3
two -> 2
one -> 1

>>> # Iterate over the values in reverse order
>>> for value in reversed(numbers.values()):
...     print(value)
...
3
2
1

 

هر حلقه این مثال برای پیمایش برعکس آیتم‌های دیکشنری ترتیب‌دار، از reversed() استفاده می‌کند.  

 

دیکشنری‌های معمولی نیز از پیمایش برعکس پشتیبانی می‌کنند. با این حال، اگر از متد reversed() برای یک شیء dict معمولی در پایتون با نسخه پایین‌تر از 3.8 استفاده کنید، یک خطای TypeError دریافت می‌کنید:

Python 3.7.9 (default, Jan 14 2021, 11:41:20)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> numbers = dict(one=1, two=2, three=3)

>>> for key in reversed(numbers):
...     print(key)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict' object is not reversible

 

اگر نیاز دارید آیتم‌های یک دیکشنری را برعکس پیمایش کنید، OrderedDict گزینه مناسبی برای شماست. استفاده از یک دیکشنری معمولی، توانایی تطابق پیمایش برعکس را کم‌تر می‌کند؛ زیرا پیمایش برعکس تا نسخه پایتون 3.8 به دیکشنری‌های عادی اضافه نشده بود.

 

مقاله پیشنهادی: متد append پایتون

 

 #  بررسی ویژگی‌های OrderedDict پایتون

از پایتون 3.6، دیکشنری‌های عادی آیتم‌ها را به همان ترتیب ورودشان نگهداری می‌کنند. این سبب کاهش کارایی OrderedDict شد، اما این کلاس همچنان ویژگی‌های منحصربه‌فردی را داراست که اشیاء dict عادی از آنان بهره‌مند نیستند.

 

با یک دیکشنری ترتیب‌دار، به متدهای اضافی و پیشرفته زیر دسترسی خواهید داشت:

 

 -  move_to_end(). متد جدیدیست که در پایتون 3.2 اضافه شده است؛ این متد به شما اجازه می‌دهد یک آیتم را به ابتدا یا انتهای دیکشنری اضافه کنید.

 

 -  popitem(). نسخه بهبودیافته‌ای از dict.popitem()است که به شما اجازه می‌دهد یک آیتم را از ابتدا یا انتهای یک دیکشنری ترتیب‌دار حذف کرده و برگردانید.

 

OrderedDict و dict در حین تست برابری نیز رفتار متفاوتی دارند. به طور دقیق‌تر می‌توان گفت که در حین مقایسه دیکشنری‌های ترتیب‌دار، ترتیب آیتم‌ها اهمیت دارد، در حالی که در دیکشنری‌های عادی اینطور نیست.  

 

در آخر، نمونه‌های OrderedDict دارای صفتی به نام .__dict__هستند که در نمونه‌های دیکشنری‌های عادی یافت نمی‌شوند. این صفت امکان اضافه کردن صفت‌های شخصی قابل بازنویسی به یک دیکشنری ترتیب‌دار موجود را می‌دهد.

 

 

 +  تغییر ترتیب آیتم‌ها با move_to_end

یکی از بزرگ‌ترین تفاوت‌ها میان OrderedDict و Dict، وجود متد move_to_end(). در OrderedDict است. این متد به شما اجازه می‌دهد یک آیتم را به ابتدا یا انتهای دیکشنری اضافه کنید، در نتیجه ابزار مناسبی برای تغییر ترتیب آیتم‌های دیکشنری است.

 

برای استفاده از move_to_end() باید به آن دو آرگومان بدهید:

 

  key: کلید آیتمی است که می‌خواهید آن را جا به جا کنید. اگر این آرگومان موجود نباشد، خطای keyError  دریافت می‌کنید.

 

  last: آرگومانی از نوع boolean که مشخص می‌کند آیتم به کدام سمت دیکشنری می‌رود. اگر مقدار این آرگومان true باشد، یعنی آیتم به انتها یا سمت راست دیکشنری منتقل شده و اگر false باشد، یعنی آیتم به ابتدا یا سمت چپ دیکشنری منتقل می‌شود.

 

در اینجا مثالی از نحوه استفاده  move_to_end() با آرگومان‌های key و last (که مقدار آن به طور پیش فرض true است) می‌‌بینیم:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

>>> numbers.move_to_end("one")
>>> numbers
OrderedDict([('two', 2), ('three', 3), ('one', 1)])

 

زمانی که .move_to_end() تنها با آرگومان false فراخوانی شود، آیتم موردنظر به انتهای صف منتقل می‌شود. به همین دلیل است که در این مثال ('one', 1) در انتها قرار می‌گیرد. توجه کنید که باقی آیتم‌ها در مکان قبلی خود باقی می‌مانند.

 

با false کردن last، آیتم به ابتدا منتقل می‌شود:

>>> numbers.move_to_end("one", last=False)
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

 

در این حالت ('one', 1) به ابتدای دیکشنری منتقل می‌شود. این ویژگی بسیار جالب و قدرتمند است. برای مثال، با این متد می‌توان آیتم‌ها را بر اساس کلیدهایشان مرتب کرد:

>>> from collections import OrderedDict
>>> letters = OrderedDict(b=2, d=4, a=1, c=3)

>>> for key in sorted(letters):
...     letters.move_to_end(key)
...
>>> letters
OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])

 

در این مثال، ابتدا یک دیکشنری ترتیب‌دار به نام letters ایجاد کردیم. سپس حلقه for دیکشنری را بر اساس کلیدها مرتب کرده و هر آیتم را به انتهای دیکشنری منتقل می‌کند. در انتها همه آیتم‌ها بر اساس کلیدشان مرتب شده‌اند.

 

ویدیو پیشنهادی: ویدیو آشنایی با dictionary comprehensions در پایتون

 

 +  حذف آیتم‌ها با popitem

یکی دیگر از ویژگی‌های جالب OrderedDict نسخه بهبودیافته .popitem() است. این متد به طور پیش‌فرض، یک آیتم را با ترتیبLIFO  (آخرین ورودی/اولین خروجی) حذف کرده و بازمیگرداند. به عبارت دیگر، آیتم را از انتها یا سمت راست دیکشنری حذف می‌کند:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> numbers.popitem()
('three', 3)
>>> numbers.popitem()
('two', 2)
>>> numbers.popitem()
('one', 1)
>>> numbers.popitem()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    numbers.popitem()
KeyError: 'dictionary is empty'

 

در اینجا تمام آیتم‌های numbers را با استفاده از .popitem() حذف کرده‌ایم. هر فراخوانی متد یک آیتم را از دیکشنری حذف می‌کند. اگر .popitem() را برای یک دیکشنری خالی ایجاد کنید، خطای keyError دریافت می‌کنید. تا اینجا، .popitem() مانند دیکشنری‌های عادی رفتار می‌کند.  

 

اما در OrderedDict، .popitem() آرگومانی از نوع boolean به نام last را قبول می‌کند که به طور پیش‌فرض حاوی مقدار true است. اگر مقدار last را برابر false قرار دهید، .popitem() عمل حذف را با ترتیب FIFO (اولین ورودی/اولین خروجی) انجام خواهد داد، یعنی آیتم‌ها از ابتدای دیکشنری حذف خواهند شد.

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> numbers.popitem(last=False)
('one', 1)
>>> numbers.popitem(last=False)
('two', 2)
>>> numbers.popitem(last=False)
('three', 3)
>>> numbers.popitem(last=False)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    numbers.popitem(last=False)
KeyError: 'dictionary is empty'

 

در این مثال، خطای keyError خواهیم داشت، زیرا دیکشنری خالی است.

 

مقاله پیشنهادی: ویدیو آموزش ChainMap در پایتون

 

 +  تست برابری دیکشنری‌ها

زمانی که دو شیء OrderedDict را از لحاظ برابری طبق مفهوم Boolean مقایسه می‌کنید، ترتیب آیتم‌ها نقش مهمی ایفا می‌کنند. برای مثال، اگر دو شیء شما آیتم‌های یکسان داشته باشند، نتیجه تست به ترتیب آنها بستگی خواهد داشت.

>>> from collections import OrderedDict
>>> letters_0 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_1 = OrderedDict(b=2, a=1, c=3, d=4)
>>> letters_2 = OrderedDict(a=1, b=2, c=3, d=4)

>>> letters_0 == letters_1
False

>>> letters_0 == letters_2
True

 

در این نسبت، letters_1 به نسبت letters_0 و letters_2 ترتیب اندک متفاوتی دارد، بنابراین نتیجه تست غلط (false) خواهد بود. در تست دوم، letters_0 و letters_2  ترتیبی یکسان دارد، پس نتیجه تست درست (True) است.

 

اگر این مثال را با دیکشنری‌های معمولی تست کنید، نتیجه متفاوتی به دست خواهد آمد:

>>> letters_0 = dict(a=1, b=2, c=3, d=4)
>>> letters_1 = dict(b=2, a=1, c=3, d=4)
>>> letters_2 = dict(a=1, b=2, c=3, d=4)

>>> letters_0 == letters_1
True

>>> letters_0 == letters_2
True

>>> letters_0 == letters_1 == letters_2
True

 

در اینجا نتیجه تست درست (true) است، زیرا تمامی مجموعه آیتم‌ها یکسان هستند و ترتیب اهمیتی ندارد.  

 

در آخر، در تست برابری میان شیء OrderedDict و یک دیکشنری معمولی، ترتیبی لحاظ می‌شود.

>>> from collections import OrderedDict
>>> letters_0 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_1 = dict(b=2, a=1, c=3, d=4)

>>> letters_0 == letters_1
True

 

در این حالت اگر دو دیکشنری آیتم‌های یکسانی داشته باشند، بدون توجه به ترتیب آیتم‌ها، نتیجه تست درست (true) خواهد بود.

 

 

 +  اضافه کردن attributeهای جدید به OrderedDict

OrderedDict دارای صفت .__dict__ است. دیکشنری‌های عادی دارای این صفت نیستند. به مثال زیر توجه کنید:

>>> from collections import OrderedDict
>>> letters = OrderedDict(b=2, d=4, a=1, c=3)
>>> letters.__dict__
{}

>>> letters1 = dict(b=2, d=4, a=1, c=3)
>>> letters1.__dict__
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    letters1.__dict__
AttributeError: 'dict' object has no attribute '__dict__'

 

در مثال اول، به صفت .__dict__ متعلق به دیکشنری ترتیب‌دار letters دسترسی پیدا می‌کنید. پایتون از این صفت برای ذخیره نمونه صفات قابل بازنویسی استفاده می‌کند. مثال دوم نشان می‌دهد که دیکشنری‌های معمولی صفت .__dict__ را ندارند.

 

از صفت .__dict__ برای ذخیره‌ی پویای نمونه صفات قابل بازنویسی استفاده می‌شود. روش‌های زیادی برای این کار وجود دارد. برای مثال می‌توانید از روش دیکشنری‌مانند استفاده کنید، مانند ordered_dict.__dict__["attr"] = value. می‌توانید از نشانه‌گذاری نقطه‌ای نیز استفاده کنید، مانند ordered_dict.attr = value.  

 

مثالی از استفاده از .__dict__ برای اضافه کردن یک تابع جدید به یک دیکشنری ترتیب‌دار مشاهده می‌کنید:

>>> from collections import OrderedDict
>>> letters = OrderedDict(b=2, d=4, a=1, c=3)

>>> letters.sorted_keys = lambda: sorted(letters.keys())
>>> vars(letters)
{'sorted_keys': <function <lambda> at 0x7fa1e2fe9160>}

>>> letters.sorted_keys()
['a', 'b', 'c', 'd']

>>> letters["e"] = 5
>>> letters.sorted_keys()
['a', 'b', 'c', 'd', 'e']

 

اکنون تابع لامبدای .sorted_keys() به دیکشنری ترتیب‌دار letters اضافه شده است. توجه داشته باشید که با دسترسی مستقیم از طریق نشانه‌گذاری نقطه‌ای یا استفاده از vars() می‌توانید مقدار .__dict__ را مشاهده کنید.

 

نکته: این نوع خاص از صفت تنها به نمونه موردنظر از کلاس اضافه می‌شود.  در مثال بالا، این نمونه letters است. این عملیات کلاس را تغییر نمی‌دهد، در نتیجه شما فقط از طریق letters، به .sorted_keys() دسترسی خواهید داشت.

 

می‌توانید از این تابع اضافه شده برای پیمایش کلیدهای دیکشنری به صورت مرتب شده، بدون به هم ریختن ترتیب اصلی آیتم‌ها استفاده کنید.

>>> for key in letters.sorted_keys():
...     print(key, "->", letters[key])
...
a -> 1
b -> 2
c -> 3
d -> 4
e -> 5

>>> letters
OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3), ('e', 5)])

 

اگر سعی کنید به دیکشنری‌های معمولی، یک نمونه صفت شخصی را به طور پویا اضافه کنید، با خطای AttributeError مواجه خواهید شد که به شما خواهد گفت دیکشنری دارای این صفت نیست. این به این دلیل است که دیکشنری‌های معمولی دارای صفت .__dict__ نیستند که بتوانند نمونه صفات جدید را در خود نگهداری کنند.

 

ویدیو پیشنهادی: ویدیو آموزش deque در پایتون

 

 #  ادغام و بروزرسانی دیکشنری‌ها با عملگرها

در پایتون 3.9، دو  عملگر جدید به دیکشنری اضافه شده است؛ اکنون شما عملگرهای ادغام (|) و به‌روزرسانی (=|) را دارید. این عملگرها، با نمونه‌های شیء OrderedDict نیز کار می‌کنند:

Python 3.9.0 (default, Oct  5 2020, 17:52:02)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import OrderedDict

>>> physicists = OrderedDict(newton="1642-1726", einstein="1879-1955")
>>> biologists = OrderedDict(darwin="1809-1882", mendel="1822-1884")

>>> scientists = physicists | biologists
>>> scientists
OrderedDict([
   ('newton', '1642-1726'),
   ('einstein', '1879-1955'),
   ('darwin', '1809-1882'),
   ('mendel', '1822-1884')
])

 

همانگونه که از نام آن مشخص است، عملگر ادغام دو دیکشنری را به یک دیکشنری تبدیل می‌کند که شامل آیتم‌های هر دو دیکشنری اولیه است. اگر دو دیکشنری کلیدهای یکسان داشته باشند، مقدار کلید در سمت راست ترین دیکشنری انتخاب می‌شود.  

 

عملگر به‌روزرسانی در مواقعی کارایی دارد که می‌خواهیم مقادیر یک دیکشنری را بدون نیاز به فراخوانی .update() به‌روزرسانی کنیم.

>>> physicists = OrderedDict(newton="1642-1726", einstein="1879-1955")

>>> physicists_1 = OrderedDict(newton="1642-1726/1727", hawking="1942-2018")
>>> physicists |= physicists_1
>>> physicists
OrderedDict([
    ('newton', '1642-1726/1727'),
    ('einstein', '1879-1955'),
    ('hawking', '1942-2018')
])

 

در این مثال، از عملگر به‌روزرسانی برای به‌روزرسانی اطلاعات طول عمر نیوتون استفاده می‌کنیم. اگر دیکشنری که مقادیر به‌روزرسانی شده را ارائه می‌دهد، کلیدهای جدیدی داشته باشد، این کلیدها به انتهای دیکشنری اولیه اضافه خواهند شد.

 

 

 #  لحاظ کردن عملکرد(performace)

عملکرد، نقش بسیار مهمی در برنامه‌نویسی دارد. دانستن سرعت یک الگوریتم یا میزان مصرف حافظه آن، مسائلی رایج است. OrderedDict در ابتدا با پایتون نوشته شد و سپس برای کارایی حداکثری متدها و عملگرهای آن، با زبان C بازنویسی شد. هر دو این پیاده‌سازی‌ها، اکنون در کتابخانه استاندارد موجودند. با این حال، نسخه نوشته‌شده با پایتون می‌تواند به عنوان جایگزینی برای نسخه C در صورت عدم وجود آن، به کار رود.  

 

در پیاده‌سازی هر دو نسخه، از یک صف پیوندی دوگانه (double linked list) برای ذخیره ترتیب آیتم‌ها استفاده شده است. با وجود زمان اجرای خطی عملگرها، صف پیوندی پیاده شده در OrderedDict برای اجرای هر چه سریع تر متدها، بهینه‌سازی شده است. بر این اساس، زمان اجرای عملیات‌ها در یک دیکشنری ترتیب‌دار O(1) است، اما نسبت به دیکشنری‌های عادی، معیار ثابت بزرگ‌تری دارد.

 

به طور کلی، OrderedDict به نسبت دیکشنری‌های معمولی عملکرد کندتری دارد. این مثالیست که زمان اجرای عملیات‌هایی را در هر دو نوع دیکشنری محاسبه می‌کند:

# time_testing.py

from collections import OrderedDict
from time import perf_counter

def average_time(dictionary):
    time_measurements = []
    for _ in range(1_000_000):
        start = perf_counter()
        dictionary["key"] = "value"
        "key" in dictionary
        "missing_key" in dictionary
        dictionary["key"]
        del dictionary["key"]
        end = perf_counter()
        time_measurements.append(end - start)
    return sum(time_measurements) / len(time_measurements) * int(1e9)

ordereddict_time = average_time(OrderedDict.fromkeys(range(1000)))
dict_time = average_time(dict.fromkeys(range(1000)))
gain = ordereddict_time / dict_time

print(f"OrderedDict: {ordereddict_time:.2f} ns")
print(f"dict:        {dict_time:.2f} ns ({gain:.2f}x faster)")

 

در اینجا، زمان متوسط (average_time()) برای اجرای چندین عملیات روی یک دیکشنری را محاسبه می‌کنید. حلقه for از time.pref_counter() برای محاسبه زمان عملیات‌ها استفاده می‌کند و زمان میانگین برای اجرای دسته عملیات‌های موردنظر را به نانوثانیه برمی‌گرداند.

 

اگر این کد را در command line اجرا کنید، نتیجه زیر را مشاهده خواهید کرد:

$ python time_testing.py
OrderedDict: 272.93 ns
dict:        197.88 ns (1.38x faster)

 

همانطور که مشاهده می‌کنید، سرعت عملیات‌های dict بیشتر از OrderedDict است.  

 

از نظر مصرف حافظه، اشیاء OrderedDict به علت دیکشنری‌های ترتیب‌دارش، ناچار به استفاده بیشتری از حافظه است. در مثال زیر این امر را مشاهده می‌کنید:

>>> import sys
>>> from collections import OrderedDict

>>> ordereddict_memory = sys.getsizeof(OrderedDict.fromkeys(range(1000)))
>>> dict_memory = sys.getsizeof(dict.fromkeys(range(1000)))
>>> gain = 100 - dict_memory / ordereddict_memory * 100

>>> print(f"OrderedDict: {ordereddict_memory} bytes")
OrderedDict: 85408 bytes

>>> print(f"dict:        {dict_memory} bytes ({gain:.2f}% lower)")
dict:        36960 bytes (56.73% lower)

 

در این مثال، از sys.getsizeof() برای محاسبه میزان مصرف حافظه دو شیء دیکشنری به بایت استفاده شده است. در خروجی، می‌توان مشاهده کرد که دیکشنری معمولی، حافظه کم‌تری اشغال می‌کند.

 

ویدیو پیشنهادی: ویدیو شمارش اتفاقات در پایتون

 

 #  انتخاب دیکشنری مناسب

تا اینجا، تفاوت‌های اندک میان dict و OrderedDict را درک کرده‌ایم و یاد گرفتیم که با وجود اینکه از نسخه 3.6 پایتون به بعد، دیکشنری‌های عادی نیز دارای ساختارهای داده ترتیب‌دار شده‌اند، استفاده از OrderedDict به دلیل داشتن برخی ویژگی‌های خاص که در dict موجود نیستند، ارزشمند است.

 

در اینجا، خلاصه‌ای از تفاوت‌ها و ویژگی‌های هر دو کلاس می‌بینیم که در حین انتخاب میان این دو، باید در نظر بگیرید:

Feature OrderedDict dict
Key insertion order maintained Yes (since Python 3.1) Yes (since Python 3.6)
Readability and intent signaling regarding the order of items High Low
Control over the order of items High (.move_to_end(), enhanced .popitem()) Low (removing and reinserting items is required)
Operations performance Low High
Memory consumption High Low
Equality tests consider the order of items Yes No
Support for reverse iteration Yes (since Python 3.5) Yes (since Python 3.8)
Ability to append new instance attributes Yes (.__dict__ attribute) No
Support for the merge (|) and update (|=) dictionary operators Yes (since Python 3.9) Yes (since Python 3.9)

 

به طور کلی، اگر ترتیب آیتم‌های دیکشنری برای شما یا کارکرد برنامه اهمیت دارد، بهتر است به OrderedDict فکر کنید.

 

ویدیو پیشنهادی: ویدیو آموزش ماژول queue در پایتون

 

 #  ساخت صف مبتنی بر دیکشنری

یکی از مواردی که بهتر است به جای dict از OrderedDict استفاده کنید، پیاده‌سازی صف مبتنی بر دیکشنریست. صف ساختار داده‌ای رایج و کاربردیست که آیتم‌ها را به ترتیب FIFO (اولین ورود/اولین خروج) نگه‌داری می‌کند. این بدین معناست که با ورود آیتم جدید به صف، قدیمی‌ترین آیتم آن خارج می‌شود.

 

به طور معمول، صف از عملیاتی به نام enqueue برای اضافه کردن آیتم استفاده می‌کند. عملیات dequeue نیز یک آیتم را از صف حذف می‌کند.  

برای ساخت یک صف مبتنی بر دیکشنری، ادیتور خود را باز کنید، یک ماژول پایتون جدید به نام queue.py بسازید و کد زیر را به آن اضافه کنید:

# queue.py

from collections import OrderedDict

class Queue:
    def __init__(self, initial_data=None, /, **kwargs):
        self.data = OrderedDict()
        if initial_data is not None:
            self.data.update(initial_data)
        if kwargs:
            self.data.update(kwargs)

    def enqueue(self, item):
        key, value = item
        if key in self.data:
            self.data.move_to_end(key)
        self.data[key] = value

    def dequeue(self):
        try:
            return self.data.popitem(last=False)
        except KeyError:
            print("Empty queue")

    def __len__(self):
        return len(self.data)

    def __repr__(self):
        return f"Queue({self.data.items()})"

 

در Queue ابتدا یک صفت نمونه به نام .data. میسازیم که حاوی دیکشنری ترتیب‌دار خالیست که برای ذخیره داده‌ها استفاده می‌کنیم. این کلاس یک آرگومان اولیه اختیاری به نام initial_data را قبول می‌کند، که می‌تواند داده اولیه برای ایجاد نمونه در اختیار ما بگذارد. به علاوه آرگومان کلیدی (kwargs) نیز قبول می‌کند که به ما اجازه می‌دهد در constructor از آرگومان‌های کلیدی استفاده کنیم.

 

سپس متد .enqueue() را استفاده می‌کنیم که به ما اجازه می‌دهد جفت‌های کلید-مقدار را به صف اضافه کنیم. در این حالت در صورتی که کلید از قبل وجود داشته باشد، از .move_to_end() و در غیر این صورت، از مکانیزم‌های تخصیص رایج استفاده می‌کنیم. برای کار کردن این آیتم، باید به آن داده دوتایی از نوع tuple یا list حاوی جفت کلید-مقدار معتبر داده شود.

 

متد .dequeue() از .popitem() با مقدار last = false برای حذف و بازگرداندن آیتم‌ها استفاده می‌کند. در اینجا، از بلوک try … except برای مدیریت خطای keyError در صورت وجود دیکشنری خالی استفاده می‌کنیم.

 

متد خاص .__len__() طول دیکشنری داخلی، یعنی .data. را برمی‌گرداند. متد خاص .__repr__() نیز در صورت پرینت صف روی صفحه نمایش، آن را به صورت رشته‌های متنی مناسب برای کاربر، نمایش می‌دهد.  

 

در اینجا مثال‌هایی از کاربرد Queue می‎‌بینیم:

>>> from queue import Queue

>>> # Create an empty queue
>>> empty_queue = Queue()
>>> empty_queue
Queue(odict_items([]))

>>> # Create a queue with initial data
>>> numbers_queue = Queue([("one", 1), ("two", 2)])
>>> numbers_queue
Queue(odict_items([('one', 1), ('two', 2)]))

>>> # Create a queue with keyword arguments
>>> letters_queue = Queue(a=1, b=2, c=3)
>>> letters_queue
Queue(odict_items([('a', 1), ('b', 2), ('c', 3)]))

>>> # Add items
>>> numbers_queue.enqueue(("three", 3))
>>> numbers_queue
Queue(odict_items([('one', 1), ('two', 2), ('three', 3)]))

>>> # Remove items
>>> numbers_queue.dequeue()
('one', 1)
>>> numbers_queue.dequeue()
('two', 2)
>>> numbers_queue.dequeue()
('three', 3)
>>> numbers_queue.dequeue()
Empty queue

 

در این مثال، ابتدا سه شیء Queue با روش‌های مختلف ایجاد می‌شوند. سپس از .enqueue() برای اضافه کردن آیتم به انتهای numbers_queue استفاده می‌شود. سپس، متد .dequeue() چندین بار برای حذف آیتم‌های numbers_queue فراخوانی می‌شود. در نظر داشته باشید که فراخوانی آخر .dequeue() سبب نمایش پیامی روی صفحه می‌شود که به شما اطلاع می‌دهد که صف خالیست.

 

ویدیو پیشنهادی: ویدیو آموزش ماژول enum در پایتون

 

 #  نتیجه گیری

برای سال‌ها، دیکشنری‌های پایتون ساختارهای داده‌ای بدون ترتیب بودند. این سبب بروز نیاز به دیکشنری‌هایی شد که در مواقعی که ترتیب آیتم‌ها مهم بودند، کارایی داشته باشند. در نتیجه، توسعه‌دهندگان پایتون OrderedDict را توسعه دادند، که مخصوصا برای با ترتیب نگه‌داشتن آیتم‌ها طراحی شده بود.

 

پایتون 3.6 ویژگی جدیدی برای دیکشنری‌های عادی معرفی کرد. اکنون، آنها نیز ترتیب آیتم‌ها را به خاطر می‌سپردند. در نتیجه، اکثر توسعه‌دهندگان پایتون شک داشتند که آیا OrderedDict همچنان موردنیاز باشد یا خیر.

 

اکنون اگر برنامه شما نیاز به دیکشنری ترتیب‌دار داشته باشد، می‌توانید میان dict و OrderedDict، تصمیم آگاهانه‌تری بگیرید.

 

در این درسنامه، یاد گرفتید که چگونه با استفاده از OrderedDict، یک صف مبتنی بر دیکشنری ایجاد کنید، که نشان می‌دهد OrderedDict همچنان در ماجراجویی‌های روزانه ما در برنامه‌نویسی با پایتون، باارزش است.

مطالب مشابه



مونگارد