تقييم الموضوع :
  • 0 أصوات - بمعدل 0
  • 1
  • 2
  • 3
  • 4
  • 5
المكررات iterator في بيثون
#1
المكررات

سنتحدث في هذا الموضوع عن مفهومين أساسيين في بيثون : مفهوم المكررات (iterator) ومفهوم الكائنات التكرارية (iterable) واللذان يتوجب فهمها مبكرا نظرا للانتشار الواسع لاستخدامهما ولضرورتهما في البرمجة، فالمكررات تمكننا من استعراض عناصر كائن تكراري بطريقة سهلة وبديهية.

##ا iterable - iterator - iteration

رأينا عدة مرات حلقة التكرار باستخدام for ... in.. التي تتيح تصفح/استعراض عدة أنواع من الكائنات ومن بينها المتتاليات. رأينا أن حلقات التكرار for تتيح كتابة كود قصير وسهل. تنبني حلقات التكرار باستخدام for على مفهوم هام يسمى المكرِّر (iterator)

### المكرِّر iterator

هو عبارة عن مؤشر ننتقل بواسطته من عنصر الى آخر داخل متتالية (list - string - tuple -....) دون الانشغال بتركيبة المتتالية. بالاضافة الى ذلك تتيح المكرِّرات فصل الكائن الذي يكرّر (iterator) عن الكائن الذي يحتوي على البيانات (iterable)، هذا الى جانب خفته في الذاكرة.

### التكراري iterable

هو الكائن الذي تقع عليه عملية التكرار، وهو كائن يمكن التنقل داخله بواسطة مكرِّر عبر for ... in .... مثلا

### التكرار iteration

هي التعليمة المستخدمة وهي مثلا for ... in... او list comprehension

في بيثون جميع الكائنات الحاوية (container objects) المدمجة في اللغة هي كائنات تكرارية (القوائم والقواميس والمجموعات والسلاسل النصية والصفوف والملفات..). لنر بعض الامثلة البسيطة:

كود :
s = {'spam', 'eggs', 'beans', 1, 2, 3}  # المجموعة هي كائن تكراري
for i in s:                             # i نستخرج كل عنصر من المجموعة بواسطة المكرّر
   print(i, end=' ')                   # 1 2 3 spam beans eggs
### تذكير جانبي: في المجموعات والقواميس ليس هناك ترتيب مسبق للعناصر. الترتيب دائما عشوائي حتى لو بدا أنه منطقي.

مثال آخر list comprehension على كائن تكراري (هنا هو المجموعة السابقة)

كود :
[x for x in s if type(x) is int]        # [1, 2, 3]
لنحاول أن نفهم كيف تفعل الحلقة التكرارية for لتستعرض هذا الكائن (هذه المجموعة):

- ستبدأ الحلقة التكرارية باستخراج مكرِّر للمجموعة... سنحاكي ذلك عبر الدالة iter التي تتيح انشاء مكرِّر على الكائن الوسيط

كود :
s = {'spam', 'eggs', 'beans', 1, 2, 3}
i = iter(s)
type(i)                                 # <class 'set_iterator'>
ماذا استطيع أن افعل بواسطة هذا المكرِّر؟ الحلقة for ، وبعد استدعاء iter ، ستستدعي وظيفة next وهكذا ستقوم الحلقة التكرارية بتصفح/استعراض كل عنصر من المجموعة:

كود :
next(i)                                 # 1
next(i)                                 # 2
next(i)                                 # 3
next(i)                                 # 'spam'
next(i)                                 # 'eggs'
next(i)                                 # 'beans'
next(i)                                 # Exception: StopIteration !
next(i)                                 # Exception: StopIteration !
next(i)                                 # Exception: StopIteration !
عندما لا يتبقى أي عنصر لتصفحه/استعراضه، يطلق بيثون استثناء من نوع StopIteration وسيستمر الاستثناء هكذا بعد ذلك، مما يوضح جيدا أننا لا نستطيع استخدام مكرِّر الا مرة واحدة (لاعادة استخدامه يجب اعادة انشائه). لطمأنة الجميع منذ الآن، عمليا في بيثون ليس علينا انشاء ومعالجة المكررات يدويا كما في المحاكاة السابقة. تتولى آليات التكرار مثل الحلقة for او الـlist comprehension بنفسها انشاء ومعالجة المكرّرات التي تحتاجها. يتبقى أن من المهم فهم آلية عمل المكررات، مما يتيح لنا بناء كائناتنا التكرارية او مكرراتنا الخاصة.

## شرح متقدم

يوجد إذا، كما فهمنا مما سبق، الكائنات التكرارية (iterable) والمكررات (iterator) وهما نوعان من الكائنات مختلفان في مفهومهما:

- الكائن التكراري (iterable) هو كائن يحتوي داخله على الوظيفةالخاصة __iter__ ترد عند استدعائها كائنا جديدا من نوع مكرِّر (iterator). يمكن استدعاء هذه الوظيفة مباشرة على الكائن او عبر الدالة iter التي استخدمناها أعلاه:

كود :
obj.__iter()__
iter(obj)
- المكرِّر (iterator) كائن خفيف يحتوي داخله على وظيفة __iter__ ترد المكرِّر نفسه، ووظيفة __next__ والتي ترد عند كل استدعاء لها عنصرا جديدا ثم ترد آخر الامر استثناء StopIteration لما لا يتبقى أي عنصر لاستعراضه/تفحصه، حيث لا يمكن استخدام مكرِّر إلاّ مرة واحدة. يمكن استدعاء الوظيفة __next__ مباشرة أو عبر الدالة next كما راينا أعلاه:

كود :
it.__next__()
next(it)
قد نتساءل لماذا يوجد وظيفة __iter__ في المكرِّر والتي ترد المكرِّر نفسه؟ السبب بسيط: الكائن التكراري (iterable) هو تكراري لأنه يحتوي على وظيفة __iter__ ترد مكرِّرا والمكرِّر (iterator) هو في نفس الوقت كائن تكراري (iterable) لأنه يحتوي على وظيفة __iter__ ترد مكرِّرا حتى لو كان هو نفسه. وبالتالي نستطيع استخدام مختلف آليات التكرار (for - list comprehension...) لتفحص/استعراض المكرِّر بالضبط كما نفعل بالنسبة للكائن التكراري.

يتبادر الى الذهن سؤال ثان: لماذا إذا لدينا مفهومان اثنان: المكرِّر والتكراري بما أننا نستطيع تفحصهما/استعراضهما بنفس الآليات؟ في الواقع (ونعيد التذكير بذلك) المكرِّر والتكراري هما كائنان مختلفان: التكراري هو الكائن الذي يحتوي على البيانات، أما المكرِّر فهو الكائن الذي بواسطته نفحص/نستعرض بيانات التكراري عنصرا بعد الآخر، وهو يتميز بخفته في الذاكرة.



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

## الدالة zip

الدالة zip تقوم بدمج كائنين تكراريين أو أكثر لتنتج مكرِّر صفوف لا يمكن تفحصه/استعراضه الا مرة واحدة. تأخذ الدالة zip اول عنصر من كل كائن تكراري مررناه اليها وتكون صفا، وهكذا مع العنصر الثاني والثالث، الخ.. عندما نمرر للدالة zip كائنات تكرارية باطوال مختلفة، ستكون النتيجة مبتورة وستعتمد الدالة أقصر طول لانهاء التكرار. نلاحظ قلة أهمية انشاء كائن وقتي في الذاكرة يحتوي على جميع الصفوف والتي قد يكون تأثيرها على الذاكرة كبير جدا، لذلك فالكائن الذي تنتجه الدالة zip هو في الواقع مكرِّر نفسه، وبالتالي لا يمكننا تفحصه/استعراضه سوى مرة واحدة:

كود :
a = [1, 2]
b = [3, 4]
z = zip(a, b)
type(z)             # <class 'zip'>
z is iter(z)        # True
[i for i in z]      # [(1, 3), (2, 4)]
[i for i in z]      # []
next(z)             # Exception: StopIteration
الميزة الأساسية للمكررات هي خفتها، بحيث يمكننا اعادة انشائها قدر حاجتنا اليها دون تاثير يذكر على الذاكرة:

كود :
z = zip(a, b)
[i for i in z]      # [(1, 3), (2, 4)]
## الوحدة itertools

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

- مكررات لامتناهية مثل cycle؛

- مكررات صالحة للتراكيب الرياضية مثل التبادل والتوليفات والجداء الديكارتي، الخ...

- مكررات لها خاصيات مشابهة لما رأيناه أعلاه في هذا الموضوع.

بالنسبة الى النقطة الاخيرة نجد chain التي تتيح تجميع (concatenate) عدة كائنات تكرارية في شكل مكرِّر:

كود :
import itertools
for x in itertools.chain((1, 2), [3, 4]):
   print(x, end=' ')                       # 1 2 3 4
ونجد islice التي تمنحنا مكرِّرا على مقطع (slice) من كائن تكراري. يمكننا اعتباره نوعا من تعميم range على اي نوع من الكائنات التكرارية:

كود :
# range
for x in range(3, 8):
   print(x, end=' ')                       # 3 4 5 6 7

# islice
import itertools
import string
support = string.ascii_lowercase
print(support)                              # abcdefghijklmnopqrstuvwxyz
for x in itertools.islice(support, 3, 8):
   print(x, end=' ')                       # d e f g h
للمزيد حول الوحدة itertools راجع الدليل: https://docs.python.org/3/library/itertools.html

## المولد generator

تمكننا المولدات من انشاء مكررات بكل يسر كما نفعل مع list comprehension لكن باستخدام القوسين ()، على خلاف الـlist comprehension التي تنشئ قائمة وقتية وبالتالي استهلاك للذاكرة، لا تستهلك المولدات شيئا يذكر فالمكررات خفيفة وسريعة وعيبها بالطبع هو عدم امكانية استخدامها ثانية بعد الانتهاء من تصفحها/استعراضها الا باعادة انشائها من جديد، لكن هذا لا يشكل مشكلة كبيرة باعتبار خفتها وسرعتها.

كود :
generator = (x*x for x in range(3))
for i in generator :
   print(i)        # 0 1 4
المولد هو اذا عبارة عن مكرِّر يتوجب اعادة انشائه عند الرغبة في اعادة استخدامه. يتميز بتوليد القيم المكرَّرة 'على الطاير' أي لا يحفظ القيم في الذاكرة العشوائية وهو بالتالي مفيد جدا من ناحية استهلاك الذاكرة.

يمكن استخدام عدة مولدات بشكل تسلسلي :

كود :
generator = (x*x for x in range(1000))
palindrome = (x for x in generator if str(x) == str(x)[::-1])
list(palindrome)
## الكلمة المفتاحية yield

في بيثون كل شيء هو عبارة عن كائن، والتوجه العام هو عدم انشاء كائنات الا حين الحاجة الحقيقية اليها والهدف بالطبع هو الحصول على كود خفيف وواضح، لذلك فالافضل دائما معالجة المكرِّرات عوض الكائنات التكرارية. غير أن المولدات لها حدودها ايضا، فالى جانب ضرورة اعادة انشائها في كل مرة، فهي لا تقبل سوى تعبير واحد (مثل الـlist comprehension) لذلك تم تعميم المولدات الى دوال توليد المولدات والتي تتيح كتابة كود فيه اكثر من تعبير وحيد وكل ما تتيحه الدوال من مرونة. في هذه الدوال نستخدم الكلمة المفتاحية yield عوض return لكي ترد لنا مولدا

كود :
def createGenerator(n) :
   for i in range(n):
       yield i*i
def palindrome(generator):
   for x in generator:
       if str(x) == str(x)[::-1]:
           yield x
كود :
g = createGenerator(100) # انشاء مولد
print(g) # المولد كائن
# < generator object createGenerator at 0x2b484b9addc0>
p = palindrome(g)
print(p)
# <generator object palindrome at 0x7fd1442e7510>
for x in p:
   print(x, end=' ')        # 0 1 4 9 121 484 676
هي كلمة مفتاحية يتم استخدامها في الدوال كما تستخدم return غير أن الدالة سترد كائن من نوع مولد generator. أهميتها تتمثل في أنها لا تستهلك ذاكرة تذكر وهي بالتالي سريعة جدا. نستخدمها عادة عندما نعالج كمية كبيرة جدا من البيانات لكننا لا نرغب سوى في استخراج او حفظ كمية محدودة منها، فالكود الذي كتبناه في الدالة لن يتم تنفيذه لان الدالة سترد لنا المولد فقط ، ثم نستخدم المولد عبر for مثلا وعندها فقط يتم تشغيل كود الدالة حتى يصل الى الكلمة yield ثم يرد اول قيمة للحلقة، وهكذا عند كل استدعاء للدالة الى أن لا يتبقى اي قيمة لردها.

لن يحدث اي شيء طالما لم نستدع المولد، ثم مباشرة بعد انطلاق حلقة التكرار على المولد، يتم تنفيذ كود الدالة. عند اول تنفيذ للكود سينطلق من البداية حتى الوصول الى اول yield ثم يرد القيمة الاولى، عند التكرار الثاني، سينطلق تنفيذ الكود من حيث توقف في المرة السابقة الى ان يجد yield ثانية، في حالتنا سيدور في حلقة. وهكذا الى ان لا يصل الى تعليمة yield اخرى اي لم يعد هناك قيمة لردها. سيعتبر المولد فارغا بشكل نهائي. لا يمكن اعادة استخدام المولد عندما يفرغ ويتوجب انشاء مولد آخر اذا استدعت الحاجة الى ذلك.

## مثال عملي:

مطلوب منا البحث عن جميع الكلمات التي يزيد طولها عن 5 حروف من نص قد يكون كبيرا جدا (جميع ما كتب في منصة تواصل اجتماعي طيلة يوم كامل على سبيل المثال...) ثم مقارنتها بكلمة معينة (xyz) لتكوين قائمة نهائية بالكلمات التي يتوفر فيها الشرطان..

### محاولتنا الاولى:

كود :
def get_words_by_size(text):
   words = []
   for word in text.split():
       if len(word) > 5:
           words.append(word)
   return words

def filter_words(text, mfilter):
   words = get_words_by_size(text)
   fwords = []
   for word in words:
       if mfilter in word:
           fwords.append(word)
   return fwords
وللتأكد جربنا السكربت على سلسلة نصية:

كود :
big_data = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse molestie, enim in facilisis molestie, tellus elit iaculis nibh, eget dictum sem neque vitae dui. Vivamus vulputate pulvinar leo, vitae dignissim sapien posuere vitae. Mauris porta interdum ligula, eget bibendum ante dignissim ut. In hac habitasse platea dictumst. Mauris ex nibh, auctor eget nulla sed, molestie elementum nunc. Vestibulum luctus tincidunt massa at vestibulum. Nullam a neque libero. Quisque et neque quis nisl placerat interdum at nec justo. Sed malesuada urna ac est lobortis, non vulputate erat sagittis. Duis vel varius ex, sit amet aliquet enim. Nunc semper mollis dictum. Suspendisse metus erat, dignissim quis tempus tempor, posuere sit amet felis.
Fusce consectetur mi ipsum, eget porttitor dui ullamcorper volutpat. Sed vel mollis libero. Phasellus faucibus metus id lobortis porttitor. In et blandit tellus. Aliquam sapien massa, dictum nec felis vel, congue blandit purus. Curabitur lectus ligula, pellentesque sed euismod nec, suscipit a arcu. Aliquam tincidunt dictum elit, quis mattis lectus lobortis at. Proin aliquam lectus at nibh cursus placerat. Integer tortor erat, hendrerit eu sodales sit amet, pharetra ac libero. Suspendisse sit amet tortor ut eros posuere suscipit. Nulla luctus consectetur lacus, eu dictum est lobortis vel. Vivamus pulvinar semper.
"""
mwords = filter_words(big_data, 'in')
[w for w in mwords]
ادى السكربت العمل المطلوب منه، لكن لو تأملناه جيدا سنكتشف أننا أنشأنا عدة قوائم وقتية تسببت بالتأكيد في استهلاك قدر من الذاكرة دون اهمية تذكر. تصور لو ان لدينا كمية من النص اكبر بكثير مما استخدمناه في تجربتنا.. ستختنق ذاكرة الحاسوب وسيتوقف في النهاية عن الاستجابة.

### المحاولة الثانية:

لجعل الكود أخف على الذاكرة سنطلب من كل دالة أن ترد مولدا عوضا عن القوائم

كود :
def get_words_by_size(text):
   for word in text.split():
       if len(word) > 5:
           yield word

def filter_words(text, mfilter):
   words = get_words_by_size(text)
   for word in words:
       if mfilter in word:
           yield word
نجرب بالسلسلة النصية السابقة (انسخها من جديد). لاحظ كيف أن المستخدم النهائي سيستدعي الدوال بنفس الطريقة، فهي تبدو له عادية، غير أنها لن ترد له قائمة بل مولدا وهذا بالطبع سيكون له أهميته من ناحية التخفيف من استخدام الذاكرة:

كود :
mwords = filter_words(big_data, 'in')
[w for w in mwords]
### المحاولة الثالثة:

سنعدل السكربت ليعمل على الملفات النصية عوض السلاسل. سنفترض أن جميع الملفات النصية محفوظة في مجلد واحد. لن نحتاج الى الدالتين السابقتين:

كود :
import os

def get_words(folder, mfilter):
   for mfile in os.listdir(folder):
       with open(os.path.join(folder, mfile)) as f:
           for mline in f:
               for word in mline.split():
                   if len(word) > 5 and mfilter in word:
                       yield word
كيف نجرب؟

هنا سنحتاج الى تحديد مجلد ونضع فيه بعض الملفات النصية.

هذا الكود ميسط للغاية لكنه يؤدي المطلوب اذا لم تعترضه استثناءات (تتعلق خاصة بالملفات ومسار المجلد). يمكن تجربته على عدد غير محدد من الملفات النصية بعد تجميعها في مجلد. الناحية الاهم هو اخفاء تعقيد السكربت على المستخدم، فهو مطالب فقط بتحديد مسار المجلد وكتابة تعبير شبيه بما يلي (يمكن استخدام set comprehension لتوليد مجموعة ولتجنب الكلمات المكررة، لكن لاحظ ضرورة اعادة انشاء المولد لكي نستخدمه في تكوين مجموعة عوض قائمة):

كود :
folder = "path/to/the/folder"
mwords = get_words(folder, 'in')
[w for w in mwords]
mwords = get_words(folder, 'in')
{w for w in mwords}
بالنسبة للمستخدم، يبدو له كل شيء بسيط وعادي، فهو يستطيع استخدام جميع الادوات التي يستخدمها على الكائنات التكرارية وخاصة الحلقة for والـlist comprehension

## للمزيد

هذا رابط لتدوينة عنوانهاFrom List Comprehensions to Generator Expressions كتبها مطور لغة بيثون Guido van Rossum على مدونته في بلوغر:

https://python-history.blogspot.com/2010...rator.html
الرد


التنقل السريع :


مستخدمين يتصفحوا هذا الموضوع: 1 ضيف