06-06-2018, 07:45 PM
المكررات
سنتحدث في هذا الموضوع عن مفهومين أساسيين في بيثون : مفهوم المكررات (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]
- ستبدأ الحلقة التكرارية باستخراج مكرِّر للمجموعة... سنحاكي ذلك عبر الدالة iter التي تتيح انشاء مكرِّر على الكائن الوسيط
كود :
s = {'spam', 'eggs', 'beans', 1, 2, 3}
i = iter(s)
type(i) # <class 'set_iterator'>
كود :
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 !
## شرح متقدم
يوجد إذا، كما فهمنا مما سبق، الكائنات التكرارية (iterable) والمكررات (iterator) وهما نوعان من الكائنات مختلفان في مفهومهما:
- الكائن التكراري (iterable) هو كائن يحتوي داخله على الوظيفةالخاصة __iter__ ترد عند استدعائها كائنا جديدا من نوع مكرِّر (iterator). يمكن استدعاء هذه الوظيفة مباشرة على الكائن او عبر الدالة iter التي استخدمناها أعلاه:
كود :
obj.__iter()__
iter(obj)
كود :
it.__next__()
next(it)
يتبادر الى الذهن سؤال ثان: لماذا إذا لدينا مفهومان اثنان: المكرِّر والتكراري بما أننا نستطيع تفحصهما/استعراضهما بنفس الآليات؟ في الواقع (ونعيد التذكير بذلك) المكرِّر والتكراري هما كائنان مختلفان: التكراري هو الكائن الذي يحتوي على البيانات، أما المكرِّر فهو الكائن الذي بواسطته نفحص/نستعرض بيانات التكراري عنصرا بعد الآخر، وهو يتميز بخفته في الذاكرة.
عندما نعالج بيانات تكرارية مثل القوائم او القواميس، يكون لدينا كائن تكراري (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)]
توفر هذه الوحدة مجموعة من الادوات في شكل مكررات قد تكون مفيدة خاصة عند البحث عن الخفة والسرعة. خدماتها تنقسم الى ثلاثة اصناف:
- مكررات لامتناهية مثل cycle؛
- مكررات صالحة للتراكيب الرياضية مثل التبادل والتوليفات والجداء الديكارتي، الخ...
- مكررات لها خاصيات مشابهة لما رأيناه أعلاه في هذا الموضوع.
بالنسبة الى النقطة الاخيرة نجد chain التي تتيح تجميع (concatenate) عدة كائنات تكرارية في شكل مكرِّر:
كود :
import itertools
for x in itertools.chain((1, 2), [3, 4]):
print(x, end=' ') # 1 2 3 4
كود :
# 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
## المولد 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)
في بيثون كل شيء هو عبارة عن كائن، والتوجه العام هو عدم انشاء كائنات الا حين الحاجة الحقيقية اليها والهدف بالطبع هو الحصول على كود خفيف وواضح، لذلك فالافضل دائما معالجة المكرِّرات عوض الكائنات التكرارية. غير أن المولدات لها حدودها ايضا، فالى جانب ضرورة اعادة انشائها في كل مرة، فهي لا تقبل سوى تعبير واحد (مثل الـ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
لن يحدث اي شيء طالما لم نستدع المولد، ثم مباشرة بعد انطلاق حلقة التكرار على المولد، يتم تنفيذ كود الدالة. عند اول تنفيذ للكود سينطلق من البداية حتى الوصول الى اول 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}
## للمزيد
هذا رابط لتدوينة عنوانهاFrom List Comprehensions to Generator Expressions كتبها مطور لغة بيثون Guido van Rossum على مدونته في بلوغر:
https://python-history.blogspot.com/2010...rator.html