Введение: предсказуемая случайность модуля random

В работе программиста часто нужны случайные числа: перемешать элементы списка, выбрать случайного победителя или сгенерировать тестовые данные. Для всего этого в Python есть встроенный модуль random.

Главное, что нужно о нем знать: числа, которые он генерирует, на самом деле не случайны. Они псевдослучайны. Это значит, что их создает предсказуемый алгоритм. Если дать ему одну и ту же "отправную точку" (называемую seed), он всегда будет создавать одну и ту же последовательность чисел.

Это не баг, а полезная особенность. Благодаря этому вы можете получать одинаковые "случайные" результаты снова и снова, что критически важно для отладки программ и воспроизводимости тестов.

Важное правило: никогда не используйте random для задач, связанных с безопасностью, например, для генерации паролей, токенов или ключей шифрования. Он предсказуем, а значит, небезопасен. Для таких целей в Python есть специальный модуль secrets.

В этой статье мы разберем:

  • Как работает random.seed() и почему это важно.

  • Основные функции: от randint до choice и shuffle.

  • Практические примеры, где это пригодится.

В конце статьи вас ждет домашнее задание на GitHub с автоматической проверкой.

2. Под капотом: как random.seed() управляет хаосом

Как мы уже выяснили, «случайность» в модуле random — это иллюзия, создаваемая алгоритмом. Ключ к управлению этой иллюзией — функция random.seed().

Что такое seed?

Представьте, что генератор случайных чисел — это огромная книга с последовательностями чисел. seed (в переводе «семя» или «зерно») — это номер страницы, с которой алгоритм начинает читать числа.

  • Если seed не указан, Python использует в качестве него какое-то постоянно меняющееся значение, например, текущее системное время. Поэтому при каждом запуске программы вы получаете новую, непредсказуемую последовательность.

  • Если вы задаете seed вручную, например, random.seed(42), вы приказываете алгоритму каждый раз начинать с одной и той же «страницы». В результате вы всегда будете получать одинаковую последовательность «случайных» чисел.

Зачем это нужно?

Воспроизводимость — это не недостаток, а мощный инструмент для разработчика.

  1. Отладка: Если ваша программа падает из-за определённой комбинации случайных данных, вы можете задать seed, чтобы гарантированно воспроизвести эту ошибку и исправить её.

  2. Тестирование: При написании тестов вы хотите, чтобы они были стабильными. Зафиксировав seed, вы обеспечиваете, что тесты всегда будут работать с одинаковым набором «случайных» входных данных.

  3. Научные расчёты: В машинном обучении или научных симуляциях seed позволяет другим исследователям в точности повторить ваши эксперименты и проверить результаты.

Давайте посмотрим на код:

import random

# --- Пример 1: Без seed ---
# При каждом запуске этого кода результат будет разным
print("Без использования seed:")
print(random.randint(1, 100))
print(random.randint(1, 100))
print("-" * 20)


# --- Пример 2: С одинаковым seed ---
# Этот блок кода всегда будет выводить одни и те же числа: 82 и 15
print("С использованием seed=42:")
random.seed(42)
print(random.randint(1, 100)) # Всегда 82
print(random.randint(1, 100)) # Всегда 15
print("-" * 20)

# Если мы сбросим seed на то же значение, последовательность начнется заново
print("Снова используем seed=42:")
random.seed(42)
print(random.randint(1, 100)) # Опять 82
print(random.randint(1, 100)) # Опять 15

Как видите, random.seed() дает вам полный контроль над псевдослучайностью. Используйте это для создания предсказуемого и тестируемого кода.

3. Инструментарий random: Обзор основных функций

Теперь, когда мы разобрались с seed, давайте рассмотрим основные инструменты, которые предоставляет модуль random. Их можно условно разделить на две категории: функции для генерации чисел и функции для работы с последовательностями (например, списками).

Часть 1: Генерация чисел

Эти функции создают числа в заданных диапазонах.

  • random.random()
    Возвращает случайное число с плавающей точкой в полуинтервале [0.0, 1.0). Это значит, что оно может быть равно 0.0, но всегда будет строго меньше 1.0. Это одна из базовых функций, на основе которой работают многие другие.

    # Получаем случайное число, например, 0.7634...
    value = random.random()
    print(value)
    
  • random.randint(a, b)
    Возвращает случайное целое число из отрезка [a, b]. Важно: оба конца диапазона, a и b, включаются в выборку. Это, пожалуй, самая часто используемая функция.

    # Бросаем шестигранный кубик
    dice_roll = random.randint(1, 6)
    print(f"Выпало: {dice_roll}")
    
  • random.uniform(a, b)
    Аналог randint, но для чисел с плавающей точкой. Возвращает случайное число в диапазоне [a, b] или [b, a].

    # Получаем случайную температуру в диапазоне
    temperature = random.uniform(15.5, 25.5)
    print(f"Температура: {temperature:.2f} °C")
    

Часть 2: Работа с последовательностями

Эти функции помогают выбирать элементы из последовательностей, перемешивать их или создавать случайные выборки.

  • random.choice(sequence)
    Выбирает и возвращает один случайный элемент из непустой последовательности (например, списка или кортежа).

    players = ["Анна", "Борис", "Виктор", "Дарья"]
    winner = random.choice(players)
    print(f"Победитель: {winner}")
    
  • random.shuffle(x)
    Перемешивает элементы последовательности x случайным образом. Внимание: функция изменяет саму последовательность (работает "in-place") и ничего не возвращает (возвращает None). Не пытайтесь присвоить её результат переменной.

    cards = ["Туз", "Король", "Дама", "Валет"]
    print(f"До перемешивания: {cards}")
    
    random.shuffle(cards) # Перемешиваем сам список cards
    
    print(f"После перемешивания: {cards}")
    
  • random.sample(population, k)
    Возвращает новый список длиной k, содержащий уникальные элементы, выбранные из последовательности population. Исходная последовательность при этом не меняется. Отлично подходит, когда нужно выбрать несколько элементов без повторений.

    # Выбираем 3 случайных уникальных приза
    prizes = ["Кружка", "Футболка", "Блокнот", "Ручка", "Стикер"]
    winners_prizes = random.sample(prizes, k=3)
    print(winners_prizes) 
    # ['Ручка', 'Кружка', 'Футболка'] - порядок случайный
    
  • random.choices(population, weights=None, k=1)
    Самая гибкая из функций выбора. Возвращает список из k элементов, при этом элементы могут повторяться.
    Самая интересная её возможность — необязательный параметр weights. Он позволяет задать «вес» или вероятность для каждого элемента. Список weights должен иметь ту же длину, что и population.

    # Имитация выпадения предмета разной редкости
    loot = ["Обычный", "Редкий", "Эпический"]
    # Шансы: 80% для обычного, 15% для редкого, 5% для эпического
    chances = [0.80, 0.15, 0.05]
    
    # "Открываем" 10 сундуков
    result = random.choices(loot, weights=chances, k=10)
    print(result)
    # ['Обычный', 'Обычный', 'Редкий', 'Обычный', 'Обычный', 'Обычный', 'Эпический', 'Обычный', 'Редкий', 'Обычный']
    

    Эта функция идеально подходит для имитации любых систем, где у разных исходов разные вероятности. Эта функция идеально подходит для имитации любых систем, где у разных исходов разные вероятности.

4. От теории к практике: Нескучные примеры

Знать функции — это хорошо, но настоящая сила модуля random раскрывается, когда мы начинаем комбинировать их для решения реальных задач. Давайте рассмотрим несколько практических примеров.

Пример 1: Генератор паролей

Напишем функцию, которая создает надежный случайный пароль заданной длины, включающий буквы, цифры и специальные символы. Для этой задачи идеально подходит random.choices, так как символы в пароле могут повторяться.

import random
import string

def generate_password(length: int) -> str:
    """Генерирует случайный пароль заданной длины."""
    # 1. Создаем "алфавит" - строку со всеми возможными символами
    all_chars = string.ascii_letters + string.digits + string.punctuation
    
    # 2. Выбираем 'length' символов из нашего алфавита.
    #    random.choices позволяет символам повторяться, что нам и нужно.
    password_list = random.choices(all_chars, k=length)
    
    # 3. Соединяем список символов в одну строку
    return "".join(password_list)

# Генерируем 16-значный пароль
new_password = generate_password(16)
print(f"Сгенерированный пароль: {new_password}")

Этот простой скрипт использует string для получения готовых наборов символов и random.choices для эффективного выбора. Результат — надежный и уникальный пароль при каждом запуске.

Пример 2: Симуляция броска костей

Представим, что мы пишем игру, где нужно имитировать бросок двух шестигранных кубиков (2d6). Здесь нам на помощь придет random.randint.

import random

def roll_dice() -> int:
    """Имитирует бросок двух шестигранных кубиков и возвращает сумму."""
    die1 = random.randint(1, 6)
    die2 = random.randint(1, 6)
    total = die1 + die2
    print(f"Бросок: {die1} + {die2} = {total}")
    return total

# Совершаем бросок
roll_result = roll_dice()

Эта функция не только возвращает результат, но и наглядно показывает, из чего он сложился. Просто и эффективно для моделирования случайных событий с равномерным распределением.

Пример 3 (продвинутый): Оценка числа π методом Монте-Карло

Этот пример демонстрирует, как с помощью случайных чисел можно решать математические задачи. Идея проста:

  1. Представим себе квадрат со стороной 1 (площадь = 1).

  2. Внутрь него впишем четверть круга с радиусом 1 (площадь = π * 1² / 4 = π/4).

  3. Начнем наугад "бросать дротики" в этот квадрат, генерируя случайные точки (x, y) внутри него.

  4. Соотношение числа дротиков, попавших в четверть круга, к общему числу бросков будет примерно равно соотношению их площадей: (π/4) / 1.

  5. Отсюда мы можем выразить π: π ≈ 4 * (точки_в_круге / все_точки).

Для генерации координат точек идеально подходит random.random().

import random

def estimate_pi(num_points: int) -> float:
    """Оценивает число Пи методом Монте-Карло."""
    points_in_circle = 0
    
    for _ in range(num_points):
        # Генерируем случайную точку (x, y) в квадрате 1x1
        x = random.random()
        y = random.random()
        
        # Проверяем, находится ли точка внутри круга с радиусом 1
        # по формуле x² + y² <= r² (где r=1)
        if x**2 + y**2 <= 1:
            points_in_circle += 1
            
    # Вычисляем Пи по формуле
    return 4 * points_in_circle / num_points

# Чем больше точек, тем точнее результат
pi_estimate = estimate_pi(1_000_000)
print(f"Примерная оценка числа Пи: {pi_estimate}")

Этот пример показывает, что модуль random — это не просто игрушка, а мощный инструмент для численного моделирования и решения сложных задач.

5. Практика: Закрепляем знания на GitHub

Теория — это важно, но без практики она быстро забывается. Настоящее понимание приходит только тогда, когда вы начинаете применять знания для решения реальных задач.

Поэтому я подготовил для вас репозиторий на GitHub, где вас ждут 5 практических задач по темам, которые мы только что разобрали.

Ссылку оставил тут.

Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.

Уверен, у вас все получится. Вперед, к практике!

Комментарии (0)