Pythonic способ управления генераторами

Предлагает ли Python немного синтаксического сахара, чтобы подсластить конструкцию генератора test, как показано ниже?

def acquire():
    print('Acquiring resource')
    yield 'A' 
    
def do_stuff():
    print('Doing stuff')
    yield 'B'
    
def release():
    print('Releasing resource')
    yield 'C'

def test():
    yield from acquire()
    yield from do_stuff()
    yield from release()
    
[u for u in test()] # has value ['A', 'B', 'C']

По сути, мне нужен синтаксис, который позволяет использовать acuire и release в одном выражении. Сначала я подумал, что контекстный менеджер будет уместен, например:

class conman:
    def __init__(self, acq, rel):
        self.acq = acq
        self.rel = rel
        
    def __enter__(self):
        try:
            while True:
                next(self.acq)
        except StopIteration:
            return
    
    def __exit__(self, _, __, ___):
        try:
            while True:
                next(self.rel)
        except StopIteration:
            return

def conmantest():
    with conman(acquire(), release()):
        yield from do_stuff()
[u for u in conmantest()]

Этот подход будет правильно выполнять итерацию через получение и освобождение генераторов, но он не передает результат в контекст. В результате список будет иметь значение ['B'], хотя он по-прежнему печатает все сообщения в правильном порядке.

Другой подход заключается в использовании декоратора

def manager(acq, rel):
    def decorator(func):
        def wrapper(*args, **kwargs):
            yield from acq
            yield from func(*args, **kwargs)
            yield from rel
            return
        return wrapper
    return decorator

@manager(acquire(), release())
def do_stuff_decorated():
    print('Doing stuff')
    yield 'B'

[u for u in do_stuff_decorated()]

Это правильно, но на практике do_stuff — это список операторов, и не всегда желательно писать вокруг них генератор.

Если бы релиз был обычной функцией python, мы могли бы попробовать этот обходной путь:

class conman2:
    def __init__(self, acq, rel):
        self.acq = acq
        self.rel = rel
        
    def __enter__(self):
        return self.acq
    
    def __exit__(self, _, __, ___):
        self.rel()
        
def release_func():
    print('Releasing stuff')

def conman2test():
    with conman2(acquire(), release_func) as r:
        yield from r
        yield from do_stuff()
[u for u in conmantest()]

Это делает все правильно, так как release_func — это произвольная функция, а не генератор, но нам пришлось передать дополнительный оператор `yield from r'. Нечто подобное используется в библиотеке SimPy для программирования дискретных событий для реализовать контекст для ресурсов, где они автоматически освобождаются, когда контекст заканчивается.

Однако я надеялся, что может быть какой-то синтаксис, например

class yielded_conman:
    def __init__(self, acq, rel):
        self.acq = acq
        self.rel = rel
        
    def __yielded_enter__(self):
        yield from self.acq()

    def __yielded_exit__(self, _, __, ___):
        yield from self.rel()

def yieldconmantest():
    with yielded_conman(acquire(), release()):
        yield from do_stuff()

[u for u in conmantest()] # has value ['A', 'B', 'C']

который делает все правильно.


person Lambda Mu    schedule 25.07.2019    source источник
comment
Это будет правильно, но, как и в случае с декоратором, это будет работать только потому, что do_stuff на самом деле является одним генератором. (Это имеет место только здесь, потому что это простой пример.)   -  person Lambda Mu    schedule 25.07.2019


Ответы (1)


Один подход с использованием contextlib:

from contextlib import contextmanager

def acquire():
    print('Acquiring resource')
    yield 'A'

def do_stuff():
    print('Doing stuff')
    yield 'B1'
    raise Exception('Something happened!')
    yield 'B2'

def release():
    print('Releasing resource')
    yield 'C'

@contextmanager
def cntx(a, b, c):
    def _fn():
        try:
            yield from a
            yield from b
        finally:
            yield from c

    try:
        yield _fn
    finally:
        pass

def fn():
    with cntx(acquire(), do_stuff(), release()) as o:
        yield from o()

[print(i) for i in fn()]

Распечатать:

Acquiring resource
A
Doing stuff
B1
Releasing resource
C
Traceback (most recent call last):
  File "main.py", line 35, in <module>
    [print(i) for i in fn()]
  File "main.py", line 35, in <listcomp>
    [print(i) for i in fn()]
  File "main.py", line 33, in fn
    yield from o()
  File "main.py", line 22, in _fn
    yield from b
  File "main.py", line 10, in do_stuff
    raise Exception('Something happened!')
Exception: Something happened!
person Andrej Kesely    schedule 25.07.2019
comment
Это хорошо справляется с управлением исключениями, но я думаю, что у него также есть недостаток, заключающийся в том, что промежуточная функция является всего лишь одним генератором, таким как do_stuff, тогда как на самом деле это означает сложный список операторов, как типичное тело с заявлением может быть что угодно. - person Lambda Mu; 25.07.2019