Что делает yield без значения в контекстном менеджере?
Заголовок: Когда выполняется функция doproc при использовании контекстного менеджера на Python?
Я использую контекстный менеджер для замера времени выполнения группы процессов. Вот мой код:
import contextlib
import time
@contextlib.contextmanager
def time_print(task_name):
t = time.time()
try:
yield
finally:
print(task_name, "took", time.time() - t, "seconds.")
def doproc():
x = 1 + 1
with time_print("processes"):
[doproc() for _ in range(500)]
# processes took 15.236166954 seconds.
Вопрос: Когда именно выполняется функция doproc
в этом коде при использовании контекстного менеджера time_print
? Я не совсем понимаю, в какой части программы происходит выполнение doproc
, и как это влияет на замер времени. Может ли кто объяснить это?
2 ответ(ов)
Выражение yield
возвращает управление тому, кто использует генератор. В этот момент генератор приостанавливается, что позволяет декоратору @contextmanager
понять, что код завершил выполнение части инициализации.
Другими словами, все, что вы хотите сделать в фазе __enter__
контекстного менеджера, должно быть выполнено до yield
.
Когда ваш контекст завершается (то есть блок кода под выражением with
закончен), декоратор @contextmanager
вызывает метод __exit__
для завершения работы контекстного менеджера и выполняет одно из двух действий:
- Если исключения не было, он возобновляет выполнение вашего генератора. Генератор продолжает исполнение с той точки, где он был приостановлен (строка с
yield
), и переходит в фазу очистки. - Если было сгенерировано исключение, декоратор использует
generator.throw()
, чтобы вызвать это исключение в генераторе. Будет казаться, что это исключение было вызвано именно строкойyield
. Так как у вас есть блокfinally
, он будет выполнен перед выходом вашего генератора из-за исключения.
Таким образом, в вашем конкретном примере последовательность действий следующая:
with time_print("processes"):
Это создает контекстный менеджер и вызывает метод
__enter__
у него.Генератор начинает выполнение, выполняется строка
t = time.time()
.Выражение
yield
приостанавливает работу генератора, управление передается обратно декоратору. Оно возвращает то, что было выдано, обратно в выражениеwith
, в случае, если есть частьas target
. ЗдесьNone
возвращается (т.к. выражениеyield
простое).Выполняется
[doproc() for _ in range(500)]
и завершает свою работу.Выполняется метод
__exit__
контекстного менеджера, исключение в него не передается.Декоратор возобновляет генератор, он продолжает с места, где был приостановлен.
Вход в блок
finally:
и выполнениеprint(task_name, "took", time.time() - t, "seconds.")
.Генератор завершает выполнение, метод
__exit__
декоратора заканчивает свою работу, все сделано.
Отличное объяснение от @Martijn Pieters. Поскольку yield в вашем случае избыточен, вы можете достичь того же результата, создав свой собственный менеджер контекста (без yield и contextlib.contextmanager). Это проще и более читаемо. В вашем случае вы можете реализовать что-то вроде следующего:
import time
class time_print(object):
def __init__(self, task_name):
self.task_name = task_name
def __enter__(self):
self.t = time.time()
def __exit__(self, exc_type, exc_val, exc_tb):
print(self.task_name, "took", time.time() - self.t, "seconds.")
def doproc():
x = 1 + 1
with time_print("processes"):
# вызывается __enter__
[doproc() for _ in range(500)]
# вызывается __exit__
Внутренне contextlib.contextmanager вызывает эти магические функции enter и exit, как объяснил @Martijn Pieters. Надеюсь, это поможет!
Объяснение '__enter__' и '__exit__' в Python
Значение оператора "with" без ключевого слова "as"
Python выборка с помощью генератора / итератора / итерируемого объекта
Python: использовать `yield from` или вернуть генератор?
Как работает это выражение с лямбдой/yield/генератором?