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

Контакт: @paulwinex

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

Хештеги для поиска:
#tricks
#libs
#pep
#basic
#regex
#qt
#django
#2to3
#source
#offtop
Download Telegram
SQLAlchemy - это один из самых популярных ORM для работы с базами данных из Python.

- поддерживат все популярные базы данных
- не привязана к какому-либо фреймворку (как, например, Django ORM)
- поддерживает асинхрон
- позволяет удобно (питонично) делать довольно сложные SQL запросы

15 июля вышла первая версия из ветки 2.0 и это хорошйи повод изучить эту библиотеку если еще не начали.

Подобрал вам ресурсы для изучения:

- Вебинар и урок про новую SQLAlchemy2.0 от Mike Bayer (автор sqlalchemy и alembic ) с онлайн конференции pythonwebconf:

https://www.youtube.com/watch?v=Uym2DHnUEno

- Для тех кто на английском не очень, есть онлайн книга на руссом от https://xn--r1a.website/massonnn_yt.
А так же видео версия:

https://www.youtube.com/watch?v=leeC0fpAY-E&list=PLN0sMOjX-lm5Pz5EeX1rb3yilzMNT6qLM

Лично я использую алхимию в связке с FastAPI и пока всё устраивает

#tricks #libs
👍22
Как проверить является ли директория пустой?

Самый простой способ:

if os.listdir(path):
...

Тоже самое с pathlib

p = Path(path)
if list(p.iterdir()):
...

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

Теперь представим что в директории 10к файлов

for i in range(10000):
Path(f'/tmp/test/test{i}.txt').touch()

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

import timeit
test_path = '/tmp/test'
count = 1000

>>> timeit.timeit('list(os.listdir(p))', setup=f'import os;p="{test_path}"', number=count)
2.281363710993901
>>> timeit.timeit('list(p.iterdir())', setup=f'from pathlib import Path;p=Path("{test_path}")', number=count)
5.6957218300012755

То есть мы получаем список всех 10к файлов просто чтобы узнать что там есть файлы. Хотя нам надо узнать есть ли по указанному пути хотя бы один файл.
Для того чтобы ускорить проверку лучше воспользоваться функцией os.scandir(). Она работает на много быстрей и возвращает итератор с объектами os.DirEntry.
Чтобы узнать есть ли в директории хоть один файл достаточно использовать функцию next()

next(os.scandir(path))

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

if next(os.scandir(path), None):
...

Либо используем функцию any(), так как она завершится сразу после нахождения первого файла или если итератор пуст.

if any(os.scandir(path)):
...

Сравним скорость
>>> timeit.timeit('next(os.scandir(p), None)', setup=f'import os;p="{test_path}"', number=count)
0.2183076049986994
>>> timeit.timeit('any(os.scandir(p))', setup=f'import os;p="{test_path}"', number=count)
0.21016486900043674

#tricks
👍164
Функция dir() - удобна для получения списка атрибутов у любого объекта.

Ранее я писал про функцию __dir__() в модуле (не путайте её с переменной __all__(), которая указывает список объектов для импорта если встречается конструкция from module import *).

Скорее всего вы уже знаете как использовать функцию dir(). Любой объект может реализовать метод __dir__() чтобы указать список имеющийхся и динамических атрибутов. И функция dir() поможет получить список этих атрибутов.

>>> dir(str)
['__add__', '__class__', '__contains__', ...]

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

>>> dir()
['__builtins__', '__doc__', '__file__', ...]

>>> def test():
>>> x = 1
>>> print(dir())
>>> test()
['x']

#basic #tricks
👍7
Что позволяет делать f-strings в 3.12.

▫️можно использовать одинаковые кавычки во всём выражении
▫️можно добавлять переносы для многострочного выражения
▫️можно использовать символ новой строки (эта проблема неактуальна)
>>> print(f"{"\n".join(
>>> ["1","2","3",
>>> f"{
>>> f"{2+2}"
>>> *(2+2)
>>> }"
>>> ]
>>> )}")
1
2
3
4444

#tricks #libs
👍12
Варианты распаковки контейнеров по отдельным переменным

Обычная распаковка по точному количеству
data = [1, 2, 3, 4, 5]
v1, v2, v3, v4, v5 = data

Распаковка с неизвестным количество но не меньше чем N
v1, *_ = data
v1, *_, v4, v5 = data

Если точно знаете позицию нужного объекта в списке, включая вложенные списки, то достать его можно двумя способами
Через индекс:
data = [[1]]
v1 = data[0][0]

Через распаковку со скобками:
data = [[1]]
(v1, ), = data

data = [[[1]]]
((v1,), ), = data

Еще примеры распаковки вложенных объектов
data = [[1, 2], [3, 4], [5, 6]]
(v1, v2), (v3, v4), (v5, v6) = data
(v1, v2), *_, (v5, *_) = data

#tricks
🔥16👍6
Когда пишешь асинхронный код нужно учитывать особенности такого подхода. Всегда требуется держать в уме, когда возвращается корутина а когда реальный результат. Между этими двумя сущностями должен быть вызов через 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
Функция 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
🔥15👍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
👍71