Python Заметки
2.31K subscribers
58 photos
2 videos
2 files
212 links
Интересные заметки и обучающие материалы по Python

Контакт: @paulwinex

⚠️ Рекламу на канале не делаю!⚠️

Хештеги для поиска:
#tricks
#libs
#pep
#basic
#regex
#qt
#django
#2to3
#source
#offtop
Download Telegram
Awesome Python Typing

Коллекця ссылок на различные инструменты повышения качества кода и удобства работы.
Типизация, линтинг, автокомплиты и другие полезности.

Есть ссылка на надавно вышедшую SQLAlchemy2, так что актуальность поддерживается.

Странно что всё еще нет в списке Ruff. Хотя, в Issues уже добавили запрос. Но автор не уверен что эта тулза подходит для его списка.

#libs
👍4
Всех, кто в теме, с праздником! 🎉🚀⭐️
🔥17👍43
Библиотека platformdirs для получения стандартных локальных путей на компьютере юзера для хранения данных приложения и конфигов.

▫️кросплатформенная (Linux, macOS, Windows, Android)
▫️изоляция разных версий приложения
▫️большой список стандартных директорий

Альтернативная библиотека: appdirs

#libs
👍8
Где хранить настройки мы уже выяснили. А как вы храните на хосте юзера чувствительные и секретные данные?

Занимаетесь обфускацией паролей и токенов делая защиту от дурака? Да, я тоже когда-то этим занимался😆

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

В каждой ОС своё решение, и удобно будет исопльзовать просплатформенные библиотеки. Самая популярная это keyring.

Использовать очень просто. Любой пароль или токен сессии нужно сохранить под каким-либо именем. Обычно это имя сервиса. Так же потребуется юзернейм.
С этими же данными можем получить пароль обратно.

>>> import keyring
keyring.set_password('myapp', 'username', 'pswd!')
print(keyring.get_password('myapp', 'username'))
# pswd!


Поддерживается основные бэкэнды из коробки и можно доставить сторонние или написать свой.

Для генерации и проверки паролей не забываем про secrets.

#libs
👍53
На днях вышел Django 5.

▫️ GeneratedField
Поля, которые автоматически рассчитываются по экспрешену (Database generated model field).

▫️Фасетный фильтр для админки
Показывает количество элементов для каждого фильтра.

▫️Async
Добавлены асинхроные функции django.contrib.auth, ORM. Ряд декораторов теперь поддерживаются асинхронными вьюшками.

▫️ORM
Новые возможносте полей, такие как поддержка словарей и функций в choices, дефолтные значения на стороне БД (Database-computed default values) с аргументом db_default и другие.

▫️Шаблоны
Новые возможности шаблонов, позволяющие писать меньше кода в формах.

#django
🎉51
Библиотеки для рабты с коллекциями файлов (секвенциями)

▫️ Поиск коллекций в директории
▫️ Проверка целостности
▫️ Поиск пересечений
▫️ Форматирование
И другие функции

➡️ CLIQUE https://clique.readthedocs.io/en/stable/
import clique
files = [
'/tmp/file1_001.png',
'/tmp/file1_002.png',
'/tmp/file1_003.png',
'/tmp/file1_005.png',
]
collection = clique.assemble(files)[0][0]
collection.head
# '/tmp/file1_'
collection.tail
# '.png'
collection.padding
# 3
collection.indexes
# <SortedSet "[1, 2, 3, 5]">
collection.holes()
# <Collection "/tmp/file1_%03d.png [4]">
collection.separate()
# [<Collection "/tmp/file1_%03d.png [1-3]">,
# <Collection "/tmp/file1_%03d.png [5]">]


➡️ PYSEQ https://pyseq.rsgalloway.com/
import pyseq
files = [
'/tmp/file1_001.png',
'/tmp/file1_002.png',
'/tmp/file1_003.png',
'/tmp/file1_005.png',
]
sequence = pyseq.Sequence(files)
sequence.head()
# 'file1_'
sequence.tail()
# '.png'
sequence.path()
# '/tmp/file1_1-5.png'
sequence.frames()
# [1, 2, 3, 5]
sequence.format('%p')
# '%03d'
sequence.missing()
# [4]


У библиотек схожий функционал но в деталях различается.
clique не умеет работать с pathlib.Path а pyseq не понимает генератор как источник. Но обе могут найти все коллекции в директории и выдать много информации о них.

#libs
👍101
Поздравляю всех с 2к24! ❄️⛄️🎁🎄🐲
22
PEP471 добавил в Python3.5 в модуль os новую функцию scandir()

▫️это генератор с соответствующими возможностями
▫️возвращает не просто строку а объект DirEntry
▫️работает в 4-10 раз быстрей чем os.listdir и os.walk

Раньше это была отдельная библиотека, которая позже стала частью CPython, как и ряд других новых библиотек в Python 3.

В настоящий момент метод Path.iterdir() всё еще использует os.listdir().
Обёртка, заставляющая обычную функцию работать как генератор

def iterdir(self):
for name in os.listdir(self):
yield self._make_child_relpath(name)


В тоже время Path.glob() и Path.rglob() уже используют os.scandir(), то есть полноценные генераторы.

#libs
👍8🔥3
This media is not supported in your browser
VIEW IN TELEGRAM
rich

Библиотека для нескучного принта!
Добавляет в ваш терминал цвет и разные способы форматирования текста.

https://github.com/willmcgugan/rich

➡️ Мой небольшой пример

#libs
👍10
Новый пакеджинг для Python uv написанный на Rust от автора быстрого линтера Ruff

Что нам обещают

▫️ Эпичная скорость
▫️ Легкий переход с pip и pip-tools
▫️ Отсутствие зависимостей и дистрибуция в виде одного автономного bin файла

Следим, надеемся, тестируем...

#libs
🔥9🤔1
Когда пишешь асинхронный код нужно учитывать особенности такого подхода. Всегда требуется держать в уме, когда возвращается корутина а когда реальный результат. Между этими двумя сущностями должен быть вызов через await.
Вот пример синхронного запроса в базу данных с помощь sqlalchemy. Query пишу инлайном для компактности.
entities = session.execute(select(EntityModel)).scalars().all()

Всё ясно и линейно. А вот он же асинхронный.
result = await session.execute(select(EntityModel))
entities = result.scalars().all()

Это значит что session.execute возвращает корутину, или awaitable объект. Сначала его нужно выполнить через await, тогда получишь объект с которым можно дальше работать.
Не хочу сказать что это мастхэв практика, но простые асинхронные запросы тоже можно сократить до одной строки. Просто использовать скобки.
entities = ( await session.execute(select(EntityModel)) ).scalars().all()

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

#tricks
👍9
Библиотека psutil предоставляет весьма широкий инструментарий для взаимодействия с процессами.
Одна из полезных функций - узнать какие файлы открыты в контексте процесса или узнать какой процесс занимает файл.

Узнаём какие файлы использует процесс

import psutil
def list_file_handlers(process_name):
for proc in psutil.process_iter():
if proc.name().lower().startswith(process_name):
for file in proc.open_files():
print(file)
list_file_handlers('python')

Функция вернёт имя процесса который занял файл. Если файл не занят то вернёт None.

def who_is_use(fpath):
for proc in psutil.process_iter():
for item in proc.open_files():
if fpath == item.path:
return proc.name()


Для использования требуются админские права.

#libs
6👍4
⭐️ Поздравляю!!! ⭐️
🌺🌼🌹🧚‍♀️👠🍀🎉

+ бонус в коментах 😊
7🤔2👍1
Библиотека APScheduler для управления заданиями в Python.
Может запускать планировщик и задания как отдельный поток (синхронный код) и как коркутины (асинхронный код), отложенные или через интервал.

Что есть в APScheduler:

▫️гибкий функционал создания задачи
▫️удобное управление созданными заданиями (pause\resume, listing, modify, reschedule)
▫️кастомизация классов библиотеки
▫️различные хранилища заданий (Memory и различные БД)
▫️интеграции в фреймворки
▫️7 вариантов планировщика

Три варианта тригеров для задач:

▫️по дате с помощью datetime
▫️через интервал с помощью datetime
▫️через интервал с помощью cron

и другие полезности

В данный момент готовится к релизу 4я версия

PS. Всегда использую вместе с FastAPI, очень рекомендую к ознакомлению.

#libs
🔥4👍2
Функция subprocess.check_output() удобна, когда нужно просто получить аутпут процесса.
info = subprocess.check_output(cmd, text=True)

Но вы не сможете таким образом получить аутпут процесса который завершился с ненулевым кодом выхода. Вместо этого у вас выбрасывается исключение
CalledProcessError: Command '[...]' returned non-zero exit status 1.

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

За генерацию исключения отвечает аргумент check, который по умолчанию равен False но именно в check_output он равен True и не может быть переопределён при вызове.

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

Классы TimeoutExpired и CalledProcessError имеют ряд атрибутов, которые хранят всю нужну инфу. Например, вызванная команда (cmd), код выхода (returncode) и то что мы ищем - аутпут процесса (output)

Итого, базовая фукнция для захвата аутпута для любого кода выхода будет выглядеть как-то так:
def get_proc_output(cmd):
try:
return subprocess.check_output(cmd, text=True)
except subprocess.CalledProcessError as e:
return e.output

#tricks
👍13😁3🔥1
Нередко требуется удалять дубликаты инстансов класса. Для этого обычно используется либо циклы со сравнением некоторых атрибутов, либо тип данных set().

При добавлении элемента в set происходит сравнение этого объекта по хешу. Если хеш совпадает с хешем уже существующего объекта, то происходит сравнение объектов на равенство. Если объекты равны, то новый объект не добавляется.

class A:
def __init__(self, pk: int):
self.pk = pk
def __repr__(self):
return f"{self.__class__.__name__}(pk={self.pk})"

set([A(pk=1), A(pk=2), A(pk=2)])
>>> {A(pk=1), A(pk=2), A(pk=2)}

Далее для краткости метод `__repr__()` я буду пропускать

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

class A:
def __init__(self, pk: int):
self.pk = pk
def __eq__(self, other):
return self.pk == other.pk

set([A(pk=1), A(pk=2), A(pk=2)])
>>> TypeError: unhashable type: 'A'


Теперь в дело вступает логика, описаная в документации.
Если вы переопределили __eq__() то следует переопределить и __hash__().

class A:
def __init__(self, pk: int):
self.pk = pk
def __eq__(self, other):
return self.pk == other.pk
def __hash__(self):
return hash(self.pk)

set([A(pk=1), A(pk=2), A(pk=2)])
>>> {A(pk=1), A(pk=2)}


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

class B(A):
pass

set([B(pk=1), B(pk=2), B(pk=2)])
>>> {B(pk=1), B(pk=2)}


Теперь следует учитывать вот такое поведение

hash(A(1)) == hash(B(1))
>>> True
set([A(1), B(1)])
>>> {A(pk=1)}


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

class A:
...
def __eq__(self, other):
return isinstance(other, self.__class__) and self.pk == other.pk

def __hash__(self):
return hash((self.pk, self.__class__))
...

Но если вдруг решите как-то изменить способ сравнения в классе В...
class B(A):
def __eq__(self, other):
return abs(self.pk) == abs(other.pk)

set([B(pk=1), B(pk=2), B(pk=2)])
>>> TypeError: unhashable type: 'B'


Снова получите ошибку. Та же логика - при переопределении метода __eq__() в новом классе метод __hash__() автоматически становится None и его тоже требуется переопределить.

#tricks
👍7🔥1
Три способа создать декоратор для метода класса.

▫️Способ 1. Обычная функция.

Единственное отличие от простого декоратора функции в том, что нужно учитывать аргумент self.
Если же он не нужен то просто пробрасываем его через *args

def decorator_func(func):
def wrapped(*args, **kwargs):
print('decorator_func')
return func(*args, **kwargs)
return wrapped

class MyClass:
@decorator_func
def method(self):
print('call method')

MyClass().method()
# decorator_func
# call method


▫️Способ 2. Методы класса.

Но что, если декоратор жестко привязан к классу и используется только в нём. И стоит задача закрепить декоратор именно за этим классом и расположить внутри него.
В таком случае можно сделать staticmethod. Это будет выглядеть страшно, но работать будет (тестировано на 3.11)
Очевидно, что декоратор должен быть объявлен раньше метода.

class MyClass:
@staticmethod
def decorator(func):
def wrapper(*args, **kwargs):
print('decorator from staticmethod')
return func(*args, **kwargs)
return wrapper

@decorator.__func__
def method(self):
print('method called')

MyClass().method()
# decorator from staticmethod
# method called


Тоже самое будет и с classmethod, но еще хуже.

class MyClass:
@classmethod
def decorator(func):
def wrapper(self, *args, **kwargs):
print('decorator from classmethod')
return func(self, *args, **kwargs)
return wrapper

@decorator.__func__
def method(self):
print('method called')

MyClass().method()
# decorator from classmethod
# method called


Где-то потерялся аргумент cls. Скорее всего это можно решить но лучше не надо. Оба варианта выглядят страшненько 🫣

▫️Способ 3. Вложенный класс и staticmethod

class MyClass:
class deco:
@staticmethod
def my_decorator(func):
def wrapper(*args, **kwargs):
print('decorator from subclass')
return func(*args, **kwargs)
return wrapper

@deco.my_decorator
def method(self):
print('method called')

MyClass().method()
# decorator from subclass
# method called


Получаем чтото вроде микса способов 1 и 2: функция вложена в отдельный класс.

Лучшей практикой является способ 1 - обычные функции.

Всего пару раз за практику я использовал 3й способ, когда декоратор был намертво привязан к классу и нигде больше не мог использоваться (например, отправлял вызов метода на воркера в другой процесс, не спрашивайте почему так, просто так было нужно 🤪)

Способ 2 не советую. Это, скорей, разминка для ума чем практический пример.

PS
- wraps пропустил для краткости
- в коментах дополнительная инфа

#tricks
👍7
Объекты datetime.timedelta поддерживают операторы деления и умножения
from datetime import timedelta

td1 = timedelta(hours=1)
# увеличим интервал в 2.5 раза
print(td1*2.5)
# 2:30:00

# разделим интервал на 2
print(td1/2)
# 0:30:00

Можно разделить один интервал на другой, включая целочисленное деление. Так мы узнаем сколько раз один период помещается в другой.
td2 = timedelta(minutes=25)
print(td1/td2)
# 2.4
print(td1//td2)
# 2

А так же остаток от делния.
print(td1%td2)
# 0:10:00

Объекты datetime.timedelta поддерживают отрицательные значения. Эти две записи идентичны.
datetime.now() - timedelta(hours=1)
datetime.now() + timedelta(hours=-1)

И, что очевидно, операторы сравнения
td1>td2
# True


А еще можно почитать про форматирование даты и времени здесь и здесь.

#tricks
🔥14👍3
Недавно была задача форматировать строки по шаблону. Шаблон обычный для метода format()

/path/to/app{version}/bin
или
/opt/{app_name}/bin:{DEFAULT_PATH}:/usr/bin

Проблема состояла в том, что некоторые переменные следует игнорировать, заменять только те, что у меня есть на данный момент (дальше идет ещё один обработчик). Если в метод строки format() не передать все переменные то будет ошибка KeyError
"/opt/{app_name}:{DEFAULT_PATH}".format(app_name="my_app")
# KeyError: 'DEFAULT_PATH'


Какие варианты решения есть?

▫️ переопределить класс srt и метод format
▫️ написать отдельную функцию парсинга строки с использованием regex
▫️ сделать словарь, который возвращает исходную переменную при отсутствии ключа

Третий вариант и рассмотрим, он самый краткий, буквально 2 строки включая вызов!
class SkipDict(dict):
def __missing__(self, key):
return f"{{{key}}}"

"/opt/{app_name}:{DEFAULT_PATH}".format_map(SkipDict(app_name='my_app'))
# "/opt/my_app:{DEFAULT_PATH}"


1. Мы создаем кастомный класс наследуя его от dict и переопределяем __missing__. Этот метод вызывается когда в словаре ключ не найден. По умолчанию он выбрасывает исключение KeyError. Всякий раз когда ключ не найден, мы возвращаем исходный вид этой переменной и ошибки теперь не будет.

2. Это не сработает если переменные указаны в формате ${name}. Это совсем другой синтаксис из bash и подобных сред.

3. Переменные можно передать и просто готовым словарём. Это же обычный конструктор объекта dict

"...".format_map(SkipDict(kwargs))

4. Вместо format() используется format_map(), просто удобней в данном случае.

5. Ну да, не две строки. Просто класс нужно создать и в одну строку. Если кому надо именно 2х-строчное решение - забирайте:
SkipDict = type('SkipDict', (dict, ),{'__missing__': lambda self, key: f"{{{key}}}"})

6. Из минусов: вы не сможете так просто определить все ли вам нужные переменные заполнены, так как пропускаются ВСЕ отсутствующие.

#tricks
👍61
При использовании PNG файлов в PySide/PyQt может появляется такой ворнинг

libpng warning: iCCP: known incorrect sRGB profile


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

sRGB (standard Red Green Blue) — стандартное цветовое пространство, которое используется в цифровой фотографии и веб-дизайне.

Указанная выше ошибка говорит о том, что профиль устарел или повреждён. Qt-движок не может его считать. Если у вас множество иконок и иных картинок оформления, то ворнингов будет сыпаться довольно много.

Решение — пересохранить файл без профиля, то есть будет использоваться цветовое пространство на усмотрение приложения.

from PIL import Image
Image.open(input_path).save(output_path, icc_profile=None)

from PySide2.QtGui import QImage, QImageWriter

image = QImage(input_path)
image.setText("icc", "")
writer = QImageWriter(output_path)
writer.write(image)

#tricks
👍14
POSIX (Portable Operating System Interface) — это набор стандартов, определяющих интерфейсы для обеспечения совместимости между операционными системами.
Данный стандарт поддерживается всеми UNIX-системами (GNU/Linux, macOS, FreeBSD, OpenBSD и другие). А вот в Windows либо частично, либо через подсистемы (такие как WSL).

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

В Python разбиением строки на токены занимается функция shlex.split(), которая имеет один важный аргумент - posix.
Этот аргумент определяет, следует ли функции использовать правила синтаксического анализа соответствующие стандарту POSIX, или использовать обратно совместимый, легаси режим.

▫️posix=True

В POSIX-совместимом режиме функция shlex.split() будет учитывать переменную окружения IFS (Internal Field Separator) для определения разделителей полей и будет более строго следовать стандарту POSIX. Из строки удаляются неэкранированные кавычки и обратные слеши.

▫️posix=False

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

Теперь смотрим некоторые примеры.
import shlex
# кавычки
text = r'"Do"Not"Separate" \"This\"'
shlex.split(text, posix=False)
# ['"Do"', 'Not"Separate"', '\\"This\\"']
shlex.split(text, posix=True)
# ['DoNotSeparate', '"This"']

# специсимволы
text = r'A\tB\nС\fD\vE'
shlex.split(text, posix=False)
# ['A\\tB\\nС\\fD\\vE']
shlex.split(text, posix=True)
# ['AtBnСfDvE']

# обратный слеш
text = r"cmd.exe c:\games\mario.exe"
shlex.split(text, posix=False)
# ['cmd.exe', 'c:\\games\\mario.exe']
shlex.split(text, posix=True)
# ['cmd.exe', 'c:gamesmario.exe']

Учитвая, что аргумент posix по умолчанию True, стоит помнить этот факт при обработке строк с Windows-путями!

Рекомендую самостоятельно поэксперементировать с этим аргументом!

#libs
👍61