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
Синтаксис f-string позволяет использовать в строке символ обратного слеша "\" но не позволяет использовать его внутри фигурных скобок.

>>> lines = ['line1','line2']
>>> print(f'ITEMS: \n {"\n".join(lines)}')
SyntaxError: f-string expression part cannot include a backslash

Решения:

▫️ вынести этот символ за скобки

n = '\n'
print(f'ITEMS: \n{n.join(lines)}')

▫️ заменить его на другое представление, например создавать нужный символ с помощью функции chr()

print(f'ITEMS: \n{chr(10).join(lines)}')

Чтобы узнать код символа следует использовать функцию ord()

>>> ord('\n')
10

А с юникодом не выйдет, там тоже слеш

>>> print(f'ITEMS: \n{"\u000a".join(lines)}')
SyntaxError: f-string expression part cannot include a backslash

#tricks
👍11😱1
Метод строки split() разделяет строку на несколько строк по указанному символу

>>> "a_b_c".split('_')
['a', 'b', 'c']

Можно указать максимальное количество разделений

>>> "a_b_c".split('_', 1)
['a', 'b_c']

Или резать с другой стороны с помощью rsplit() (right split)

>>> "a_b_c".rsplit('_', 1)
['a_b', 'c']

А что будет если оставить аргументы пустыми?

>>> "a_b_c".split()
['a_b_c']

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

>>> "a b c".split()
['a', 'b', 'c']

То есть это равнозначно такому вызову?

>>> "a b c".split(" ")
['a', 'b', 'c']

Кажется да, но нет! Давайте попробуем добавить пробелов между буквами

>>> "a   b   c".split(" ")
['a', '', '', 'b', '', '', 'c']

И вот картина уже не так предсказуема 😕
А вот что будет по умолчанию

>>> "a   b   c".split()
['a', 'b', 'c']

Всё снова красиво! 🤩

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

>>> "a\t  b\n c  ".split()
['a', 'b', 'c']

Аналогичный способ можно собрать с помощью регулярного выражения. Но пробелы по краям строки придется обрабатывать дополнительно.

>>> import re
>>> re.split(r"\s+", ' a b c '.strip())
['a', 'b', 'c']

Здесь тоже можно указать количество разделений

>>> re.split(r"\s+", 'a b c', 1)
['a', 'b c']

А что если мы хотим написать красиво, то есть split() без аргументов, но при этом указать количество разделений? В этом случае первым аргументом передаём None

>>> "a\n  b c".split(None, 1)
['a', 'b c']

Данный метод не учитывает строки с пробелами, взятые в кавычки

'a "b c" '.split()
['a', '"b', 'c"']

Но для таких случаев есть другие способы.

#tricks #basic
👍25😱1😢1
Как не передавать аргумент в функцию если она его не ждёт?

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

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

▫️ Добавить во всех функциях в аргументы **kwargs. Это решит все проблемы. Строго говоря, это надо было сделать сразу. Теперь таски не будут падать из-за неизвестного аргумента. И теперь следует не забывать добавлять **kwargs в новых функциях. Но что, если нет возможности изменять код? Тогда...

▫️Проверить, может ли функция принять аргумент с определённым именем. И если не может то не передавать. Это можно сделать с помощью стандартной функции inspect.signature

from inspect import signature

def func(x, y, z=True):
pass

sig = signature(func)
print(sig)
# <Signature (x, y, z=True)>
print(sig.parameters)
mappingproxy(OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">), ('z', <Parameter "z=True">)]))

Теперь можно проверить, ожидает ли функция параметр с определённым именем

print('lock' in sig.parameters)
# False

Финальный псевдокод

from tasks import my_task, LockClass
from inspect import signature

task_kwargs = {}
lock = LockClass()
if 'lock' in signature(my_task).parameters:
task_kwargs['lock'] = lock
my_task(**task_kwargs)

Конечно же, наличие этого имени не гарантирует что функция ожидает именно этот тип. Но это уже нюансы реализации 😼

#tricks
👍17🤔2🔥1
Как получить путь к файлу текущего класса если метод получения пути находится в родительском классе в другом файле?

Например, представим такую ситуацию:

# module1.py ###

class BaseCls:
@classmethod
def get_path(cls):
print(__file__)


# module2.py ###

from module1 import BaseCls

class MainCls(BaseCls):
pass

Что покажет код:

import module2
module2.MainCls.get_path()

Мы ожидаем что путь будет к файлу module2.py, но переменная __file__ объявлена внутри файла module1.py и поэтому будет указывать именно на него.

Чтобы получить правильный путь нам следует:

▫️ получить имя модуля текущего класса
module_name = module2.MainCls.__module__
▫️ найти этот модуль в списке импортированных модулей
mod = sys.modules[module_name]
▫️ получить значение переменной __file__
filepath = mod.
__file__

Вся эта процедура, причём для любого типа объекта, есть в функции inspect.getfile(). Так что наш метод должен выглядеть так:

# module1.py
import inspect

class BaseCls:
@classmethod
def get_path(cls):
print(inspect.getfile(cls))

Теперь из вызов этого метода из класса MainCls найдёт путь к файлу module2.py

#tricks
👍6
У тех, кто часто работает в терминале, есть привычка вызова особо часто используемых команд. например cd, ls, mc...
Вместо команды exit можно использовать Ctrl+D, и это удобно.

И так уж вышло, что эта привычка невольно у меня включается и при работе в REPL. Для быстрого выхода я жму Ctrl+D, и это работает, но только в Linux. В Windows это совсем не работает, так как там надо нажимать Ctrl+Z. И был бы Windows не такой mustdie если бы этого хватило, но требуется еще нажать Enter (если знаете быстрый выход из REPL на винде, то подсказывайте, я не WinUser). Иногда мне быстрей и привычней вбить exit и нажать Enter, как в bash, но и тут подстава - еще нужны скобки вызова🤬.

В общем, настолько высосанную из пальца проблему надо еще поискать 😆, но я нашел для неё решение!

q = type('q', (object,), {'__repr__': lambda *args: exit()})()

Этот код вставляется в стартап скрипт REPL и создаёт новый объект q. Теперь для выхода из REPL достаточно написать символ q и нажать Enter. Работает одинаково на Linux и Windows.

Как это работает?
▫️ динамически создаётся новый тип объекта с помощью конструкции type(NAME, (BASETYPES,), {ATTRS,})
▫️ в атрибутах создаётся оверрайд метода __repr__, который отвечает за распечатку объекта в REPL
▫️ в этом методе вызывается команда exit()

То есть команда выхода срабатывает как только вы пытаетесь распечатать этот объект в консоли. Именно отображение его репрезентации как объекта а не через не print(), который использует метод __str__.

Аналогично работающий код выглядит так:

class Q:
def __repr__(self):
exit()
q = Q()

Из минусов можно выделить следующее:
▫️ имя q занято, но никто не мешает сделать что-то более уникальное
▫️ Нужно как-то добавлять это в стартап. Тут нам поможет startup script

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

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

#tricks
😁7👍3👎1
SQLAlchemy - это один из самых популярных ORM для работы с базами данных из Python.

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

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

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

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

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

- Для тех кто на английском не очень, есть онлайн книга на руссом от https://t.iss.one/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