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
Кеширование, это способ оптимизировать скорость программы за счёт повторного переиспользования рассчитанных данных. При этом инвалидация кеша вопрос сложный и неоднозначный. Тем не менее, есть ряд простых случаев, когда кеш вполне уместен.

Имеется функция, которая принимает аргументы и возвращает некое значение. При одинаковых аргументах результат всегда одинаковый.
Давайте приведём такой очевидный пример:

def add(a, b):
return a + b

Каждому понятно, что, если в эту функцию мы будем отправлять одни и те же данные, мы будем получать одинаковый результат, это важно. Тогда зачем нам каждый раз это пересчитывать? Давайте кешировать!
Для этого используем готовое решение из стандартной библиотеки functools.lru_cache() (Python3.4+)

from functools import lru_cache
import time

@lru_cache(maxsize=64)
def add(a, b):
time.sleep(1)
return a + b

Добавил задержку чтобы имитировать расчёты. Параметр декоратора maxsize указывает сколько именно разных пар аргументы-результат мы будем хранить.
Теперь вызываем функцию с замером времени. Используем массив с повторяющимися значениями

>>> for i in [1, 2, 3, 2, 1, 4]:
>>> start = time.perf_counter()
>>> add(2, i)
>>> end = time.perf_counter()-start
>>> print(f'i={i}, Time={end}')

i=1, Time=1.0007981109
i=2, Time=1.0008854520
i=3, Time=1.0008842469
i=2, Time=0.0000204799 # из кеша
i=1, Time=0.0000132510 # из кеша
i=4, Time=1.0008038339

В распечатке времени видно, как только входящие данные повторяются, вместо пересчёта нам возвращается готовое значение из кеша.

Такое кеширование будет неверным, если результат зависит не только от входящих данных. Учитывайте это!

#tricks
В предыдущем посте⬆️ был пример кеширования функции. В стандартной библиотеке есть еще один способ, но для классов.
Это functools.cached_property (Python3.8+). Логика работы точно такая же, но:

- он предназначен только для метода класса.
- аналогичен декоратору property, то есть мы получим не функцию а свойство класса.
- не принимает аргументов (кроме self), так же как и property.
- кеш сохраняет в атрибутах класса (внутри ˍˍdictˍˍ).

Эдакий частный случай lru_cache для класса. В результате, вместо такой записи

class MyClass:
@property
@functools.lru_cache(maxsize=1)
def value(self):
return 123

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

from functools import cached_property

class MyClass:
@cached_property
def value(self):
return 123

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

#tricks
👍1
Правильно ли вы используете аргумент shell у методов subprocess?
Вкратце опишу разницу состояний этого аргумента.
(полный разбор — тема для статьи в блоге, возможно позже)

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

shell=True
- К вашей команде добавится исполняемый файл /bin/sh или cmd.exe
- Ваша команда будет аргументом флага -с, поэтому команду нужно передавать строкой
- Команду необходимо передавать с готовым экранированием и лексическим разбором пробелов.

Правильно:
subprocess.check_output('ls -sl', shell=True)
Команда выглядит так:
/bin/sh -c "ls -sl"
Здесь видно, что мы передаём значение аргумента -c а не саму команду.

Неправильно
subprocess.check_output(['ls', '-sl'], shell=True)
Команда выглядит так:
/bin/sh -c "ls" -sl
Ошибки не будет, но аргументы используются неверно. Получите не то что ожидаете.

shell=False
- команду нужно передать списком
- ожидается, что первым аргументом будет исполняемый файл
- будет запущен непосредственно файл из первого аргумента, без /bin/sh или cmd.exe
- автоматическое экранирование пробелов в аргументах списка

Правильно
subprocess.check_output(['ls', '-sl'], shell=False) 
Команда будет выглядеть так
ls -sl

Неправильно
subprocess.check_output('ls -sl', shell=False)
Эта команда завершится ошибкой: No such file or directory
То есть система пытается найти файл "ls -sl" а не файл "ls"

А также, если не используется shell то путь к исполняемому файлу требуется писать абсолютным, даже стандартные системные утилиты.
___________________
- для Windows всё аналогично
- для других методов из subprocess всё аналогично

#tricks #libs
Дополнение к посту про shell в subprocess.

Чем полезен режим вызова через shell? То есть, когда вы ставите аргумент shell=True.
Ваша команда запустится не напрямую, а через системный шел. А это значит что доступны все возможности шела.

Например:
- распаковка пути с символом "~"
subprocess.check_output('ls ~/', shell=True)

- распаковка переменных окружения
subprocess.check_output('ls $HOME', shell=True)

- использование пайпа команд
subprocess.check_output('cat $HOME/output.log | grep -n error', shell=True))

В общем, те, кто активно использует терминал, могут остальное додумать сами 😉

#tricks
👍1
У словаря есть полезный метод get() который может "аккуратно" спросить значение по ключу и вернуть что-то по умолчанию если такого ключа не нашлось.
Я встречал два способа записать эту логику, очень похожие но имеющие серьезную разницу.
По умолчанию, если ключа нет в словаре, метод возвращает None или то что указано в аргументе default.

>>> my_dict.get('unknown_key', default=123)
123
>>> my_dict.get('unknown_key')
None

Нас интересует первый вариант. Его можно записать еще и таким способом

>>> my_dict.get('unknown_key') or 123

Чем он отличается от варианта с аргументом default? На первый взгляд ничем.
Если ключ не существует, то вернется None. Сработает оператор or и мы получим значение 123.
Но основная опасность кроется в операторе or! Дело в том, что значение из аргумента default вернется только если КЛЮЧ ОТСУТСТВУЕТ В СЛОВАРЕ. Если ключ найден, то вернется его значение.

Если же мы пишем вариантом с or, то правила меняется. Значение 123 мы получим если ключ отсутствует в словаре или если найденное значение равно False в виде bool. Например, если ключ всё же был найден но значение 0, мы всё равно получим 123, несмотря на то, что 0 может быть вполне валидным значением.


>>> d = {"key": 0}
>>> d.get("key", 5)
0
>>> d.get("key") or 5
5
# во втором случае неоднозначный результат



⚠️ Будьте внимательны! Точно представляйте, что вы хотите получить от своего кода.

#tricks
🥚 Забавная пасхалка
Попытка заглянуть в будущее и узнать, появятся ли в Python фигурный скобки даёт вполне чёткий ответ

from __future__ import braces
File "<input>", line 1
SyntaxError: not a chance
Все мы знаем, что в Python всё является объектом. Это значит, что всё можно сохранить в переменную, передать аргументом или вернуть из функции через return.
Но известно ли вам, что объектом можно сделать даже срез списка?! То есть сохранить в переменную алгоритм среза и применить его позже.
Это можно сделать с помощью builtin функции slice().

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

>>> array = list(range(10))

Теперь создадим несколько срезов

>>> half = slice(None, len(array)//2)
>>> step_by_2 = slice(None, None, 2)
>>> invert = slice(None, None, -1)

Используется очень просто

>>> array[half]
[0, 1, 2, 3, 4]
>>> array[step_by_2]
[0, 2, 4, 6, 8]
>>> array[invert]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]


#tricks
Сколько вы знаете базовых "легальных" способов форматирования строк с помощью переменных в Python? Я насчитал 5!

1️⃣ Оператор +

>>> greting = 'Hello'
>>> name = 'World'
>>> print(greting + ' ' + name)
'Hello World'

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

2️⃣ Оператор %

>>> name = 'World'
>>> print('Hello %s' % name)
'Hello World'

Быстрый, но не удобный. Устарел.

3️⃣ Метод str.format()
Есть много фишек применения этого метода. Для примера используем самый простой.

>>> name = 'World'
>>> print('Hello {}'.format(name))
'Hello World'

Наверное, самый функциональный метод с множеством возможностей. Рекомендован к использованию.

4️⃣ f-string

>>> name = 'World'
>>> print(f'Hello {name}')
'Hello World'

Относительно новый и удобный по синтаксису метод. Работает быстрей чем format(). Поддерживает аналогичные фишки форматирования (но не все). Еще развивается и обновляется в новых версиях Python.
Рекомендован.

5️⃣ string.Template

templ = string.Template('Hello $name') print(templ.substitute(name='World'))
'Hello World'

Самый медленный, но самый безопасный способ собрать строку. Он в 10 раз медленней самого медленного способа. Но никаких инлайн-экспрешенов или фигурных скобочек.
__________
Есть еще ряд других модулей, такие как textwrap, jinja или собственные методы строки. Но данный пост о простых способах вставки переменных в строку.

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

from timeit import timeit

def t1():
# складываем 10 строк через + из переменной
t = 'text'
for _ in range(1000):
s = t + t + t + t + t + t + t + t + t

def t2():
# склеиваем список строк через метод join
arr = ['text'] * 10
for _ in range(1000):
s = ''.join(arr)

def t3():
# складываем через + но не из переменной а непосредственно инлайн объекты
for _ in range(1000):
s = 'text' + 'text' + 'text' +
... # всего 10 раз

Теперь каждую строку склейки запустим по 10М раз

>>> timeit(t1, number=10000)
0.21951690399964718
>>> timeit(t2, number=10000)
1.4978306379998685
>>> timeit(t3, number=10000)
0.2213820789993406

Хм, а нам говорили что через "+" это плохо и медленно ))) 😁
Тут стоит учитывать, что речь идёт о склейке множества длинных строк.
Давайте изменим условия:

def t4():
t = 'text'*100
for _ in range(1000):
s = t + t + t + t + t + t + t + t + t

def t5():
arr = ['text'*100] * 10
for _ in range(1000):
s = ''.join(arr)

def t6():
for _ in range(1000):
s = 'text'*100 + 'text'*100 + ... # всего 10 раз


>>> timeit(t4, number=10000)
12.795130728000004
>>> timeit(t5, number=10000)
2.642637542999182
>>> timeit(t6, number=10000)
0.2184546610005782

Вот, уже другой разговор, сразу видна разница, в среднем в 6 раз. Но погодите, почему последний тест t6() по скорости такой же как и t3()? Ведь строки теперь в 100 раз длиннее!
Это вопросы оптимизации кода, какие простые изменения ускоряют или замедляют выполнение программы. Мы столкнулись с примером обхода обращения к переменной. Например, именно так работает директива #define в С++, во время компиляции подставляя значение переменной вместо ссылки на неё.
В Python это тоже работает, но часто ли вы сможете встретить такой способ работы со строками? К сожалению, способ почти только теоретический.

В целом, тесты показали то, что мы хотели. Делаем выводы самостоятельно.

Полный листинг 🌍

#tricks
⭐️ Между прочим...
Читаете мой канал на телефоне? Код в примерах бывает растягивается в достаточно длинные строки. На мобиле смотреть не очень удобно. Особенно, учитывая, что переносы в Python это тоже синтаксис.
Но если повернуть телефон горизонтально, то всё становится куда приятней!

Казалось бы, это же очевидно! Но я сам не сразу догадался 😁
😉 Трик про flatten-список.
Задача: из списка списков сделать одноуровневый список

>>> arr = [[1, 2], [3, 4], [5, 6]]

Допустим, есть список интов

arr = [1, 2, 3, 4, 5]

Как получить их сумму? Очень просто!

>>> sum(arr)
15

И тут вы подумаете: Вау, какой удобный метод. Он просто берет список объектов и склеивает их через "+". Удобно же!
Такс, если list, как тип, поддерживает оператор сложения, то я же могу тогда сделать такой финт:

>>> arr = [[1, 2], [3, 4], [5, 6]]
>>> sum(arr)
Traceback (most recent call last):
File "<input>", line 2, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'list'

О нет! Счастье было так близко! 😭😢😩

Стоп, давайте разбираться. Если функция sum() просто прибавляет очередной аргумент списка к предыдущему, то с чем складывается самый первый элемент? Должно быть какое-то стартовое значение. И оно есть, это ноль "0". Потому-то мы и видим такую ошибку.
На наше счастье мы можем указать стартовое значение вместо ноля, чтобы получить сумму, используя в качестве начала другое число:

>>> arr = [1, 2, 3, 4, 5]
>>> sum(arr, 5)
20

И, следуя этой логике.....

>>> arr = [[1, 2], [3, 4], [5, 6]]
>>> sum(arr, [])
[1, 2, 3, 4, 5, 6]

YESSSS!!!! 😎🥰🤟

Вы, скорее всего, ломанётесь проверять другие типы и будете правы. С другими тоже работает. Но Python не упустит случай вас потроллить)))

>>> words = ['Hello ', 'world', '!']
>>> sum(words, '')
TypeError: sum() can't sum strings [use ''.join(seq) instead]

Не используйте "+" для склейки строк!

Скорее всего именно эта ошибка сделана на случай если вы захотите склеить большой текстовый документ, прочитанный через readlines().
_______
Это далеко не единственный способ сделать flatten-список. Пост скорей про функцию sum.

#tricks
Простая задача: получить рандомную строку.
Такое может потребоваться для создания секретного ключа, токена, одноразовой ссылки и тд. Как бы вы решали такую задачу?

Допустим, требуется строка в 20 символов.
Чаще всего решают примерно так:

from string import ascii_letters, digits
from random import choice
token = ''.join(random.choice(string.ascii_letters+string.digits) for _ in range(20))


Редко, но встречается и такой способ

import uuid
token = str(uuid.uuid4())[:20]

Но самый верный способ это готовый модуль secrets (Python3.6+) из стандартной библиотеки.

import secrets
token = secrets.token_urlsafe(15)
или
token = secrets.token_hex(10)

Почему лучше?

- Короче запись
- Более читаемый и понятный код
- Быстрей работает

Время на 1М запусков:

- random+string : ~14.3sec
- uuid : ~5.5sec
- модуль secrets : ~1.2sec

________
Прошу не путать получение рандомной строки и получение контрольной суммы.
Это разные по назначению задачи.

#tricks
Как соединить два списка? Список поддерживает оператор "+", так что это легко:

>>> l1 = [1, 2, 3]
>>> l2 = [4, 5, 6]
>>> l3 = l1 + l2
>>> print(l3)
[1, 2, 3, 4, 5, 6]

А как тоже самое повторить со словарями?

>>> d1 = dict(k1=1, k2=2)
>>> d2 = dict(k3=3, k4=4)
>>> d3 = d1+d2
TypeError: unsupported operand type(s) for +: 'dict' and 'dict'

Да, словари такой оператор не поддерживают. Самое распространённое решение это метод словаря update()

d1.update(d2)

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

d3 = dict( d1.items() | d2.items() )

Или еще короче

d3 = {**d1, **d2}

Если количество словарей неизвестно и все они в одном списке, то вариантов еще больше.
Допустим, есть такой список со словорями:

dict_array = [
{'k1': 1, 'k2': 2},
{'k3': 3, 'k4': 4},
{'k5': 5, 'k6': 6},
]

Для объединения всех в один мегасловарь делаем так:

>>> from operator import or_
>>> from functools import reduce
>>> d3 = dict(reduce(or_, [x.items() for x in dict_array]))
{'k3': 3, 'k6': 6, 'k1': 1, 'k4': 4, 'k5': 5, 'k2': 2}

Если важна последовательность ключей, то еще есть способ

>>> from itertools import chain
>>> d3 = dict(chain.from_iterable(d.items() for d in dict_array))
{'k1': 1, 'k2': 2, 'k3': 3, 'k4': 4, 'k5': 5, 'k6': 6}

Или так

d3 = dict(chain(*map(dict.items, array)))

Есть вариант и покороче

>>> from collections import ChainMap
>>> d3 = dict(ChainMap(*array))

>>> print(d3)
{'k1': 1, 'k5': 5, 'k6': 6, 'k3': 3, 'k2': 2, 'k4': 4}

И даже еще короче, в одну строку и с сохранением порядка:

>>> d3 = dict(j for i in array for j in i.items())
>>> print(d3)
{'k': 1, 'k2': 2, 'k3': 3, 'k4': 4, 'k5': 5, 'k6': 6}

Можно ещё несколько вариантов придумать, но, думаю, достаточно 😁
__________
Все примеры создают новый словарь, не изменяя старые. Но если в исходных словарях есть другие словари или списки то для независимой копии нужно пройтись еще функцией copy.deepcopy() 😉

#tricks
Чтобы запустить два контекст менеджера одновременно можно написать так:

with open('script.py') as inp:
with open('_bkp.py', 'w') as out:
out.write(inp.read())

А можно и более компактно в одну команду:

with open('script.py') as inp, open('_bkp.py', 'w') as out:
out.write(inp.read())


#tricks
Стандартная функция enumerate() очень удобна для получения индекса итерации

>>> mylist = ['one', 'two', 'three']
>>> for i, item in enumerate(mylist):
>>> print(i, item)
0 one
1 two
2 three

Вот бы со словарями так! Чтобы удобно получить индекс, ключ и значение!
Это можно сделать так:

>>> mydict = {10: 'item1', 20: 'item2', 30: 'item3'}
>>> for i, key in enumerate(mydict):
>>> value = mydict[key]
>>> print(i, key, value)
0 10 item1
1 20 item2
2 30 item3

Третья строка явно лишняя, приходится дополнительно доставать значение по ключу. Как нам сократить код? Хочется распаковать ключ и значение сразу. Например так:

>>> for i, key, value in enumerate(mydict.items()):
>>> print(i, key, value)

И получаем ошибку

ValueError: not enough values to unpack (expected 3, got 2)

Справедливо, ведь enumerate() возвращает всегда кортеж из 2х элементов, а мы распаковывем его в 3 переменные.
Но есть одна хитрость, которая позволит сделать то что мы задумали! Скобочки!

>>> for i, (key, value) in enumerate(mydict.items()):
>>> print(i, key, value)
0 10 item1
1 20 item2
2 30 item3

Теперь норм 😎

#tricks
Работаете в PyCharm? Тогда этот пост для вас!

В Python3 добавлен синтаксис аннотаций. То есть, в объявлении функции можно указать какие типы данных у нас тут крутятся. От простых, до сложносоставных.
Например, есть такая функция:

def my_func(x: int, y: float) -> float:
val = x * y
return val

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

Между тем, в Python2 также можно делать аннотации так, чтобы PyCharm их понял. Записываются они иначе, с помощью комментариев (type hint). Вот та же функция для Python2:

def my_func(x, y): # type: (int, float) -> float
val = x * y
return val

Интерпретатору не мешает, а для IDE подсказки😊

Но знаете ли вы, что такой способ можно использовать и не только для аннотирования функции? Можно указать тип любой переменной в любой строке!
Например, у вас есть внешний API в котором типы не объявлены вообще никак. А хочется иметь автокомплиты для возвращаемых значений.
Вот пример:

import some_api

def my_func() -> str:
value = some_api.get_value()
# ... life without autocomplete is pain(((
return value

Для переменной value IDE не сможет сообразить автокомплиты или проверку типов. Но с помощью такого же type hint мы можем ему помочь! Даже подсветка будет работать)

...
value = some_api.get_value() # type: str
# autocomplete for str here!!!
...

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

Запоминаем формат:

[code]  # type: [Type]

#tricks
Перед вами простой словарик:

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