Съставяне на контекстен мениджър на Python: пъзел

Озадачен съм как да подредя всички неща, които контекстните мениджъри на Python могат да правят, на подходящите места.

Доколкото разбирам, елементите, които потенциално могат да влязат в изграждането на контекстен мениджър, включват:

  • О: Нещо, което винаги се случва
  • B: Необходима е известна подготовка за C
  • C: Създайте и установете обект X, използван в контекста
  • D: Направете някои неща, които се случват с помощта на успешно установен X преди стартиране на контекста
  • E: Връщане на X в контекста (за използване от as)
  • F: Завършете с X, когато всичко е наред в края на контекста
  • G: Справете се с последствията от провала в C и B, преди да влезете в контекста
  • H: Справете се с последствията от провала в контекста

Мисля, че приблизително разбирам къде отива всеки от тези елементи във функцията на контекстния мениджър, но съм напълно на загуба как да ги подредя в класове.

Има ли шаблон за функции и класове на контекстния мениджър, който показва дали всеки от тези елементи влиза и в двете функции и (особено) в класове? Прегледах много примери тук и другаде, но не открих нито един, който да е изчерпателен, и много, които използват действителен код, който не винаги мога да съпоставя с всеки от градивните елементи по-горе.


Мисля, че основно разбирам как се държи контекстният мениджър, когато е внедрен чрез функция:

from contextlib import contextmanager     
@contextmanager
def log_file_open(oec_data, build_description, log_dir):
    # A: Something that always happens
    try:
        # B: Some stuff needed to make a_thing
        a_thing = establish_thing_in_a_way_that_might_fail() # C
        # D: Some things that happen using a_thing at context start
        yield a_thing # E
        # F: Wrap up with a_thing when all is well
    except:
        # G: Deal the consequences of failure in try or...
        # H: Deal the consequences of failure in context
    finally:
        # Could F go here instead?

Например, за да отворя файл, в който трябва да се запише нещо при успешно отваряне и затваряне, но който трябва да се изчисти, ако има проблем, мога да напиша

from contextlib import contextmanager     
@contextmanager
def log_file_open(oec_data, build_description, log_dir):
    print('Entering context...')
    try:
        usable_file_name = get_some_name()
        a_thing =  open(usable_file_name, mode='w')
        a_thing.write('Logging context started.')
        yield a_thing
        a_thing.write('Logging context ended.')
    except:
        a_thing.close()
        os.remove(a_thing.name)
        raise

Но не съм сигурен, че това е правилно и съм объркан как се съпоставя с използването на __enter()__ и __exit()__ в класове. Дали (схематично):

def __init__(self):
    # A: Something that always happens

def __enter__(self):
    try:
        # B: Some stuff needed to make a_thing
        a_thing = establish_thing_in_a_way_that_might_fail() # C
        # D: Some things that happen using a_thing at context start
     except:
        # G: Deal the consequences of failure in try
        a_thing = some_appropriate_blank_value
     finally:
        return a_thing # E

 def __exit__(self, type, value, traceback):
        if type is None:
            # F: Wrap up with a_thing when all is well
            return True
        else:
            # H: Deal the consequences of failure in context
            return False

person orome    schedule 06.01.2014    source източник


Отговори (2)


Вие смесвате обработката на грешки при генерирането на стойността на контекста и обработката на грешки в самия контекст. Много по-добре е да напишете:

@contextmanager
def fn(...):
    value = ...      # A, B, C, D: setup
    try:
        yield value  # E: pass value to client
    except:          # or better, finally:
        ...          # F, H: cleanup

По този начин знаете, че имате работа само с изключения, които са възникнали в клиентския код, и рационализирате своя код за почистване, тъй като знаете, че настройката е успешна. Обикновено няма смисъл да се опитвате да обработвате изключения в кода за настройка; не искате клиентският код да трябва да обработва None контекстна стойност. Това означава, че __enter__ е просто:

def __enter__(self):
    self.value = ...   # A, B, C, D: setup
    return self.value  # E: pass value to client

Ако __enter__ предизвика изключение, тогава __exit__ няма да бъде извикано.

Имайте предвид също, че finally е по-добро от except, освен ако не планирате да потискате изключения от клиентския код, което много рядко е полезно. Така че __exit__ е просто:

def __exit__(self, type, value, traceback):
    ...                # F, H: cleanup
    return False       # don't suppress any exception
person ecatmur    schedule 06.01.2014

Мисля, че вашето разбиране е най-вече правилно. Контекстният мениджър е обект, който управлява контекста чрез своите __enter__ и __exit__ методи. Така че това, което се случва в __init__, остава вярно за живота на обекта. Нека да разгледаме конкретен пример:

class CMan(object):
    def __init__(self, *parameters):
        "Creates a new context manager"
        print "Creating object..."

    def __enter__(self):
        "Enters the manager (opening the file)"
        print "Entering context..."
        a_thing = self # Or any other relevant value to be used in this context
        print "Returning %s" % a_thing
        return a_thing

    def __exit__(self, type, value, traceback):
        "Exits the context"
        if type is None:
            print "Exiting with no exception -> Wrapping up"
            return
        print "Exiting with exception %s" % type

Което ще се използва като това:

>>> with CMan(1,2,3) as x:
...     print 1 + 1
Creating object...
Entering context...
Returning <__main__.CMan object at 0x02514F70>
2
Exiting with no exception -> Wrapping up

Имайте предвид, че създаването на обекта в движение не е задължително:

>>> mgr = CMan(1,2,3)
Creating object...
>>> with mgr as x:
...     print 1 + 1
Entering context...
Returning <__main__.CMan object at 0x02514F70>
2
Exiting with no exception -> Wrapping up

И накрая, върнатата стойност на __exit__ определя дали трябва да бъде повдигнато изключение. Ако стойността се изчисли на False (напр. False, 0, None...), всяко изключение ще бъде повдигнато. В противен случай това означава, че контекстният мениджър е обработил изключението и не е необходимо то да се повдига. Например:

>>> class Arithmetic(object):
...     def __enter__(self):
...         return self
...     def __exit__(self, type, value, traceback):
...         if type == ZeroDivisionError:
...             print "I dont care -> Ignoring"
...             return True
...         else:
...             print "Unknown error: Panicking !"
...             return False

>>> with Arithmetic() as a:
...     print 1 / 0 # Divide by 0
I dont care -> Ignoring

>>> with Arithmetic() as a:
...     print 1 + "2" # Type error
Unknown error: Panicking !
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Имайте предвид, че в случай на грешка при разделяне на 0, тъй като __exit__ върна True, грешката не се разпространява. В други случаи той е повдигнат след излизане от контекстния мениджър. Можете да помислите за обаждане до контекстен мениджър:

>>> with X as x:
...     f(x)

като еквивалентен на:

>>> x = X.__enter__()
>>> try:
...     exc = None
...     f(x)     
... except Exception as e:
...     exc = e
... finally:
...     handled = X.__exit__(exc)
...     if exc and not handled:
...         raise exc

Разбира се, ако изключение бъде повдигнато вътре във вашия метод __enter__ или __exit__, то трябва да бъде обработено по подходящ начин, напр. ако генерирането на a_thing може да се провали. Можете да намерите много ресурси в мрежата, като потърсите „Python with statement“, което обикновено е начина, по който се отнасяте към този модел (въпреки че Context manager е наистина по-правилен)

person val    schedule 06.01.2014