ما الذي يمكنك استخدام وظائف مولد بايثون؟
سؤال
لقد بدأت في تعلم بايثون وصادفت وظائف المولد، تلك التي تحتوي على بيان العائد فيها.أريد أن أعرف ما هي أنواع المشاكل التي تكون هذه الوظائف جيدة في حلها.
المحلول
المولدات تعطيك تقييم كسول.يمكنك استخدامها عن طريق التكرار عليها، إما بشكل صريح مع "for" أو ضمنيًا عن طريق تمريرها إلى أي دالة أو بناء يتكرر.يمكنك التفكير في المولدات على أنها تقوم بإرجاع عناصر متعددة، كما لو أنها تقوم بإرجاع قائمة، ولكن بدلاً من إرجاعها جميعًا مرة واحدة، فإنها تعيدها واحدًا تلو الآخر، وتتوقف وظيفة المولد مؤقتًا حتى يتم طلب العنصر التالي.
تعتبر المولدات مفيدة لحساب مجموعات كبيرة من النتائج (خاصة الحسابات التي تتضمن الحلقات نفسها) حيث لا تعرف ما إذا كنت ستحتاج إلى جميع النتائج، أو عندما لا تريد تخصيص الذاكرة لجميع النتائج في نفس الوقت .أو للحالات التي يستخدم فيها المولد آخر المولد، أو يستهلك بعض الموارد الأخرى، ويكون الأمر أكثر ملاءمة إذا حدث ذلك في وقت متأخر قدر الإمكان.
استخدام آخر للمولدات (وهذا هو نفسه بالفعل) هو استبدال عمليات الاسترجاعات بالتكرار.في بعض المواقف، تريد أن تقوم إحدى الوظائف بالكثير من العمل وتقوم أحيانًا بإبلاغ المتصل.تقليديا كنت تستخدم وظيفة رد الاتصال لهذا الغرض.تقوم بتمرير رد الاتصال هذا إلى وظيفة العمل وسوف تقوم باستدعاء رد الاتصال هذا بشكل دوري.نهج المولد هو أن وظيفة العمل (المولد الآن) لا تعرف شيئًا عن رد الاتصال، وتنتج فقط عندما تريد الإبلاغ عن شيء ما.المتصل، بدلاً من كتابة رد اتصال منفصل وتمريره إلى وظيفة العمل، يقوم بكل عمل التقارير في حلقة "من أجل" صغيرة حول المولد.
على سبيل المثال، لنفترض أنك كتبت برنامج "بحث في نظام الملفات".يمكنك إجراء البحث بالكامل، وجمع النتائج ثم عرضها واحدة تلو الأخرى.يجب جمع كل النتائج قبل إظهار النتيجة الأولى، وستكون جميع النتائج في الذاكرة في نفس الوقت.أو يمكنك عرض النتائج أثناء العثور عليها، الأمر الذي سيكون أكثر كفاءة في استخدام الذاكرة وأكثر ودية تجاه المستخدم.يمكن إجراء هذا الأخير عن طريق تمرير وظيفة طباعة النتائج إلى وظيفة البحث في نظام الملفات، أو يمكن القيام به عن طريق جعل وظيفة البحث مجرد مولد وتكرار النتيجة.
إذا كنت تريد رؤية مثال على الطريقتين الأخيرتين، فراجع os.path.walk() (وظيفة المشي في نظام الملفات القديمة مع رد الاتصال) وos.walk() (المولد الجديد للمشي في نظام الملفات.) بالطبع، إذا إذا كنت تريد حقًا جمع كل النتائج في قائمة، فإن منهج المولد يعد أمرًا تافهًا للتحويل إلى منهج القائمة الكبيرة:
big_list = list(the_generator)
نصائح أخرى
أحد أسباب استخدام المولد هو جعل الحل أكثر وضوحًا لبعض أنواع الحلول.
والآخر هو التعامل مع النتائج واحدة تلو الأخرى، وتجنب إنشاء قوائم ضخمة من النتائج التي قد تقوم بمعالجتها بشكل منفصل على أي حال.
إذا كان لديك وظيفة فيبوناتشي يصل إلى ن مثل هذا:
# function version
def fibon(n):
a = b = 1
result = []
for i in xrange(n):
result.append(a)
a, b = b, a + b
return result
يمكنك بسهولة كتابة الوظيفة على النحو التالي:
# generator version
def fibon(n):
a = b = 1
for i in xrange(n):
yield a
a, b = b, a + b
الوظيفة أكثر وضوحا.وإذا كنت تستخدم الوظيفة مثل هذا:
for x in fibon(1000000):
print x,
في هذا المثال، إذا كنت تستخدم إصدار المولد، فلن يتم إنشاء قائمة العناصر البالغ عددها 1000000 على الإطلاق، بل سيتم إنشاء قيمة واحدة فقط في كل مرة.لن يكون هذا هو الحال عند استخدام إصدار القائمة، حيث سيتم إنشاء القائمة أولاً.
راجع قسم "التحفيز" في بيب 255.
الاستخدام غير الواضح للمولدات هو إنشاء وظائف قابلة للمقاطعة، والتي تتيح لك القيام بأشياء مثل تحديث واجهة المستخدم أو تشغيل عدة مهام "في وقت واحد" (متداخلة، في الواقع) دون استخدام سلاسل الرسائل.
أجد هذا التفسير الذي يزيل شكوكي.لأن هناك احتمال أن الشخص الذي لا يعرف Generators
أيضا لا أعرف عنه yield
يعود
بيان الإرجاع هو المكان الذي يتم فيه تدمير جميع المتغيرات المحلية وإعادة القيمة الناتجة (إرجاعها) إلى المتصل.إذا تم استدعاء نفس الوظيفة في وقت لاحق، فستحصل الوظيفة على مجموعة جديدة من المتغيرات.
أَثْمَر
ولكن ماذا لو لم يتم التخلص من المتغيرات المحلية عندما نخرج من الوظيفة؟وهذا يعني أننا نستطيع resume the function
حيث توقفنا.وهنا يأتي مفهوم generators
يتم تقديم و yield
بيان يستأنف حيث function
متروك مهمل.
def generate_integers(N):
for i in xrange(N):
yield i
In [1]: gen = generate_integers(3)
In [2]: gen
<generator object at 0x8117f90>
In [3]: gen.next()
0
In [4]: gen.next()
1
In [5]: gen.next()
إذن هذا هو الفرق بين return
و yield
البيانات في بايثون.
بيان العائد هو ما يجعل الوظيفة وظيفة مولد.
لذا فإن المولدات هي أداة بسيطة وقوية لإنشاء التكرارات.وهي مكتوبة مثل الوظائف العادية، ولكنها تستخدم yield
بيان عندما يريدون العودة البيانات.في كل مرة يتم استدعاء next()، يستأنف المولد من حيث توقف (يتذكر جميع قيم البيانات وأي بيان تم تنفيذه آخر مرة).
مثال العالم الحقيقي
لنفترض أن لديك 100 مليون نطاق في جدول MySQL الخاص بك، وترغب في تحديث ترتيب Alexa لكل مجال.
أول شيء تحتاجه هو تحديد أسماء النطاقات الخاصة بك من قاعدة البيانات.
لنفترض أن اسم الجدول الخاص بك هو domains
واسم العمود هو domain
.
إذا كنت تستخدم SELECT domain FROM domains
ستعيد 100 مليون صف مما سيستهلك الكثير من الذاكرة.لذلك قد يتعطل الخادم الخاص بك.
لذلك قررت تشغيل البرنامج على دفعات.لنفترض أن حجم الدفعة لدينا هو 1000.
في الدفعة الأولى، سنقوم بالاستعلام عن أول 1000 صف، والتحقق من تصنيف Alexa لكل مجال وتحديث صف قاعدة البيانات.
في الدفعة الثانية سنعمل على الـ 1000 صف التالية.وفي الدفعة الثالثة ستكون من 2001 إلى 3000 وهكذا.
نحتاج الآن إلى وظيفة المولد التي تولد دفعاتنا.
هذه هي وظيفة المولد لدينا:
def ResultGenerator(cursor, batchsize=1000):
while True:
results = cursor.fetchmany(batchsize)
if not results:
break
for result in results:
yield result
كما ترون، وظيفتنا تبقى yield
في النتائج.إذا استخدمت الكلمة الأساسية return
بدلاً من yield
, ، فستنتهي الوظيفة بأكملها بمجرد وصولها إلى العودة.
return - returns only once
yield - returns multiple times
إذا كانت الوظيفة تستخدم الكلمة الأساسية yield
فهو مولد.
الآن يمكنك التكرار مثل هذا:
db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
doSomethingWith(result)
db.close()
التخزين المؤقت.عندما يكون من الفعال جلب البيانات في أجزاء كبيرة، ولكن معالجتها في أجزاء صغيرة، فقد يساعد المولد:
def bufferedFetch():
while True:
buffer = getBigChunkOfData()
# insert some code to break on 'end of data'
for i in buffer:
yield i
يتيح لك ما سبق فصل التخزين المؤقت عن المعالجة بسهولة.يمكن لوظيفة المستهلك الآن الحصول على القيم واحدة تلو الأخرى دون القلق بشأن التخزين المؤقت.
لقد وجدت أن المولدات مفيدة جدًا في تنظيف التعليمات البرمجية الخاصة بك ومن خلال إعطائك طريقة فريدة جدًا لتغليف التعليمات البرمجية وتقسيمها إلى وحدات.في الحالة التي تحتاج فيها إلى شيء ما لإخراج القيم باستمرار بناءً على المعالجة الداخلية الخاصة به وعندما يلزم استدعاء هذا الشيء من أي مكان في التعليمات البرمجية الخاصة بك (وليس فقط داخل حلقة أو كتلة على سبيل المثال)، فإن المولدات هي ال ميزة للاستخدام.
مثال مجرد هو مولد أرقام فيبوناتشي الذي لا يعيش داخل حلقة وعندما يتم استدعاؤه من أي مكان سيعيد دائمًا الرقم التالي في التسلسل:
def fib():
first = 0
second = 1
yield first
yield second
while 1:
next = first + second
yield next
first = second
second = next
fibgen1 = fib()
fibgen2 = fib()
الآن لديك كائنين منشئي أرقام فيبوناتشي يمكنك الاتصال بهم من أي مكان في الكود الخاص بك وسيقومون دائمًا بإرجاع أرقام فيبوناتشي أكبر من أي وقت مضى بالتسلسل كما يلي:
>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5
إن الشيء الجميل في المولدات هو أنها تقوم بتغليف الحالة دون الحاجة إلى المرور عبر أطواق إنشاء الكائنات.إحدى طرق التفكير فيها هي أنها "وظائف" تتذكر حالتها الداخلية.
حصلت على مثال فيبوناتشي من مولدات بايثون - ما هي؟ ومع القليل من الخيال، يمكنك التوصل إلى الكثير من المواقف الأخرى التي تشكل فيها المولدات بديلاً رائعًا لها for
الحلقات وغيرها من بنيات التكرار التقليدية.
التفسير البسيط :اعتبر أ for
إفادة
for item in iterable:
do_stuff()
في كثير من الأحيان، جميع العناصر الموجودة في iterable
لا يلزم أن تكون هناك منذ البداية، ولكن يمكن إنشاؤها بسرعة كما هو مطلوب.يمكن أن يكون هذا أكثر كفاءة في كليهما
- المساحة (لن تحتاج أبدًا إلى تخزين جميع العناصر في وقت واحد) و
- الوقت (قد ينتهي التكرار قبل الحاجة إلى كافة العناصر).
وفي أحيان أخرى، لا تعرف حتى جميع العناصر مسبقًا.على سبيل المثال:
for command in user_input():
do_stuff_with(command)
ليس لديك طريقة لمعرفة جميع أوامر المستخدم مسبقًا، ولكن يمكنك استخدام حلقة لطيفة مثل هذه إذا كان لديك مولد يسلمك الأوامر:
def user_input():
while True:
wait_for_command()
cmd = get_command()
yield cmd
باستخدام المولدات، يمكنك أيضًا إجراء التكرار عبر تسلسلات لا نهائية، وهو أمر غير ممكن بالطبع عند التكرار عبر الحاويات.
استخداماتي المفضلة هي عمليات "التصفية" و"التقليل".
لنفترض أننا نقرأ ملفًا ونريد فقط الأسطر التي تبدأ بـ "##".
def filter2sharps( aSequence ):
for l in aSequence:
if l.startswith("##"):
yield l
يمكننا بعد ذلك استخدام وظيفة المولد في حلقة مناسبة
source= file( ... )
for line in filter2sharps( source.readlines() ):
print line
source.close()
مثال التخفيض مشابه.لنفترض أن لدينا ملفًا نحتاج إلى تحديد موقع الكتل فيه <Location>...</Location>
خطوط.[ليست علامات HTML، ولكن الخطوط التي تبدو وكأنها علامات.]
def reduceLocation( aSequence ):
keep= False
block= None
for line in aSequence:
if line.startswith("</Location"):
block.append( line )
yield block
block= None
keep= False
elif line.startsWith("<Location"):
block= [ line ]
keep= True
elif keep:
block.append( line )
else:
pass
if block is not None:
yield block # A partial block, icky
مرة أخرى، يمكننا استخدام هذا المولد في حلقة for مناسبة.
source = file( ... )
for b in reduceLocation( source.readlines() ):
print b
source.close()
الفكرة هي أن وظيفة المولد تسمح لنا بتصفية التسلسل أو تقليله، مما يؤدي إلى إنتاج تسلسل آخر بقيمة واحدة في كل مرة.
أحد الأمثلة العملية حيث يمكنك الاستفادة من المولد هو إذا كان لديك شكل ما وتريد التكرار على زواياه أو حوافه أو أي شيء آخر.لمشروعي الخاص (كود المصدر هنا) كان لدي مستطيل:
class Rect():
def __init__(self, x, y, width, height):
self.l_top = (x, y)
self.r_top = (x+width, y)
self.r_bot = (x+width, y+height)
self.l_bot = (x, y+height)
def __iter__(self):
yield self.l_top
yield self.r_top
yield self.r_bot
yield self.l_bot
يمكنني الآن إنشاء مستطيل وحلقة حول زواياه:
myrect=Rect(50, 50, 100, 100)
for corner in myrect:
print(corner)
بدلاً من __iter__
يمكن أن يكون لديك طريقة iter_corners
واتصل بذلك مع for corner in myrect.iter_corners()
.إنه أكثر أناقة في الاستخدام __iter__
منذ ذلك الحين يمكننا استخدام اسم مثيل الفئة مباشرةً في ملف for
تعبير.
بعض الإجابات الجيدة هنا، ومع ذلك، أوصي أيضًا بقراءة كاملة لبايثون دروس البرمجة الوظيفية مما يساعد في شرح بعض حالات الاستخدام الأكثر فاعلية للمولدات.
- ومن المثير للاهتمام بشكل خاص أنه أصبح من الممكن الآن القيام بذلك تحديث متغير العائد من خارج وظيفة المولد, ، مما يجعل من الممكن إنشاء كوروتينات ديناميكية ومتشابكة بجهد قليل نسبيًا.
- انظر أيضا بيب 342:Coroutines عبر المولدات المحسنة للمزيد من المعلومات.
أستخدم المولدات عندما يعمل خادم الويب الخاص بنا كوكيل:
- يطلب العميل عنوان URL الوكيل من الخادم
- يبدأ الخادم في تحميل عنوان URL المستهدف
- يتنازل الخادم عن إعادة النتائج إلى العميل بمجرد حصوله عليها
نظرًا لعدم ذكر طريقة إرسال المولد، فإليك مثالًا:
def test():
for i in xrange(5):
val = yield
print(val)
t = test()
# Proceed to 'yield' statement
next(t)
# Send value to yield
t.send(1)
t.send('2')
t.send([3])
يُظهر إمكانية إرسال قيمة إلى مولد قيد التشغيل.دورة أكثر تقدمًا حول المولدات في الفيديو أدناه (بما في ذلك yield
من الشرح، ومولدات المعالجة المتوازية، والهروب من حد التكرار، وما إلى ذلك.)
أكوام من الاشياء.في أي وقت تريد إنشاء تسلسل من العناصر، ولكن لا تريد "تجسيدها" جميعًا في قائمة مرة واحدة.على سبيل المثال، يمكن أن يكون لديك مولد بسيط يُرجع الأعداد الأولية:
def primes():
primes_found = set()
primes_found.add(2)
yield 2
for i in itertools.count(1):
candidate = i * 2 + 1
if not all(candidate % prime for prime in primes_found):
primes_found.add(candidate)
yield candidate
يمكنك بعد ذلك استخدام ذلك لإنشاء منتجات الأعداد الأولية اللاحقة:
def prime_products():
primeiter = primes()
prev = primeiter.next()
for prime in primeiter:
yield prime * prev
prev = prime
هذه أمثلة تافهة إلى حد ما، ولكن يمكنك أن ترى كيف يمكن أن تكون مفيدة لمعالجة مجموعات البيانات الكبيرة (التي يحتمل أن تكون لا نهائية!) دون إنشائها مسبقًا، وهو ما يعد واحدًا فقط من الاستخدامات الأكثر وضوحًا.
جيد أيضًا لطباعة الأعداد الأولية حتى n:
def genprime(n=10):
for num in range(3, n+1):
for factor in range(2, num):
if num%factor == 0:
break
else:
yield(num)
for prime_num in genprime(100):
print(prime_num)