ما الذي يمكنك استخدام وظائف مولد بايثون؟

StackOverflow https://stackoverflow.com/questions/102535

  •  01-07-2019
  •  | 
  •  

سؤال

لقد بدأت في تعلم بايثون وصادفت وظائف المولد، تلك التي تحتوي على بيان العائد فيها.أريد أن أعرف ما هي أنواع المشاكل التي تكون هذه الوظائف جيدة في حلها.

هل كانت مفيدة؟

المحلول

المولدات تعطيك تقييم كسول.يمكنك استخدامها عن طريق التكرار عليها، إما بشكل صريح مع "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 تعبير.

تجنب بشكل أساسي وظائف رد الاتصال عند التكرار على حالة الحفاظ على الإدخال.

يرى هنا و هنا للحصول على نظرة عامة على ما يمكن القيام به باستخدام المولدات.

بعض الإجابات الجيدة هنا، ومع ذلك، أوصي أيضًا بقراءة كاملة لبايثون دروس البرمجة الوظيفية مما يساعد في شرح بعض حالات الاستخدام الأكثر فاعلية للمولدات.

أستخدم المولدات عندما يعمل خادم الويب الخاص بنا كوكيل:

  1. يطلب العميل عنوان URL الوكيل من الخادم
  2. يبدأ الخادم في تحميل عنوان URL المستهدف
  3. يتنازل الخادم عن إعادة النتائج إلى العميل بمجرد حصوله عليها

نظرًا لعدم ذكر طريقة إرسال المولد، فإليك مثالًا:

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 من الشرح، ومولدات المعالجة المتوازية، والهروب من حد التكرار، وما إلى ذلك.)

ديفيد بيزلي يتحدث عن المولدات في PyCon 2014

أكوام من الاشياء.في أي وقت تريد إنشاء تسلسل من العناصر، ولكن لا تريد "تجسيدها" جميعًا في قائمة مرة واحدة.على سبيل المثال، يمكن أن يكون لديك مولد بسيط يُرجع الأعداد الأولية:

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)
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top