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
Перед вами простой словарик:

data = {1: 'value1', True: 'value2'}

С первого взгляда всё нормально. Давайте смотреть что у нас теперь есть в словаре

>>> data[1]
value2
>>> data[True]
value2

Кажется мы сломали питон) Но на самом деле нет. Это ошибка разработчика а не Python.

Дело в том, что на уровне данных для Python нет разницы между 1 и True.
Сам тип bool это производный клас от int

>>> issubclass(bool, int)
True

Значение True это частный случай int, равный 1. Поэтому у них одинаковый хеш, и словарь их воспринимает как один и тот же ключ

>>> hash(1)
1
>>> hash(True)
1

Так что же у нас сейчас в словаре?

>>> data 
{1: 'value2'}

Ключи добавляются в порядке их следования. И если такой ключ уже существует, то вместо создания нового ключа просто обновляется его значение. Поэтому у нас всего один ключ и это 1 а не True.

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

C "0" и False всё аналогично.

#tricks
Как удалить из списка повторяющиеся элементы, сохранив порядок?

array = ['item1', 'item2', 'item3', 'item3', 'item1', 'item3', 'item2', 'item4']

Обычно используют преобразование в множество и обратно

unq = list(set(array))

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

🔸Способ 0

unq = []
for item in array:
if item not in unq:
unq.append(item)

Теперь посмотрим как это записать короче

🔸 Способ 1
Создаем пустой список и в простом генераторе сначала проверяем а потом добавляем элемент если его еще нет в списке.

unq = []
[unq.append(item) for item in array if item not in unq]

🔸 Способ 2
Аналогичный, но с помощью set().

_set = set()
unq = [x for x in array if x not in _set and not _set.add(x)]

Здесь вторая проверка это хитрый "костыль". Функция на самом деле ничего не возвращает, просто нам надо её вызвать сразу после первой проверки, если она вернула True. Ответ функции add() инвертируем с помощью not чтобы оба условия сработали.

🔸 Способ 3
В одну строку как обычно с помощью set, но с последующей сортировкой для восстановления порядка.

unq = sorted(list(set(array)), key=array.index)

🔸 Способ 4
Здесь используем тот факт, что в словаре два одинаковых ключа быть не может и что ключи словаря теперь упорядочены (Python3+). Преобразуем элементы в ключи словаря и обратно в список.

unq = list(dict.fromkeys(array))
____________________
Способы 2-4 НЕ подходят, если элементы списка нехешируемые. То есть они не могут быть в качестве ключа словаря или элемента множества. Например, если у вас список словарей. В этом случае подходит только Способ 1.

#tricks
Часто требуется красиво распечатать ряд переменных через запятую. Возможно это требуется для дебага, а может является частью CLI.
Обычно решается через метод строки join()

>>>
args = ['val1', 'val2', 'val3']
>>>
print(", ".join(args))
val1, val2, val3

Если не все аргументы это строк то нужно их еще дополнительно преобразовать в строки.

>>> args = [1, 2, 3, 4]
>>>

print(", ".join([str(x) for x in args]))
или
>>>
print(", ".join(map(str, args)))
1, 2, 3, 4

Но самый простой способ это обычная функция print() (Python3)

>>> args = [1, 2, 3, 4]
print(*args, sep=", ")
1, 2, 3, 4

К сожалению такой способ не позволит легко сохранить получившуюся строку в переменную чтобы, например, использовать в логинге. Это возможно, но избыточно.

#tricks
Те кто работал с Python2 наверняка помнят, как приходилось поправлять расширение python-файла из переменной ˍˍfileˍˍ чтобы получить именно .py а не .pyc

source_path = os.path.splitext(__file__)[0] + '.py'

В Python3 эта проблема ушла. Всегда возвращается путь именно к исходному файлу .py.
Ну сразу бы так)

#2to3
В фреймворке PyQt (и PySide тоже) часто встречается настройка чего-либо с помощью так называемых флагов.

widget.setWindowFlags(Qt.Window)

Взаимодействие нескольких флагов делается с помощью бинарных (или побитовых) операторов.
Несколько флагов можно указать с помощью оператора "|"

list_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

исключить флаг из уже имеющегося набора можно так

list_item.setFlags(list_item.flags() ^ Qt.ItemIsEnabled)

Добавить новый флаг к имеющимся можно так

list_item.setFlags(list_item.flags() | Qt.ItemIsEnabled)

А проверка наличия делается так

is_enabled = item.flags() & Qt.ItemIsEnabled > 0

Почему именно так? Всё дело в том как именно работают побитовые операторы. Но об этом в следующем посте.

#qt
Давайте разберёмся как работают побитовые операторы.

Всего есть 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