Что такое функция-генератор (generator function) в Python?

Ответ

Функция-генератор в Python — это специальная функция, которая возвращает итератор (генератор) и позволяет лениво генерировать последовательность значений. Вместо ключевого слова return она использует yield. При каждом вызове next() у генератора функция выполняется до следующего yield, возвращает значение и приостанавливается, сохраняя своё состояние до следующего вызова.

Зачем это нужно в тестировании? Генераторы полезны для создания больших или бесконечных потоков тестовых данных без загрузки всей последовательности в память сразу.

Пример: Генератор тестовых данных для проверки граничных значений:

def generate_test_ids(start=1, step=100):
    """Генератор, который выдаёт идентификаторы для тестов."""
    current_id = start
    while True:  # Бесконечная последовательность
        yield current_id
        current_id += step

# Использование в тесте
id_generator = generate_test_ids()

test_data = [
    (next(id_generator), "valid"),   # 1
    (next(id_generator), "valid"),   # 101
    (0, "invalid"),                  # Граничное значение 0
    (-1, "invalid"),                 # Отрицательное значение
]

@pytest.mark.parametrize("user_id, expected_status", test_data)
def test_user_by_id(api_client, user_id, expected_status):
    response = api_client.get(f"/users/{user_id}")
    if expected_status == "valid":
        assert response.status_code == 200
    else:
        assert response.status_code == 404

Пример: Генератор для чтения большой тестовой лог-файла построчно:

def read_large_log_file(file_path):
    """Генератор для ленивого чтения большого файла построчно."""
    with open(file_path, 'r') as file:
        for line in file:
            # Можно добавить фильтрацию
            if "ERROR" in line:
                yield line.strip()  # Возвращаем только строки с ошибками

# В тесте проверяем наличие критических ошибок в логе
def test_log_has_no_critical_errors():
    error_lines = list(read_large_log_file("app.log"))
    for error in error_lines:
        assert "CRITICAL" not in error, f"Found critical error: {error}"

Ключевые отличия от обычной функции:

  • Ленивые вычисления: Значения генерируются по требованию.
  • Экономия памяти: Не нужно хранить всю последовательность в памяти.
  • Сохранение состояния: Функция "помнит" своё место между вызовами.

Ответ 18+ 🔞

А, генераторы в питоне! Ну это же просто ёперный театр, я тебе щас объясню, в чём их магия, а то народ иногда нихуя не понимает, зачем это нужно.

Смотри, обычная функция — она как бухающий сосед: раз — и всё вывалил тебе на голову, весь свой бред, и пошёл спать. А генератор — он хитрая жопа, он тебе выдаёт информацию по чуть-чуть, как будто подкармливает с ложечки. Вместо того чтобы наорать return и сбросить всё сразу, он говорит yield, типа «на, держи одну штуку, а я тут постою, подожду, пока ты за следующей придёшь».

Ну и нахуя это в тестах? Представь, тебе надо проверить овердохуища пользовательских айдишников. Если ты их все в список запихнёшь — память сожрёшь, как не в себя. А генератору похуй, он их тебе будет выдавать по одному, прямо из печки горяченькие, и память цела.

Вот, смотри, как это для тестовых данных работает:

def generate_test_ids(start=1, step=100):
    """Генератор, который выдаёт идентификаторы для тестов."""
    current_id = start
    while True:  # Бесконечная последовательность
        yield current_id
        current_id += step

# Использование в тесте
id_generator = generate_test_ids()

test_data = [
    (next(id_generator), "valid"),   # 1
    (next(id_generator), "valid"),   # 101
    (0, "invalid"),                  # Граничное значение 0
    (-1, "invalid"),                 # Отрицательное значение
]

@pytest.mark.parametrize("user_id, expected_status", test_data)
def test_user_by_id(api_client, user_id, expected_status):
    response = api_client.get(f"/users/{user_id}")
    if expected_status == "valid":
        assert response.status_code == 200
    else:
        assert response.status_code == 404

Видишь? Мы не создаём список из миллиона айдишников. Мы просто говорим генератору: «давай, следующий!», и он нам его подкидывает. Удобно, блядь, волнение ебать как удобно.

Или вот ещё пример, чтоб совсем въехать. Допустим, лог-файл размером с твою совесть — гигабайт на десять. Читать его весь — это самоубийство.

def read_large_log_file(file_path):
    """Генератор для ленивого чтения большого файла построчно."""
    with open(file_path, 'r') as file:
        for line in file:
            # Можно добавить фильтрацию
            if "ERROR" in line:
                yield line.strip()  # Возвращаем только строки с ошибками

# В тесте проверяем наличие критических ошибок в логе
def test_log_has_no_critical_errors():
    error_lines = list(read_large_log_file("app.log"))
    for error in error_lines:
        assert "CRITICAL" not in error, f"Found critical error: {error}"

Генератор открывает файл и читает его по одной строке за раз. Не грузит весь файл в оперативку, а аккуратно, как заправский вор, вытаскивает только то, что нужно — строки с «ERROR». Память не ебёт, работает быстро. Красота, ёпта!

Так в чём же, блядь, соль? Чем он отличается от обычной функции?

  • Ленивые вычисления: Не делает всё и сразу. Сидит, ждёт команды. Как ленивый студент на паре: работает, только когда на него смотрят.
  • Экономия памяти: Не хранит всю хуйню в памяти разом. Выдал значение — забыл, живёт дальше.
  • Сохранение состояния: Это вообще магия. После yield он засыпает, но помнит, на каком месте остановился. Просыпается по вызову next() и продолжает ровно с того же места, будто ничего и не было. Как будто у него память, блядь, золотая рыбки, только в хорошем смысле.

Вот и вся история. Генератор — это не функция, а такой хитрый поставщик данных, который не парится и работает ровно столько, сколько надо.