python - هل تنفذ "أخيرًا" دائمًا في بيثون




exception-handling try-catch-finally (4)

لأي كتلة تجريبية محتملة في بيثون ، هل من المضمون أن يتم تنفيذ الكتلة النهائية دائمًا؟

على سبيل المثال ، دعنا نقول أنني أعود بينما في كتلة except :

try:
    1/0
except ZeroDivisionError:
    return
finally:
    print("Does this code run?")

أو ربما أعيد طرح Exception :

try:
    1/0
except ZeroDivisionError:
    raise
finally:
    print("What about this code?")

يُظهر الاختبار أنه يتم finally تنفيذ الأمثلة المذكورة أعلاه ، ولكني أتصور أن هناك سيناريوهات أخرى لم أفكر فيها.

هل هناك أي سيناريوهات يمكن أن تفشل فيها الكتلة في التنفيذ في بيثون؟


"مضمون" هي كلمة أقوى بكثير من أي تنفيذ يستحق في finally . ما هو مضمون هو أنه إذا كان التنفيذ يتدفق خارج try بأكملها - finally ، فسيتم تمريره في finally للقيام بذلك. ما هو غير مضمون هو أن التنفيذ سوف يخرج من try - finally .

  • finally في مولد أو coroutine غير متزامن قد لا يعمل أبدًا ، إذا كان الكائن لا ينفذ أبدًا. هناك الكثير من الطرق التي يمكن أن تحدث ؛ هنا واحد:

    def gen(text):
        try:
            for line in text:
                try:
                    yield int(line)
                except:
                    # Ignore blank lines - but catch too much!
                    pass
        finally:
            print('Doing important cleanup')
    
    text = ['1', '', '2', '', '3']
    
    if any(n > 1 for n in gen(text)):
        print('Found a number')
    
    print('Oops, no cleanup.')

    لاحظ أن هذا المثال صعب بعض الشيء: عندما يتم جمع المولد في القمامة ، يحاول Python تشغيل الكتلة الأخيرة من خلال طرح استثناء GeneratorExit ، ولكن هنا نلاحظ هذا الاستثناء ثم yield مرة أخرى ، وعند هذه النقطة يطبع Python تحذيرًا (" تجاهل المولد GeneratorExit ") ويستسلم. انظر PEP 342 (Coroutines عبر المولدات المحسّنة) للحصول على التفاصيل.

    من بين الطرق الأخرى التي قد لا ينفذ بها المولد أو coroutine الاستنتاج إذا لم يكن الكائن GC'ed أبدًا (نعم ، هذا ممكن ، حتى في CPython) ، أو إذا كان هناك __aexit__ async with await s في __aexit__ ، أو إذا كان الكائن في await ق في finally كتلة. ليس المقصود ان تكون هذه القائمة مطلقة.

  • finally في خيط خفي قد لا يتم تنفيذه أبدًا إذا خرجت جميع الخيط غير الخفي أولاً

  • سيقوم os._exit بإيقاف العملية فورًا دون تنفيذ الكتل finally .

  • قد يتسبب os.fork finally في تنفيذ الكتل مرتين . بالإضافة إلى المشكلات العادية التي تتوقعها من حدوث الأشياء مرتين ، فقد يتسبب ذلك في تعارضات وصول متزامنة (أعطال ، أكشاك ، ...) إذا لم تتم مزامنة الوصول إلى الموارد المشتركة بشكل صحيح .

    نظرًا لأن multiprocessing تستخدم fork-without-exec لإنشاء العمليات os._exit عند استخدام طريقة بدء fork (الافتراضي في Unix) ، ثم تقوم باستدعاء os._exit في العامل بمجرد انتهاء مهمة العامل ، finally قد يكون تفاعل multiprocessing مشكلة. ).

  • خطأ تجزئة مستوى C سيمنع finally من تشغيل الكتل.
  • kill -SIGKILL سيمنع finally كتل من التشغيل. SIGHUP SIGTERM و SIGHUP finally من تشغيل الكتل ما لم SIGHUP بتثبيت معالج للتحكم في إيقاف التشغيل بنفسك ؛ افتراضيًا ، لا تتعامل Python مع SIGTERM أو SIGHUP .
  • استثناء في finally يمكن أن يمنع التنظيف من إكمال. إحدى الحالات الجديرة بالملاحظة بشكل خاص هي إذا قام المستخدم بالتحكم في control-C تمامًا كما بدأنا في تنفيذ الحظر finally . سيقوم Python برفع KeyboardInterrupt وتخطي كل سطر من محتويات الكتلة النهائية. (رمز KeyboardInterrupt آمن من الصعب للغاية الكتابة).
  • إذا فقد الكمبيوتر الطاقة ، أو إذا كان سباتًا ولا يستيقظ ، فلن تعمل الكتل finally .

في finally ليس نظام المعاملات. أنها لا توفر ضمانات الذرية أو أي شيء من هذا القبيل. قد تبدو بعض هذه الأمثلة واضحة ، لكن من السهل أن ننسى أن مثل هذه الأشياء يمكن أن تحدث والاعتماد عليها finally .


حسنا ، نعم ولا.

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

(ما الذي كان بإمكانك التحكم فيه عن طريق تشغيل الكود في سؤالك ببساطة)

الحالة الوحيدة التي يمكنني تخيلها حيث لن يتم تنفيذ الكتلة النهائية هي عندما يتعطل مترجم Python نفسه على سبيل المثال داخل رمز C أو بسبب انقطاع التيار الكهربائي.


لقد وجدت هذا واحد دون استخدام وظيفة مولد:

import multiprocessing
import time

def fun(arg):
  try:
    print("tried " + str(arg))
    time.sleep(arg)
  finally:
    print("finally cleaned up " + str(arg))
  return foo

list = [1, 2, 3]
multiprocessing.Pool().map(fun, list)

يمكن أن يكون النوم أي رمز قد يعمل لفترة غير متناسقة من الوقت.

ما يبدو أنه يحدث هنا هو أن العملية الموازية الأولى لإنهاء يترك كتلة المحاولة بنجاح ، ولكن بعد ذلك يحاول إرجاع من الدالة قيمة (foo) التي لم يتم تعريفها في أي مكان ، مما يسبب استثناء. هذا الاستثناء يقتل الخريطة دون السماح للعمليات الأخرى بالوصول إلى كتلها النهائية.

وأيضًا ، إذا أضفت bar = bazz الخط bar = bazz مباشرةً بعد استدعاء sleep () في كتلة المحاولة. بعد ذلك ، تطرح العملية الأولى التي تصل إلى هذا الخط استثناءً (لأنه لم يتم تعريف bazz) ، مما يؤدي إلى تشغيل بلوكها أخيرًا ، ولكن بعد ذلك يقتل الخريطة ، مما يؤدي إلى اختفاء كتل المحاولات الأخرى دون الوصول إلى كتلها النهائية ، و العملية الأولى لا تصل إلى بيان عودتها ، إما.

ما يعنيه هذا بالنسبة للمعالجة المتعددة لـ Python هو أنه لا يمكنك الوثوق في آلية معالجة الاستثناءات لتنظيف الموارد في جميع العمليات إذا كان حتى إحدى العمليات قد يكون لها استثناء. سيكون من الضروري معالجة إشارة إضافية أو إدارة الموارد خارج مكالمة خريطة المعالجة المتعددة.


نعم فعلا. وأخيرا يفوز دائما.

الطريقة الوحيدة لإلحاق الهزيمة به هي إيقاف التنفيذ قبل finally: على فرصة للتنفيذ (مثل تعطل المترجم الفوري ، أو إيقاف تشغيل الكمبيوتر ، أو إيقاف مولد للأبد).

أتصور أن هناك سيناريوهات أخرى لم أفكر فيها.

إليك زوجين أكثر ربما لم تفكر بهما:

def foo():
    # finally always wins
    try:
        return 1
    finally:
        return 2

def bar():
    # even if he has to eat an unhandled exception, finally wins
    try:
        raise Exception('boom')
    finally:
        return 'no boom'

بناءً على كيفية تركك للمترجم الشفهي ، في بعض الأحيان يمكنك "الإلغاء" أخيرًا ، ولكن ليس هكذا:

>>> import sys
>>> try:
...     sys.exit()
... finally:
...     print('finally wins!')
... 
finally wins!
$

باستخدام os._exit غير os._exit (هذا يقع تحت عنوان "تعطل المترجم" في رأيي):

>>> import os
>>> try:
...     os._exit(1)
... finally:
...     print('finally!')
... 
$

أقوم حاليًا بتشغيل هذا الرمز لاختبار ما إذا كان لا يزال سيتم تنفيذه نهائيًا بعد وفاة الكون بالحرارة:

try:
    while True:
       sleep(1)
finally:
    print('done')

ومع ذلك ، ما زلت في انتظار النتيجة ، لذلك تحقق مرة أخرى هنا في وقت لاحق.





finally