Получи случайную криптовалюту за регистрацию!

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

Логотип телеграм канала @advice17 — Советы разработчикам (python и не только) С
Логотип телеграм канала @advice17 — Советы разработчикам (python и не только)
Адрес канала: @advice17
Категории: Технологии
Язык: Русский
Количество подписчиков: 4.59K
Описание канала:

Советы для разработчиков ПО от @Tishka17
Поддержать материально по кнопке в канале, либо https://www.tinkoff.ru/cf/2NkdXaljivI
Programming, python, software architecture и все такое

Рейтинги и Отзывы

3.00

2 отзыва

Оценить канал advice17 и оставить отзыв — могут только зарегестрированные пользователи. Все отзывы проходят модерацию.

5 звезд

0

4 звезд

0

3 звезд

2

2 звезд

0

1 звезд

0


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

2022-05-06 18:22:06 Потокобезопасность и конкурентный доступ

Большинство приложений, которые мы пишем, используют конкурентность.
Это могут быть многопоточные веб-приложения, телеграм-боты с 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.5K viewsedited  15:22
Открыть/Комментировать
2022-04-03 22:05:52 Запуск программ в фоне на 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.8K viewsedited  19:05
Открыть/Комментировать
2022-03-25 10:42:43 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.7K viewsedited  07:42
Открыть/Комментировать
2022-03-23 17:54:45 Доп материалы:
* https://ru.wikipedia.org/wiki/Внедрение_зависимости
* https://martinfowler.com/articles/injection.html
* https://fastapi.tiangolo.com/tutorial/dependencies/
* https://github.com/adriangb/di
2.9K viewsedited  14:54
Открыть/Комментировать
2022-03-23 17:54:43 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.2K viewsedited  14:54
Открыть/Комментировать
2022-03-21 18:38:41 Советы разработчикам (python и не только) pinned «Оглавление Недостатки глобальных переменных Ошибки при обработке настроек Вложенные классы и функции Объекты и словари Приватные и публичные атрибуты Форматирование и инъекции FastAPI и Dependency Injection SQL, соединения и слои абстракции Запуск программ…»
15:38
Открыть/Комментировать
2022-03-21 18:35:52 Оглавление

Недостатки глобальных переменных
Ошибки при обработке настроек
Вложенные классы и функции
Объекты и словари
Приватные и публичные атрибуты
Форматирование и инъекции
FastAPI и Dependency Injection
SQL, соединения и слои абстракции
Запуск программ в фоне на Linux
Потокобезопасность и конкурентный доступ
Пулы объектов и соединений
БД и Миграции
Обработка исключений
Работа import и структура проекта
Механика импорта и побочные эффекты
Переменные окружения и dotenv
Pull, poll, pool и spool
Ссылки и `is`
Управление памятью в Python
3.1K viewsedited  15:35
Открыть/Комментировать
2022-03-20 16:07:05 Channel photo updated
13:07
Открыть/Комментировать
2022-03-20 16:02:49 Про форматирование и инъекции.

В Python есть минимум 3 разных способа форматирования строк:
* f-строки
* .format
* %

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

Компьютер ожидает, что обрабатываемый текст будет оформлен согласно определенным правилам и вы не можете просто так вставить туда произвольную строку не нарушив при этом структуру текста.
Речь идет о формировании SQL, XML, HTML, JSON, консольной команды, URL, регулярных выражений и ещё кучи различных типов строк.

Как правило такая проблема решается одним из двух способов:
1. Ручное экранирование данных
2. Вызов специальных библиотечных методов, подставляющих данные безопасно.

Как правило лучше выбирать второй способ, так он проще и оставляет меньше шансов ошибиться.

Рассмотрим примеры.

1. SQL
Допустим вы хотите прочитать данные из БД и пишете такой запрос:
cur.execute(f"SELECT * FROM users WHERE login = '{somelogin}'")
Он будет работать в каких-то случаях и обязательно сломается, если переменная somelogin будет содержать, например, кавычку.
В некоторых случаях это может привести к исполнению произвольного SQL кода.

Замена f-строки на .format или форматирование через % ничего не изменит.
Правильно тут было бы сделать так (использовать тут ? или другой символ зависит от вашей СУБД):
cur.execute("SELECT * FROM users WHERE login = ?", (somelogin,))

2. HTTP
Если вы делаете GET запрос, вы можете написать такой код:
requests.get(f"http://site.com?search={query}")
Он будет работать некорректно если переменная query, например, содержит знаки &?
Правильно написать так:
requests.get("http://site.com", params={"search": query})

3. Shell
Следующий код сломается, если имя папки содержит пробел или точку с запятой, и может привести к исполнению произвольных команд.
subprocess.run(f"ls {dirname}", shell=True)
Его стоит заменить на
subprocess.run(["ls", dirname])

4. HTML
В случае подстановки данных в HTML стоит воспользоваться специальными шаблонизаторами. Например, jinja.


Доп материалы:
* https://xkcd.ru/327/
* https://ru.wikipedia.org/wiki/Внедрение_SQL-кода
* https://ru.wikipedia.org/wiki/Межсайтовый_скриптинг
3.5K viewsedited  13:02
Открыть/Комментировать
2022-03-20 16:01:52 Про приватные и публичные атрибуты

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

1. Разделение публичного API класса/библиотеки и деталей реализации.
На уровне договоренности принято называть атрибуты, предназначенные "для внутреннего использования" начиная с одинарной черточкой _.
Такое обозначение является лишь соглашением и не подкрепляется никаким механизмом в языке. Но при этом линтеры как правило проверяют доступ к таким атрибутам.

Термин "protected-атрибуты" тут может быть применен с большой натяжкой и не в том же смысле как в Java или C++
С другой стороны, в той же Java есть способы нарушить защиту предоставляемую модификаторами доступа.

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

2. Защита от переопределения атрибута наследниками.
Иногда мы хотим, чтобы методы класса обращались именно к его атрибуту, а если наследник определит свой, это никак не затрагивало нашу логику.
Это корректная ситуация, хотя и не очень частая. Для этого в питоне поддерживается механизм "name mangling", который включается использованием двойного подчеркивания в имени атрибута (__).
Конечно, к такому атрибуту все ещё возможно орбатиться и переопределить его, но это требует явных намерений от разработчика.
Это снова отличается от механики работы "private" атрибутов в других языках, хотя и пересекается с ними по одному из сценариев применения


Таким образом:
* если вы хотите указать разработчику, что атрибут (класс, функция, глобальная переменная) не являются частью API класса/модуля - испольузйте одну черточку
* если вы действительно хотите защититься от переопределения атрибуты наследниками класса - используйте две черточки
* в остальных случаях - работайте над архитектурой проекта

Подробнее:
* https://docs.python.org/3/tutorial/classes.html#private-variables
* https://martinfowler.com/bliki/AccessModifier.html
2.9K viewsedited  13:01
Открыть/Комментировать