Когда в языках программирования речь заходит о случайных числах, то подчеркивается, что производящие их "генераторы" (специальные функции ил�� объекты) выдают на самом деле псевдослучайные значения. Они, исходя из начального так называемого "зерна", вычисляются по определенной формуле. Таким образом, в самой программе настоящей случайности нет, потому что если известны инициирующие данные и алгоритм, все остальные числа предопределены. Нам значения кажутся случайными лишь потому, что мы не ведаем ни зерен, ни формул.

В Си стартовое зерно задано. В примере ниже каждый раз при выполнении программы будет выводиться один и тот же ряд чисел.

#include <stdio.h>
#include <stdlib.h>

int main() {
    for (int i = 1; i <= 5; i++) {  // пять раз
        printf("%d ", rand());  // rand() генерирует число
    }
}

Первый и n-ный запуски программы на выполнение:

1804289383 846930886 1681692777 1714636915 1957747793
1804289383 846930886 1681692777 1714636915 1957747793

Зерно можно переопределить, передав свое число в функции srand, и ряд будет другим, но все-равно при каждом выполнении программы одинаковым. Поэтому в качестве инициирующего значения часто используют системное время, которое будет взято в момент запуска программы. Разные моменты исполнения — разное системное время.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    srand(time(NULL));  // установка зерна
    for (int i = 1; i <= 5; i++)
        printf("%d ", rand());
}

Первый и n-ный запуски:

1370043538 851170309 353619136 1517173109 1831772572
1055320706 651477646 2063951383 44502261 1674710625

Конечно, время можно подкрутить, но для пользовательских приложений (не связанных с криптографией и безопасностью) такой случайности обычно достаточно.

В Python нужды менять зерно нет (но если надо задать явно, то соответствующая функция есть как в модуле random, так и библиотеке NumPy), потому что сам интерпретатор берет его из внешних источников при выполнении программы. Так в Linux это "шум" (энтропия), полученный из драйверов устройств. Предсказать и повторить его сложнее, чем системное время. Поэтому можно говорить о почти истинной случайности.

from random import random

for i in range(5):  # пять раз
    value = random()  # получаем случайное число
    print(round(value, 2), end=' ')  # округляем и выводим

Первый и n-ный запуски:

0.89 0.21 0.45 0.71 0.35
0.56 0.27 0.7 0.96 0.05

Сейчас нас не интересует вопрос, как сделать числа более случайными. Обратим внимание, как в Си явно, так и Python неявно, чтобы в программе завелась непредсказуемая и неповторяемая для нее случайность, зерно должно быть получено извне. Программа о том, что происходит во внешней для нее среде, ничего не знает. Система, из которой она получает данные, выступает для нее черным ящиком. Чтобы в программе появилась истинная для нее случайность, и она, и черный ящик должны быть открытыми и обмениваться данными.

Когда мы подкидываем монету, сторона ее падения предопределена в момент броска (флуктуации на тему "вдруг подул ветер" мы игнорируем). Это только для нас орел или решка — случайность. Мы как ограниченная в своих возможностях "программа" не знаем, как влияет сила подкидывания, угол, исходное положение и другое на конечный результат, не измеряем их. Вся эта физика для нас играет роль черного ящика.

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

Обычно более крупные системы включают в себя другие в качестве составных компонентов. Например, организм — это взаимодействующие между собой системы органов (дыхательная, нервная и др.); каждая в свою очередь состоит из отдельных органов. Представим, что в модельной системе-программе ее подсистемы-функции обмениваются данными, выполняющими роль зерен для получателей.

import numpy as np

def first(s):
    # Используем внешние данные как зерно для этой системы:
    np.random.seed(s)
    # На самом деле зерно меняется для всей программы,
    # но приходится этим пренебречь.
    # Получаем состояние системы, оно предопределено зерном:
    a = np.random.randint(58934, size=5)
    print('a:', a)
    # Система может передать любой элемент своих данных в другую:
    second(np.random.choice(a))

def second(s):
    np.random.seed(s)
    b = np.random.randint(100, 1000, size=10)
    print('b:', b)
    third(np.random.choice(b))

def third(s):
    np.random.seed(s)
    c = np.random.randint(-100, 100, size=5)
    print('c:', c)
    q = input('Вызвать First? ')
    if q in ('y', 'Y', 'Yes', 'yes'):
        first(abs(np.random.choice(c)))

main_seed = np.random.randint(100)
first(main_seed)
a: [43707  6482 55488 34149 50499]
b: [100 379 621 703 310 668 759 846 128 872]
c: [ 24  -7 -99 -28  44]
Вызвать First? y
a: [42241  4089 26117 55284  7200]
b: [151 315 870 274 409 801 620 830 191 645]
c: [  7 -48 -97  63  80]
Вызвать First? n

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

При всем этом при каждом запуске нашей программы будут генерироваться разные числа. Причина кроется в том, что сама программа все еще остается открытой системой. Когда main_seed присваивается случайное число, до этого в самой программе зерно еще нигде не задавалось. Следовательно, раз мы имеем дело с Python, оно было взято из внешних источников. В результате при каждом запуске main_seed имеет разное из сотни значение. Но если мы "закроем" систему вызовом функции seed в основной ветке, то при каждом выполнении программы будем получать одни и те же массивы. (Здесь не имеются в виду повторные вызовы функций в процессе одного выполнения. Они будут выдавать разные массивы, так в них будет передаваться отличное от предыдущего зерно.)

Представим, что на каком-то уровне объединения подсистем в единую систему было включено все, что только можно. Ничего за ее пределами больше не существует. В таком случае ей больше не с чем обмениваться данными. Зерно больше нельзя получить извне. Это значит, оно должно быть задано в момент запуска этой программы и предопределит весь ход ее выполнения. Кроме того, не будем забывать, что вариаций зерен может оказаться ограниченное количество, поэтому и способов развития системы не бесконечно.

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