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
Хотите оформить свои CLI скрипты красивым прогрессбаром?
Стоит посмотреть на библиотеку progress.
Она даёт возможность создавать красивые прогрессбары легко и быстро.

Но что если не хочется добавлять лишнюю зависимость только ради одной бегущей полоски?
Создать свою функцию с подобным функционалом — не проблема!

Вот базовый набросок:

import time
for i in range(0, 100+1, 4):
time.sleep(0.1)
print('\r{:>3}% [{}{}]'.format(i, "#"*i, '-'*(100-i)), end='')
print()

Запустите код в интерактиве и увидите что работает не хуже.
Вся хитрость в отсутствии переноса на новую строку в конце строки и в символе "\r" — возврат "каретки" (читай "курсора") в начало строки. После чего мы перезаписываем предыдущую строку.
Главное в самом конце не забыть обычный print() чтобы перейти на новую строку.

Расширенный вариант в виде контекст менеджера. 🌎

#libs #tricks
Ранее я делал серию постов про битовые операторы.
Вот вам ещё один наглядный пример как это используется в Python в модуле re.

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

pattern = re.compile(r"(\w+)+")
words = pattern.search(text, re.DOTALL)

А как указать несколько флагов? Ведь явно будут ситуации когда нам потребуется больше одного. Кто читал посты по битовые операторы уже понял как.

pattern.search(text, re.DOTALL | re.VERBOSE)

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

>>> for n in [1, 2, 4, 8, 16, 32, 64, 128, 256]:
>>>
print(bin(n))

0b1
0b10
0b100
0b1000
0b10000
0b100000
0b1000000
0b10000000
0b100000000

Чтобы было понятней, давайте напишем тоже самое но иначе, добавим ведущие нули:

000000001
000000010
000000100
000001000
000010000
000100000
001000000
010000000
100000000

Не понятно что тут происходит? Читай три поста про битовые операторы начиная с этого ➡️ https://t.iss.one/pythonotes/45

В общем, это пример применения побитовых операций в самом Python.
Теперь вы знаете Python еще немного лучше)

#tricks #regex #libs
Каждый модуль в Python имеет атрибут ˍˍnameˍˍ. В него записывается строка, содержащая полное имя модуля. Например:

>>> from my_package import my_module
>>> print(my_module.__name__)
'my_package.my_module'

Очень удобно для логгинга когда в имя логгера требуется записать имя модуля, в котором происходят события лога.
Но однажды мне потребовалось создать логгер не для модуля в целом, а для отдельной функции. Что в этом случае можно сделать?
Такой объект как функция тоже имеет атрибут ˍˍnameˍˍ

>>> class MyClass:
>>> def func(self):
>>> pass
>>> print(MyClass.func.__name__)
'func'

Итого нам следует собрать полное имя таким образом:

>>> full_name = '.'.join([__name__, MyClass.__name__, MyClass.func.__name__])
>>> print(full_name)
'my_package.my_module.MyClass.func'

А что если класс вложен в другой класс?

>>> class MyClass:
>>> class SubClass:
>>> def func(self):
>>> pass
>>> full_name = '.'.join([__name__, MyClass.__name__, MyClass.SubClass.__name__, MyClass.SubClass.func.__name__])
>>> print(full_name)
'my_package.my_module.MyClass.SubClass.func'

А что если вложений несколько?

>>> class MyClass:
>>> class SubClass1:
>>> class SubClass2:
>>> def func(self):
>>> pass

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

>>> from somewhere.something import OtherClass

>>> class MyClass2(OtherClass):
>>> pass

Cтрашно подумать как нам достать настоящее "полное имя" функции! А если там динамическое определение наследования или метаклассы? Вообще можно забить на решение и придумать другой способ 😵

На помощь приходит PEP3155 и его имплементация "Qualified name".

Всё очень просто, начиная с Python 3.3 классы и функции имеют атрибут ˍˍqualnameˍˍ, который и содержит полное или "честное" или "квалифицированное" имя объекта. Именно в него уже записан готовый полный адрес объекта. И теперь даже такая конструкция сработает правильно:

>>> # some_module.py
>>>
>>> class OtherClass:
>>> class SubClass1:
>>> class SubClass2:
>>> def func(self):
>>> pass
>>>
>>> # my_module.py
>>> from some_module import OtherClass
>>> class FinalClass(OtherClass.SubClass1.SubClass2):
>>> pass
>>>
>>> full_name = '.'.join([__name__, FinalClass.func.__qualname__])
'my_package.my_module.OtherClass.SubClass1.SubClass2.func'

Тут стоит заметить, что если функция находится не в текущем модуле а откуда-то импортирована (как в моём примере) то собирать имя с локальным атрибутом ˍˍnameˍˍ уже не имеет особого смысла. Тогда лучше использовать имя модуля исходного класса:

>>> full_name = '.'.join([FinalClass.__module__, FinalClass.func.__qualname__])
'some_module.OtherClass.SubClass1.SubClass2.func'

#pep
В прошлом посте мы получали полное имя метода. В атрибут ˍˍqualnameˍˍ записано имя только до класса, к которому метод принадлежит. Как быть если нужно полное имя с модулем, а мы имеем только объект метода без инстанса или ссылки на класс?

func = get_my_method()

В результате этого псведо-выражения мы имеем только сам метод класса. Есть ли в нём информация к какому классу он принадлежит? Конечно есть, это атрибут ˍˍselfˍˍ. Он ссылается на инстанс. А оттуда, как не трудно догадаться, можно получить и ссылку на класс через атрибут ˍˍclassˍˍ:


cls = func.__self__.__class__

итого полное имя будет выглядеть так

full_name = '.'.join([
func.__self__.__class__.__module__,
func.__qualname__]
)

#tricks
Наверняка вы часто используете генераторы списков (List Comprehension). Я тоже, даже порой злоупотребляю ими) Но что поделать, если они такие удобные!

Давайте разберёмся как работают составные итерации в генераторе списка.
Для начала посмотрим простой вид.

>>> array = [1, 2, 3, 4]
>>> [x for x in array]

Этот код просто делает копию списка. Добавляем некоторое выражение:

>>> [x*2 for x in array]

Этот генератор создаёт новый список элементы которого в 2 раза больше чем в оригинальном списке. Добавим условие:

>>> [x*2 for x in array if x%2]

Предыдущему примеру добавили фильтр, который проходят только нечётные числа.
В целом, я бы советовал на этом и остановиться. Не нужно усложнять простое!

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

>>> array1 = [1, 2, 3]
>>> array2 = [4, 5, 6]
>>> print([f'{i}-{j}' for i in array1 for j in array2])
['1-4', '1-5', '1-6', '2-4', '2-5', '2-6', '3-4', '3-5', '3-6']

Давайте обозначу каждый цикл условными скобками чтобы было понятней (синтаксически это неверно).

[f'{i}-{j}' (for i in array1) (for j in array2)]

Можем добавить условие и сюда? Можем!

>>> print([f'{i}-{j}' (for i in array1 if i >= 2) (for j in array2 if j < 5)])
['2-4', '3-4']

При этом во второй итерации мы можем использовать переменную из первой

>>> array = [(1, 2, 3), (4, 5, 6)]
>>> new = [y for x in array for y in x]

Кстати, мы разложили вложенные кортежи в один плоский список.

Можем ли мы использовать больше двух итераций? Да хоть десять!

>>> a1 = [9, 2, 4, 5]
>>> a2 = [4, 5, 9, 1]
>>> a3 = [5, 9, 3, 0]
>>> match = [x for x in a1 for y in a2 for z in a3 if x == y and x == z]
[9, 5]

Нашли числа которые встречаются во всех списках.

Можно еще сложней? Конечно, Python довольно многое позволяет делать.
Но, как говорят про Python: если ЭТО можно сделать то еще не значит что ЭТО нужно делать.

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

Для примера возьмем простой список

ls = [1, 2, 3, 4, 6]

И поехали!

>>> ls[-1] = 5
[1, 2, 3, 4, 5]

Обычное изменение по индексу.

>>> ls[2:4]
[3, 4]

Обычный срез который создаёт новый список на основе старого.

>>> ls[::-1]
[5, 4, 3, 2, 1]

Реверс списка, тоже создаёт новый. Оригинальный список остался без изменений.
Воспользуемся оператором присвоения:

>>> ls[2:4] = [7, 8]
[1, 2, 7, 8, 5]

Заменили диапазон элементов в оригинальном списке.

>>> ls[4:] = [0, 0, 0, 0]
[1, 2, 7, 8, 0, 0, 0, 0]

Указав диапазон сверх имеющегося мы расширили список по аналогии с методом extend(), но при этом еще и немного захватили конец списка. Всё это в одно действие!

>>> ls[:0] = [9, 8, 7]
[9, 8, 7, 1, 2, 7, 8, 0, 0, 0, 0]

Добавили элементы в начало

>>> del ls[-4:]
[9, 8, 7, 1, 2, 7, 8]

Удалили часть элементов списка

>>> ls[1:3] = []
[9, 1, 2, 7, 8]

Еще один способ удалить элементы

>>> ls[:] = []

А этим способом можно пользоваться для очистки списка в Python2, в котором еще не было метода clear().

Кажется мы "потратили" весь наш список)))
Сделаем новый и продолжим.

>>> ls = [1, 2, 3, 4, 5, 6]
>>> ls[::2] = [7,8,9]
[7, 2, 8, 4, 9, 6]

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

s = slice(3, 4)

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

>>> ls[s] = [0]*5
[7, 2, 8, 0, 0, 0, 0, 0, 9, 6]

Заменили один элемент на несколько элементов, расширив исходный список из центра.

И напоследок. Следующие два действия равнозначны и дадут одинаковый результат — полная замена списка.

>>> ls[:] = [1,2,3,4]
>>> ls = [1,2,3,4]

#tricks
В Python всё является объектами.
Это значит что у каждой сущности есть тип и какие-либо методы.
Мы знаем что есть методы у строк

>>> 'string'.upper()

у списков

>>> [1,2,3].count(2)

у словарей

>>> {"key": 123}.items()

А есть ли какие-то методы у простых чисел? Не много, но есть!

Например, возьмём простой int

>>> a = 22

Метод bit_length() покажет сколько потребуется бит для отображения данного числа в двоичном представлении, исключая ведущие нули.

>>> a.bit_length()
5

Проверяем

>>> bin(a).lstrip('-0b')
'10110'

Всё верно.

Проверим float

>>> b = 10.5

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

>>> b.is_integer()
False

Получить наш float в виде простой десятичной дроби

>>> b.as_integer_ratio()
(21, 2)

Конечно же Python не имеет типа "десятичная дробь", поэтому мы просто получаем кортеж из двух элементов: числитель и знаменатель.

У int тоже есть такой метод (Python3.8+), но он работает "хитро". Целое число всегда равно дроби где в числителе это же число а в знаменателе 1. Поэтому данный метод у int всегда возвращает (x, 1). 😕

Кстати, чтобы обойтись без переменной просто возьмите число в скобки

>>> (10.0).is_integer()
True

#tricks
У строки в Python есть два очень похожих метода. На столько похожих что кажется они делают одно и тоже.
Это метод isdigit() и isnumeric()

Давайте посмотрим зачем нам два одинаковых метода? И так ли они одинаковы?

Очевидно что isdigit() говорит нам, состоит ли строка только из чисел 0-9

>>> '12'.isdigit()
True
>>> '12x'.isdigit()
False
>>> '-12'.isdigit()
False
>>> '12.5'.isdigit()
False

Можно предположить что isnumeric() делает более глубокий анализ и распознаёт в строке float или отрицательное число.

>>> '15'.isnumeric()
True
>>> '-15'.isnumeric()
False
>>> '15.2'.isnumeric()
False

Нет, всё так же как и с другим методом. В чем же тогда разница? Для начала посмотрим следующие примеры:

>>> '5'.isdigit(), '5'.isnumeric() # Обычная цифра 5
# True, True
>>> '꧕'.isdigit(), '꧕'.isnumeric() # Яванская 5
# True, True
>>> '෩'.isdigit(), '෩'.isnumeric() # Синхала 3
# True, True
>>> '৩'.isdigit(), '৩'.isnumeric() # Бенгальская 3
# True, True
>>> '༣'.isdigit(), '༣'.isnumeric() # Тибетская 3
# True, True
>>> '³'.isdigit(), '³'.isnumeric() # 3 верхний индекс (степень)
# True, True
>>> '𝟝'.isdigit(), '𝟝'.isnumeric() # Математическая двойная 5
# True, True
>>> '๔'.isdigit(), '๔'.isnumeric() # Тайская 4
# True, True
>>> '➑'.isdigit(), '➑'.isnumeric() # 8 в круге
# True, True

А теперь примеры в которых, по мнению Python, результаты не равны

>>> '¾'.isdigit(), '¾'.isnumeric() # дробь три четверти
# False, True
>>> '⅕'.isdigit(), '⅕'.isnumeric() # дробь одна пятая
# False, True
>>> '𒐶'.isdigit(), '𒐶'.isnumeric() # клинопись 3
# False, True
>>> '三'.isdigit(), '三'.isnumeric() # 3 из унифицированной идеограммы
# False, True
>>> '⑩'.isdigit(), '⑩'.isnumeric() # цифра 10 в круге
# False, True
>>> 'Ⅳ'.isdigit(), 'Ⅳ'.isnumeric() # Римская 4
# False, True
>>> '𑇪'.isdigit(), '𑇪'.isnumeric() # Сенегальская архаическая 10
# False, True
>>> '𐌢'.isdigit(), '𐌢'.isnumeric() # Этрусская цифра 10
# False, True
>>> 'ↂ'.isdigit(), 'ↂ'.isnumeric() # Римская цифра 10000
# False, True
>>> '〇'.isdigit(), '〇'.isnumeric() # Символ ККЯ ноль
# False, True

Получается, что isdigit() говорит нам, является ли символ десятичной цифрой или спецсимволом, имеющим цифирное значение после преобразования.
В свою очередь isnumeric() включает все дополнительные символы юникода которые имеют отношения к числовым и цифровым представлениям.

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

>>> '🕙'.isdigit(), '🕙'.isnumeric() # эмодзи 10 часов
# False, False
>>> '7️⃣'.isdigit(), '7️⃣'.isnumeric() # эмодзи 7
# False, False

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

>>> '෩'.isdecimal(), int('෩') # Синхала 3
# True, 3
>>> '➑'.isdecimal(), int('➑') # 8 в круге
# False, ValueError

Какие выводы?

🔸 При определении цифры в строке isdigit() подходит лучше чем isnumeric(), но оба не гарантируют успешную конвертацию в int
🔸 Для однозначного определения возможности преобразования строки в int лучше подходит метод isdecimal()
🔸 Для однозначного определения символов 0...9 лучше использовать regex

Полный список символов юникода которые определяются как numeric

#basic
В продолжение прошлого поста про цифры в мире строк.
Почему методы isdigit() и isnumeric() не определяют в строке float и отрицательные значения?
Дело в том, что эти методы работают с ЦИФРАМИ, то есть с единичным символом. А строка "-2" или "3.4" это уже ЧИСЛО. То есть не символ а значение, записанное несколькими символами.

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

В юникоде есть символы цифр с точками "🄀⒈⒉⒊⒋⒌⒍⒎⒏⒐"
Каждая из них это ОДИН СИМВОЛ, поэтому он будет считаться цифрой

>>> '⒌'.isdigit(), '⒌'.isnumeric()
True, True

Но когда мы пишем это выражение в два символа ( 5+точка), то это не работает.

>>> '5.'.isdigit(), '5.'.isnumeric()
False, False

А еще есть такие символы

>>> '⑴⑵⑶⑷⑸'.isdigit()
True
>>> '🄁🄂🄃'.isdigit()
True

Но они не преобразуются в десятичные цифры

>>> '⒈'.isdecimal()
False
>>> '🄃'.isdecimal()
False
>>> '⑶'.isdecimal()
False


#basic
"Ну и как же нам перекидывать строки ви числа?" спросите вы. Проверять каждый символ, очистив строку от лишних знаков и точек. Потом конвертить допустимые символы в числа и восстанавливать знак, дробную чусть и тд???
Самый быстрый способ это просто "попробовать" 😜

text = "-0.3"
try:
num = float(text)
except ValueError:
print('Dough!')

Всё остальное это уже парсинг и разбор символов для иных целей.

#basic
Допустим, имеется у нас задача: разделить одно число на другое и получить отдельно целое число и остаток от деления.

Например, исходные числа 15 и 2. В результате должны получить 7 и 1. То есть 7 раз двойка входит в состав 15 целиком и потом остаётся еще 1.

Как будем действовать?
Очевидно же, целое число вхождений получаем через floor division

>>> 15//2
7

Остаток через деление по модулю

>>> 15%2
1

Но можно сделать проще (ох, ну куда уж проще то 😄). В Python есть builtin функция divmod которая делает эти два действия в одно.

>>> divmod(15, 2)
(7, 1)

#tricks
Python 3.9 готовит нам приятный сюрприз в PEP584.
Ранее мы обсуждали как можно удобно сложить вместе два словаря получив новый словарь. Было много вариатов, но ни одного идеального. Наконец-то в Python добавили оператор для слияния словарей!!! Это оператор "|".
А это значит, что начиная с 3.9 соединить словари можно таким синтаксисом:

dct3 = dct1 | dct2

Чтобы обновить словарь, можно использовать такой синтаксис

dct1 |= dct2

Данный функционал уже можно опробовать, установив первые релизы.

#pep
В Python есть стандартный модуль sched для синхронного планировщика задач. Что??? Синхронных??? В наш-то век "асинхронщины" и "параллельщины"!
Спокойно, сначала смотрим код, потом разбираемся.

Работает это так:

- создаём планировщик
- добавляем задачи в очередь с таймаутом и приоритетом
- запускаем и ждём пока завершится вся очередь

Смотрим пример:

import sched, time

def func(name):
t = round(time.time()-start_time, 2)
print(f"Execute {name} ({t}s)")

# просто отметка времени старта
start_time = time.time()
# создаём планировщик
s = sched.scheduler(
time.time, # функция замера времени
time.sleep # функция ожидания
)
# добавляем задачи
s.enter(0, 1, func, argument=('ev1',))
s.enter(2, 1, func, argument=('ev2',))
s.enter(1, 1, func, argument=('ev3',))
# запускаем очередь на исполнение
s.run()

Описание аргументов

Вывод получаем в соответствии со временем задержки:

Execute ev1 (0.0s)
Execute ev3 (1.0s)
Execute ev2 (2.0s)

Функция run() запускает планировщик и начинается выполнение задач в порядке очереди по времени и приоритету.
Время указывается с момента старта очереди. Если время совпадает то сортировка идёт по приоритету.

Что значит синхронный планировщик? Это значит что задачи будут выполняться строго по очереди в одном потоке. Никаких мультипотоков и мультипроцессов, модуль прост как бревно!

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

Параметр delay следует понимать не как "запусти через N сек" а как "запусти не раньше чем через N сек". Время запуска следующей зависит от выполнения предыдущих задач.

Где это может пригодиться? Очередь задач, между которыми должен быть промежуток времени по какой-либо причине.

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

Конечно, всё это можно решить банальным time.sleep() в нужном месте, но sched даёт несколько более удобный интерфейс управления задачами.

#libs
В прошлом посте мы рассмотрели синхронный планировщик задач sched. Функционал вроде не плох, но синхронное выполнение с блокировкой всё портит☹️. Можем ли мы как-то поправить ситуацию?

На самом деле можем (помимо отправки всего в subprocess).

Функция run() принимает аргумент blocking, который по умолчанию True.
То есть, если мы укажем blocking=False то получим неблокирующее выполнение? Нет. Этот параметр работает иначе.

Если мы делаем неблокирующий запуск, то после вызова метода run(blocking=False) планировщик выполнит все задачи, которым пришло время исполниться в обычном блокирующем синхронном режиме и вернёт время, через которое следует запуститься следующей задаче. То есть через какое время нужно запустить run(...) еще раз.
Получается, что вместо ожидания таймаута без полезной нагрузки планировщик освобождает поток и сообщает через сколько ему пора будет продолжить работу. А что делать с этой информацией, решаете сами.

Выполнение следующих задач произойдет после следующего вызова run() и если пришло их время выполниться.

Например, вместо ожидания следующей задачи будем делать что-то полезное:

import sched, time

def func(name):
# задача
t = round(time.time()-start_time, 2)
print(f"Execute {name} ({t}s)")

start_time = time.time()
# создаём планировщик
s = sched.scheduler(time.time, time.sleep)
s.enter(0, 1, func, argument=('ev1',))
s.enter(1, 1, func, argument=('ev2',))
s.enter(2, 1, func, argument=('ev3',))

delay = 0
last_run_time = time.time()
while True:
# выполнение заданий планировщика
if last_run_time + delay < time.time():
delay = s.run(blocking=False)
if delay is None:
break
last_run_time = time.time()

# здесь делаем что-то полезное
print('Делаем что-то полезное...')
time.sleep(0.1)

print('Complete all tasks')

Планировщик остаётся по-прежнему синхронным, но теперь вместо бесполезного ожидания мы можем запустить другой код на исполнение и знаем когда следует вернуться к планировщику.
___________________
PPS. Да, в этих примерах я нагло использую глобальные переменные))) Не делайте так на реальных проектах
Вы всё еще проверяете секретные данные оператором сравнения?

>>> if password == user_password:
>>> ...

Это небезопасный способ сравнения для стендалон приложений. Он уязвим к такому типу атаки как timing attack, позволяющий делать выводы и угадывать пароль на основании времени проверки.

Чтобы сделать безопасное сравнение используйте метод secrets.compare_digest(). Он защитит операцию проверки от подобных атак.

>>> import secrets
>>> if secrets.compare_digest(password, user_password):
>>> ...

Возможно вы ранее слышали про метод hmac.compare_digest(). Он не только делает то же самое, это один и тот же метод!

>>> import secrets
>>> import hmac
>>> hmac.compare_digest is secrets.compare_digest
True

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

assert isinstance(value, int), "Value must be type int"

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

Есть такая builtin константа __debug__, которая по умолчанию имеет значение True (и это не изменить в коде). Именно она указывает, будут ли работать ваши assert'ы.

Стоит запустить интерпретатор в режиме оптимизации (флаг -O), константа __debug__ будет равна False, и все ваши проверки будут проигнорированы.

python -O script.py

Поэтому всегда используйте raise.

if not isinstance(value, int):
raise TypeError("Value must be type int")

#tricks
В стандартной поставке Python есть один полезный инструмент в библиотеке collections, это класс deque.
он очень похож на простой список но он намного быстрее работает в некоторых случаях.
Например для обработки элементов в начале списка у него есть дополнительные методы: extendleft(), appendleft() и popleft().

Запустим пару тестов!!! 🚀

>>> from collections import deque
>>> import time
>>>
>>> st = time.perf_counter()
>>> for _ in range(1000):
>>> l1 = deque()
>>> for i in range(5000):
>>> l1.append(i)
>>> en = round(time.perf_counter()-st, 3)
>>> print(f'Test deque: {en}sec')
>>>
>>> st = time.perf_counter()
>>> for _ in range(1000):
>>> l2 = list()
>>> for i in range(5000):
>>> l2.append(i)
>>> en = round(time.perf_counter()-st, 3)
>>> print(f'Test list: {en}sec')
Test deque: 0.452sec
Test list: 0.436sec

Добавление в конец списка работает примерно одинаково. Теперь попробуем вставлять элемент в начало массива.

>>> st = time.perf_counter()
>>> for _ in range(1000):
>>> l1 = deque()
>>> for i in range(5000):
>>> l1.appendleft(i)
>>> en = round(time.perf_counter()-st, 3)
>>> print(f'Test deque: {en}sec')
>>>
>>> st = time.perf_counter()
>>> for _ in range(1000):
>>> l2 = list()
>>> for i in range(5000):
>>> l2.insert(0, i)
>>> en = round(time.perf_counter()-st, 3)
>>> print(f'Test list: {en}sec')
Test deque: 0.435sec
Test list: 6.347sec

Прирост производительности почти в 15 раз! 😲

Тестим функцию pop() (опустим код теста для краткости)

Test deque: 0.48sec
Test list: 0.529sec

Теперь pop(0) для list и popleft() для deque

Test deque: 0.476sec
Test list: 3.101sec

Быстрей примерно в 6.5 раз.

Почему так быстро? Дело в том что deque это некий аналог такого типа данных как "linked list data structure", в Python это занывается "двусвязные списки" (doubly-linked lists). Это список, но не в привычном представлении, а с особой оптимизированной структурой. При создании такого массива данные никуда не переносятся а только линкуются оттуда где были.
Да, это похоже на Python-лист, но линковка происходит иначе. Вместо того чтобы собирать некий стек ссылок и назвать его списком, в doubly-linked list каждый элемент просто ссылается на следующий.
Такая структура позволяет значительно ускорить, создание списка, изменение с любой стороны.

Где это может пригодиться? Конечно же в двусторонних очередях (double-ended queue). То есть когда мы добавляем элементы в начало а забираем с конца, или наоборот.
Минус такого подхода в просадке производительности для произвольного доступа к элементам в середине очереди.

Полный код тестов 🌎
______________
Реальное описание несколько сложней, я постарался передать основную суть.

#libs #tricks
Схематичное представление структуры 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