هل من الآمن الإنتاج من داخل كتلة "مع" في بايثون (ولماذا)؟
سؤال
يبدو أن الجمع بين coroutines والحصول على الموارد قد يكون له بعض العواقب غير المقصودة (أو غير البديهية).
السؤال الأساسي هو ما إذا كان شيء مثل هذا يعمل أم لا:
def coroutine():
with open(path, 'r') as fh:
for line in fh:
yield line
وهو ما يفعله.(يمكنك اختباره!)
القلق الأعمق هو ذلك with
من المفترض أن يكون شيئًا بديلاً لـ finally
, حيث تتأكد من تحرير المورد في نهاية الكتلة.يمكن لـ Coroutines تعليق واستئناف التنفيذ من داخل ال with
كتلة، لذلك كيف يتم حل الصراع؟
على سبيل المثال، إذا قمت بفتح ملف بالقراءة/الكتابة داخل وخارج الكوروتين بينما لم يتم إرجاع الكوروتين بعد:
def coroutine():
with open('test.txt', 'rw+') as fh:
for line in fh:
yield line
a = coroutine()
assert a.next() # Open the filehandle inside the coroutine first.
with open('test.txt', 'rw+') as fh: # Then open it outside.
for line in fh:
print 'Outside coroutine: %r' % repr(line)
assert a.next() # Can we still use it?
تحديث
كنت سأذهب إلى تنافس مقبض الملف المقفل ضد الكتابة في المثال السابق، ولكن نظرًا لأن معظم أنظمة التشغيل تخصص معالجات الملفات لكل عملية، فلن يكون هناك أي تنافس هناك.(مجد لـMiles للإشارة إلى المثال الذي لم يكن منطقيًا جدًا.) إليك مثالي المنقح، والذي يوضح حالة طريق مسدود حقيقي:
import threading
lock = threading.Lock()
def coroutine():
with lock:
yield 'spam'
yield 'eggs'
generator = coroutine()
assert generator.next()
with lock: # Deadlock!
print 'Outside the coroutine got the lock'
assert generator.next()
المحلول
لا أفهم حقًا ما هو التعارض الذي تسأل عنه، ولا مشكلة المثال:من الجيد أن يكون لديك مقابض متعايشة ومستقلة لنفس الملف.
شيء واحد لم أكن أعرفه أنني تعلمته ردًا على سؤالك وهو أن هناك طريقة إغلاق () جديدة على المولدات:
close()
يثير جديدGeneratorExit
استثناء داخل المولد لإنهاء التكرار.عند تلقي هذا الاستثناء، يجب أن يتم رفع كود المولدGeneratorExit
أوStopIteration
.
close()
يتم استدعاؤه عندما يتم جمع البيانات المهملة للمولد، وهذا يعني أن كود المولد يحصل على فرصة أخيرة للتشغيل قبل تدمير المولد.هذه الفرصة الأخيرة تعني ذلكtry...finally
يمكن الآن ضمان عمل البيانات الموجودة في المولدات؛الfinally
سيحصل البند الآن دائمًا على فرصة للتشغيل.يبدو هذا وكأنه جزء بسيط من التوافه اللغوية، ولكن باستخدام المولدات وtry...finally
هو في الواقع ضروري من أجل تنفيذwith
بيان وصفه PEP 343.http://docs.python.org/whatsnew/2.5.html#pep-342-new-generator-features
بحيث يتعامل مع الوضع حيث أ with
يتم استخدام العبارة في المولد، ولكنها تنتج في المنتصف ولكنها لا تعود أبدًا - وهي عبارة عن مدير السياق __exit__
سيتم استدعاء الطريقة عندما يتم جمع البيانات المهملة للمولد.
يحرر:
فيما يتعلق بمشكلة التعامل مع الملف:أنسى أحيانًا وجود منصات لا تشبه POSIX.:)
بقدر ما تذهب الأقفال ، أعتقد أن Rafał Dowgird يضرب الرأس على الظفر عندما يقول "عليك فقط أن تدرك أن المولد يشبه أي كائن آخر يحمل الموارد". لا أعتقد with
العبارة ذات صلة حقًا هنا، نظرًا لأن هذه الوظيفة تعاني من نفس مشكلات الجمود:
def coroutine():
lock.acquire()
yield 'spam'
yield 'eggs'
lock.release()
generator = coroutine()
generator.next()
lock.acquire() # whoops!
نصائح أخرى
لا أعتقد أن هناك صراعاً حقيقياً.عليك فقط أن تدرك أن المولد يشبه تمامًا أي كائن آخر يحتوي على موارد، لذا تقع على عاتق المنشئ مسؤولية التأكد من الانتهاء منه بشكل صحيح (وتجنب التعارض/التوقف التام مع الموارد التي يحتفظ بها الكائن).المشكلة الوحيدة (الثانوية) التي أراها هنا هي أن المولدات لا تنفذ بروتوكول إدارة السياق (على الأقل اعتبارًا من Python 2.5)، لذلك لا يمكنك فقط:
with coroutine() as cr:
doSomething(cr)
ولكن بدلا من ذلك يجب أن:
cr = coroutine()
try:
doSomething(cr)
finally:
cr.close()
جامع القمامة يفعل close()
على أية حال، ولكن من الممارسات السيئة الاعتماد على ذلك لتحرير الموارد.
لأن yield
يمكنني تنفيذ تعليمات برمجية عشوائية، سأكون حذرًا جدًا من الاحتفاظ بقفل على بيان العائد.يمكنك الحصول على تأثير مماثل بعدة طرق أخرى، بما في ذلك استدعاء طريقة أو وظائف ربما تم تجاوزها أو تعديلها بطريقة أخرى.
ومع ذلك، تكون المولدات دائمًا (دائمًا تقريبًا) "مغلقة"، إما بعلامة صريحة close()
الاتصال، أو فقط عن طريق جمع القمامة.إغلاق مولد يلقي أ GeneratorExit
استثناء داخل المولد وبالتالي تشغيل الجمل أخيرًا، مع تنظيف العبارة، وما إلى ذلك.يمكنك التقاط الاستثناء، ولكن يجب عليك رمي الوظيفة أو الخروج منها (أي.اقذف ال StopIteration
استثناء)، بدلا من العائد.ربما يكون من الممارسات السيئة الاعتماد على أداة تجميع البيانات المهملة لإغلاق المولد في الحالات التي كتبتها، لأن ذلك قد يحدث في وقت متأخر عما قد تريده، وإذا اتصل شخص ما بـ sys._exit()، فقد لا تتم عملية التنظيف على الإطلاق .
ستكون هذه هي الطريقة التي توقعت أن تسير بها الأمور.نعم، لن تقوم الكتلة بتحرير مواردها حتى تكتمل، وبهذا المعنى يكون المورد قد نجا من تداخله المعجمي.ومع ذلك، فإن هذا لا يختلف عن إجراء استدعاء دالة حاول استخدام نفس المورد داخل كتلة - لا شيء يساعدك في حالة وجود الكتلة لا تم إنهاؤها بعد، ل أيا كان سبب.إنه ليس في الحقيقة أي شيء خاص بالمولدات.
الشيء الوحيد الذي قد يستحق القلق بشأنه هو السلوك إذا كان المولد كذلك أبداً تم استئنافه.كنت أتوقع with
كتلة لتتصرف مثل finally
حظر واستدعاء __exit__
جزء من الإنهاء، ولكن لا يبدو أن هذا هو الحال.
بالنسبة إلى TLDR، انظر إليها بهذه الطريقة:
with Context():
yield 1
pass # explicitly do nothing *after* yield
# exit context after explicitly doing nothing
ال Context
ينتهي بعد pass
تم (أيلا شئ)، pass
ينفذ بعد yield
تم (أييستأنف التنفيذ).لذلك with
ينتهي بعد يتم استئناف التحكم في yield
.
تلدر:أ with
يبقى السياق محتجزًا عندما yield
التحكم بالإصدارات.
هناك في الواقع قاعدتان فقط ذات صلة هنا:
متى
with
الافراج عن مواردها؟إنه يفعل ذلك مرة واحدة ومباشرة بعد تم الانتهاء من كتلته.السابق يعني أنه لا يفرج عنه خلال أ
yield
, ، لأن ذلك قد يحدث عدة مرات.الأخير يعني أنه يطلق سراحه بعدyield
أكملت.متى
yield
مكتمل؟افكر في
yield
كنداء عكسي:يتم تمرير التحكم إلى المتصل، وليس إلى المتصل به.بصورة مماثلة،yield
يكتمل عند إعادة التحكم إليه، تمامًا كما يحدث عندما ترجع المكالمة التحكم.
لاحظ أن كلاهما with
و yield
يعملون على النحو المنشود هنا!نقطة أ with lock
هو حماية المورد ويظل محميًا خلال فترة yield
.يمكنك دائمًا إطلاق هذه الحماية بشكل صريح:
def safe_generator():
while True:
with lock():
# keep lock for critical operation
result = protected_operation()
# release lock before releasing control
yield result