Что делает 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/генератором?