Рубрика: Программирование

Посты, связанные с разработкой ПО.

О поиске в словарях

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

Ловим IndexError, если индекс отсутствует в списке, и KeyError, если ключ отсутствует в словаре. Однако, лучше ловить LookupError, который является предком обоих исключений:

>>> issubclass(KeyError, LookupError)
True
>>> issubclass(IndexError, LookupError)
True

Пример:

config = {}

try:
    admin = config['db'][0]['admins']['list'][0]
except LookupError:
    admin = 'all'

Независимо от того, не найден ли будет какой-то ключ словаря или индекс списка – будет поймана одна и та же ошибка LookupError.

Альтернативно, вы можете сразу обновлять записи словаря (если они не найдены) методом dict.setdefault(key, default). Этот метод проверяет, есть ли ключ в словаре, если его нет, то в словарь добавляется значение по умолчанию, и оно же возвращается. А если ключ был в словаре, то вернется значение по этому ключу. Поэтому такой неуклюжий код:

if 'workers' not in config:
    config['workers'] = 8
workers = config['workers']

Может быть переписан как:

workers = config.setdefault('workers', 8) 

Заметьте, что повторный вызов с другим default не поменяет уже записанное в первый раз значение:

>>> d = {}
>>> d.setdefault('foo', 10)
10
>>> d.setdefault('foo', 20)
10

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

Красивого всем кода!

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Стратегия на Python

​​«Расскажите про любой шаблон проектирования на ваш выбор.»

Случалось слышать такое на собеседованиях? Большинство людей в этот момент начинают рассказывать про синглтон (одиночку). Потому что он… простой? Да, вообще-то не очень. Попробуйте сходу вспомнить, как там реализовать его через метакласс. Да и часто ли приходится? Скорее всего вы пользуетесь уже готовым кодом для синглтона. Его даже называют «анти-паттерном», потому что он часто маскирует плохой дизайн кода, вызывает проблемы при тестировании и нарушает принцип единственной отвественности класса (и порождает себя, и делает какую-то работу). А еще, он может вызывать проблемы с многопоточностью или «многопроцессностью» в случае с Python. Поэтому хвастать знанием синглотона – не лучшая стратегия на собеседовании…

Стратегия выбора работника

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

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

Реализация этого шаблона может быть не только объектная, но и функциональная. С последней и начнем:

# стратегия печатать на экран
def console_writer(info):
    print(info)

# стратегия выводить в файл
def file_writer(info):
    with open('log.txt', 'a') as file:
        file.write(info + '\n')

def client(writer):
    writer('Hello world!')
    writer('Good bye!')

# пользователь выбирает стратегию
if input('Write to file? [Y/N]') == 'Y':
    client(writer=file_writer)
else:
    client(writer=console_writer)

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

В объектном варианте:

class Adder:
    def do_work(self, x, y):
        return x + y

class Multiplicator:
    def do_work(self, x, y):
        return x * y

class Calculator:
    def set_strategy(self, strategy):
        self.strategy = strategy

    def calculate(self, x, y):
        print('Result is', self.strategy.do_work(x, y))

calc = Calculator()
calc.set_strategy(Adder())
calc.calculate(10, 20)

calc.set_strategy(Multiplicator())
calc.calculate(10, 20)

Мы обеспечили горячую заменяя алгоритмов для класса Calculator. Для простоты, здесь я не применял наследование (спасибо динамической природе Python), но в серьезных проектах, вам следовало бы написать что-то подобное:

from abc import ABC, abstractmethod

class BaseStrategy(ABC):
    @abstractmethod
    def do_work(self, x, y):
        pass

class Adder(BaseStrategy):
    def do_work(self, x, y):
        return x + y

class Multiplicator(BaseStrategy):
    def do_work(self, x, y):
        return x * y

class Calculator:
    def set_strategy(self, strategy: BaseStrategy):
        self.strategy = strategy

    def calculate(self, x, y):
        print('Result is', self.strategy.do_work(x, y))

Здесь мы создаем общий интерфейс стратегий BaseStrategy – как абстрактный класс ABC. Далее в каждой стратегии реализуем этот интерфейс.

Надеюсь, было полезно. Если хотите еще больше подробностей, то читайте 1, 2, 3.

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Ключ сортировки key

Методы sort, sorted могут принимать именованный аргумент key. Он должен быть функцией (или чем-то другим вызываемым – callable) с одним аргументом. Смысл key в том, что он вызывается ровно один раз для каждого из элементов списка (итератора и т.п.), которой мы сортируем, и указывает порядок сортировки: элементы выстраиваются ровно в том порядке, в каком бы выстроился сортированный список результатом вызова key на всех элементах:

  1. Применить key ко всем элементам
  2. Отсортировать результаты key по порядку, используя обычное сравнение «больше-меньше» для (чисел, строк и т.п.)
  3. Выстроить исходные данные согласно этому порядку.

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

С числами все понятно: 10 > 6, 5 < 7. Строки сортируются лексикографически (как статьи в словаре: А < АА < ААА < ААБ < ААВ < АБ < Б < … < ЯЯЯ). А вот сортировка по длине строки потребует использовать key , потому что признак уже нестандартный:

>>> sorted(['Wolf', 'Sparrow', 'Cat'], key=len)
['Cat', 'Wolf', 'Sparrow']

>>> len('Cat')
3
>>> len('Wolf')
4
>>> len('Sparrow')
7

Или другой пример. У нас есть список координат точек [(x, y), ...]. Хотим расположить их по расстоянию от начала координат (0, 0):

pts = [(10, 20), (-100, 150), (0, 0), (40, -30)]

print(sorted(pts, key=lambda p: p[0] ** 2 + p[1] ** 2))

# [(-100, 150), (40, -30), (10, 20), (0, 0)]

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

drinks = [
    # напиток, цена
    ('Juice', 100),
    ('Beer', 200),
    ('Soda', 50),
    ('Cocktail', 400),
    ('Water', 20)
]

print(sorted(drinks, key=lambda drink: drink[1]))
# [('Water', 20), ('Soda', 50), ('Juice', 100), ('Beer', 200), ('Cocktail', 400)]


# без key:
print(sorted(drinks))  # отсортирует по названию напитка
# [('Beer', 200), ('Cocktail', 400), ('Juice', 100), ('Soda', 50), ('Water', 20)]

Бонус: если хотите поменять порядок сортировки на обратный, можно либо в лямбде поставить минус перед возвращаемым значениям, но лучше в sorted передать reverse=True.

print(sorted(drinks, key=lambda drink: -drink[1]))
# или лучше
print(sorted(drinks, key=lambda drink: drink[1], reverse=True))
# [('Cocktail', 400), ('Beer', 200), ('Juice', 100), ('Soda', 50), ('Water', 20)]

Сортировка по нескольким признакам сразу

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

print(sorted(drinks, key=lambda dr: (len(dr[0]), -dr[1])))

# [('Beer', 200), ('Soda', 50), ('Juice', 100), ('Water', 20), ('Cocktail', 400)]

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

>>> list(map(lambda dr: (len(dr[0]), -dr[1]), drinks))
[(5, -100), (4, -200), (4, -50), (8, -400), (5, -20)]

Видите, первым теперь идет длина строки, а потом цена с минусом. Поэтому первыми после сортировки пойдут элементы с четверкой в ключе, а среди двух (4, -200), (4, -50) порядок сохранится, потому что -200 < -50.

Модуль operator

Вместо лямбды можно взять одну из библиотечных функций из модуля operator. Есть несколько вариантов для разных ситуаций.

Функция itemgetter(i) берет i-тый элемент кортежа или списка (или ищет по ключу i в dict):

from operator import itemgetter

print(sorted(drinks, key=itemgetter(1)))
# [('Water', 20), ('Soda', 50), ('Juice', 100), ('Beer', 200), ('Cocktail', 400)]

Для словарей:

# преобразуем список кортежей в список словарей
drinks_dict = [{'n': name, 'pr': price} for name, price in drinks]  
print(drinks)

# вывод: [{'n': 'Juice', 'pr': 100}, {'n': 'Beer', 'pr': 200}, {'n': 'Soda', 'pr': 50}, {'n': 'Cocktail', 'pr': 400}, {'n': 'Water', 'pr': 20}]

print(sorted(drinks, key=itemgetter('pr')))

# вывод: [{'n': 'Water', 'pr': 20}, {'n': 'Soda', 'pr': 50}, {'n': 'Juice', 'pr': 100}, {'n': 'Beer', 'pr': 200}, {'n': 'Cocktail', 'pr': 400}]

Теперь представим, что у нас есть класс Drink, и нужно сортировать по атрибуту price. Это можно сделать лямбдой или функцией attrgetter, которая получает атрибут объекта по имени этого атрибута:

class Drink:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    def __repr__(self):
        return f'Drink("{self.name}", {self.price})'

drinks_cls = [
    # напиток, цена
    Drink('Juice', 100),
    Drink('Beer', 200),
    Drink('Soda', 50),
    Drink('Cocktail', 400),
    Drink('Water', 20)
]

print(sorted(drinks_cls, key=lambda drink: drink.price))

# или
from operator import attrgetter
print(sorted(drinks_cls, key=attrgetter('price')))

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

from operator import methodcaller

sorted(items, key=methodcaller('get_reserve', category='home'))

# тоже самое что:

sorted(items, key=lambda item: item.get_reserve(category='home'))

Исследование производительности

Вариант с лямбдой немного медленнее (потому что операторы написаны на Си, а лямбду – мы пишем на Python). Проведем тесты производительности:

from random import shuffle
from timeit import timeit
from operator import itemgetter

data = [{'ident': x, 'value': 'foo'} for x in range(1000)]
shuffle(data)

def sort_itemgetter(data):
    data.sort(key=itemgetter('ident'))

def sort_lambda(data):
    data.sort(key=lambda it: it['ident'])

print('sort_itemgetter:', timeit('sort_itemgetter(list(data))', globals=globals(), number=10000))
print('sort_lambda:', timeit('sort_lambda(list(data))', globals=globals(), number=10000))

# sort_itemgetter: 1.6157471220000001
# sort_lambda: 1.8793544059999998

Потому что:

ig = itemgetter('ident')
la = lambda it: it['ident']
di = {'ident': 10}

print('itemgetter:', timeit('ig(di)', globals=globals(), number=1000000))
print('lambda:', timeit('la(di)', globals=globals(), number=1000000))

# itemgetter: 0.083
# lambda: 0.11

itemgetter быстрее, чем lambda, ибо он написан на Си.

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

min и max

Методы min и max также поддерживают key. Они вернут соответственно элемент, у которого key вернет наименьшее или наибольшее значение. На примере длины строк:

names = ['Wolf', 'Sparrow', 'Cat']
min(names, key=len)  # 'Cat'
max(names, key=len)  # 'Sparrow'

Самая ближняя от начала координат точка и самая дальняя:

pts = [(10, 20), (-100, 150), (0, 0), (40, -30)]
min(pts, key=lambda p: p[0] ** 2 + p[1] ** 2)  # (0, 0)
max(pts, key=lambda p: p[0] ** 2 + p[1] ** 2)  # (-100, 150)

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Коронавирус: предсказание на Python

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

КДПВ: вирус + Китай + график

Нам понадобятся библиотеки:

  • pandas – для загрузки данных
  • matplotlib – для построения графика
  • scipy – для построения предсказания
  • numpy – для работы с массивами

Установите их, если они еще не установлены, набрав в терминале:

pip install pandas matplotlib scipy numpy

Подключим модули:

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import curve_fit
from datetime import timedelta

Я собрал данные по заболевшим в последние дни в CSV файл и закачал его на свой сайт по адресу: https://tirinox.ru/wp-content/uploads/2020/01/corona.csv

Pandas умеет читать данные прямо из интернета по URL:

df = pd.read_csv('https://tirinox.ru/wp-content/uploads/2020/01/corona.csv', parse_dates=['day'])

print(df.head())

""" Вывод:
         day  infected  dead
0 2020-01-20       278     4
1 2020-01-21       326     6
2 2020-01-22       547     8
3 2020-01-23       639    14
4 2020-01-24       916    25
"""

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

df.plot(kind='bar', x='day', y=['infected', 'dead'])
plt.show()
График роста вируса по дням
infected = зараженные люди, dead = погибшие

Очевидно, что прирост с каждым днем все больше и больше. Явно нелинейная зависимость! Это и понятно. Представим, что 1 человек в среднем заражает двоих. Те двое, каждый заразив еще 2, добавят 4 новых человека. А те 4 заразят 8. Затем 16, 32, 64. К этому нужно не забыть добавить уже больных.

Такой закон называется экспоненциальным. Опишем экспоненциальную функцию в параметрическом виде. Параметры a, b, c еще предстоит подобрать, чтобы максимально точно удовлетворить существующим данным:

def fit_func_exp(x, a, b, c):
    return a * np.exp(c * x) + b

Теперь вычислим оптимальные параметры, обеспечивающие наименьшую ошибку:

# зависимая переменная - ее будем предсказывать
infected = df['infected']

# дни - преобразуем их в целые числа от 0 до максимального
days = range(len(df['day']))

# у нас 3 параметра в функции: a, b, c – начальное приближение - единицы
p0 = np.ones(3)

# впишем кривую в наши данные
(a, b, c), _ = curve_fit(fit_func_exp, days, infected, p0=p0)

Теперь, зная параметры, рассчитаем функцию, скажем, до 60 дней с момента начала эпидемии:

# предскажем динамику вируса на 60 дней (начало соответствует 20 января)
MAX_DAY = 60

x_days = np.linspace(0, MAX_DAY - 1, MAX_DAY)
y_infect = fit_func_exp(x_days, a, b, c)

Построим график и убедимся, что он хорошо описывает экспериментальные данные:

plt.xlabel('Дни')
plt.ylabel('Больные')

# график в log шкале
plt.yscale('log')

# это данные текущей статистики - нарисуем их синими точками
plt.scatter(days, infected, marker='D', label='Реальные')

# это красная линия – предсказание (первые 22 дня)
plt.plot(x_days[:22], y_infect[:22], 'r', label='Предсказание')
plt.legend()

plt.show()

Вот, что у нас получится:

График предсказания.

Ого! Рост поражает: сотни, тысячи, десятки тысяч! Узнаем, на какой день число зараженных людей достигнет населения всей Земли:

# население Земли
EARTH_POPULATION = 7_530_000_000

# найдем номер дня, когда количество зараженных достигнет популяции Земли
doom_index = np.argmax(y_infect >= EARTH_POPULATION)
doom_day = x_days[doom_index]

# вычислим дату
day0 = df['day'][0]
doom_date = day0 + timedelta(days=int(doom_day))

# дата конца!
print(f'Doom date: {doom_date:%d.%m.%Y}')

Doom date: 13.03.2020

13 марта…

Полный код загрузил в gist.

UPD: добавил табличку:

+------------+------------------+----------------+
|    Дата    | Число заболевших | Число погибших |
+------------+------------------+----------------+
| 20.01.2020 |        54        |       7        |
| 21.01.2020 |       239        |       9        |
| 22.01.2020 |       492        |       11       |
| 23.01.2020 |       839        |       15       |
| 24.01.2020 |       1314       |       21       |
| 25.01.2020 |       1963       |       30       |
| 26.01.2020 |       2853       |       45       |
| 27.01.2020 |       4072       |       68       |
| 28.01.2020 |       5740       |      104       |
| 29.01.2020 |       8024       |      161       |
| 30.01.2020 |      11150       |      252       |
| 31.01.2020 |      15431       |      394       |
| 01.02.2020 |      21293       |      618       |
| 02.02.2020 |      29317       |      971       |
| 03.02.2020 |      40304       |      1528      |
| 04.02.2020 |      55347       |      2405      |
| 05.02.2020 |      75942       |      3787      |
| 06.02.2020 |      104139      |      5965      |
| 07.02.2020 |      142745      |      9398      |
| 08.02.2020 |      195601      |     14807      |
| 09.02.2020 |      267968      |     23331      |
| 10.02.2020 |      367047      |     36763      |
| 11.02.2020 |      502698      |     57930      |
| 12.02.2020 |      688423      |     91288      |
| 13.02.2020 |      942703      |     143854     |
| 14.02.2020 |     1290844      |     226690     |
| 15.02.2020 |     1767494      |     357228     |
| 16.02.2020 |     2420088      |     562938     |
| 17.02.2020 |     3313572      |     887108     |
| 18.02.2020 |     4536865      |    1397953     |
| 19.02.2020 |     6211706      |    2202971     |
| 20.02.2020 |     8504777      |    3471566     |
| 21.02.2020 |     11644280     |    5470689     |
| 22.02.2020 |     15942657     |    8621023     |
| 23.02.2020 |     21827679     |    13585498    |
| 24.02.2020 |     29885019     |    21408803    |
| 25.02.2020 |     40916535     |    33737214    |
| 26.02.2020 |     56020077     |    53165030    |
| 27.02.2020 |     76698735     |    83780495    |
| 28.02.2020 |    105010434     |   132026095    |
| 29.02.2020 |    143772730     |   208054274    |
| 01.03.2020 |    196843216     |   327863828    |
| 02.03.2020 |    269503423     |   516666580    |
| 03.03.2020 |    368984436     |   814192761    |
| 04.03.2020 |    505186524     |   1283051543   |
| 05.03.2020 |    691664408     |   2021906043   |
| 06.03.2020 |    946976216     |   3186235247   |
| 07.03.2020 |    1296530371    |   5021051835   |
| 08.03.2020 |    1775114218    |   7912460814   |
| 09.03.2020 |    2430356032    |  12468908548   |
| 10.03.2020 |    3327464945    |  19649219634   |
| 11.03.2020 |    4555720506    |  30964364746   |
| 12.03.2020 |    6237357709    |  48795417935   |
| 13.03.2020 |    8539731720    |  76894611953   |
| 14.03.2020 |   11691972927    |  121174929895  |
| 15.03.2020 |   16007789810    |  190954388899  |
| 16.03.2020 |   21916688952    |  300916853606  |
| 17.03.2020 |   30006719188    |  474201998218  |
| 18.03.2020 |   41082993743    |  747274645537  |
| 19.03.2020 |   56247814448    | 1177598150075  |
+------------+------------------+----------------+

Предостережение

Данные расчеты чисто теоретические. Как будет в реальности, мне неизвестно. Но я уверен, что власти Китая и других стран справятся с эпидемией быстрее, чем она поразит весь мир.

Узнавать оперативно о статистике вируса можно через телеграм бот @NovelCoronaVirusBot.

Желаю вам здоровья и не подхватить даже обычной простуды! Меньше бывайте в людных местах, часто мойте руки с мылом, носите маски, при любых симптомах обращайтесь к врачу!

Если заболели, не пытайтесь скрыть болезнь, сбив температуру!

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Майним красивый Bitcoin адрес на Python

КДПВ: чувак долбит скалу киркой в поисках монет

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

Я буду использовать библиотеку bit, если вы с ней еще не знакомы, прочтите эту статью. Здесь нам очень кстати будет высокая скорость этой библиотеки в плане генерации кошельков.

Итак, я хочу, чтобы мой биткоин адрес начинался с какого-то слова, например, с названия моего канала PyWay. Нулевой символ, увы, мы изменить не можем, так как он связан с идентификатором сети биткоин (главная, segwit или тестовая). Поэтому оставим его в покое. Часть строки с первого символа должна начинаться с заданного слова:

PATTERN = 'PyWay'

# ф-ция проверки
def predicate(addr: str):
    return addr[1:].startswith(PATTERN)

Адреса всегда получаются случайными, поэтому нам нужен метод грубой силы (брутфорс) – многократная генерация адресов, пока не найдем подходящий:

from bit import Key

while True:
    k = Key()  #  новый случайны ключ
    if predicate(k.segwit_address):
        print(f'{k.segwit_address} with WIF private key {k.to_wif()}')
        break

Время поиска зависит от длины шаблона экспоненциально!

Короткие последовательности ищутся быстро, а длинные – экстремально долго.

Так 2-3 символьные слова ищутся почти моментально. 4 символа заставляют задуматься, а 5 – ждать минут 10-20. 6 символов – могут уйти часы. И так далее! Это криптография!

Чтобы ускорить поиск, запустим несколько процессов поиска с помощью модуля multiprocessing:

from bit import Key
from multiprocessing import Process, Value


PATTERN = 'PyWay'

# сколько процессов?
N_PROCESSES = 10


# предикат проверки адреса на наше желание
def predicate(addr: str):
    return addr[1:].startswith(PATTERN)


# рабочая функция
def worker(predicate: callable, stop: Value, counter: Value):
    # пока нам не посигналили о завершении из другого процесса
    while not stop.value:
        # новый ключ
        k = Key()
        
        # проверяем
        if predicate(k.segwit_address):
            print('done!')
            print(f'{k.segwit_address} with WIF private key {k.to_wif()}')
            
            # сигналим другим и выходим
            stop.value = True 
            break
            
        # каждые 10_000 итераций – точку рисуем
        counter.value += 1
        if counter.value % 10_000 == 0:
            print('.', end='')


if __name__ == '__main__':
    # эти переменные необычные - они позволяют обмениваться инфой между процессами
    stop = Value('b', False)
    counter = Value('i', 0)

    procs = []
    for worker_id in range(N_PROCESSES):
        # создадим процесс, передав рабочего и аргументы
        proc = Process(target=worker, args=(predicate, stop, counter))
        proc.start()
        procs.append(proc)

    # будем ждать пока все процессы не завершаться
    for proc in procs:
        proc.join()  # ждет процесс

FAQ

Почему у меня долго не находит даже простое слово?

Возможно, проблема в вашем слове. В нем есть символы, которых не может быть в адресе: o, O, 0, l, I. Они похожи по написанию, и были исключены, чтобы избежать лишних ошибок при передаче адресов. Естественно, в адресе не может быть русских букв и знаков пунктуации. Только английские буквы (кроме тех, что привел выше) и цифры кроме 0!

Почему бы не искать последовательно?

Если начать проверять все адреса подряд, это будет немного быстрее. Главное не начинать с 0 и с круглых чисел, иначе ключ будет легко подобрать! Пример последовательного поиска со случайной начальной точки seed:

seed = random.randint(0, 1_000_000_000_000)

while not stop.value:
    k = Key.from_int(seed)
    seed += 1

Почему процессы, а не потоки?

Из-за GIL. Потоки будут тормозить друг друга, а процессы работают независимо.

Если хочу слово в конце адреса или в середине?

Поменяйте условие:

return addr.endswith(PATTERN)  # в конце

return PATTERN in addr  # можно в середине 

PATTERN = PATTERN.lower()
return PATTERN in addr.lower()  # в любом месте без учета регистра - самый быстрый поиск

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈