Пары из списка

В Python есть элегантный прием, который позволяет получить пары соседних элементов из списка. Нужно использовать функцию zip, передав в нее сам список и его же со сдвигом 1:

a = [1, 2, 3, 4, 5, 6]

for x1, x2 in zip(a, a[1:]):
    print(x1, x2)

Вывод:

1 2
2 3
3 4
4 5
5 6

Специально для канала @pyway.

🎲 Великий random

Генераторы случайных чисел (аббр. ГСЧ или RNG) можно разделить на псевдослучайные генераторы (pseudo random number generator – PRNG) и настоящие генераторы (true random number generator – TRNG). Настоящие случайное число может быть получено, например, честным бросанием (без мухлежа) игрального кубика. Но, цифровая техника, в т.ч. и компьютер — вещь точная и детерминированная. И нет так очевидно, где нам там брать случайные числа. Да, бывают аппаратные ГСЧ, построенные на аналоговых шумах или квантовых эффектах, но они не всегда доступны простым пользователям. Однако математики разработали алгоритмы, по которым можно с помощью простых и точных операций (типа сложения и деления) получать «иллюзию» случайности.

Давайте для начала рассмотрим линейный конгруэнтный метод и попробуем сконструировать свой рандом. Все начинается с зерна (seed). x[0] = seed. Следующие случайное число будет равно x[i + 1] = (a * x[i] + b) mod c. Каждое из них будет в пределах [0..c). Вот реализация:

class MyRandom:
    def __init__(self, seed=42):
        self._state = seed

    def random(self):
        self._state = (5 * self._state + 9) % 17
        return self._state


r = MyRandom(42)
print([r.random() for _ in range(10)])
# [15, 16, 4, 12, 1, 14, 11, 13, 6, 5]

r2 = MyRandom(24)
print([r2.random() for _ in range(10)])
# [10, 8, 15, 16, 4, 12, 1, 14, 11, 13]

r3 = MyRandom(42)
print([r3.random() for _ in range(10)])
# [15, 16, 4, 12, 1, 14, 11, 13, 6, 5]

Первое. Последовательности кажутся случайными, но на самом деле качество их невелико. Через некоторые время числа начинают повторятся. Последовательность периодична. Второе. Наш псевдослучайный генератор выдает одинаковые последовательности для одинаковых seed. Алгоритм детерминирован. Последнее свойство бывает вредно и полезно. Представим, что вы проводите эксперимент. Допустим, учите нейросеть. Инициализировав веса случайными числами, вы получаете какой-то результат. Далее вы меняете что-то в архитектуре сети и запускаете снова, и получаете иной результат. Но как убедиться, повлияли ли ваши изменения в коде, или просто иная случайная инициализация изменила результат. Имеет смысл зафиксировать seed генератора случайных чисел константой в начале программы. При следующем запуске мы получим точно такую же инициализацию сети, как и в предыдущем.

Но, если мы не хотим повторяемости, то можно инициализировать генератор какой-то меняющейся от запуска к запуску переменной (например, временем):

import time
r4 = MyRandom(int(time.time()))
print([r4.random() for _ in range(10)])
# [3, 7, 10, 8, 15, 16, 4, 12, 1, 14]

Для получение случайных величин в Python есть несколько способов. Мы рассмотрим следующие:

• Встроенный модуль random
• numpy.random из библиотеки NumPy
• Функцию os.urandom
• Встроенный модуль secrets
• Встроенный модуль uuid

Модуль random

Самый популярный вариант: модель встроенный random. Модуль random предоставляет набор функций для генерации псевдослучайных чисел. Реализована генерация на языке Си (исходник) по более хитрому алгоритму «вихрь Мерсенна», разработанному в 1997 году. Он дает более «качественные» псевдослучайные числа. Но они по-прежнему получается из начального зерна (seed) путем совершения математических операций. Зная seed и алгоритм можно воспроизвести последовательность случайных чисел; более того существуют алгоритмы позволяющие вычислить из последовательности чисел ее seed. Поэтому такие алгоритмы не пригодны для генерации конфиденциальных данных: паролей, и ключей доступа. Но он вполне сгодится для генерации случайностей в играх (не азартных) и прочих приложений, где не страшно, если кто-то сможет воспроизвести и продолжить последовательностей случайных чисел. Воспроизводимость случайностей поможет вам в задачах статистики, в симуляциях различных процессов.

Приступим:

>>> import random

random.seed(new_seed) – сброс ГСЧ с новым seed:

>>> random.seed(4242)
>>> random.random()
0.8624508153567833
>>> random.random()
0.41569372364698065

>>> random.seed(4242)
>>> random.random()
0.8624508153567833
>>> random.random()
0.41569372364698065

Когда мы второй раз задали тот же seed, ГСЧ выдает точно такие же случайные числа. Если мы не задаем seed, то ГСЧ будет скорее всего инициализирован системным временем, и значения будут отличаться от запуска к запуску.

random.randint(a, b) – случайное целое число от a до b (включительно):

>>> random.randint(5, 8)
5
>>> [random.randint(5, 8) for _ in range(10)]
[6, 8, 5, 8, 6, 6, 8, 5, 5, 6]

random.randrange(a, b, step) – случайное целое число от a до b (не включая b) с шагом step. Аргументы имеют такой же смысл, как у функции range. Если мы зададим только a, получим число в [0, a) с шагом 1; если задаем a и b, то в число будет в диапазоне [a, b):

>>> [random.randrange(10) for _ in range(5)]
[9, 3, 7, 0, 4]
>>> [random.randrange(10, 20) for _ in range(5)]
[15, 10, 15, 12, 18]
>>> [random.randrange(10, 20, 2) for _ in range(5)]
[14, 14, 18, 16, 16]

random.choice(seq) – выбирает из последовательности seq случайный элемент. Последовательность должна иметь длину (len). Например list, tuple, range – подойдут, а произвольные генераторы – нет.

>>> alist = [1, 2, 3, 4, 5, 6]
>>> random.choice(alist)
5
>>> random.choice(alist)
3
>>> random.choice(alist)
1

random.choices(population, weights=None, *, cum_weights=None, k=1) – позволяет выбрать k элементов из population. Выбранные элементы могут повторяться. Можно задать веса каждого элемента через weight, или кумулятивные веса через cum_weights. Веса определяют вероятность соответствующего элемента быть выбранным. Если мы не задали никакие веса, то любой элемент считается равновероятным. Кумулятивные веса – это значит, каждый следующий вес является суммой предыдущего и некоторой добавки, которая и есть вес соответствующего элемента. Пример: weights=[10, 5, 30, 5] эквивалентно cum_weights=[10, 15, 45, 50], причем последний вариант предпочтительнее, так как с кумулятивными весами функция работает быстрее.

>>> random.choices([1, 2, 3], k=10)
[1, 3, 1, 1, 2, 2, 1, 3, 3, 1]

📎 Пример. Выбор с весами (80% шанс получить 1, 15% для 2 и 5% для 3):

>>> random.choices([1, 2, 3], k=10, weights=[80, 15, 5])
[1, 1, 1, 1, 2, 1, 3, 1, 1, 1]

📎 Пример. Генерация случайной строки:

>>> import string
>>> ''.join(random.choices(string.ascii_letters, k=10))
'ncNAzTldvg'

random.shuffle(x) – перемешивает саму последовательность x, ничего не возвращает.

>>> x = [10, 20, 30, 40]
>>> random.shuffle(x)
>>> x
[10, 40, 20, 30]
>>> random.shuffle(x)
>>> x
[20, 30, 10, 40]

Если последовательность неизменяема (например, кортеж), то используйте random.sample(x, k=len(x)), которая вернет перемешанный список, не трогая исходную последовательность.

>>> random.sample(x, k=len(x))
[40, 30, 10, 20]

random.random() – случайное вещественное число от 0.0 до 1.0, не включая 1.0, т.е. в диапазоне [0, 1). Равновероятное распределение.

>>> random.random()
0.8505907349159074
>>> random.random()
0.49760476981102786

random.uniform(a, b) – случайное вещественное число на промежутке [a, b], равноверотяно.

>>> random.uniform(5, 7)
6.812839982463059
>>> random.uniform(5, 7)
6.564395491702289
>>> random.uniform(5, 7)
5.875898672403455

random.gauss(mu, sigma) и random.normalvariate(mu, sigma) нормальные распределения с медианой μ и с среднеквадратичным отклонением σ .

Нормальные распределения

random.triangular(low, high, mode) – треугольное разпределние от low до high с модой mode ∈ [low, high].

Треугольные распределения

random.betavariate(alpha, beta)бета-распределение.

Бета-распределения

random.expovariate(lambd)экспоненциальное распределение.

random.gammavariate(alpha, beta)гамма-распределение (не путать с гамма-функцией).

Гамма-распределения

random.lognormvariate(mu, sigma)логнормальное распределение. Если случайная величина имеет логнормальное распределение, то её логарифм имеет нормальное распределение.

Логнормальные распределения

random.vonmisesvariate(mu, kappa) – распределение вон Мизеса (также известное как круглое нормальное распределение или распределение Тихонова) является непрерывным распределением вероятности на круге.

Распределения вон Мизеса

random.paretovariate(alpha)распределение Парето.

Распределения Парето

random.weibullvariate(alpha, beta)распеделение Вейбулла.

Распределения Вейбулла

Внутри модуля random скрывается класс Random. Можно создавать экземпляры этого класса, которые не будут делить состояние с остальными функциями random. Этот класс содержит методы с аналогичными названиями, что и функции модуля:

>>> my_random = random.Random(42)
>>> my_random.normalvariate(1, 2.5)
1.6133158542696586
>>> my_random.random()
0.27502931836911926
>>> my_random.choice([1, 2, 3])
1

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

Класс random.SystemRandom() – альтернативные класс для случайных чисел, который берет случайные числа не из встроенного алгоритма, а из системного os.urandom, о котором будет рассказано в конце статьи.

Случайные числа в библиотеке NumPy

ГСЧ из NumPy пригодится на случай необходимости генерации случайных многомерных массивов.

numpy.random.seed(n) – задать seed для ГСЧ.

rand(d0, d1, …, dn) – многомерный массив случайных вещественных чисел в диапазоне [0, 1). Размерности указываются через запятую.

>>> import numpy as np
>>> np.random.rand(3, 2)
array([[0.10249247, 0.21503386],
       [0.40189789, 0.23972727],
       [0.28861301, 0.12995166]])

randn(d0, d1, …, dn) – тоже, что и rand, но случайные числа будут распределены нормально вокруг 0 со СКО = 1.

>>> np.random.randn(3, 2)
array([[ 1.13506644,  1.1115104 ],
       [-0.43613352, -0.03630799],
       [ 0.69787228,  1.24875159]])

randint(low[, high, size, dtype]) – случайные целые числа в диапазоне [low, high) в многомерном массиве размера size (целое число или кортеж размерностей).

>>> np.random.randint(10, 20, 5)
array([18, 18, 10, 19, 15])
>>> np.random.randint(10, 20, (3, 2))
array([[10, 13],
       [12, 14],
       [19, 14]])

random_integers(low[, high, size]) – случайные целые числа в диапазоне [low, high] в многомерном массиве размера size (целое число или кортеж размерностей).

>>> np.random.random_integers(10, 20, (3, 2))
array([[10, 20],
       [16, 14],
       [12, 18]])

randint никогда не возвращает верхнюю границу диапазона (high), random_integers – может вернуть и high.

random_sample([size]), random([size]), ranf([size]), sample([size]) – эти четыре функции называются по-разному, но делают одно и тоже. Возвращают многомерный массив случайных вещественных чисел в диапазоне [0, 1). Размерности указываются числом для 1D массива или кортежем для массива большего ранга.

>>> np.random.ranf(3)
array([0.60612404, 0.04881742, 0.17121467])
>>> np.random.sample(4)
array([0.71248954, 0.8613707 , 0.72469335, 0.62528553])
>>> np.random.random_sample((3, 4))
array([[0.39140157, 0.17538846, 0.55895275, 0.58363394],
       [0.52779193, 0.90067421, 0.63571978, 0.62386877],
       [0.52287003, 0.49077399, 0.57247767, 0.15221763]])

numpy.random.choice(a, size=None, replace=True, p=None) – случайно выбирает из 1D массива один и несколько элементов.

a – одномерный массив или число. Если вместо массива – число, то оно будет преобразовано в np.arange(a).

size – размерность возвращаемой величины. По умолчанию size=None, дает один единственный элемент, если size – целое число, то вернется 1D-массив, если size — кортеж, то вернется массив размерностей из этого кортежа.

replace – допускается ли повтор элементов, т.е. «возвращаем ли мы выбранный шар обратно в корзину». По умолчанию – да. Если мы запретим возврат, то мы не сможем извлечь больше элементов, чем есть в исходном массиве.

p – массив вероятностей для каждого элемента быть выбранным. Если не задано, распределение вероятностей равномерно.

📎 Пример. Допуская повторы:

>>> np.random.choice([1, 2, 3, 4], 3)
array([1, 3, 3])

📎 Пример. Не допуская повторы:

>>> np.random.choice([1, 2, 3, 4], 3, replace=False)
array([1, 3, 4])

📎 Пример. Задаем вероятности:

>>> np.random.choice([1, 2, 3, 4], 4, p=[0.1, 0.7, 0.0, 0.2])
array([2, 2, 1, 2])

📎 Пример. Выбор строк:

>>> np.random.choice(["foo", "bar", "dub"])
'dub'
>>> np.random.choice(["foo", "bar", "dub"], size=[2, 2])
array([['bar', 'bar'],
       ['bar', 'dub']], dtype='<U3')

bytes(length) – возвращает length случайных байт.

>>> np.random.bytes(10)
b'\x19~\xd0w\xc2\xb6\xe5M\xb1R'

shuffle(x) и permutation(x) – перемешивают последовательность x. shuffle модифицирует исходную последовательность, а permutation – возвращает новую перемешанную последовательность, не трогая исходную.

>>> x = np.arange(10)
>>> x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

>>> np.random.shuffle(x)
>>> x
array([8, 6, 0, 3, 1, 2, 4, 9, 7, 5])

>>> y = np.random.permutation(x)
>>> y
array([4, 8, 7, 5, 9, 3, 6, 0, 2, 1])

>>> x
array([8, 6, 0, 3, 1, 2, 4, 9, 7, 5])

Также в NumPy имеется еще более богатый выбор различных распределений случайных величин, чем у обычного random. Не будет подробно останавливаться на каждой функции, так как это уже больше статистика, чем программирование. Из названия функций легко понять, какое распределение они представляют. Главная особенность, что у каждый из этих функций есть аргумент size – кортеж размерностей возвращаемого многомерного массива или целое число, если нужен одномерный массив:

  • beta(a, b[, size])
  • binomial(n, p[, size])
  • chisquare(df[, size])
  • dirichlet(alpha[, size])
  • exponential([scale, size])
  • f(dfnum, dfden[, size])
  • gamma(shape[, scale, size])
  • geometric(p[, size])
  • gumbel([loc, scale, size])
  • hypergeometric(ngood, nbad, nsample[, size])
  • laplace([loc, scale, size])
  • logistic([loc, scale, size])
  • lognormal([mean, sigma, size])
  • logseries(p[, size])
  • multinomial(n, pvals[, size])
  • multivariate_normal(mean, cov[, size, …)
  • negative_binomial(n, p[, size])
  • noncentral_chisquare(df, nonc[, size])
  • noncentral_f(dfnum, dfden, nonc[, size])
  • normal([loc, scale, size])
  • pareto(a[, size])
  • poisson([lam, size])
  • power(a[, size])
  • rayleigh([scale, size])
  • standard_cauchy([size])
  • standard_exponential([size])
  • standard_gamma(shape[, size])
  • standard_normal([size])
  • standard_t(df[, size])
  • triangular(left, mode, right[, size])
  • uniform([low, high, size])
  • vonmises(mu, kappa[, size])
  • wald(mean, scale[, size])
  • weibull(a[, size])
  • zipf(a[, size])

📎 Пример. Генерация двух коррелирующих временных рядов из двумерного нормального распределения (multivariate_normal):

import numpy as np
import matplotlib.pyplot as plt


def corr2cov(p: np.ndarray, s: np.ndarray) -> np.ndarray:
    """Ковариационная матрица от корреляции и стандартных отклонений"""
    d = np.diag(s)
    return d @ p @ d


# Начало с корреляционной матрицы и стандартных отклонений
# 0.9 это корреляция между А и B, а корреляция
# самой переменной равна 1.0
corr = np.array([[1., 0.9],
                [0.9, 1.]])

stdev = np.array([3., 1.])
mean = np.array([5., -5.])
cov = corr2cov(corr, stdev)

# `size` это длина временных рядов для 2д данных
data = np.random.multivariate_normal(mean=mean, cov=cov, size=5000)

x, y = data.T

f, (ax1, ax2) = plt.subplots(1, 2)

ax1.plot(x, y, 'x')

ax2.plot(x[:100])
ax2.plot(y[:100])
plt.show()
Коррелирующие временные ряды

Криптографически безопасный ГСЧ

Криптографически безопасный ГСЧ (КБГСЧ) – по-прежнему псевдослучайный и детерминированный генератор, однако он использует широкий набор источников энтропии в системе. Энтропия – мера неопределенности, хаотичности системы. Случайности могут быть получены из

  • Различных системных идентификаторов
  • Времен возникновения разных системных событий в ядре и драйверах
  • Движения мыши, нажатия клавиш и т.п.
  • Аппаратный ГСЧ, например встроенный в процессоры Intel Ivy Bridge.

КБГСЧ в Python базируется на функции os.urandom(), которая в свою очередь использует:

  • Чтение из /dev/urandom на Unix-like системах.
  • CryptGenRandom() функцию на Windows.

Для os.urandom нет понятия seed. Последовательность случайных байт не должна быть воспроизводима. Аргумент функции – число случайных байт.

📎 Пример.

>>> import os
>>> x = os.urandom(10)

# объект типа bytes
>>> x  
b'\xf0\xba\xf8\x86\xb6\xc4Aa*\xe7'

# тоже самое как 16-ричная строка
>>> x.hex()  
'f0baf886b6c441612ae7'

# тоже самое как список чисел
>>> list(x)   
[240, 186, 248, 134, 182, 196, 65, 97, 42, 231]

В стандартной библиотеке Python несколько модулей используют функцию os.urandom:

  • random.SystemRandom() – все функции обычного Random, но источник случайностей – os.urandom
  • модуль secrets – удобства для генерации случайных токенов, ключей и т.п.
  • uuid – генерация токенов по стандарту UUID (Universally Unique IDentifier)

Модуль secrets

По сути – обертка над os.urandom.

  1. secrets.token_bytes – тоже самое, что и os.urandom (по умолчанию, если размер не указан дает 32 байта).
  2. secrets.token_hex – тоже самое, только возвращает 16-ричную строку.
  3. secrets.token_urlsafe – случайная строка, пригодная для URL адресов.
  4. secrets.choice – безопасная версия random.choice

📎 Пример. Укоротитель ссылок:

from secrets import token_urlsafe

DATABASE = {}


def shorten(url: str, nbytes: int = 5) -> str:
    token = token_urlsafe(nbytes=nbytes)
    if token in DATABASE:
        # если уже есть такая ссылка – генерируем еще одну рекурсивно
        return shorten(url, nbytes=nbytes)
    else:
        DATABASE[token] = url
        return 'https://bit.ly/' + token


print(shorten('https://google.com'))
print(shorten('https://yandex.ru'))

# https://bit.ly/vZ1VZug
# https://bit.ly/x966uWI

Ссылки в примеры получились длиннее (7 символов), чем мы просили (5 байт). Это объясняется тем, что внутри token_urlsafe использует кодировку base64, где каждый символ представляет 6 бит данных; чтобы закодировать 5 * 8 = 40 бит, понадобилось как минимум 7 6-битных символов (7 * 6 = 42 бита).

Модуль uuid

UUID (Universally Unique IDentifier) – универсальный уникальный идентификатор, уникальность которого «гарантирована» в пространстве и времени. Имеет длину 128 бит (16 байт). Наиболее интересен для нас вариант uuid4, так как он использует случайность из os.random.

>>> uuid.uuid4()
UUID('cd955a9e-445d-47de-95e2-3d8de8c61696')

>>> u = uuid.uuid4()
>>> u
UUID('7dfb1170-af20-4218-9b76-bc4d7ae6a309')

>>> u.hex
'7dfb1170af2042189b76bc4d7ae6a309'

>>> u.bytes
b'}\xfb\x11p\xaf B\x18\x9bv\xbcMz\xe6\xa3\t' 

>>> len(u.bytes)
16

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

Производительность

Резонный вопрос: почему бы не использовать random.SystemRandom() (или os.urandom) везде, где можно?
Оказывается, есть существенное препятствие. Пул энтропии КБГСЧ ограничен. Если он исчерпан, то придется подождать, пока он заполнится вновь. Проведем небольшой бенчмарк на пропускную способность генераторов случайных чисел:

import random
import timeit

r_secure = random.SystemRandom()
r_common = random.Random()
n_bits = 1024


def prng():
    r_common.getrandbits(n_bits)


def csprng():
    r_secure.getrandbits(n_bits)


setup = 'import random; from __main__ import prng, csprng'

if __name__ == '__main__':
    number = 50000
    repeat = 10
    data_size_mb_bytes = number * repeat * n_bits / (8 * 1024**2)
    for f in ('prng()', 'csprng()'):
        best_time = min(timeit.repeat(f, setup=setup, number=number, repeat=repeat))
        speed = data_size_mb_bytes / best_time
        print('{:10s} {:0.2f} mb/sec random throughput.'.format(f, speed))

Результаты:

prng() 1794.74 mb/sec random throughput.
csprng() 94.13 mb/sec random throughput.

Почти в 20 раз обычный ГСЧ быстрее, чем КБГСЧ.

Вывод: нужна безопасность – обязательно используем secrets, random.SystemRandom, uuid.uuid4 или просто os.urandom, а если нужно много и быстро генерировать неконфиденциальные случайные данные – random и numpy.random.

Специально для канала @pyway.

⛓ Цепочки сравнений

Распространенная ситуация: проверка того, что переменная находится в заданных пределах. Можно было бы использовать логический оператор and:

if x >= 5 and x < 20:

Однако Python предоставляет нам синтаксическое удобство, которое выглядит более «математичным». Такая запись и короче, и понятнее:

if 5 <= x < 20:

В качестве операторов сравнения могут быть любые из списка в любых сочетаниях:

">", "<", "==", ">=", "<=", "!=", "is" ["not"], ["not"] "in"

Т.е. запись вида a < b > c вполне законна, хоть и трудна для понимания.

Формально, если мы имеем N операций OP1…OPN и N + 1 выражений (a, b … y, z), то запись вида:

a OP1 b OP2 c … y OPN z 

Это эквивалентно записи:

a OP1 b and b OP2 c and … and y OPN z

📎 Примеры:

x = 5
print(1 < x < 10)
print(x < 10 < x*10 < 100)
print(10 > x <= 9)
print(5 == x > 4)
a, b, c, d, e, f = 0, 5, 12, 0, 15, 15
print(a <= b < c > d is not e is f)

Специально для канала @pyway.

Итераторы и генераторы

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

Итератор – более общая концепция, чем генератор.

Итератор – это интерфейс доступа к элементам коллекций и потоков данных. Он требует реализации единственного метода – «дай мне следующий элемент». Если вы пишите свой итератор на Python 3 вам нужно реализовать в классе метод __next__. Если элементы исчерпаны итератор возбудит исключение StopIteration.

📎 Пример. Итератор счетчик – выдает числа от low до high:

class Counter:
    def __init__(self, low, high):
        self.current = low
        self.high = high
    def __iter__(self):
        return self
    def __next__(self): 
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

Генератор – это итератор

Генератор – это итератор, но не наоборот. Не любой итератор является генератором.

Есть два способа получить генератор:

📎 1. Генераторное выражение (что-то типа list comprehension, но возвращает генератор, а не список). Используются круглые скобки:

>>> g = (2 * i for i in range(5))
>>> type(g)
<class 'generator'>
>>> next(g)
0
>>> next(g)
2

📎 2. Генераторные функции. Это функции, где есть хотя бы одно выражение yield. Когда мы запускаем генератор, функция выполняет до первого выражения yield. То, что мы передали в yield будет возвращено наружу. Генератор при этом встанет «на паузу» до следующей итерации. При следующей итерации выполнение генератора продолжится до очередного yield.

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

Пример. Генератор чисел Фибоначчи (бесконечный):

def fib():
    a, b = 0, 1
    while 1:
        yield a
        a, b = b, a + b

>>> fib_g = fib()
>>> next(fib_g)
0
>>> next(fib_g)
1
>>> next(fib_g)
1
>>> next(fib_g)
2
>>> next(fib_g)
3
>>> next(fib_g)
5

Вызвав генераторную функцию fib() мы получили генератор. Затем мы итерируем этот генератор функцией next().

Передача данных в генератор

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

send() – отправить данные в генератор. Переданное значение вернется из той конструкции yield, на которой возникла последняя пауза генератора. При этом генератор будет прокручен на один шаг, как если бы мы вызвали next:

val = yield i  # генератор вернет i, но внутри получит val из аргумента метода send

Пример. Этот генератор просто выдает числа от 0 и далее, при этом печатает в поток вывода все, что мы ему отправляем.

def my_gen():
    i = 0
    while True:
        val = yield i
        print('Got inside generator:', val)
        i += 1

>>> g = my_gen()
>>> next(g)
0
>>> g.send("hello")
Got inside generator: hello
1
>>> g.send("world")
Got inside generator: world
2

Обратите внимание, что первый раз нельзя посылать в генератор данные, пока мы не прокрутили его до первого yield. Нужно либо взывать next(g) или g.send(None) – это одно и тоже.

Не будет ошибкой отправлять данные генератору, который не получает их (нет использования значения конструкции yield). Например, нашему генератору fib() можно отравить все, что угодно, он просто проигнорирует.

throw() – бросить исключение внутри генератора. Исключение будет возбуждено из того выражение yield, где генератор последний раз остановился.

>>> g = my_gen()   # my_gen из прошлого примера

>>> g.throw(TypeError, 'my error')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in my_gen
TypeError: my error

close() – закрыть генератор. Бросает внутри генератора особое исключение GeneratorExit. Это исключение, даже если оно не обработано, не распространится в код, вызвавший close(). Но, если мы поймали это исключение внутри генератора, то после закрытия генератора нельзя уже делать yield, рискуя получить RuntimeError. Остальные виды исключений будут распространяться из генератора в код, его вызывающий. Попытка итерировать закрытый итератор приведет к исключению StopIteration (закрытый генератор – пустой итератор).

>>> g = my_gen()
>>> next(g)
0
>>> next(g)
Got inside generator: None
1
>>> g.close()
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Бонус

Как взять из итератора (в том числе из генератора) N первых значений?

Можно, конечно, написать свою функцию. Но зачем, если она уже есть в стандартном модуле itertools. Этот модуль содержит множество вспомогательных функций для работы с итераторами. Нам понадобится itertools.islice. Первый аргумент – итератор (ну или генератор), остальные три – как в range.

>>> list(itertools.islice(fib(), 10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

>>> list(itertools.islice(fib(), 10, 20, 2))
[55, 144, 377, 987, 2584]

В первом примере мы передаем в функцию itertools.islice наш генератор чисел Фибоначчи и число чисел, которые надо вычислить (в нашем случае – 10).

Мы также применяем функцию list, чтобы посмотреть список значений, потому что itertools.islice возвращает не спикок, а именно новый итератор, в котором будут только интересные нам значений из исходного итератора.

Во втором примеры аргументов 4 штуки. В этом случае второй аргумент – начальный номер = 10, третий – конечный номер = 20 – (не включительно), и четвертый – шаг = 2. (Очень похоже на range, не так ли?)

Специально для канала @pyway.

⭐ Звезды в Python

Звездочка (этот символ называется «астериск») – один из самых многоликих операторов в Python. Едва ли хватит пальцев руки, чтобы перечислить все его применения. Давайте по порядку.

Умножение и размножение

Самое простое применение одиночного астериска: умножение чисел. Двойного – возведение числа в степень.

>>> 3 * 4
12
>>> 3 ** 3
27
>>> 4 ** 0.5
2.0

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

>>> [1, 2, 3] * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> ("abc", "foo", "bar") * 3
('abc', 'foo', 'bar', 'abc', 'foo', 'bar', 'abc', 'foo', 'bar')
>>> "hello world" * 4
'hello worldhello worldhello worldhello world'
>>> [1, 2, 3] * 0
[]
>>> "wat" * -10
''

Звезды в аргументах функций

Одна звездочка позволяет получить все или некоторые позиционные аргументы функции в виде кортежа. Позиционные – это те, которые просто подряд передаются без указания имени. Те, что с именем, это аргументы ключевого слова. Разберемся сначала с первыми. Ниже идут два примера. В первом мы получаем все позиционные аргументы в кортеж с названием args. Во втором случае мы обязуем пользователя наших функций передать как минимум 2 аргумента (они попадут в x и y) и дополнительно произвольное число (можно ни одного) аргументов, которые попадут в кортеж rest. Я специально развал их по-разному, не обязательно всегда называть их args. Обратите внимание, что это всегда будет кортеж, даже если мы передадим один аргумент, то получим кортеж из одного элемента.

def foo(*args):
    print('You passed {} args, they are {}'.format(len(args), args))

def foofoo(x, y, *rest):
    print('x = {}, y = {}, rest = {}'.format(x, y, rest))

>>> foo()
You passed 0 args, they are ()
>>> foo(10, 20, "str", {})
You passed 4 args, they are (10, 20, 'str', {})

>>> foofoo(11, 22)
x = 11, y = 22, rest = ()
>>> foofoo(12, 13, 15)
x = 12, y = 13, rest = (15,)
>>> foofoo(12, 13, 15, 20)
x = 12, y = 13, rest = (15, 20)

Пример такой функции мы недавно рассматривали – это хорошо знакомый нам print. Как известно, он принимает произвольное число аргументов, пользуясь выше описанным механизмом. Можете пройти по ссылке и увить его сигнатуру.

Важно, чтобы «звездная переменная» была после всех позиционных аргументов, иначе мы получим ошибку SyntaxError.

Две звездочки перед названием аргумента позволяют нам получить произвольное число произвольно названных именованных аргументов (еще их называют аргументами ключевых слов). Такую переменную часто называют **kwargs (от key-word arguments). В нее будет записан словарик (dict) из пар название ключевого слова (строка) и значение аргумента. Давайте обратимся к примерам:

def baz(**kwargs):
    print(kwargs)

>>> baz(x=1, y=2, z="hello")
{'y': 2, 'x': 1, 'z': 'hello'}
>>> baz()
{}

Как видно, без аргументов мы получили пустой словарь. А с именованными аргументами получили словарь, где их имена – ключи-строки, а их значения – собственно сами переданные значений. В функцию baz нельзя передать аргументы без имен, будет ошибка, потому что без имен – позиционные аргументы, а мы никак их не обозначили.

def foobaz(x, y, **kwargs):
    print('x = {} and y = {}'.format(x, y))
    print('also:', kwargs)

>>> foobaz(2, 3)
x = 2 and y = 3
('also:', {})

>>> foobaz(2, 3, other=77, z=88)
x = 2 and y = 3
('also:', {'other': 77, 'z': 88})

>>> foobaz(x=100, y=200, z=300)
x = 100 and y = 200
('also:', {'z': 300})

Тут мы требуем ровно два позиционных аргумента (x и – обязательные аргументы) и любое количество аргументов ключевых слов, которые попадут в kwargs. Нюанс: мы может передать x и y по именам (последний пример), но они не попадут в kwargs, а останутся только в своих соответствующих переменных x и y, и только z попал в kwargs, потому что мы заранее его не объявили.

Можно сочетать *args и **kwags в одной функции, причем именно в этом порядке.

def megafunc(x, y, *args, **kwargs):
    print('x = {} and y = {}'.format(x, y))
    print('also args: {}'.format(args))
    print('also kwargs {}'.format(kwargs))

>>> megafunc(10, 15)
x = 10 and y = 15
also args: ()
also kwargs {}

>>> megafunc(10, 15, 20)
x = 10 and y = 15
also args: (20,)
also kwargs {}

>>> megafunc(10, 15, 20, 22)
x = 10 and y = 15
also args: (20, 22)
also kwargs {}

>>> megafunc(10, 15, 20, 25, troll=30, dwarf=40)
x = 10 and y = 15
also args: (20, 25)
also kwargs {'troll': 30, 'dwarf': 40}

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

def strict_foo(x, *, cat, dog):
    print(x, cat, dog)

>>> strict_foo(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: strict_foo() takes 1 positional argument but 3 were given

>>> strict_foo(1, cat=2, dog=3)
1 2 3

Имя x мы можем и не указывать, просто передадим значение, но cat и dog мы обязаны указать по имени. Зачем это надо? Если функция принимает много разных аргументов, то мы можем принудить указывать имена, чтобы пользователь не перепутал их порядок. Еще такой код просто выглядит более читаемым.

Раскрытие коллекций в аргументах функций при вызове

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

Передавая кортеж или список со одной звездочкой – он раскроется в череду позиционных аргументов. Справа от звездочки может быть как имя переменной, так и литерал коллекции или даже вызов функции. Определим две функции: в одной foo – переменное число позиционных аргументов, в другой fixed_foo – фиксированное (три).

def foo(*args):
    print(args)

>>> foo(*[1, 2, 3])
(1, 2, 3)
>>> letters = ('a', 'b', 'c', 'd')
>>> foo(*letters)
('a', 'b', 'c', 'd')

def fixed_foo(a, b, c):
    print(a, b, c, sep=' and ')

>>> fixed_foo(*(1, 2, 3))
1 and 2 and 3
>>> fixed_foo(*(1, 2, 3, 4))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: fixed_foo() takes 3 positional arguments but 4 were given

В foo мы вольны передать список или кортеж любой длины, а в fixed_foo мы обязаны передать список или кортеж длины 3, не больше, не меньше.

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

>>> foo(*[1, 2, 3], *['a', 'b', 'c'])
(1, 2, 3, 'a', 'b', 'c')

Можно догадаться, что двойная звездочка несет схожую функциональность – раскрывает словарь в именованные аргументы функции.

def baz(a, b, c):
    print('a = ', a)
    print('b = ', b)
    print('c = ', c)

>>> baz(**{'a': 1, 'b': 2, 'c': 3})
a =  1
b =  2
c =  3

Если у нас нет **kwargs, то передаваемый словарь должен содержать ровно столько пар ключ-значение, сколько есть аргументов в функции (без значения по-умолчанию, естественно), причем ключи должен совпадать по именам с названиями аргументов (а вот порядок не важен). То есть при ‘a’: 1 в a попадет 1 и так далее.

>>> baz(10, **{'b': 20, 'c': 30})
a =  10
b =  20
c =  30

>>> baz(b=10, **{'a': 20, 'c': 30})
a =  20
b =  10
c =  30

В примерах выше мы передаем один аргумент явно и два аргумента словарем.

Возможны разнообразные варианты вызова функции. Даже такие:

def uber_func(a, b, c, d, x=10, y=20):
    print(f'a = {a} and b = {b} and c = {c} and d = {d}, x = {x}, y = {y}')

>>> uber_func(*[1, 2], 3, **{'d': 100, 'x': 200})
a = 1 and b = 2 and c = 3 and d = 100, x = 200, y = 20

Склеивание списков и словарей

Мы можем «встраивать» одни списки, кортежи и словари в другие с помощью астерисков. Это значит, что мы добавляем элементы одной коллекции в другую. Одинарная звездочка – для списков и кортежей:

>>> ( *(1, 2), *(3, 4) )
(1, 2, 3, 4)

>>> (1, 2) + (3, 4)  # тоже самое же?
(1, 2, 3, 4)

>>> [ *(1, 2), *[3, 4] ]
[1, 2, 3, 4]

>>> ( *[1, 2], *(3, 4) )
(1, 2, 3, 4)

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

Со словарями это немного полезнее. Применение двойной звезды (**) позволяет обновить один словарь элементами другого или нескольких других.

>>> d = { 'x': 10, 'y': 20 }
>>> d2 = { 'a': 100, 'b': 200 }

>>> { **d, **d2 }
{'x': 10, 'y': 20, 'a': 100, 'b': 200}

>>> { **d, **d2, 'other': 'foo' }
{'x': 10, 'y': 20, 'a': 100, 'b': 200, 'other': 'foo'}

Работает похоже, как метод update, но не меняет исходные словари, а создает новый. Если есть совпадающие ключи, то будет взято значение последнего из них:

>>> d_old = { 'president': 'Medvedev' }
>>> d_new = { 'president': 'Putin' }
>>> { **d_old, **d_new }
{'president': 'Putin'}

Распаковка

Позволяет раскидывать по переменным содержимое сложных структур из списков и кортежей. В переменную со звездочкой попадут в виде списка все остальные значения распакуемой структуры, кроме указанных явно. Лучше это понять на примерах:

>>> numbers = [1, 2, 3, 4, 5, 6]

>>> *a, = numbers   # да, там реально одинокая запятая после a
>>> a
[1, 2, 3, 4, 5, 6]

>>> *a, b, c = numbers
>>> a, b, c
([1, 2, 3, 4], 5, 6)

>>> a, b, *middle, c = numbers
>>> a, b, middle, c
(1, 2, [3, 4, 5], 6)

>>> [a, b, *middle, c] = numbers   # скобки можно любые
>>> a, b, middle, c
(1, 2, [3, 4, 5], 6)

Имя со звездочкой может быть как в начале, так и в конце кортежа и даже в середине. В последнем и в предпоследнем примере мы берем первый (a), второй (b) элементы; потом все, кроме последнего в middle пойдут как список, и последний в – c.

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

Не важно в какие скобки мы обернем верхний уровень: в круглые, квадратные или в никакие. Обычно скобки опускают.

На одном уровне может быть только 1 элемент со звездой.

>>> *n1, x, *n2 = numbers   # так нельзя!
  File "<stdin>", line 1
SyntaxError: two starred expressions in assignment

На нескольких уровней могут быть свои звездные выражения:

>>> a, [x, y, *rest], *others, last = 100, [20, 30, 40, 50, 60], 120, 140, 160
>>> print(a, x, y, rest, others, last)
100 20 30 [40, 50, 60] [120, 140] 160

При таком присваивании значения переменных копируются.

Справа от звездного присваиванию могут быть любые итераблы, например range:

>>> x1, *middle, x2 = range(10)
>>> x1, middle, x2
(0, [1, 2, 3, 4, 5, 6, 7, 8], 9)

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

Специально для канала PyWay.

Индексирование в Python

Положительные и отрицательные индексы

Допустим у нас есть список или кортеж.

x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
t = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Без потери общности будем работать только со списком х (с кортежем t – тоже самое).

Легко получить i-тый элемент этого списка по индексу.

Внимание! Индексы в Python считаются с нуля (0), как в С++ и Java.

>>> x[0]
0
>>> x[7]
7
>>> x[11]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

В последней строке мы вылезли за пределы (у нас в списке последний индекс – 10) и получили исключение IndexError.

Но что будет, если мы обратимся к элементу с отрицательным индексом? В С++ такой операцией вы бы прострелили себе ногу. А в Python? IndexError? Нет!

>>> x[-1]
10
>>> x[-2]
9
>>> x[-10]
1
>>> x[-11]
0

Это совершенно легально. Мы просто получаем элементы не с начала списка, а с конца (-i-тый элемент).
x[-1] – последний элемент.
x[-2] – предпоследний элемент.

Это аналогично конструкции x[len(x)-i]:

>>> x[len(x)-1]
10

Обратите внимание, что начальный (слева) элемент в отрицательной нотации имеет индекс -11.

Срезы

Срезы, они же slices, позволяют вам получить какую-то часть списка или кортежа.

Форма x[start:end] даст элементы от индекса start (включительно) до end (не включая end). Если не указать start – мы начнем с 0-го элемента, если не указать end – то закончим последним элементом (включительно). Соотвественно, x[:] это тоже самое, что и просто x.

>>> x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> x[2:8]
[2, 3, 4, 5, 6, 7]
>>> x[:8]
[0, 1, 2, 3, 4, 5, 6, 7]
>>> x[2:]
[2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> x[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Если end <= start, получим пустой список.

>>> x[5:3]
[]

Аналогично мы можем получать срезы с отчетом от конца списка с помощью отрицательных индексов.

>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> a[-4:-2]
[7, 8]

В этом случае также start < end, иначе будет пустой список.

Форма x[start:end:step] даст элементы от индекса start (включительно) до end (не включая end), в шагом step. Если step равен 1, то эта форма аналогична предыдущей рассмотренной x[start:end].

>>> x[::2]
[0, 2, 4, 6, 8, 10]
>>> x[::3]
[0, 3, 6, 9]
>>> x[2:8:2]
[2, 4, 6]

x[::2] – каждый второй элемент, а x[::3] – каждый третий. 

Отрицательный шаг вернет нам элементы в обратном порядке:

>>> x[::-1]
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# как если бы:
>>> list(reversed(x))
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# в обратном порядке с шагом 2
>>> x[::-2]
[10, 8, 6, 4, 2, 0]

Запись в список по срезу

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

Если размеры равны (в примере два элемента в срезе и два элемента во втором списке) – происходит замена элементов.

>>> a = [1,2,3,4,5]
>>> a[1:3] = [22, 33]
>>> a
[1, 22, 33, 4, 5]

Если они не равны по размеру, то в результате список расширяется или сжимается.

>>> a = [1, 2, 3, 4, 5]
# размер среза = 1 элемент, а вставляем два (массив расширился)
>>> a[2:3] = [0, 0]
>>> a
[1, 2, 0, 0, 4, 5]

# тут вообще пустой размер среза = вставка подсписка по индексу 1
>>> a[1:1] = [8, 9]
>>> a
[1, 8, 9, 2, 0, 0, 4, 5]

# начиная с элемента 1 и кончая предпоследним элементом мы уберем (присвоив пустой список)
>>> a[1:-1] = []
>>> a
[1, 5]

Именованные срезы

Можно заранее создавать срезы с какими-то параметрами без привязки к списку или кортежу встроенной функцией slice. А потом применить этот срез к какому-то списку.

>>> a = [0, 1, 2, 3, 4, 5]
>>> LASTTHREE = slice(-3, None)
>>> LASTTHREE
slice(-3, None, None)
>>> a[LASTTHREE]
[3, 4, 5]

Вместо пустых мест для start, end или step здесь мы пишем None.

В заключение к этому разделу хочу сказать, что срезы списков возвращают списки, срезы кортежей – кортежи.

Индексирование своих объектов

В конце концов, мы можете определить самостоятельно поведение оператор индексации [], определив для своего класса магические методы __getitem__, __setitem__ и __delitem__. Первый вызывается при получении значения по индекса (или индексам), второй – если мы попытаемся нашему объекту что-то присвоить по индексу. А третий – если мы будет пытаться делать del по индексу. Необязательно реализовывать их все. Можно только один, например:

# при чтении по индексу из этого класса, мы получим удвоенных индекс
class MyClass:
    def __getitem__(self, key):
       return key * 2

myobj = MyClass()
myobj[3]  # вернет 6
myobj["privet!"] # приколись, будет: 'privet!privet!'

В качестве ключей можно использовать не только целые числа, но и строки или любые другие значения, в том числе slice и Ellipsis. Как вы будете обрабатывать их – решать вам. Естественно, логика, описанная в предыдущих разделах, здесь будет только в том случае, если вы ее сами так реализуете.

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

class IntegerNumbers:
  def __getitem__(self, key):
    if isinstance(key, int):
      return key
    elif isinstance(key, slice):
      return list(range(key.start, key.stop, key.step))
    else:
      raise ValueError

ints = IntegerNumbers() 
print(ints[10])  # 10
print(ints[1:10:2]) # [1, 3, 5, 7, 9]
print(ints["wwdwd"]) # так нельзя

Можно иметь несколько индексов. Ниже мы суммируем все значения индексов.

class MultiIndex:
  def __getitem__(self, keys): 
    # все индексы (если их 2 и больше попадут) в keys с типом tuple
    return sum(keys)  # просуммируем их

prod = MultiIndex()
print(prod[10, 20])  # напечает 30

Удачи в программировании и жизни!

Специально для канала PyWay.

Defaultdict

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

x = dict()

# или лучше

x = {}

Что будет, если мы обратимся к несуществующему элементу?

>>> x['abc']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'abc'

Возникает исключение KeyError. Можно попробовать отловить его конструкцией try-except, что выглядит и работает достаточно громоздко, можно проверить вхождение ключа операцией in или, наконец, воспользоваться методом get (в котором, кстати, мы можем указать значение по умолчанию, если ключа нет).

# плохо...
try:
    v = x['abc']
except:
    v = 'no value'

# лучше
v = x['abc'] if 'abc' in x else 'no value'

# еще лучше
v = x.get('abc', 'no value')

Это хорошо, но можно еще лучше!

Рассмотрим пример: допустим у нас есть задача по каждому ключевому слову из текста составить список позиций этого ключевого слова (номер слова) в тексте.

text = """
We develop a methodology for automatically analyzing text to aid in discriminating firms that encounter catastrophic 
financial events. The dictionaries we create from Management Discussion and Analysis Sections (MD&A) of 10-Ks 
discriminate fraudulent from non-fraudulent firms 75% of the time and bankrupt from nonbankrupt firms 80% of the 
time. Our results compare favorably with quantitative prediction methods. We further test for complementarities by 
merging quantitative data with text data. We achieve our best prediction results for both bankruptcy (83.87%) and 
fraud (81.97%) with the combined data, showing that that the text of the MD&A complements the quantitative financial 
information.
"""

key_words = [
    "quantitative",
    "results",
    "automatically"
]

# решение не претендует на общую эффективность, сделано для демонстрации

def solution1(text, keywords):
    # разбивка текста на слова и удаление лишних символов
    all_words = map(lambda word: word.strip(' .)(%\n').lower(), text.split(' '))

    kw_map = {}

    for word_no, word in enumerate(all_words):
        if word in key_words:
            if word in kw_map:
                kw_map[word].append(word_no)
            else:
                kw_map[word] = [word_no]

    return kw_map

print(solution1(text, key_words))

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

from collections import defaultdict

...

    kw_map = defaultdict(list)

    for word_no, word in enumerate(all_words):
        if word in key_words:
            kw_map[word].append(word_no)

    return kw_map

Ссылка на код.

Чем же отличается defaultdict от dict? И что значит параметр list?

Когда мы обращаемся к несуществующему ключу словаря, то defaultdict вызывает функцию, указанную при создании (в данном случае list) без параметров. И результат работы этой функции и будет присвоен новом элементу словаря с ключом, который раннее не существовал. А как только он появился, то исключении уже не будет брошено, и выполнится та операция, которую мы хотим (в данном случае append).

list – это встроенная функция, которая позволяет нам сконструировать список, если вызвана без параметров.

>>> print([])
[]
>>> print(list())   # тоже самое
[]

Поэкспериментируем в интерпретаторе:

>>> from collections import defaultdict
>>> d = defaultdict(list)   # создали нашdefaultdict
>>> d
defaultdict(<type 'list'>, {})
>>> d['test']   # усп! этого ключа еще нет в словаре
[]
>>> d   # а теперь есть, хотя мы всего лишь обратились на чтение
defaultdict(<type 'list'>, {'test': []})
>>> d['test2'].append('foo')   # test2 тоже нет, но он создасться как [] и append сработает
>>> d
defaultdict(<type 'list'>, {'test': [], 'test2': ['foo']})

Вместо list попробуем, например, int. То есть при обращении к несуществующему элементу defaultdict будет добавлено целое число (0 по умолчанию).

>>> d2 = defaultdict(int)
>>> d2['a']
0
>>> d2['b'] += 10   # операция += сработает, такое не пройдет с обычным dict
>>> d2
defaultdict(<type 'int'>, {'a': 0, 'b': 10})

defaultdict при создании принимает любую ф-цию, а не только встроенные! В следующем примеры мы создадим словарь по-умолчанию со значением элемента – числом 3 (с помощью лямбда ф-ции lambda: 3):

>>> d3 = defaultdict(lambda: 3)
>>> d3['fef']
3
>>> d3['rrr'] += 3
>>> d3
defaultdict(<function <lambda> at 0x10e0376e0>, {'fef': 3, 'rrr': 6})

И самое Питон-чудо напоследок! Смотрите как элегантно мы создадим тип данных «дерево» всего одной строкой!

def tree(): return defaultdict(tree)

Теперь мы можем:

x = tree()
x['a']['b']['c'] = 'test'
print(x)
defaultdict(<function tree at 0x10e037578>, {'a': defaultdict(<function tree at 0x10e037578>, {'b': defaultdict(<function tree at 0x10e037578>, {'c': 'test'})})})

Все поддеревья создались автоматически благодаря рекурсии! Каждый раз, когда мы хотим получить доступ к несуществующему поддереву, вызывается снова ф-ция tree, которая создает такое же поддерево, конструктор по-умолчанию которого тот же самый tree.

Специально для канала PyWay.

Prettytable

Мы любим консольные приложения, но как много способов мы знаем, чтобы оформить вывод в консоль кроме простого print? Я уже рассказывал на своем телеграм-канале про TQDM для красивых прогресс-баров. Теперь расскажу, как делать красивые таблицы.

Рекомендую библиотеку PrettyTable (https://github.com/jazzband/prettytable).

Установка:

pip install prettytable

Простой пример. Зададим сначала поля, которые отобразятся в заголовке, а потом добавим строчки.

from prettytable import PrettyTable

x = PrettyTable()

# зададим названия полей в заголовках
x.field_names = ["City name", "Area", "Population", "Annual Rainfall"]

# добавим строки данных
x.add_row(["Adelaide",1295, 1158259, 600.5])
x.add_row(["Brisbane",5905, 1857594, 1146.4])
x.add_row(["Darwin", 112, 120900, 1714.7])
x.add_row(["Hobart", 1357, 205556, 619.5])
x.add_row(["Sydney", 2058, 4336374, 1214.8])
x.add_row(["Melbourne", 1566, 3806092, 646.9])
x.add_row(["Perth", 5386, 1554769, 869.4])

print(x) # проще простого!

Получим на экране:

+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
|  Adelaide | 1295 |  1158259   |      600.5      |
|  Brisbane | 5905 |  1857594   |      1146.4     |
|   Darwin  | 112  |   120900   |      1714.7     |
|   Hobart  | 1357 |   205556   |      619.5      |
|   Sydney  | 2058 |  4336374   |      1214.8     |
| Melbourne | 1566 |  3806092   |      646.9      |
|   Perth   | 5386 |  1554769   |      869.4      |
+-----------+------+------------+-----------------+

Того же эффекта можно достичь, если добавлять данных по столбцам (иногда так удобнее).

x = PrettyTable()

x.add_column("City name", 
["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"])
x.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
x.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 
1554769])
x.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 
869.4])

print(x)

Даже если вы не фанат консоли, то иногда нужно сохранить отчет о работе вашей программы в текстовый файл. Туда мы тоже можем добавить и нашу таблицу.

with open('1.txt', 'w') as f:
    f.write(x.get_string())

Если мы работаем с базой данных, например Sqlite, то можно печатать результаты ваших запросов красивыми таблицами вот так:

import sqlite3
from prettytable import from_cursor

# тут вы загружаете вашу БД и делаете запрос, какой надо.
connection = sqlite3.connect("mydb.db")
cursor = connection.cursor()
cursor.execute("SELECT field1, field2, field3 FROM my_table")
mytable = from_cursor(cursor)

print(mytable)

Параметры таблицы можно кастомизировать, меняя практически все, что придет в голову. Также вывод таблицы можно сортировать, выделять нужные колонки и диапазоны рядов. Читайте официальную документацию или пишите комментарии.

Специально для канала PyWay.

Балда iMessage

Вышла первая версия моей Балды для iMessage (можно играть в сообщениях только на устройствах Apple с операционной системой iOS 10 и выше). Игра написана в рамках изучения языка Swift.

Ссылка: https://itunes.apple.com/ru/app/balda-s-druz-ami/id980303614?l=en&mt=8

simulator-screen-shot-10-nov-2016-16-19-11simulator-screen-shot-10-nov-2016-16-19-19 simulator-screen-shot-10-nov-2016-16-19-30

Открытый код

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

Мой профиль на GITHUB: https://github.com/tirinox

Правда там нужно поработать над описанием, над тестами. Может, написать на тематические ресурсы для пиара.

Сейчас выложены следующий проекты:

Erudite Push Stream

https://github.com/tirinox/erudite_push_stream

Сервер для получения сообщений по TCP от сервера. Пришел из сервера Эрудита. Некоторая замена nginx_push_stream_module. Т. е. у нас есть бэкенд на PHP, он хочет уведомить пользователя о событии, поэтому обращается к erudite_push_stream. Пользователь в свою очередь держит открыт к TCP соединение к нему и получает сообщение.

Написан на Go Lang.

GoWaiter

https://github.com/tirinox/gowaiter

Тоже из инфраструктуры Эрудита. Умеет устанавливать таймеры и дергать HTTP запросы, когда таймер истекает. Так же умеет дергать их заранее заданному расписанию (на подобие CRON). Очень простой сервис с минимумом настроек.

Написан на Go Lang.