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
Схематичное представление структуры linked list
Часто используете Python в терминале? Скорее всего вас не особо устраивает дефолтный REPL.
Советую попробовать прокаченные версии интерактивного шела:

🔸bpython

- подсветка синтаксиса
- автокомплиты
- инлайн подсказки параметров функций
- история команд
- авто отступы

Установка:
pip3 install bpython

Сайт 🌎
https://bpython-interpreter.org/

🔸 ptpython

- подсветка синтаксиса
- автокомплиты
- поддержка мышки
- авто отступы
- цветовые темы

Установка:
pip3 install ptpython

Сайт 🌎
https://github.com/prompt-toolkit/ptpython

Также можно глянуть:

➡️ www.asmeurer.com/mypython
➡️ xon.sh
➡️ ipython.org

#libs
Порой бывает необходимо работать с JSON файлами ручками, читая или изменяя данные. И очень не удобно, когда юникод в файле записан в виде кодированных символов.

>>> import json
>>> data = {'title': 'Привет Медвед!'}
>>> print(json.dumps(data))
'{"title": "\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442 \\u041c\\u0435\\u0434\\u0432\\u0435\\u0434!"}'

Эх, безобразие! Ни прочитать нормально, ни поправить.
Чтобы такое поведение изменить, достаточно добавить аргумент ensure_ascii=False

>>> json.dumps(data, ensure_ascii=False)
'{"title": "Привет Медвед!"}'

Теперь символы не кодируются в Unicode. В файл запишется в таком же виде.
____________________
Для тех кто в танке (всё еще на Python 2 🚂 ).
Строку следует делать как unicode, и для записи в файл использовать модуль codecs.

>>> import json, codecs
>>> data = {'title': u'Привет Медвед!'}
>>> with codecs.open(path, "w", encoding='utf-8') as f:
>>> json.dump(data, f, ensure_ascii=False)

#libs #tricks
Бывают задачи когда из большого массива объектов требуется отфильтровать эти объекты по категориям.
В результате получаем словарь примерно такого вида:

data = {
"category1": [item1, item2, ...],
"category2": [item1, item2, ...],
...
}

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

items = [...]
sorted_items = {}

for item in items:
cat = get_category(item)
if cat not in sorted_items:
sorted_items[cat] = []
sorted_items[cat].append(item)

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

Альтернативный код делающий тоже самое:

for item in items:
cat = get_category(item)
if cat not in sorted_items:
sorted_items[cat] = [item]
else:
sorted_items[cat].append(item)

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

from collections import defauldict

items = [...]
sorted_items = defaultdict(list)

for item in items:
sorted_items[get_category(item)].append(item)

Если указанного ключа нет в словаре, то он создаётся сразу со списком в значении и возвращается как будто он там и был.

#tricks #libs
Есть забавный трик с defaultdict, это бесконечно-рекурсивное дерево.
Самое забавное что создать его можно в одну короткую строку.

def tree(): return defaultdict(tree)

Теперь можете создавать вложенные словари любой глубины!

levels = tree()
levels['lvl1']['lvl2']['lvl3'] = 'item'

И таким образом строить любую иерархическую систему.
Примеры и описание на странице автора:

🌎 https://gist.github.com/hrldcpr/2012250

#tricks
В Python очень классной идеей является возможность переопределять взаимодействие объекта с любыми операторами, в том числе и унарные операторы.
И это можно очень интересно применить!

Долгое время меня смущало отсутствие возможности инкрементировать число на 1 с помощью синтаксиса С++. То есть вот так:

val++

Эта команда просто прибавляет единицу к значению val. В Python аналогичная операция делается так:

val += 1

Ну такой cебе ZEN😭.
Давайте напишем класс, который сможет провернуть что-то подобное. А именно, сделаем чтобы величина значения увеличивалась на 1 с помощью такой записи:

>>> c = Counter()
>>> print(c)
0
>>> +c
1

Да, вместо x++ мы сделаем +x, чтобы не конфликтовать со стандартным синтаксисом. Но и это уже не плохо!

Для взаимодействия с оператором "+" есть магический метод __add__, но для бинарного оператора. То есть когда операнда два. Для унарной версии оператора "+" есть метод __pos__, что означает positive. То есть как себя ведёт объект когда его пытаются сделать положительным. Вот его и используем:

class Counter(object):
def __init__(self, init=0):
self.val = init

def __pos__(self, *args):
self.val += 1

def __repr__(self):
return 'Count: {}'.format(self.val)

Чтобы удобно было смотреть на результат, добавил метод __repr__. Проверяем что получится.

>>> c = Counter()
>>> print(c)
Count: 0
>>> +c
Count: 1
>>> +c
Count: 2

Отлично, сделали счётчик с необычным синтаксисом! Добавим аналогичный метод для оператора "-" __neg__, что означает negate и описывает реакцию на попытку сделать число отрицательным. Теперь можем двигать значение в обе стороны:

>>> class Counter(object):
...
>>> def __neg__(self):
>>> self.val -= 1

>>> c = Counter(5)
>>> print(c)
Count: 5
>>> +c
Count: 6
>>> -c
Count: 5

Стоит учесть, что данная реализация методов изменяет сам объект а не возвращает новый изменённый, как это принято в Python. Так что весь пример не очень Pythonic-way). Но такое решение реализует необходимый нам минималистичный синтаксис.

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

#tricks
Расширенный вариант каунтера из прошлого примера. Оказалось ему есть куда еще развиваться и без унарных операторов))). Добавим обработку бинарных операторов "+" и "-".

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

    def __add__(self, other):
self.val += int(other)

def __sub__(self, other):
self.val += int(other)

Теперь можно делать так:

>>> c = Counter(3)
>>> c + 2
>>> print(c)
Count: 5

Добавим обработку для случая когда наш каунтер справа

    def __radd__(self, other):
self.__add__(other)

def __rsub__(self, other):
self.__sub__(other)

>>> c = Counter(3)
>>> 2 + с
>>> print(c)
Count: 5

Добавим во все методы возвращаемое значение и будем возвращать self:

    def __pos__(self, *args):
self.val += 1
return self

def __neg__(self):
self.val -= 1
return self
...

Тоже самое в остальных.
Теперь такой синтаксис тоже допустим (и тоже антипаттерн!):

>>> c = Counter()
>>> print(c)
Count: 0
>>> ++c
Count: 2
>>> ++++c
Count: 6
>>> ---c
Count: 3

А так же теперь доступен такой вариант

>>> 
c += 2
Count: 5

Следующие три записи идентичны по результату

с += 2
с + 2
++c

Во всех случаях каунтер с увеличится на 2

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

🌎 Ссылка на полный листинг
__________________________
PS. Если инкремент изменить на 0.5, то у нас получится синтаксис весьма похожий на C.
То есть, чтобы прибавить 1 надо будет написать ++x 😎, но это совсем уже будет вне всяких приличий 🥴

#tricks
В стандартной библиотеке os есть интересный метод os.nice().
Как написано в документации:

Add increment to the process’s "niceness".

Добавляет "любезности" процессу??? Ну почти...

В Unix системах есть стандартная утилита nice, которая может контролировать приоритет использования CPU процессом.
Обычно это значение 0, что значит стандартный приоритет. Но если добавить "любезности", то процесс будет больше отдавать другим, оставляя себе минимальный приоритет. Или наоборот.

Запустим тест чтобы визуально увидеть разницу.

🔸 Загрузите процессор на 100%.
Можно поставить любую тяжёлую задачу.
Например, сохраните этот код в файл и запустите в консоли (Python3)

🔸 Напишите функцию которая активно использует процессор и считает потраченное время.

import time

def compute():
array = []
for i in range(10):
start = time.perf_counter()
for i in range(1000000):
x = 2*2
array.append(time.perf_counter()-start)
return sum(array) / len(array)

Моя функция запускает 10 раз некий код и возвращает среднее время.

🔸 Тестируем

import os
# запускаем первый тест в дефолтным приоритетом
t1 = compute()
# меняем приоритет на минимальный
os.nice(19)
# запускаем второй тест
t2 = compute()
# смотрим реузльтат
print("Test 1", t1, "sec")
print("Test 2", t2, "sec")
print("Diff", t2/t1, "times")

В консоль распечатается время каждого теста и разница времени.

🌎 Код тестов

🔸 Где может пригодиться?

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

🔸 Что еще нужно помнить?

- Чтобы добавить приоритет нужно в функцию os.nice() отправить отрицательное значение. Так как данный метод всегда прибавляет число к текущему приоритету.
- Понижение приоритета доступно любому юзеру, повышение доступно только если процесс запущен от суперюзера.
- Узнать текущее значение: os.nice(0)
- Запускаемые подпроцессы наследуют приоритет.
- работает только на Linux.
- Доступный диапазон значений -20...19.

#libs
Как сохранить картинку непосредственно в Python-модуль?
Для этого нам пригодится библиотека base64.
Этот способ кодирование позволяет любые бинарные или текстовые данные закодировать с помощью 64 ASCII символов. То есть получится простая строка.

Зачем это вообще?

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

Например:
- встроить в URL в GET запрос как параметр
- встроить в тело email
- сохранить в Python-модуль как переменную
- сохранить любой конфиг, например JSON
- записать в базу данных
- зашить в HTML (XML) или CSS

Чаще всего так кодируют изображения в HTML и в CSS. Есть даже специальные сервисы для кодирование изображений.

Давайте закодируем и декодируем картинку.

Кодирование:

>>> import base64
>>> src_path = 'image1.png'
>>> with open(src_path, 'rb') as f:
>>> raw_data = f.read()
>>> image_encoded = base64.encodebytes(raw_data)
>>> print(image_encoded)
b'iVBORw...Jggg==\n'

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

Декодирование:

>>> save_path = "image2.png"
>>> raw_data = base64.decodebytes(image_encoded)
>>> with open(save_path, 'wb') as f:
>>> f.write(raw_data)

Картинка восстановлена обратно в файл.

Стоит помнить что:

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

#libs #tricks
Ещё немного про base64.

Собрал пример со встроенной в код картинкой. Это иконка для окна на PySide2. Файл кодирован в base64 и просто сохранён в переменной.

Для использования этих данных даже не пришлось сохранять их в новый файл. Иконка создаётся на лету с помощью метода QPixmap.loadFromData()

...
raw_data = base64.decodebytes(ico_encoded)
ico = QPixmap()
ico.loadFromData(raw_data, "PNG")
...

🌎 Полный пример смотрите в gists.

#libs #tricks #qt
Наверняка вы слышали о самом простом способе установить PIP, это скрипт get-pip.py. Скачиваем его, скармливаем интерпретатору и PIP установлен!

python get-pip.py

Но что это...??? Скрипт размером 1.8 МБ? Я думаю вы уже догадались почему. Смотрим докстринг в начале файла:

...
This is a base85 encoding of a zip file, this zip file contains
an entire copy of pip (version ХХ.Х.Х)
...

Да, в скрипте сохранён ZIP архив! Он закодирован с помощью base85 (аналог base64) записан в переменной DATA.
Если вы сомневались что данный способ хранения данных бывает полезен, то вот вам яркий пример удобного инструмента на его основе. Один простой файл в котором есть всё что ему нужно 😎

#libs
Как быстро распечатать красиво время имея в наличии число секунд ?

Конечно, можно посчитать сколько в этих секундах минут, часов и потом посчитать остаток, но есть способ быстрей! Это стандартный класс datetime.timedelta

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

>>> from datetime import timedelta
>>> str(timedelta(seconds=1024))
'0:17:04'

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

>>> print(timedelta(minutes=128))
2:08:18

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

>>> print(timedelta(minutes=256.5))
4:16:30

Вот так можно распечатать полтора часа

>>> print(timedelta(hours=1.5))
1:30:00

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

>>> print(timedelta(hours=64.32))
2 days, 16:19:12
>>> print(timedelta(weeks=20.32))
142 days, 5:45:36

А еще они поддерживают математические операции

>>> print(timedelta(minutes=5) + timedelta(hours=2))
2:05:00

#libs #tricks
Признавайтесь, кто хотел бы написать свой vim из которого можно легко выйти? )))

Это не так сложно как может показаться. По крайней мере движок для интерфейса уже есть в Python из коробки!
Это библиотека curses!

Для Windows она не идет в поставке. Нужно установить windows-curses
pip install windows-curses

Эта библиотека превращает терминал в "ASCII canvas" и можете "рисовать" в нём с помощью любых символов что пожелаете!

Что можно сделать? Вот несколько идей:

🖥 Крутое меню для вашего CLI

📝 Свой текстовый редактор в терминале (vim, nano)

📺 Оконный интерфейс в терминале (Norton Commander, Midnight Commander)

🐍 Игра на базе терминала (тетрис, карты, змейка)
/Каждый питонист должен написать "Змейку"! /

📊 Приложение с анимированными элементами статистики (htop)

🚀 Свой REPL c блекджеком и автокомплитами (bpython, ptpython)

__________________
🌎 Мои эксперементы за вечер, простое меню для выбора действий.
А это мануал на devdungeon.com для тех кто хочет проникнуться этой темой

#libs
Для тех кто бегло просмотрел материал прошлого поста.

Вы могли не заметить ссылку на крутую библиотеку Urwid!
Это обертка для curses, реализующая интерфейс для создания оконных GUI в терминале 😳!

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

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

🌎 Моё меню из прошлого поста, переделанное на Urwid
Это меню понимает клики мышкой.

#libs
Есть такое понятие как Switch Statement. Это некоторая конструкция в языке программирования предназначенная для множественного ветвления алгоритма.
вот примеры реализаций в разных языках:

JavaScrpt
C++ (или здесь)
C#
Ruby
PHP
Go
Delphi
и даже Pascal

В целом, шаблон такой:

switch query:
case match1:
...
case match1:
...

А что у нас в Python?

if condition1:
...
elif condition2:
...
elif condition2:
...
else:
...

Вполне рабочий вариант. Но явно отличается от примеров выше.

И тут внезапно!!! 23 июня 2020г выходит в свет PEP622
И что мы видим? Планы на Python 3.10 по добавлению Switch Statement! Называется он Structural Pattern Matching, но по сути мы получаем тот же синтаксис что и в Switch Statement.

match some_expression:
case pattern_1:
...
case pattern_2:
...

В данный момент статус его еще Draft. Интересно как он еще изменится и доживет ли концепция до релиза? Учитывая что один из автором сам Guido van Rossum, можно сказать что внедрят точно!
Пока рано его разбирать, просто подождем...

#pep
This media is not supported in your browser
VIEW IN TELEGRAM
Есть у QLabel есть одна особенность. Её минимальный размер определяется текстом, который в неё записан. Это приводит к тому что длинный текст принудительно увеличивает ширину интерфейса.
В большинстве случаев это выглядит плохо.
Как с этим бороться?

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

🔸 Делать перенос строки. Тогда мы получим изменение размера в другую сторону, что тоже поломает интерфейс.

🔸 Переопределить paintEvent() и сделать кастомный рендеринг текста. Можно, но слишком сложно для такой задачи.

Проще всего обрезать текст под текущий размер виджета используя класс QFontMetrics.
Он имеет готовый метод elidedText(), который просто вызываем по событию resizeEvent.
Я также добавил установку ToolTip чтобы всегда можно было увидеть полный текст при наведении курсора.

🌎 Код здесь

#qt #source
Когда-то давно, когда я сел за компьютер примерно третий раз в жизни, я изобрёл... самый быстрый и универсальный конвертор форматов изображений🤓

Схематично он довольно просто (ведь всё гениальное просто):

rename my_image.bmp -> my_image.jpg

Ох уж это наивное IT-детство))) Я действительно думал что победил систему. Если Paint открыл, то значит конвертация прошла успешно! 😭

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

По умолчанию она понимает следующие форматы:

rgb
gif
pbm
pgm
ppm
tiff
rast
xbm
jpeg
bmp
png
webp
exr

Вы всегда можете добавить свой формат. Для этого в список imghdr.tests нужно добавить функцию проверки с определённым набором аргументов: байты и открытый файл.
Вернуть функция должна либо строку с типом формата либо None.

Пример проверки файла с изменённым расширением

>>> import imghdr
>>> imghdr.what('image.png')
'jpeg'

Как же происходит проверка? Очевидно, что не на основе расширения файла. Каждый тип файла содержит заранее известный паттерн данных (magic number). Функции проверки просто ищут этот паттерн в бинарных данных файла. Если совпадение есть, то формат определён.

Всё станет понятно когда посмотрите исходники этого модуля. Код тестов состоит из 2-3 строк, при этом одна из них это return.

#libs
Аналогично imghdr в Python есть стандартная утилита sndhdr.
Определение форматов аудио файлов!

Тест этой библиотеки возвращает не просто формат в виде строки, а именованный кортеж с дополнительными данными о файле. Например длительность, количество каналов или частота семплирования.

Поддерживаются следующие форматы:

aifc
aiff
au
hcom
sndr
sndt
voc
wav
8svx
sb
ub
ul

Эммм... а где же MP3? Где Flac??? Может они не вписываются в концепцию библиотеки по каким-либо признакам? Или это задел на развитие? Программисты не используют MP3?
(Только WAV, только хардкор!)
Даже в исходниках есть вопрос без ответа...

#libs
Вы прониклись идеей определять тип файла не по расширению из имени а по содержимому (по сигнатуре или по magic number)?
Не хватает стандартных библиотек imghdr и sndhdr?
Тогда специально для вас есть решение — библиотека fleep

Только посмотрите на количество поддерживаемых форматов. Изображения, звук, видео, документы, шрифты, 3D пакеты... На данный момент 104 формата!

Стоит заметить, что:

🔸 автор скорее всего разрабатывал её на Windows 10, стоит хорошо проверить её перед использованием на других платформах.

🔸 добавление новых форматов происходит через обновления JSON файла, но я бы сделал под каждый формат отдельный файл. Просто так удобней расширять список форматов не изменяя исходников.

🔸 Список сигнатур файлов для добавления можно найти в интернете, например на вики: https://ru.wikipedia.org/wiki/Список_сигнатур_файлов

🔸 На хабре есть статья от автора
https://habr.com/ru/post/345822/

PS. Также можете взглянуть на filetype

#libs
fleep не поддерживает нужный тип файла? Не нашли подходящую сигнатуру в интернете?
Тогда пробуйте ➡️ puremagic, еще больше типов! Возможно самая актуальная библиотека по данной теме.

Всё ещë нет нужной сигнатуры? Видимо, у вас сложный случай. Остаëтся только найти "магическое число" вашего файла самостоятельно.

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

🔸 mcedit.
Редактор который идёт в поставке с mc (Linux). Жмем F3 для просмотра и сразу F4 для переключения режима.

🔸 xxd (что это?)

Пример для Linux

xxd myfile.ext | head

head не даёт прочитать весь файл. Нам нужно лишь начало.
Для Windows тот же xxd, который идет в поставке с Git

...\Git\usr\bin\xxd.exe -l 100 myfile.ext

Флаг "l" аналогичен head на Linux
Теперь проходимся по нескольким файлам этого формата и ищем совпадающие первые биты, которые всегда одинаковы. Нужное число найдено!

#libs #tricks
Так что же такое этот Magic Number?
Это набор байтов, уникальный для определённого типа файла. Он еще называется Сигнатура файла.

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

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

В Python для скомпилированных PYС-файлов магическое число отличается от версии к версии. По нему можно определить версию интерпретатора, которым скомпилирован этот байт-код.

Пример библиотеки для определения версии

Получить magic number текущей версии можно так:

Python 3
>>> from importlib import util
>>> util.MAGIC_NUMBER.hex()

Python 2
>>> import imp
>>> imp.get_magic().encode('hex')

Интерпретатор использует это значение для проверки PYC-файлов перед импортом.
Если версия не подходящая вы увидете ошибку:

RuntimeError: Bad magic number in .pyc file

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

Итого, сигнатура файла помогает:

🔸 быстро определить формат файла вне зависимости от имени (например для запуска соответствующего приложения в OS)
🔸 обозначать совместимость одного и того же бинарного формата с разными версиями софта (пошло из Unix систем)
🔸 разделить бинарники программ по вариантам сборки
🔸 восстанавливать файлы при потере таблицы файлов диска
🔸 использовать быстрый поиск файлов по типу без обращения к таблице файлов
🔸 получить типа файла, передаваемого по сети, не качая его целиком

Список этим, конечно же, не ограничивается.

#libs #tricks