0

Что делает yield без значения в контекстном менеджере?

20

Заголовок: Когда выполняется функция 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 ответ(ов)

0

Выражение yield возвращает управление тому, кто использует генератор. В этот момент генератор приостанавливается, что позволяет декоратору @contextmanager понять, что код завершил выполнение части инициализации.

Другими словами, все, что вы хотите сделать в фазе __enter__ контекстного менеджера, должно быть выполнено до yield.

Когда ваш контекст завершается (то есть блок кода под выражением with закончен), декоратор @contextmanager вызывает метод __exit__ для завершения работы контекстного менеджера и выполняет одно из двух действий:

  • Если исключения не было, он возобновляет выполнение вашего генератора. Генератор продолжает исполнение с той точки, где он был приостановлен (строка с yield), и переходит в фазу очистки.
  • Если было сгенерировано исключение, декоратор использует generator.throw(), чтобы вызвать это исключение в генераторе. Будет казаться, что это исключение было вызвано именно строкой yield. Так как у вас есть блок finally, он будет выполнен перед выходом вашего генератора из-за исключения.

Таким образом, в вашем конкретном примере последовательность действий следующая:

  1. with time_print("processes"):

    Это создает контекстный менеджер и вызывает метод __enter__ у него.

  2. Генератор начинает выполнение, выполняется строка t = time.time().

  3. Выражение yield приостанавливает работу генератора, управление передается обратно декоратору. Оно возвращает то, что было выдано, обратно в выражение with, в случае, если есть часть as target. Здесь None возвращается (т.к. выражение yield простое).

  4. Выполняется [doproc() for _ in range(500)] и завершает свою работу.

  5. Выполняется метод __exit__ контекстного менеджера, исключение в него не передается.

  6. Декоратор возобновляет генератор, он продолжает с места, где был приостановлен.

  7. Вход в блок finally: и выполнение print(task_name, "took", time.time() - t, "seconds.").

  8. Генератор завершает выполнение, метод __exit__ декоратора заканчивает свою работу, все сделано.

0

Отличное объяснение от @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. Надеюсь, это поможет!

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь