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
Давайте разберёмся как работают побитовые операторы.

Всего есть 6 основных операторов:

| OR
& AND
^ XOR (исключающее OR)
~ NOT (унарная операция)
>> сдвиг вправо
<< сдвиг влево

Эти операторы работают с числами в двоичном представлении. Условно говоря, они ставят числа в двоичном виде друг над другом и по очереди обрабатывают каждый столбик с битами.

Например, берём два числа, и сразу смотрим как оно выглядит в двоичном виде (Python отбрасывает ведущие нули, так что рядом допишу более удобную форму)

>>> a = 3
>>> bin(a)
'0b11' # 011
>>> b = 6
>>> bin(b)
'0b110' # 110

🔸 Оператор OR
в результат пишет 1 если в одном из элементов есть 1

>>> 3|6
7

в двоичном виде это выглядит так (запишем столбиком)

011
|110
=111

В каждом столбце был найден 1, поэтому в результате все биты равны 1

🔸 Оператор AND
В результат ставит 1 только если оба бита равны 1

>>> 3&6
2

Бинарный вид

011
&110
=010

Только на 2й позиции оба бита равны 1.

🔸 Оператор XOR
Пишет 1 на бит результата, для которого только один из соответствующих битов операндов равен 1.

>>> 3^6
5

011
&110
=101

🔸 Оператор NOT
Заменяет каждый бит на противоположный. Эта операция унарная, то есть поддерживает только один операнд.

>>> ~3
-4

~011
=100

Здесь всё понятно. Но давайте попробуем другое число:

~50
=-51

~110010
=-110011

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

Побитовая операция НЕ для числа x соответствует -(x+1)

🔸 Сдвиг
Здесь всё просто. Все биты сдвигаются на указанное количество шагов подставляя нули

>>> 3 << 1
6

011 << 1
110

Кстати, преобразовать бинарное представление обратно в число можно с помощью функции int() указав разрядность системы исчисления.

>>> int('11001', 2)
25

__________________
А зачем нам вся эта информация?
Узнаем в следующем посте...

#triсks
Лично я на практике встречал использование побитовых операторов в двух ситуациях (их конечно намного больше).

🔸1. Сдвиг, который соответствует некоторой математической операции (арифметический сдвиг) но работает несравнимо быстрей. Например сдвиг влево равен выражению a*2**b:

a<<b == a*2**b

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

🔸2. Числовые маски.
Что это такое?
Создаем несколько переменных, в которых в бинарном представлении все ячейки заполнены
нулями кроме одной позиции. И у каждой переменной используется своя уникальная позиция для бита 1.

FLAG1 = int('001', 2) # 2
FLAG2 = int('010', 2) # 4
FLAG3 = int('100', 2) # 8

Теперь с помощью оператора OR можем объединять все биты в одну маску

>>> flags = FLAG1 | FLAG2
3 # 011

А после проверить, входит ли определённый флаг в состав битов маски?

>>> flags & FLAG3
0 # 000 нет совпадений
>>> flags & FLAG2
2 # 010 совпал второй бит

Если результат больше 0 то флаг присутствует в маске. Если результат 0 то такого флага нет.

Чтобы получить тип bool можем писать так

bool(flags & FLAG2)

или так

flags & FLAG2 > 0

и, очевидно, так

if flags & FLAG1:
...

Где могут пригодиться такие маски? Один пример был в предыдущем посте про флаги в Qt фреймворке. Также такой способ часто используют в организации прав доступа к ресурсам.

READ = int('001', 2) # 2
WRITE = int('010', 2) # 4
DELETE = int('100', 2) # 8

USER = READ
MODERATOR = READ | WRITE
ADMIN = READ | WRITE | DELETE

can_write = ADMIN & WRITE

Не сложно представить альтернативу на простом Python

READ = 1
WRITE = 2
DELETE = 3

USER = [READ]
MODERATOR = [READ, WRITE]
ADMIN = [READ, WRITE, DELETE]

can_write = WRITE in ADMIN

Оператор in работает довольно шустро, но всё равно медленней чем побитовый оператор.

#tricks
Всё начиналось с библиотеки six, что означает цифру 6 и является результатом умножения 2*3 (напомню что six это библиотека для написания кода одновременно совместимого для Python 2 и 3).
Но как обычно всегда найдется тот, кому не всё понравится и он напишет свой вариант) В итоге получаем небольшой ряд "числовых" библиотек примерно для одного и того же

https://pypi.org/project/six/
https://pypi.org/project/eight/
https://pypi.org/project/nine/

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

https://pypi.org/project/one/
https://pypi.org/project/two/
https://pypi.org/project/three/
four - свободно
https://pypi.org/project/five/
https://pypi.org/project/six/
https://pypi.org/project/seven/
https://pypi.org/project/eight/
https://pypi.org/project/nine/
ten - свободно
https://pypi.org/project/eleven/
https://pypi.org/project/twelve/
thirteen - свободно
fourteen - свободно
fifteen - свободно
https://pypi.org/project/sixteen/
seventeen - свободно
nineteen - свободно
twenty - свободно

Назначения у этих проектов, конечно, разные. Есть и заброшенные и популярные. Но места еще есть 😊 Занимаем пока свободно!

PS.
Всех уделал Em Fresh со своей линейкой Python-альбомов😁 (жмакнуть show more)

PPS. Всех читательниц моего канала поздравляю с праздником 🌼 🥳 💐

#offtop #libs #2to3
Обычная практика удаления одинаковых значений из списка с помощью множества

array = [1, 2, 3, 4, 5, 4, 3]
uniq = list(set(array))

Альтернативная запись с помощью литералов вместо функций

uniq = [*{*array}]

#tricks
Начиная с версии Python 3.7 появился встроенный профайлер времени импорта модуля. Чтобы его активировать достаточно к запуску интерпретатора добавить аргумент -X importtime

python3 -X importtime

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

Если нет возможности добавить аргумент в команду, то используйте переменную PYTHONPROFILEIMPORTTIME, результат аналогичен

Linux:
export PYTHONPROFILEIMPORTTIME=1

Windows
set PYTHONPROFILEIMPORTTIME=1

#tricks
В посте про правильное использование аргумента shell упоминалось что в некоторых случаях атрибуты следует отправлять списком а не строкой. Что делать, если команда приходит именно строкой? Как её преобразовать в список?
Ответ очевиден

>>> cmd_str = 'ls -sl'
>>> cmd_lst = cmd_str.split(' ')
>>> print(cmd_lst)
['ls', '-sl']

Но что, если команда имеет более сложный вид в плане пробелов?

>>> cmd_str = 'mkdir "My Folder Name"'
>>> print(cmd_str.split(' '))
['mkdir', '"My', 'Folder', 'Name"']

Определённо что-то пошло не так! Имя директории содержит пробелы, поэтому весь путь взят в кавычки. Нам определённо не надо её разделять на аргументы. Чтобы сделать всё правильно нам потребуется распарсить строку чтобы обнаружить, что там в кавычках а что нет.
Но в стандартной поставке Python давно уже есть готовое решение, моудль shlex (shell lexical analyzers) который всё это умеет.

>>> import shlex
>>> shlex.split('mkdir "My Folder Name"')
['mkdir', 'My Folder Name']

Теперь команда сработает верно.
И не переживайте что кавычки пропали, subprocess сам всё разрулит с пробелами 😎.

На Windows всё аналогично.

#libs
Знаете ли вы про "магические" методы классов ˍˍgetattributeˍˍ() и ˍˍgetattrˍˍ()?

ˍˍgetattributeˍˍ вызывается всякий раз когда идёт обращение к атрибуту объекта. Например метод или какая-то переменная.

ˍˍgetattrˍˍ вызывается когда атрибут не найден.

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

Но пост на самом деле не об этом. Дело в том, что в Python 3.7 добавили возможность определять функцию ˍˍgetattrˍˍ() в модуле с аналогичной функциональностью! (PEP562)

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

А еще можно создать функцию ˍˍdirˍˍ(), определяющую поведение для стандартной функции dir().

Пример модуля:

# example.py

my_variables = {'var1': 1, "var2": 2}

def __getattr__(name):
try:
return my_variables[name]
except KeyError:
raise AttributeError

def __dir__():
return list(my_variables.keys())

Как использовать модуль

>>> import example
>>> print(example.var1)
1
>>> print(example.var3)
AttributeError

>>> print(dir(example)))
['var1', 'var2']

#tricks #pep
Небольшой экспрешн, с помощью которого можно выяснить, находимся ли мы в интерактивном режиме интерпретатора или нет.

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

is_interactive = bool(getattr(sys, 'ps1', sys.flags.interactive))

Возможно, кто-то скажет: "зачем такие сложности??? Просто проверяем наличие переменной ˍˍfileˍˍ, в интерактивном режиме она не создаётся!"

Всё верно, так оно и работает, но только до того момента, пока вы не импортируете какой-либо модуль. Внутри неймспейса этого модуля переменная ˍˍfileˍˍ определённо существует, даже если вы импортнули его в интерактиве. То есть внутри модуля способ не работает. А с помощью нашего выражения проверка правильно сработает в любом случае.

PS. Не работает с некоторыми режимами эмуляции интерпретатора.

#tricks
Как проверяется текущая версия Python?
Допустим, вам нужно выбрать конкретное действие в зависимости от версии используемого интерпретатора.
В моём примере мне нужна версия Python больше чем 3.5.
Самый очевидный способ проверки мажорной версии такой:

>>> import sys
>>> print sys.version_info.major > 3
True

С минорной версией будет чуть длинней

>>> print sys.version_info.major == 3 and sys.version_info.minor > 5

Можно ли сократить эту запись?
Конечно! Иначе поста бы не было 😊

Вспоминаем как работает сравнение итерируемых объектов в Python. Элементы сравниваются попарно. Если первая пара совпала, проверяется следующая пара, и так пока не найдутся разные элементы или закончится один из итераторов. Например, сравним кортежи.

>>> (1, 2) > (1, 3)
False
>>> (1, 4, 5) > (1, 4, 3)
True

Теперь преобразуем version_info в кортеж (или список)

>>> tuple(sys.version_info)
(3, 7, 3, 'final', 0)

И можем использовать его для сравнения кортежей

>>> tuple(sys.version_info) > (3, 5)
True

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

>>> sys.version_info > (3, 5)
True

Можно еще как-то иначе? Можно!
Строки тоже итерируемый объект. Они сравниваются не по длине или кодам символов, а так же попарно, и по порядку символа в таблице (или в алфавите).

>>> 'a' > 'b'
False
>>> 'e' > 'c'
True
>>> '5' < '4'
False

А что у нас есть еще в sys? Правильно, переменная version.

>>> print (sys.version)
'3.7.3 (default, Dec 20 2019, 18:57:59) \n[GCC 8.3.0]'

Сравниваем эту переменную с другой строкой:

>>> sys.version > '3.5'
True

⚠️ Этот метод со строкой перестанет работать когда версия Python станет 3.10+. Не используйте его!

# tricks
В стандартной библиотеке Python есть поддержка нескольких текстовых форматов файлов.
Я имею в виду общепринятые форматы хранения текстовых данных. Чаще всего это конфигурационные файлы.
И вот что может читать Python из коробки:

🔸 JSON (JavaScript Object Notation)

Модуль json

Один из лидеров по популярности. Используется во многих сферах, от простых конфигов до протоколов передачи данных.
Формат простой и понятный. Очень похож на простой Python-код.
https://ru.wikipedia.org/wiki/JSON
https://www.json.org/

🔸 CSV (Comma-Separated Values)

Модуль csv

Формат описания табличных данных. Его используют аналитики, датасаентисты и Exel-мастера. Что-то вроде текстовой базы данных.
https://ru.wikipedia.org/wiki/CSV
https://www.w3.org/TR/tabular-data-primer/

🔸 XML (eXtensible Markup Language)

Модуль xml

Самый популярный формат в WEB, так как любая HTML страница (то есть все страницы в сети) это XML. Многие программы используют эту разметку для сохранения данных. Удобный формат многоуровневой вложенности объектов с атрибутами.
https://www.xml.com/
https://ru.wikipedia.org/wiki/XML

🔸 INI (Initialization file)

Модуль configparser

Очень простой формат конфига для Windows с возможностью группировать параметры.
Мало популярен, но в простых случаях вполне подходит.
https://ru.wikipedia.org/wiki/.ini


Есть еще один популярный формат для конфигов, но к сожалению не в стандартной поставке. Я решил его тоже упомянуть.

🔹 YAML (Yet Another Markup Language)

В основном используется для конфигов.
https://ru.wikipedia.org/wiki/YAML
https://yaml.org/

Установка:

pip install pyyaml

___________
Конечно же существуют и другие форматы. CFG или CONF (парсер для него был в стандартной библиотеки Python2 в модуле ConfigParser), TOML и другие. Но в большинстве случаев стандартно поддерживаемых форматов хватает чтобы закрыть все потребности.

#libs
Почему форматов для хранения простых конфигов так много? Всё как обычно. В какой-то момент возникает ситуация:

АДМИН: Этот конфиг просто не поддерживает то что я от него хочу!
ПРОГЕР: Подержите моё пиво...

И рождается новый формат)))

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

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

На мой взгляд самый удобный в коде это JSON, а самый читаемый и удобный в ручном редактировании это YAML.

Никто не мешает вам создать собственный формат, если не достаточно того что имеется или если слишком много свободного времени)

PS. 4 формата рядом для сравнения синтаксиса 🌎

#libs
Что за магический модуль ˍˍfutureˍˍ?

Это имплементация поведения интерпретатора из будущих версий Python.

Разные версии в Python развиваются параллельно. Разработчики ядра Python внедрили модуль ˍˍfutureˍˍ чтобы помочь писать код на старых версиях, но совместимый с новыми версиями интерпретатора. Это помогает более мягко обновить Python, не переписывая всю кодовую базу. Можно сказать что это бекпорт фичей.

Модуль ˍˍfutureˍˍ позволяет "импортировать" функционал Python3 работая с Python2, или использовать фишки из Python3.9 имея в распоряжении только 3.6.

Но что именно делает этот модуль? Если вы откроете исходник, то ничего магического вы там не найдете. Простой класс, несколько флагов и набор инстансов этого класса. По идее, если его импортнуть, ничего поменяться не может, так как там нет никакого исполняемого кода! Только объявления объектов.

А ответ кроется в необходимости делать импорты из ˍˍfutureˍˍ самой первой строкой. Дело в том, что парсер специально заточен на поиск импорта этого модуля в первой строке. Он смотрит что вы там импортнули и специальным образом изменяет поведение интерпретатора, которое в него вшито с помощью бекпортов. Весь дальнейший синтаксический разбор будет с учётом изменений. Именно поэтому импорт ˍˍfutureˍˍ обязательно должен быть первым! Не можем же мы половину модуля исполнять по-старым правилам а вторую половину по-новым)))

#2to3
Что именно мы можем импортнуть из будущих версий? Явно не всё, иначе это была бы, собственно, новая версия. В ˍˍfutureˍˍ выносятся только ключевой функционал, от которого серьезно зависит синтаксис или использование возможностей языка. Самое очевидное это директива print, которая в Python3 стала функцией print().

from __future__ import print_function

Что стало удобней с этой функцией?

🔸 Теперь это функция а не ключевое слово, её можно передать как аргумент или вернуть как результат

def get_logger():
if condition:
return some_handler.write
else:
return print

🔸 С помощью аргументов sep и end можем настроить минимальное форматирование вывода.

sep: разделитель для нескольких объектов
end: последний символ вместо перехода на новую строку

>>> items1 = [1, 2, 3]
>>> items2 = [4, 5, 6]
>>> print(*items1, sep='-', end='-')
>>> print(*items2, sep='-')
1-2-3-4-5-6

🔸 Аргумент flush форсированно пробрасывает буфер аутпута в файл. Полезно для вывода из блокирующих операций. Например, когда вам нужно в stdout выводить прогресс операции, запущенной в subprocess. Если не сделать flush то весь аутпут прилетит только по завершению процесса.

for i in range(100):
print(f'Progress: {i}%', flush=True)
time.sleep(1)

Этот прогресс мы можем отслеживать в реальном времени.
А вот так приходилось делать раньше:

import sys
sys.stdout.write(text + '\n')
sys.stdout.flush()

🔸 Аргумент file позволяет перенаправить вывод в другой поток. Например в файл, сеть или что угодно, что имеет метод write.

print(text, file=open('filename.txt', 'w'))
___________
Ну да, теперь приходится писать лишние скобочки и сложно переучиться на новый лад. Но плюсов, я думаю, больше.

#2to3 #tricks
Вторая по частоте future-функция, которую я использовал, это абсолютный импорт

from __future__ import absolute_import

Что она делает?
Изменения, которые вносит эта инъекция описаны в PEP328
Покажу простой пример.

Допустим, есть такой пакет:

/my_package
/__init__.py
/main.py
/string.py

Смотрим код в my_package/main.py

# main.py
import string

Простой пример готов) Вопрос в том, какой модуль импортируется в данном случае? Есть два варианта:

1. модуль в моём пакете my_package.string
2. стандартный модуль string

И вот тут вступает в дело приоритет импортов. В Python2 порядок следующий: помимо иных источников, раньше ищется модуль внутри текущего пакета, а потом в стандартных библиотеках. Таким образом мы импортнём my_package.string.

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

from my_package import string

или относительный импорт, но с указанием пути относительно текущего модуля main

from . import string

Еще одной неоднозначностью меньше 😎

Подробней про импорты здесь:
https://docs.python.org/3/tutorial/modules.html

#2to3 #pep #basic
Ранее я уже упоминал о другой фишке из ˍˍfutureˍˍ , это оператор деления.

from __future__ import division

Суть проста. Раньше сложность типа данных результата поределялась типом самого сложного операнда.
Например:

int/int => int
int/float => float

В первом случае оба операнда int, значит и результат будет int. Во втором float более сложный тип, поэтому результат будет float.

Если нам требуется получить дробное значение при делении двух int то приходилось форсированно один из операндов конверировать в float.

12/float(5) => float

Но с новой "философией" это не требуется. В Python3 "floor division" заменили на "true division" а старый способ теперь работает через оператор "//".

>>> 3/2
1.5
>>> 3//2
1


То есть теперь деление int на int даёт float если результат не целое число.
В классах теперь доступны методы __floordiv__() и __truediv__() для определения поведения с этими операторами.

Данный переход описан в PEP238.

#pep #2to3 #basic
Когда разрабатываете свой GUI с помощью PyQt для какого-либо софта бывает необходимо позаимствовать цвета из текущего стиля интерфейса. Например, чтобы правильно раскрасить свои виджеты, подогнав их по цвету. Ведь бывает, что ваш GUI используется в разных софтах. Причём некоторые со светлой темой а другие с тёмной.
По умолчанию стили наследуются, но если вы задаёте какую-либо раскраску для части виджета через свой styleSheet, то требуется ссылаться на цвета текущего стиля.

Как это сделать? Как получить нужный цвет из палитры имеющегося стиля? Это достаточно просто, нужно использовать класс QPalette и его роли.

Например, мне нужно достать цвет текста из одного виджета и применить его в другом как цвет фона (не важно зачем именно так, просто захотелось😊).

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

from PySide2.QtGui import QPalette
color = main_window.palette().color(QPalette.Text)

теперь можем использовать этот цвет в стилях

my_widget.setStyleSheet(f'background-color: {color.name()};')

Готово, мы динамически переопределили дефолтный стиль используя текущий стиль окна!

На самом деле есть запись покороче, в одну строку и без лишних переменных. Не очень-то по правилам CSS, но Qt это понимает.

my_widget.setStyleSheet('background-color: palette(Text);')

Этот способ не подходит если вам нужно как-то модифицировать цвет перед применением в своих стилях. В этом случае потребуется первый способ.
Зато он прекрасно сработает в файле .qss, то есть не придётся в коде прописывать раскраску отдельных элементов через ссылки на палитру, всё красиво сохранится в отдельном файле .qss!

QListView#my_widget::item:selected {
background: palette(Midlight);
}

Про имеющиеся роли можно почитать здесь 🌍

#qt #tricks
Думаете ˍˍfutureˍˍ нужен только для Python2? Нет, это процесс постоянный. В Python3 тоже есть свои "future".

from __future__ import annotations

Похоже на бекпорт аннотаций типов из Python3 в Python2, но это не так. Это имплементация PEP 563 Postponed Evaluation of Annotations которая будет дефолтной только в Python4.

Что делает этот future?
Если вы активно используете аннотации типов в Python3 то знаете, как они обычно выглядят.

def func(x: int) -> int:
return x * 2

При этом у объекта функции появляется атрибут ˍˍannotationsˍˍ

>>> print(func.__annotations__)
{'x': <class 'int'>, 'return': <class 'int'>}

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

В более сложных случаях это будут инстансы объектов из модуля typing

from typing import List

def func(x: int) -> List[int]:
return [x] * 10

>>> print(func.__annotations__)
{'x': <class 'int'>, 'return': typing.List[int]}

Что происходит если мы импортим annotations из ˍˍfutureˍˍ? Изменяется способ определения аннотаций.

Дело в том, что все эти инстансы типов создаются в момент определения функции когда модуль импортируется. Так же как и значения по умолчанию они должны исполниться и вернуть какой-то объект. В нашем случае это ссылка на тип int или инстанс класса typing.List.
И тут возникает две проблемы, описаные в PEP.
Если коротко то звучит это так:

1. Если в подсказке указывается тип, который еще не определён, это вызовет ошибку. Приходится описывать его просто строкой или менять порядок определения.

2. Определение ссылок на типы порой занимает много времени, так как требуется импорт модулей и создание инстансов.

Но если вы импортируете наш future, то активируется так называемое отложенное определение ссылок (Postponed Evaluation of Annotations).

В результате вместо создания инстансов и ссылок в ˍˍannotationsˍˍ просто записывается строка с этой подсказкой.

from __future__ import annotations

def func(x: int) -> int:
return x * 2

>>> print(func.__annotations__)
{'x': 'int', 'return': 'int'}

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

>>> import typing
>>> typing.get_type_hints(func)
{'x': <class 'int'>, 'return': <class 'int'>}

Но делать это можно уже только по необходимости.

#pep
Небольшой трик с регулярными выражениями который редко вижу в чужом коде.

Допустим, вам нужно распарсить простой текст и вытащить оттуда пары имя+телефон. Вернуть всё это надо в виде списка словарей. Возьмем очень простой пример текста.

>>> text = '''
>>> Alex:8999123456
>>> Mike:+799987654
>>> Oleg:+344456789
>>> '''

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

(\w+):([\d+]+)

Как мы будем формировать словарь из найденных групп?

>>> import re
>>> results = []
>>> for match in re.finditer(r"(\w+):([\d+]+)", text):
>>> results.append({
>>> "name": match.group(1),
>>> "phone": match.group(2)
>>> })
>>> print(results)
[{'name': 'Alex', 'phone': '8999123456'}, ...]

Можно немного сократить запись используя zip

>>> results = []
>>> for match in re.finditer(r"(\w+):([\d+]+)", text):
>>> results.append(dict(zip(['name', 'phone'], match.groups())))

Но есть способ лучше! Это именованные группы в regex. Можно в паттерне указать имя группы и результат сразу забрать в виде словаря.

>>> for match in re.finditer(r"(?P<name>\w+):(?P<phone>[\d+]+)", text):
>>> results.append(match.groupdict())

То есть всё что я сделал, это добавил в начале группы (внутри сбокочек) такую запись:

(?P<group-name>...)

Теперь найденная группа имеет имя и можно обратиться к ней как к элементу списка

>>> name = match['name']

Либо забрать сразу весь словарь методом groupdict()

>>> match.groupdict()

#tricks #regex
Как разделить один список на два по определённому признаку элементов? Самый очевидный способ выглядит так:

array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
even, odd = [], []

for item in array:
if item%2:
odd.append(item)
else:
even.append(item)

Смотрим что получается

>>> odd
[1, 3, 5, 7, 9]
>>> even
[2, 4, 6, 8]

А теперь способ в одну строку!
(ну кто бы сомневался 😂)

_ = [[even.append, odd.append][x%2](x) for x in array]

Я специально сохранил результат в переменную "_", так как этот результат нам не нужен. Вся магия происходит во время выполнения генератора списка.
Выражение x%2 возвращает нам 1 или 0, что является индексом списка [even.append, odd.append]. Таким образом происходит выбор метода append() для нужного списка и сразу после этого мы вызываем его, отправляя очередной элемент в выбранный список.

Точно так же в выражение можно ставить сравнение, которое вернёт bool. Ведь вы помните, что bool это производный тип от int, а значит True это 1 а False это 0.

>>> arr = [100, 200]
>>> print(arr[True])
200
>>> print(arr[False])
100

В нашем случае можно сделать так

more, less = [], []
[[less.append, more.append][x>5](x) for x in array]

Можно даже никуда не сохранять результат, всё равно то что нам нужно произойдет.

>>> print(more, less)
[6, 7, 8, 9], [1, 2, 3, 4, 5]

#tricks
В предыдущем посте мы запускали нужную нам функцию внутри генератора списка. Выглядит странно, когда мы не сохраняем результат этого генератора, так как обычно нужен именно он.
Обычно, в таких случаях более наглядно смотрится функция map():

array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
more, less = [], []
map(lambda x: [less.append, more.append][x>5](x), array)

Проверяем результат

>>> print(more, less)
[] []

Хм, не сработало! Что произошло? Почему итерация не запустилась?
Дело в том, что в Python3 многие итеративные функции (если не все) стали генераторами. А что такое генератор? Это не тот генератор списка который List Comprehensions, а именно Generator.
Объект, который внутри себя имеет алгоритм получения следующего элемента и он не станет запускать итерацию пока мы не начнём запрашивать эти элементы.

Что возвращает функция map()?

>>> print(map(...))
<map object at 0x0000023114ADE3C8>

Тоже самое произошло и с функцией range

>>> print(range(10))
range(0, 10)

Значит всё что остаётся, это запустить итерацию по элементам генератора. Как это сделать максимально коротко?
Можно просто в цикле for

for _ in map(...): pass

Либо конвертнуть в список

list(map(...))

Или оператором *

[*map(...)]

Ну а полностью будет выглядеть так

[*map(lambda x: [less.append, more.append][x>5](x), array)]

Теперь генератор сразу вычисляется. Хотя, как по мне, такая запись менее "читабельна")

#tricks
В моих триках вы часто можете видеть способы сокращения кода. Когда длинный код заменяется на однострочный. Скорее всего, я продолжу эту практику, но хочу напомнить о важной вещи!

⚠️ Понятное лучше чем запутанное ⚠️

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

Если это можно сделать в Python, то это не значит что это нужно сделать.

Смотрим реальный пример из моей практики, когда я увлёкся и загнал все сравнения в один оператор if

>>> if any([fnmatch(rel, pat) for pat in include_files]) and not any([fnmatch(rel, pat) for pat in ignore_files]) and not any([any([fnmatch(name, pat) for pat in ignore_files]) for name in Path(rel).parts]):
>>> do_comething()

Да, это всего две строки))) Страшная мешанина!

Почему это плохо?
🔸 Это круто когда "заработало" но не круто когда через время смотришь на код и не поймешь что он делает.
🔸 Когда через время попробуешь что-то поправить, становится проще переписать заново (мой случай 😉)
🔸 Если работаете в команде, можно получить знатных [подставить страшное слово]лей. Такой код просто невозможно поддерживать.
Можно получить знатной брани на свою голову от себя же, пока не вспомнишь что это ты и писал)
🔸Если вы решились немного сгладить вину и расставить комментарии, то в однострочном выражении это не всегда получится. Символ игнора перехода на новую строку не позволит вам за ним писать комментарий.
Оба выражения ниже вызовут ошибку синтаксиса

x = 1 + \   # comment
2

x = 1 + \
# comment
2

Делайте код проще, избегайте сокращений, которые снижают читабельность кода.

⚠️ Простое лучше сложного ⚠️

А тем, кто еще не понял: import this