Советы разработчикам (python и не только) @advice17 Телеграм канал Советы разработчикам (python и не только) Советы разработчикам (python и не только) - телеграм канал @advice17 Советы для разработчиков ПО от @Tishka17 Поддержать материально: https://www.tinkoff.ru/cf/2NkdXaljivI Programming, python, software architecture и все такое категория «‎IT, Технологии, Гаджеты» Метки: Разработчик

Советы разработчикам (python и не только)

@advice17 1 945 подписчиков канал

Советы для разработчиков ПО от @Tishka17

Поддержать материально: https://www.tinkoff.ru/cf/2NkdXaljivI

Programming, python, software architecture и все такое

Открыть

Информация

Телеграм канал «Советы разработчикам (python и не только)» публичная ссылка: @advice17 добавлен в каталог 11 июля 2022 года в категорию «IT, Технологии, Гаджеты». На канал подписано 1 945 человек. Языки обнаруженные на канале: Русский, English. Каналу добавлены метки: Разработчик. Дата создания канала: 20 марта 2022 года. Последнее обновление информации(аватар, название, описание) в каталоге: 14 августа 2022 года.

Ссылка t.me: https://t.me/advice17
Подписчики: 1 945
Категория: IT, Технологии, Гаджеты
Языки: Русский, English
Метки: Разработчик
Дата создания: 20 марта 2022 года в 15:57

ℹ️ Что бы подписаться на канал «Советы разработчикам (python и не только)» У вас должен быть установлен телеграм.
▫️ Если просматриваете эту страницу через устройство на котором установлен Telegram, то просто нажмите эту ссылку (или кнопку «открыть» выше) и должно появиться окно выбора приложения Telegram.
▫️ Если у вас открыта страница на устройстве на котором не установлен Telegram, вы можете присоедниться к каналу с помощью QRCODE.
▫️ Вы можете найти канал через поиск в Telegram. Для этого введите в поиск логин «advice17» или название «Советы разработчикам (python и не только)». Если не получается найти канал, вы можете отправить публичную ссылку @advice17 в сохраненные сообщения и перейти по ней.
Когда откроется канал нажмите снизу кнопку «Присоединиться» (JOIN).

📊 Статистика канала advice17 по количуству подписчиков за последнее время.

📢 Купить рекламу в телеграм каналах.

Отзывы о канале «‎Советы разработчикам (python и не только)»‎ @advice17

Рекомендуем

Взгляните на эти телеграм ресурсы, похожие на канал @advice17, они тоже могут вас заинтересовать.

Каталог каналов ️ TGRAM
Каталог каналов ️ TGRAM
@tgram_me
Король ставок 👑 Ставки на спорт - бесплатные прогнозы на сегодня
Король ставок 👑 Ставки на спорт - бесплатные прогнозы на сегодня
@betking_besplatnie_prognozi
IT Верстальщик
IT Верстальщик
@tgverstka
В городе
В городе
@yandexinthecity
Национальное Объединение Организаций в сфере Технологий Информационного Моделирования (НОТИМ)
Национальное Объединение Организаций в сфере Технологий Информационного Моделирования (НОТИМ)
@Technologiesofinformationmodelin
Лайфхаки | Кулинария & Рецепты
Лайфхаки | Кулинария & Рецепты
@ProCrafts

Последние сообщения

Что бы увидеть другие сообщения, подпишитесь на канал @advice17

Pull, poll, pool, spool


Есть несколько терминов, которые для русского уха звучат одинаково:

1. pull - с английского переводится как "тянуть". Идет в паре с термином push.

Как правило, термином pull обозначают команду получения данных с сервера. Соответственно, push отправляет их на сервер.

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

Термин применяется, например, когда мы говорим о методике сбора метрик работы приложения.

2. poll, polling - опрос.

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

В случае HTTP API клиент производит периодический опрос сервера на предмет наличия новых данных. В противоположность этому, при использовании же websockets, между клиентом и сервером есть постоянно соединение по которому передаются сообщения Если же используется webhook, то сервер сам соединяется с нашим приложением по HTTP для уведомления о наличии новых данных. В зависимости от реализации, такой запрос может содержать сами данные или только уведомление об их наличии.

Так же есть режим long polling, который от обычного polling отличается тем, что при отсутствии новых данных сервер не возвращает пустоту сразу, а ещё какое-то время держит соединение открытым.

Эти термины применяются при разработке веб приложений, платежных сервисов, телеграм-ботов и т.п.

3. pool - обычно означает паттерн "Объектный пул", когда мы не создаем объекты заново, а переиспользуем ранее созданные. см. также https://tgram.me/advice17/19

4. spooling — спулинг, буферизация задач.

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

Применяется, например, когда идет речь о выводе на печать.

Дополнительные материалы:

* https://man7.org/linux/man-pages/man2/poll.2.html
* https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-prsod/7262f540-dd18-46a3-b645-8ea9b59753dc
* https://git-scm.com/docs/git-pull
* https://prometheus.io/docs/introduction/faq/#why-do-you-pull-rather-than-push

823 14:30

Переменные окружения и dotenv

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

В общем случае у нас есть два стандартных варианта передачи настроек:
* файлы конфигурации
* переменные окружения

Файлы конфигурации удобны, когда настроек много и они имеют сложную структуру. Но при запуске сервиса в некоторых окружениях, таких как AWS Lambda, Kubernetes и Heroku доставка таких файлов с настройками до работающего экземпляра приложения может быть нетривиальна.
В противовес этому, во многих случаях такие сервисы позволяют через свои способы настройки указать переменные окружения, с которыми будет запущен процесс. Да, мы не сможем передать в этом случае сложные иерархические структуры, но зачастую это не нужно.

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

Чтобы передать переменные окружения нашему приложению, мы можем:
* При использовании bash/sh/zsh сделать export этих переменных или указать их перед командой которую мы выполняем
* Так же для bash удобно подготовить один или несколько файлов с инструкциями export и применять их с помощью команды source
* При запуске через Pycharm переменные окружения можно задать в настройках конфигурации запуска. Иногда удобно иметь несколько таких конфигураций, чтобы отлаживать софт с разными настройками. Другие IDE имеют аналогичные возможности.
* Так же вы всегда можете задать переменные окружения глобально средствами вашей ОС. Но тогда для смены их возможно придется перезайти в учетную запись.
* При запуске сервиса через systemd вы можете указать переменные окружения прямо в service файле или указать из какого файла их необходимо прочитать
* При запуске через docker так же они указываются в команде запуска контейнера docker run напрямую или через --env-file. В случае`docker-compose` эти возможности сохраняются

Стоит отметить относительную популярность библиотек типа python-dotenv. Они позволяют во время работы приложения прочитать конфигурационный файл своего формата и поменять переменные окружения текущего процесса согласно этому файлу. Опасность этого подхода в том, что в момент загрузки этого файла приложение уже работает и при наличии других архитектурных проблем эти значения могут конфликтовать с уже инициализированными объектами, что потребует дополнительных усилий для поддержания работоспособности кода. Некоторые реализации библиотек для чтения .env-файлов могут искать файлы не только в текущем каталоге, но и вверх в каждом родительском каталоге, что может приводить к непредсказуемому поведению кода.

Хочу также обратить внимание, что хотя формат конфига python-dotenv похож на используемый docker, systemd или bash файл, эти все форматы не совместимы. Где-то вы можете ставить пробелы около знака =, где-то допустимо или требуется писать export, где-то невозможно задать многострочные значения и т.д.


Дополнительные материалы:
* https://12factor.net/
* https://www.freedesktop.org/software/systemd/man/systemd.exec.html
* https://docs.docker.com/compose/env-file/
* https://ru.wikipedia.org/wiki/Переменная_среды

2.0K 11:39

Механика импорта и побочные эффекты

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

Каждый раз, когда вы импортируете новый, ранее не импортированный, модуль, питон выполняет его код. Даже если вы делаете from .. import, это все равно требует однократного выполнения исходного кода модуля.
Питон хранит в памяти все импортированные модули, поэтому код будет выполнен один раз. Они доступны через sys.modules

Фактически строка import x означает:
1. Найди модуль x в кэше, если там его нет - выполни его код и сохрани модуль в кэш;
2. Создай в текущей области видимости переменную x и присвой ей загруженный модуль в качестве значения.

Соответственно, from x import y означает:
1. Найди модуль x в кэше, если там его нет - выполни его код и сохрани модуль в кэш;
2. Создай в текущей области видимости переменную y и присвой ей в качестве значения атрибут y из модуля x.

Ну и наконец, from x import y as z означает:
1. Найди модуль x в кэше, если там его нет - выполни его код и сохрани модуль в кэш;
2. Создай в текущей области видимости переменную z и присвой ей в качестве значения атрибут y из модуля x.

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

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

Если вы импортируете модуль ради выполнения какой-то логики в коде модуля, то это будет неожиданно для поддерживающего код и может сломаться при любом изменении в порядке импортов. Вместо этого стоит такой код поместить в функцию и вызывать её уже по месту, например в функции main.

Дополнительные материалы:
* https://peps.python.org/pep-0008/#imports
* https://www.flake8rules.com/rules/F401.html
* https://docs.python.org/3/reference/import.html

3.9K 19:38

Про импорты и структуру проекта

Когда вы импортируете какой-то модуль в вашем коде, питон не учитывает, в каком файле этот импорт находится, влияет только то, как был запущен код.
Если модуль не был раньше загружен, питон пытается его найти по очереди в нескольких папках, которые можно посмотреть в переменной sys.path

По умолчанию она содержит примерно такие каталоги (в некоторых ситуациях, например, при использовании embedded версии python, состав может отличаться):
* каталог, добавляемый при запуске
* каталоги указанные в переменной окружения PYTHONPATH
* каталог текущего активированного виртуального окружения
* каталог установки python

1. Если вы запускаете ваш скрипт командой python scriptname.py, то первым в списке будет тот каталог, где находится запускаемый скрипт. Текущий каталог не имеет значения.
2. Если вы запускаете ваш код командой python -m packagename, то первым в списке будет текущий каталог. При запуске питон попытается найти и импортировать packagename по общим правилам.
3. Если вы запускаете код с помощью других инструментов вроде pytest, они тоже могут сами добавлять что-то в sys.path.

Скорее всего, вам не стоит самостоятельно менять sys.path, так как алгоритм его заполнения стандартный и привычен для всех. Если по каким-то причинам вас он не устраивает, возможно у вас неверная структура проекта.

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

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

1. Вынести запускаемые скрипты на верхний уровень, а остальной код упаковать в пакет.

Упаковка кода в пакет с уникальным именем позволяет исключить конфликты имен. А вынесение всех запускаемых файлов на один уровень делает состав sys.path предсказуемым.

Выглядеть это будет примерно так:

├── appname
│ ├── __init__.py
│ ├── other_module.py
│ └── some_module.py
├── cli_module.py
└── requirements.txt

2. Создать распространяемый пакет (рекомендую).

В этом случае вы упаковываете весь код в пакет, что помогает исключить конфликты имен.
Для запуска команд вы можете использовать синтаксис python -m appname.cli_module или заполнить секцию entry_points в файле с описанием проекта (setup.cfg, pyproject.toml), после чего иметь свои кастомные консольные команды. В обоих случаях вы сможете запускать код, находясь в любом каталоге, без необходимости указывать полные пути к файлам.

Для удобства разработки с таким подходом удобно устанавливать пакет в editable-режиме с помощью команды типа pip install -e .

Структура будет примерно такой:

├── pyproject.toml
└── src
└── appname
├── __init__.py
├── cli_module.py
├── other_module.py
└── some_module.py

Дополнительные материалы:
* https://packaging.python.org/en/latest/
* https://docs.python.org/3/reference/import.html
* https://docs.python.org/3/library/sys.html#sys.path
* https://ru.wikipedia.org/wiki/Рабочий_каталог

7.8K 15:03

Обработка исключений

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

Говоря о каждой конкретной строке кода можно выделить два вида исключений:
* ожидаемые (возможные) - исключения, которые мы предполагаем, что могут возникнуть в данном месте, и знаем какие действия принимать в этой ситуации
* неожиданные - исключения, которые не должны были возникнуть в этом месте, но возникли из-за неверно написанного кода и мы не имеем стратегии поведения в этой ситуации


Отсюда следуют следующие советы:
1. Всегда указывайте исключение, которое вы ловите. Если вы не знаете, что за исключение может возникнуть - вы не знаете корректно ли обрабатывать его вашим способом.
2. Указывайте максимально конкретный класс исключения.
3. Оборачивайте в try/except наименьший возможный код. Если требуется - разбивайте его на несколько выражений
4. Обрабатывайте исключения именно там, где у вас достаточно информации для принятия решения, что делать в данной ситуации.


Частой ошибкой новичков бывает написать просто except: или except Exception - не делайте так.
* Обработка всех подряд исключений, как правило, вообще не корректна, так как туда входит, например, KeyboardInterrupt, по которому ожидается как раз завершение программы. Но это допустимо, если после такой обработки вы пробросите исключение дальше.
* Обработка же Exception актуальна на уровне фреймворка, когда у нас есть стандартный способ реакции на неизвестные ошибки - выдача клиенту ответа с кодом 500, повтор обработки сообщения из очереди т.п.

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

Говоря о реализации адаптеров для различных сервисов или БД хорошей идеей будет ввести свои классы исключений

* Реализуя адаптер мы хотим скрыть детали реализации. Например, мы реализовали класс, реализующий хранение определенных сущностей в БД. Затем после очередного рефакторинга мы вынесли это в отдельный микросервис. Интерфейс адаптера при этом не изменился и мы ожидаем что использующий такой адаптер код не будет меняться. И если исключения вроде OSError или ValueError достаточно нейтральны и почти не говорят о реализации, то классы исключения, принадлежащие конкретной используемой библиотеки не стоит прокидывать извне такого адаптера.

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

Дополнительные материалы:
* https://martinfowler.com/articles/replaceThrowWithNotification.html
* https://peps.python.org/pep-0008/#programming-recommendations

3.8K 10:58

БД и миграции

При работе с базами данных нам необходимо, чтобы структура данных в БД соответствовала коду:
Во-первых, мы должны изначально создать необходимые таблицы или коллекции с правильными индексами и т.п.
Во-вторых, при изменении кода приложения мы должны актуализировать эту структуру, создать новые таблицы, переложить данные по другому и т.п. Даже если сама СУБД не подразумевает фиксированной схемы лежащих в ней данных, вы можете захотеть уменьшить разнообразие вариантов хранения.

Кроме этого у нас есть дополнительные ограничения:
* Код не должен иметь права отключать проверки в СУБД, менять и создавать индексы и настраивать связи;
* Запуск нескольких копий кода одновременно (актуально для веб-приложений) не должен приводить БД в неработающее состояние;
* Приложение может быть развернуто на нескольких окружениях, которые обновляются независимо;
* Иногда должна быть возможность вернуть базу данных в предыдущее состояние из-за ошибок;
* Иногда мы хотим, чтобы несколько версий кода работали одновременно. Например, при green-blue/canary deployment.

Таким образом я бы выделил следующие подходы:
1. Состав и структура таблиц должны определяться на момент проектирования/реализации версии кода. Таблицы не должны генерироваться динамически во время работы приложения;
2. Для приведения структуры БД в нужное состояние пишутся скрипты миграции;
3. Скрипты миграции вызываются администратором при деплое приложения. Приложение не должно самостоятельно вызывать скрипты миграции при старте или в другой момент во время работы;
4. Каждый скрипт миграции должен содержать все необходимые данные для его работы. Скрипт миграции не должен обращаться к основному коду приложения, так как код будет меняться, а миграция должна оставаться работоспособной;
5. Скрипт миграции не должен редактироваться после выпуска очередной версии приложения. Если вы забыли мигрировать часть данных, придется делать ещё одну миграцию;
6. Миграции необходимо проверять/тестировать. Тестовые базы данных должны обновляться только с помощью миграций;
7. Инструменты для автоматической генерации миграций могут помочь в работе, но вы должны проверять и редактировать сгенерированный код.

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

Если же вы делаете эти вещи, вероятно вы используете БД неправильно:
1. Создаете таблицы во время работы программы;
2. Вызываете meta.create_all() (или аналог для вашей ORM) для создания структур БД для ваших моделей;
3. Вызываете миграции автоматически при старте приложения;
4. Импортируете в миграциях модели или другой код из основной части проекта;
5. Меняете код миграций после того как они могли быть использованы;
6. Не запускаете миграции нигде кроме прода;
7. Не читаете код автоматически сгенерированных миграций.

Дополнительно хочу отметить, что миграции - это не обязательно простые изменения структуры, такие как добавление или удаление колонки/таблицы. Иногда вам потребуется произвести какую-то длительную работу по модификации данных (например, посчитать значение колонки для БД из миллиарда записей).

И хотя обычно эти советы дают для реляционных СУБД, так как те требуют соблюдения структуры таблиц, они также применимы и для документо-ориентированных баз данных. Вы можете обойтись без миграции для добавления nullable поля в MongoDB, но скорее всего вам потребуется её делать в том или ином виде, если вы захотите разбить колонку на две или вместо одного числа начать хранить список.

Дополнительные материалы:
* https://habr.com/ru/company/yandex/blog/511892/
* https://habr.com/ru/company/flant/blog/471620/
* https://alembic.sqlalchemy.org/en/latest/

3.8K 09:50

Пулы объектов и соединений.

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

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

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

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


Примеры:

* requests.Session - кроме дополнительной логики по управлению куками содержит внутри пул соединений с серверами, по которыми в дальнейшем посылаются HTTP(s) запросы. Использовать requests без Session скорее всего будет плохой идеей

* aiohttp.ClientSession - аналогичный объект для асинхронного "аналога" - библиотеки aiohttp. Несмотря на то, что в примерах из документации зачастую сессия создается по месту запроса, рекомендуется инициализировать её один раз и в дальнейшем переиспользовать

* psycopg2.pool - модуль с несколькими вариантами пулов соединений с СУБД Postgresql.

* Engine из SQLAlchemy также использует пул соединений. При этом возможна настройка таких параметров как время жизни соединения, дополнительные проверки его доступности, размер пула. В том числе возможно и использование NullPool, который по факту не является пулом, но совместим с ним по интерфейсу.

Дополнительные материалы:

* https://habr.com/ru/company/otus/blog/443312/
* https://habr.com/ru/post/443378/
* https://docs.sqlalchemy.org/en/14/core/pooling.html
* https://docs.aiohttp.org/en/stable/client_reference.html
* https://docs.python-requests.org/en/latest/user/advanced/#session-objects

3.4K 18:33

Потокобезопасность и конкурентный доступ

Большинство приложений, которые мы пишем, используют конкурентность.
Это могут быть многопоточные веб-приложения, телеграм-боты с asyncio, GUI приложения с фоновой обработкой и т.п.

При разработке таких приложений стоит задумываться, к каким объектам вы имеете доступ только из одного логического потока, а какие из них используются конкурентно. Если вы работаете с одним объектом из нескольких потоков/asyncio тасков возможна ситуация, называющаяся "состоянием гонки" (race condition). Это состояние, когда результат работы кода зависит от того в какой последовательности выполняются действия внутри конкурентных операций.

Приведу пример:
counter = 0

def do():
global counter
if counter < 1:
sleep(0.01)
counter += 1

Если вы запустите такой код последовательно несколько раз, значение counter будет равно 1. Однако, если вы запустите его в несколько потоков, то возможны произвольные значения. Замена threading на asyncio проблему в данном случае не решит.

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

Можно придумать достаточное количество других примеров состояния гонки, которые могут достаточно разнообразные последствия для работы программы: от нарушения логики работы кода непредсказуемым образом, до утечек памяти или падений с segmentation fault. Есть разные методы борьбы с таким состоянием: можно использовать блокировки, _compare-and-swap_ алгоритмы, но самое надежное - отказаться от использования общих данных совсем.


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

Примеры:
requests.Session - потокобезопасный объект. Вы можете посылать запросы из нескольких потоков используя одну сессию. Однако тут возможны логические ошибки, если сервер пришлет разные куки в ответ на конкурентные запросы.

asyncio.Queue - не потокобезопасен, однако безопасен для использования в конкурентных тасках asyncio.

sqlite3.Connection - не потокобезопасен. По умолчанию, sqlite дополнительно выдает ошибку, если вы попытаетесь использовать соединение не из того потока, где вы его создали. Отключение этой проверки не сделает соединение безопасным для использования из нескольких потоков, это просто дополнительная защита.
Как дополнительный фактор против конкурентного использования стоит отметить транзакции субд. Ведь если вы используете одно соединение, вы работаете в одной транзакции. И когда один из потоков решит её зафиксировать (commit), а второй - откатить (rollback) результат будет неизвестен.

Session из SQLAlchemy - не потокобезопасна по тем же причинам, как и соединения с СУБД. А вот Engine, который используется для создания сессий, уже потокобезопасен.

Объекты интерфейса tkinter, Qt, Android SDK и других GUI фреймворков также не рассчитаны на использование из нескольких потоков. В этом случае у вас, как правило, есть один поток для работы с GUI и только из него вы можете обновлять элементы интерфейса. Также эти фреймворки предоставляют инструменты для передачи в этот поток информации о необходимости обновить интерфейс (например, механизм signal-slot).

Доп ссылки:
* https://ru.wikipedia.org/wiki/Состояние_гонки
* https://ru.wikipedia.org/wiki/GIL
* https://docs.sqlalchemy.org/en/14/orm/session_basics.html#is-the-session-thread-safe

3.3K 15:22

Запуск программ в фоне на Linux

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

1. Разовый запуск команд, которые долго работают;
2. Запуск сервиса, который должен постоянно или периодически работать.

Если в первом случае речь идет о запуске команды из консоли, то, возможно, мы захотим увидеть её вывод или продолжить работать в этой консоли через какое-то время. Если мы работаем в консоли нашего компьютера, ничего дополнительно делать не требуется. Однако если мы подключаемся к серверу по SSH, то при разрыве соединения приложение через какое-то время будет закрыто. Для таких случаев актуально использовать такие программы как screen или tmux, которые позволяют запустить сессию консоли, не привязанную к конкретному терминалу. Кроме того они умеют эмулировать несколько консолей в рамках одной. При этом вы сначала запускаете screen, в котором уже вводите нужные команды. Если потом вы отключитесь от терминала, вы сможете вывести список открытых сессий screen и подключиться к ним для продолжения работы.

Альтернативным вариантом для запуска долгой команды, без необходимости продолжить взаимодействие со скриптом может стать systemd-run.

В случае запуска постоянно работающего сервиса частыми требованиями будут:
* автоматический старт после перезагрузке сервера;
* перезапуск в случае аварийного завершения;
* просмотр статуса;
* ручная остановка и перезапуск;
* сбор логов;
* ограничение прав, настройка последовательности запуска и т.п.


В современных серверных дистрибутивах Linux для этого используется systemd. Это предустановленное приложение, которое занимается обслуживанием всех системных фоновых сервисов и связанных с этим задач. Так же вместе с ним идет journald, который с этих сервисов собирает логи. Добавление своего сервиса сводится к созданию service-файла и использованию команд типа systemctl или journalctl.

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

В случае запуска периодических задач раньше использовался cron, однако сейчас его задачи также выполняется systemd (systemd-timers). В отличие от предшественника он имеет более богатые возможности настройки и возможность сбора логов. Настройка делается практически так же, как для запуска постоянно работающих сервисов. Более того, в современных системах cron на самом деле эмулируется тем же systemd.

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

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


Дополнительные материалы:
* https://habr.com/ru/post/503816/
* https://github.com/tmux/tmux/wiki/Getting-Started
* https://systemd.io/
* https://github.com/tmux/
* https://www.gnu.org/software/screen/manual/
* https://docs.docker.com/

3.6K 19:05

SQL, соединения и слои абстракции

При написании приложения, работающего с БД без использования ORM возникают вопросы о его структурировании и жизненном цикле объектов БД.

Если не рассматривать другие сущности, то условно такое приложение можно поделить на следующие слои (да простит меня Дядюшка Боб):

* Адаптеры для базы данных
* Бизнес-логика
* Контроллеры и представления
* Интеграционный слой


Рассмотрим чуть подробнее:

Адаптеры для базы данных
Это классы или функции, скрывающие в себе детали построения запросов в базу данных. Именно тут пишется SQL код, происходит разбор данных, полученных из курсора в понятные остальному коду классы.

Зачастую удобно сделать класс, который будет содержать текущее соединение с БД в своем поле и методы, делающие внутри один или несколько SQL-запросов, имеющих смысл с точки зрения основной логики программы. Этот код НЕ должен сам создавать соединение, воспользуйтесь Dependency Injection. Он так же не должен управлять транзакциями. Благодаря этому можно в дальнейшем комбинировать вызовы его методов. Также стоит избегать использования в интерфейсе этого класса слишком абстрактных методов, чтобы не переносить детали работы с БД в слой бизнес-логики.

Бизнес-логика
Этот слой содержит код обработки конкретных сценариев использования программы (use cases), то есть основную логику программы. Она абстрагирована от деталей работы базы данных или представления данных для пользователя. Для работы с базой данных она обращается к соответствующим адаптерам. Именно бизнес-логика знает о том, какие операции с базой данных являются неделимыми и управляет транзакциями.

Бизнес-логика не знает о том, откуда взялся адаптер БД, а просто использует его.

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

Они реагируют на возникающие в вашем фреймворке события и вызывают бизнес-логику для его обработки. Контроллеры обрабатывают входящие данные, трансформируют их в понятные бизнес-логике структуры, а представления делают обратные преобразования.

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

Если же фреймворк предоставляет механизм middleware, то такой объект также имеет представление о жизненном цикле события и может создавать соединение и передавать в контроллеры нужные объекты. Зачастую именно такой подход удобнее.

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

Итого:
1. Адаптер БД реализует единичные действия с базой и скрывает SQL код
2. Бизнес-логика оперирует транзакциями и вызывает методы адаптеров
3. Контроллер вызывает один или несколько use case в рамках соединения которое он получил сам или из мидлвари.

В начале обработки чего-либо (веб-запроса, сообщения в телеге и т.п.) достаем соединение, в конце обработки - возвращаем туда, откуда достали.
Также стоит отметить, что соединение с БД не обязательно физически открывать каждый раз, стоит использовать пулы соединений для ускорения.

Дополнительные материалы:
* https://ru.wikipedia.org/wiki/Адаптер_(шаблон_проектирования)
* https://martinfowler.com/eaaCatalog/repository.html
* https://www.ozon.ru/product/144499396/

3.5K 07:42

FastAPI и Dependency Injection.

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

Что такое Dependency Injection?
Это достаточно простая концепция, которая говорит: если объекту что-то нужно, он не должен знать, как оно создается.

Пример:
Ваш класс User хочет уметь ходить в Shop.
Он мог бы создать магазин сам, но тогда мы не сможем всем пользователям дать один магазин. И вообще придется переносить в него всю логику строительства магазинов со всеми вариантами её применения.
Вместо этого предлагается чтобы User получал экземпляр Shop в __init__.
И уже наш код, создающий пользователей, сможет передать туда нужный магазин. При чем если у нас пользователи бывают в разных местах системы мы сможем в каждом случае иметь правильные магазины и свою логику работы с ними.

Таким образом весь механизм DI состоит из двух частей:
1. Есть класс/функция, которая от чего-то зависит
2. Есть логика, которая подставляет эту зависимость

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


В случае FastAPI, у нас есть механизм автоматической подстановки зависимостей в наши view-функции (это обычно называется IoC-контейнер).
Он состоит аналогично из двух частей
* С помощью Depends мы обозначаем зависимость. Если зависимость идентифицируется по аннотации типа параметра функции, то Depends используется без параметров.
* С помощью app.dependency_overrides мы определяем фабрику, возвращающую эту зависимость или генераторную функцию

В простом случае это может выглядеть вот так:
# определяем view
@router.get
def my_view_funcion(param: Session = Depends()):
...

# в мейне создаем приложение и настраиваем
app = FastApi()
app.dependency_overrides[Session] = some_session_factory

Типичной ошибкой, которую допускает даже автор fastapi является указать настоящую функцию создания зависимостей в Depends
# так делать не стоит
def my_view_funcion(param: Session = Depends(create_session)): ...

В этом случае вы конечно все ещё можете переопределить зависимость в тестах, но ваш код оказывается сцеплен с кодом создания объекта Session и более того, вы не можете настроить логику работы функции create_session при создании приложения.
То есть это не DI, а только его половинка.


Функционально Depends в fastapi можно использовать не только для DI, но и для переиспользования части логики или даже параметров запроса и это накладывает свой отпечаток.
Дело в том, что FastAPI генерирует open api (swagger) спецификацию для view-функции не только на основе её параметров, но так же на основе параметров её зависимостей.

Поэтому, если наша зависимость (класс Session) имеет конструктор с параметрами, мы увидим их в swagger-спецификации и для отключения этого нет встроенных средств.
Избежать это можно двумя способами:

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

Первый способ в коде выглядит так
class MyProto(abc.ABC):
...

@router.get
def my_view_funcion(param: MyProto = Depends()):
...

app = FastApi()
app.dependency_overrides[MyProto] = some_session_factory

Второй способ - так:
def get_session_stub():
raise NotImplementedError # это реально тело этой функции

@router.get
def my_view_funcion(param: Session = Depends(get_session_stub)):
...

app = FastApi()
app.dependency_overrides[get_session_stub] = some_session_factory

3.0K 14:54