PYTHON IN DEPTH🐍
729 subscribers
2 photos
10 links
ПАЙ8
Download Telegram
Грамотно ставим запятые

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

>>> capitals = ['Beijing', 'New Delhi']

Добавим в него ещё город и сделаем diff двух файлов перед коммитом.

>>> capitals = ['Beijing', 'New Delhi', 'Mexico City']

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

>>> capitals = [
'Beijing',
'New Delhi'
'Mexico City'
]

Так на diff'е будет видно, какой именно элемент появился, и делать ревью тоже будет проще. Кстати, вы заметили, что я забыла запятую? Это частая ошибка, и довольно неприятная, потому что при этом не только не будет брошено исключение, но и объединятся две последние строки:

>>> capitals
['Beijing', 'New DelhiMexico City']

Упс😕. Строки, не разделенные запятой, в Python конкатенируются. Это удобно для того, чтобы размещать в коде длинные тексты без переноса строки:

>>> why_so = ('Это очень очень '
'очень длинная строка или '
'документ на вашем сайте.')
>>> why_so
'Это очень очень очень длинная строка или документ на вашем сайте.'

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

>>> capitals = [
'Beijing',
'New Delhi',
'Mexico City',
]

Она не повлияет на выполнение программы, зато уменьшит вероятность ошибки программиста. При этом diff будет читаться лучше, потому что утилита подсветит только новую строку.

Плюсы запятой в конце коллекции:
🤖один и тот же формат всех строк
🤓меньше ошибок по невнимательности
🐙удобно ставить под контроль версий
Кеширование небольших чисел в CPython

Поговорим о небольших числах в CPython. Возьмем, например, два числа, 10 и 300:

>>> a = 10
>>> b = 300
>>> a is 10
True
>>> b is 300
False

Что?! Почему a указыввет на свое значение, а b y нет?

Все дело в том, что интерпретатор хранит числа в диапазоне от - 5 до 256 в специальном массиве. Поэтому когда мы пишем x = 10, то вообще-то получаем ссылку на объект, который уже существует в памяти:

>>> c = 1
>>> id(1)
94492411548416
>>> id(c)
94492411548416

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

>>> id(19)
94579033949504
>>> id(0b10011)
94579033949504
>>> id(0o23)
94579033949504
>>> id(0x13)
94579033949504

Числа вне диапазона - 5, 256 интерпретатор тоже будет пытаться оптимизировать, но только если они находятся в одной строке (или одном файле). В командной строке интерпретатора так не работает:

>>> d = 400
>>> d is 400
False

а так будет:

>>> e = 500; f = 500
>>> e is f
True

В общем и целом то, что нужно запомнить -- это что небольшие числа в интерпретаторе хранятся не так, как большие и что с ними следует пользоваться оператором ==, а не is.

И это снова не фича, а деталь реализации для экономии ресурсов. 🐍
Please open Telegram to view this post
VIEW IN TELEGRAM
Pythonic циклы

Поговорим о циклах в Python. Вот пример цикла, который мог бы написать человек с C или Java бэкграундом:

items = [‘a’, ‘b’, ‘c’]
i = 0
while i < len(items):
print(items[i])
i += 1

Что с ним не так?
🤖здесь мы вручную отслеживаем индекс, а значит, создаем себе возможность сделать ошибку на единицу. Легко заехать в элемент items[3], заменив < на <=;
🤖 если тело цикла длинное, то частая ошибка это сделать так, что условие выхода никогда не выполнится, и войти в бесконечный цикл;
🤖 в Python можно выразительнее.

А именно:

for item in items:
print(item)

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

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

Но что если вам правда, очень, действительно нужен индекс? Иногда это бывает, и тогда на помощь спешит конструкция enumerate(), которая предоставляет и сам элемент и его индекс:

for i, item in enumerate(items):
print(i)

В общем, если вам в Python цикле нужно делать операции не с индексами, а с самими объектами, то средства языка позволяют писать чистый и менее подверженный ошибкам код. Если индексы все же нужны, выбирайте enumerate(): пользоваться индексами напрямую — это не pythonic.

Закончу цитатой.

«В программировании есть только две сложные вещи: инвалидация кеша, выбор имени переменной, и ошибки на единицу»
(Джефф Этвуд, создатель StackOverflow).
Странная переменная в Python.

Когда человек впервые видит в программе что-то вроде

_, v in my_dict.items():
pass

он обычно теряется. Что это за переменная “_”?

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

>>> a + b
20
>>> _
20
>>> _ * 2
40

В интерпретаторе это позволяет вообще не писать названия переменных и работать быстрее:

>>> list()
[]
>>> _.append(‘чепуха’)
>>> _.append(‘небылица’)
>>> _.append(‘выдумка’)
>>> _
[‘чепуха’, ‘небылица’, ‘выдумка’]

При этом _ -- абсолютно валидное имя. Настолько, что в скриптах его принято использовать для dummy-переменных, значениями которых пользоваться не собираются. Например:

for _ in range(4):
print(“Шутка, повторенная дважды, становится в 4 раза смешнее”)

Или вот так:

_, c = ['чепуха', 'небылица', 'выдумка']
print(‘{} {}’.format(a, c))
'чепуха, выдумка'

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

for _, v in my_dict.items():
pass

можно было с тем же успехом использовать values().

В общем, в Python у переменной _ две функции:
🥳 она хранит последнее вычисленное интерпретатором значение,
🥳 и ее принято использовать, чтобы проигнорировать значение переменной.
Одинарное подчеркивание в конце переменной

Недавно говорила об одинарном символе подчеркивания в Python. Теперь поговорим о подчеркивании в конце переменной:

my_argument_ = 10

Нет никаких причин так делать, кроме одной: имя, которое вы хотите дать переменной, конфликтурет с именем встроенной функции или ключевого слова в Python. В PEP8, документе, который закрепляет соглашения по кодстайлу в Python, написано следующее:

single_trailing_underscore_: используется по соглашению для избежания конфликтов с ключевыми словами языка, например:

Tkinter.Toplevel(master, class_='ClassName')

Поэтому в этом примере лучше просто написать my_argument, без подчеркивания. А вот если вам понадобится, например, что-то из id, class, del, ..., то можно добавить _. Правда, даже в этом случае я сама обычно выбираю более объясняющее имя, например, person_id, class_name, delimeter, чтобы не путать коллег и сделать код более читаемым.

В общем, в Python есть соглашение по использованию подчеркивания в конце переменной. Подчеркивание обычно добавляют, когда название переменной конфликтует с уже занятыми интерпретатором именами. Мое мнение, что в случае конфликта лучше просто попытаться выбрать другое имя.
List astonishment

Допустим, мы хотим написать функцию, которая будет принимать на вход список. Естественным ходом выглядит написать что-то вроде:

>>> def foo(data=[]):
... data.append(5)
... return data

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

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]

Вот это поворот! Причина такого поведения в том, что в Python значения дефолтных аргументов вычисляются только один раз -- при объявлении функции. То есть, после того, как инструкция def выполнена, список data уже создан. При этом пока функция существует, пересоздаваться список больше не будет. Это объясняет и вот такое поведение:

>>> foo(data=[1, 2, 3])
[1, 2, 3, 5]
>>> foo()
[5, 5, 5]

Видно, что, когда мы передали другой список в foo(), к нему добавилось значение 5. При этом аргумент по умолчанию не изменился.

Как с этим жить? Либо вообще не использовать изменяемые объекты в качестве дефолтных значний, либо инициализировать их как None:

>>> def bar(data=None):
... if data is None:
... data = []
... data.append(5)
... return data

Такое определение создает новый список в локальном неймспейсе. Теперь все чисто😏

>>> bar()
[5]
>>> bar()
[5]
>>> bar(data=[1, 2, 3])
[1, 2, 3, 5]

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

def baz(arg1, arg2, _cache={}):
# Если есть в кеше, выходим
if (arg1, arg2) in _cache:
return _cache[(arg1, arg2)]

# Вычисляем результат
result = ...
# Сохраняем в кеш
_cache[(arg1, arg2)] = result
return result

Сохранение результатов для предотвращения повторных вычислений называется мемоизацией.

В Python дефолтные аргументы инициализируются только при объявлении функции. Это значит, что изменения, которые делаются в мутабельных аргументах при вызове функции, сохраняются. Поэтому лучше не использовать списки, словари и экземпляры самописных классов как значения по умолчанию. Либо использовать специально, как кеш, в котором можно хранить полезную информацию🐍

Закончу цитатой:
Не повторяйте одну и ту же ошибку несколько раз. Напишите функцию с этой ошибкой и вызывайте, когда будет нужно.
for ... else?

Рассмотрим пример использования ключевого слова else в цикле while:

>>> i = 1
>>> while i < 4:
... print(i)
... i += 1
... else:
... print('Вышли из цикла!')

Если выполнить этот цикл, то мы получим:

1
2
3
Вышли из цикла!

Здесь блок else выполняется только тогда, когда условие выхода из цикла перестает быть верным. Особенность здесь в том, что если вывалиться из цикла через break или через исключение, то блок else выполняться не будет.

Это работает не только с циклами while, но и с циклами for:

>>> for value in values:
... if value == 4:
... print('Нашли!:)')
... break
... else:
... print('Не нашли:(')

И так же можно с try-except блоками. На самом деле исключения в Python это не try-except-finally, а try-except-else-finally, ниже пример:

>>> num = 0
>>> try:
... result = 1 / num
... except ZeroDivisionError:
... print('Деление на ноль!')
... else:
... print('Конечное число.')
... finally:
... print('Вычисления закончены.')
...
Деление на ноль!
Вычисления закончены.

А если задать num = float('Inf'), то в результате деления мы получим 0 и вывод будет:

Конечное число.
Вычисления закончены.

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

В общем, есть конструкции while-else и for-else, в которых else отрабатывает в случае <<штатного>> завершения цикла. Еще else можно использовать в try-except блоке, чтобы выполнить какой-то код только в случае успешного выполнения блока try. Во всех случаях использование else позволяет более гибко управлять потоком выполнения 🐍
Timsort

Для сортировки в CPython используется алгоритм Timsort, который его создатель Тим Петерс застенчиво назвал в честь себя😏. Timsort — это комбинация сортировки вставками и сортировки слиянием, заточенная под работу с реальными данными. Дело в том, что на практике массивы, которые нужно сортировать, часто бывают частично упорядочены и Timsort пользуется этим предположением для ускорения работы.

Логика работы Timsort на самом деле довольно прозрачная:
🐠 Timsort делит входной массив на подмассивы;
🐠 Сортирует подмассивы вставками;
🐠 Объединяет их попарно сортировкой слиянием;
🐠 И возвращает отсортированный массив.

Но дьявол кроется в деталях.

Делим на подмассивы

Части, на которые алгоритм делит входной массив, в описании алгоритма автор называет run'ами. Так на какое количество run'ов следует разделить входной массив? Здесь два соображения. Во-первых, части должны быть достаточно короткими, чтобы их было выгодно сортировать вставками. А еще мы хотим, чтобы пары последовательностей, которые позже поступят в сортировку слиянием, были примерно одинаковой длины. Дальше происходит магия, которая учитывает эти соображения и выбирает длину подпоследовательности minrun между 32 и 64 элементами. При этом если входной массив короче 64 элементов, то на части он не делится.

Сортируем run'ы

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

Сливаем подмассивы

По мере того, как упорядоченные run'ы формируются, сортировкой слияния они объединяются попарно во всё более крупные отсортированные куски. При этом, если алгоритм при слиянии последовательностей встречает два участка вида

a = [1, 2, 3, 4]
b = [5, 6, 7, 8]

то по понятным причинам может объединить их быстрее, чем поэлементным перебором. Эта модификация сортировки называется galloping, поскольку в этом режиме алгоритм переносит на новое место не один элемент, а сразу несколько. Досортировки и слияния повторяются до тех пор, пока не достигается полная упорядоченность входного массива.

Вывод

Объем алгоритма в исходнике примерно 2к строк и, как можно догадаться по описанию, код довольно запутанный. Зато за счет использования упорядоченности в реальных данных сложность Timsort в лучшем случае линейная. В худшем случае, если входные данные случайные, timsort справится за O(n log n), а это неплохо для сортировки. Кстати, несмотря на то, что Timsort изначально был придуман в Python-сообществе, сейчас он используется еще в Java, Swift и Android platform.

Вот что нужно запомнить о Timsort:
🐠 Timsort это гибридный алгоритм сортировки, сочетающий модифицированные варианты сортировки вставками и сортировки слиянием;
🐠 Timsort пользуется тем, что реальные данные часто бывают частично упорядочены;
🐠 В лучшем случае сложность O(n), в худшем O(n log n)).

Визуализацию работы алгоритма можно посмотреть здесь. Видно, как сначала строятся run'ы и как они сливаются. Обратите внимание, что в обоих прогонах всегда объединяются примерно равные по размеру части 🐍
Введение в множества

Set' ы в Python реализованы так, что максимально напоминают математические множества. Давайте пройдемся по основным свойствам и возможностям множеств в Python, и разберемся, как их использовать.

В математике множество -- это набор объектов произвольной природы. В Python множество тоже может содержать переменные разных типов, например:

>>> A = {"My hovercraft is full of eels", 42, (3.14, 2.72)}

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

>>> A.add([-1, 0])
TypeError: unhashable type: 'list'

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

Инициализировать множество можно используя фигурные скобки или через конструктор класса set(). Эти инициализации эквивалентны:

>>> B = {1, 2, 3}
>>> B = set((1, 2, 3))

Только не запутайтесь: такая инструкция

C = {}

создаст не множество, а словарь.

В версиях интерпретатора 2.7 и выше работают set comprehensions:

>>> C = {x for x in range(1, 5)}
>>> C
{1, 2, 3, 4}

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

>>> D = [1, 2, 3, 3]
>>> D = list(set(D))
>>> D
[1, 2, 3]

Кстати, множества не сохраняют порядок, поэтому если он нужен, то добро пожаловать в списки. Из-за того, что порядка нет, во множествах нет ни индексирования, ни слайсов. В то же время циклы по множеству можно делать обычным pythonic-способом:

>>> for elem in C:
... print(elem)

Над множествами в Python можно делать те же операции, что и в математике: находить объединение, пересечение, проверять принадлежность к множеству и так далее. Для этого можно пользоваться операторами, а можно методами множеств:

A | B A.union(B)
A & B A.intersection(B)
A - B A.difference(B)
A <= B A.issubset(B)
A => B A.issuperset(B)


Обратите внимание, что операторы принимают только set’ы, а методы -- любые iterable контейнеры. Есть мнение, что операторы менее читаемые, но оба подхода в целом равноправны.

И еще у класса set есть методы, которые удобны для работы со множествами как с коллекциями:

add() — добавить элемент,
remove() — удалить элемент,
pop() — извлечь с удалением,
update() — объединить с другим множеством,
clear() — очистить множество.

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

>>> F = {"Seregia", "Vasia"}
>>> F.update("Alisa")

то получаем ожидаемый результат:

>>> F
{'l', 'a', 'Seregia', 'Vasia', 'A', 'i', 's'}

Это абсолютно валидный код и он отработает, поэтому такую ошибку по невнимательности можно искать довольно долго. Так что не попадайте в ловушку методов add() и update().

В общем, Python set'ы сильно напоминают математические множества: могут содержать объекты разных типов, требуют уникальности элементов, не сохраняют порядок и имеют методы, позволяющие их объединять, пересекать, etc... Кроме того, множества имеют интерфейс для работы с ними как с коллекциями.

Как определиться в выборе коллекции?
🐙 Если важен порядок — используйте списки.
🐙 Если нужно отображение ключ-значение — используйте словари.
🐙 Если нужен набор уникальных элементов — используйте множества.
Умножение контейнеров

Один интересный факт из Python. Вы, наверное, знали, что для строк определен оператор умножения и что можно делать так:

>>> 'привет ' * 4
'привет привет привет привет '

То же, вообще говоря, работает с другими контейнерами, например, с кортежами:

>>> ('привет',) * 3
('привет', 'привет', 'привет')

И с этим все хорошо, пока внутри контейнера не появляется ссылка на изменяемый объект. Например, на словарь:

>>> d = [{'key': 'val'}] * 2
>>> d
[{'key': 'val'}, {'key': 'val'}]

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

>>> d[0]['key'] = ''
>>> d
[{'key': ''}, {'key': ''}]

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

Уверена, что множественное присваивание в Python видели все. Выглядит оно вот так:

>>> x, y, z = 1, 2, 3
>>> x
1
>>> z
3

На самом деле на низком уровне создается tuple и в цикле инициализируется значениями 1, 2, 3. Поэтому с тем же успехом можно не опускать скобки и написать вот так:

>>> (x, y, z) = (1, 2, 3)
>>> x
1
>>> z
3

И это тоже сработает. Поэтому множественное присваивание еще называют tuple unpacking. Да и чаще всего его, кажется, используют именно с кортежами. Но вообще так можно писать с любыми iterable типами:

>>> x, y = [1, 2]
>>> y
2

И со строками тоже:

>>> x, y = 'hi'
>>> x
'h'
>>> y
'i'

Множественное присваивание, если применять его с оператором *, позволяет делать еще вот такие крутые штуки:

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> first, *rest = numbers
>>> first
1
>>> rest
[2, 3, 4, 5, 6]

По сути * здесь заменяет слайсы. С тем же успехом можно было написать:

>>> first, last = numbers[0], numbers[1:]
>>> first
1
>>> last
[2, 3, 4, 5, 6]

Но через распаковку получается читаемее. Можно даже <<вырезать>> середину:

>>> first, *middle, last = numbers

И если вам не нужны все промежуточные индексы, то хороший тон это вообще использовать нижнее подчеркивание:

>>> first, *_, last = [1, 2, 3, 4, 5, 6]

Кстати, если речь о вложенных объектах, то обратите внимание, что копия будет глубокой:

>>> color, point = ('blue', (0, 0, 255))
>>> color
'blue'
>>> point
(0, 0, 255)

В некоторых случаях это очень удобно.

В общем, множественное присваивание можно использовать не только с кортежами, но и списками, строками и любыми iterable типами. Его плюс в том, что оно позволяет не использовать индексы, а значит, уменьшает склонность к ошибкам и делает код читаемее. А еще оно используется в <<args, kwargs>> синтаксисе, который часто смущает начинающих, но об этом в следующий раз. 🐠
Введение в декораторы

В нескольких следующих постах я хочу поговорить о декораторах. Будет базовое определение, мотивация их использовать, всякие хитрости, а еще куча примеров.

Прежде, чем говорить о декораторах, нужно кое- что узнать о функциях в Python. Допустим, у нас есть функция, которая здоровается с Юпи:

def hey_Jupi():
print("Привет, Юпи!")

Функции в Python -- это объекты первого класса, ничем не хуже, чем int'ы или словари. Это значит, что:

🐙 Функцию можно присвоить переменной:

say_hi = hey_Jupi
say_hi()
# Привет, Юпи!

🐙 Функцию можно вернуть из функции:

def wrapper(func):
print("Юпи пришла.")
return func

hello_Jupi = wrapper(hey_Jupi)
# Юпи пришла.
hello_Jupi()
# Привет, Юпи!

🐙 Функцию можно определить внутри другой функции:

def deco(func):
def wrapper():
print("Юпи пришла.")
func()
return wrapper

hey_Jupi = deco(hey_Jupi)
hey_Jupi()
# Юпи пришла.
# Привет, Юпи!

Смотрите, что получилось на последнем шаге. На этапе создания deco никакой код не выполняется -- мы заходим в deco, видим, что здесь определена функция wrapper и возвращаем ее. Таким образом мы подменяем исходную hey_Jupi на wrapper и получаем новое поведение hey_Jupi, не изменяя ее код!

Это и называется декоратор. Это настолько удобный и мощный инструмент, что в Python для него придумали специальный синтаксический сахар. При условии, что функция deco у нас уже определена так же, как выше, можно добавить название декоратора с символом @ перед определением функции и получить эквивалентное поведение:

@deco
def hey_Jupi():
print("Привет, Юпи!")

hey_Jupi()
# Юпи пришла.
# Привет, Юпи!

Кстати, этот же декоратор можно применить и к любой другой функции:

@deco
def take_five():
print("Юпи, дай пять!")

take_five()
# Юпи пришла.
# Юпи, дай пять!

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

🐙 Декораторы используют в веб фреймворках для проверки авторизации или для разделения групп пользователей. Например, часть методов доступна только авторизованным пользователям, либо пользователям с определенной ролью, а остальные методы -- всем. Для этого нужные методы оборачивают в декораторы, которые делают необходимые проверки.
🐙 Декораторы позволяют проверить, что аргументы функции имеют нужный тип и значения. Это можно сделать на входе в функцию, но иногда проверки переносят в функцию-обертку.
🐙 С помощью декоратора можно замерять время выполнения функций.

В следующих постах разберемся, как комбинировать декораторы и передавать в декоратор параметры. Всем пять!🐠
Комбинируем декораторы

Допустим, у нас есть функции, которые работают со строками. Возьмем, например, функцию, которая возвращает строчку из Zen of Python:

def readability():
return 'Readability counts'

print(readability())
Readability counts

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

def bold(func):
def wrapper():
return '<b>' + func() + '</b>'
return wrapper

def italic(func):
def wrapper():
return '<i>' + func() + '</i>'
return wrapper

Как уже договаривались в прошлом посте запись:

@bold
def readable():
return 'Readability counts'

Эквивалентна записи

readable = bold(readable)

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

print(readable())
<b>Readability counts</b>

А что если мы хотим получить текст одновременно и жирный и с курсивом? Можно сделать композицию декораторов:

@italic
@bold
def readable():
return 'Readability counts'

print(readable())
<i><b>Readability counts</b></i>

Обратите внимание, что результат в зависимости от порядка применения декораторов будет разным:

@bold
@italic
def readable():
return 'Readability counts'

print(readable())
<b><i>Readability counts</i></b>

Так получается из-за разного порядка выполнения функций: в первом случае мы сначала добавляем тэг <<жирный>>, а сверху оборачиваем в <<курсив>>:

readable = italic(bold(readable))

А во втором -- наоборот:

readable = bold(italic(readable))
Исключения
Представьте, вы приступаете к работе над новым проектом и обнаруживаете в логах следующие сообщения, которые смешаны с успешными запросами к серверу:

Unexpected exception:

Что-то постоянно ломается, но без каких-либо сообщений.

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

try:
raise KeyError
except Exception as e:
print(f"Unexpected exception: {e}")

При выполнении этого примера получим следующий результат:

Unexpected exception:

Проблема заключается в том, что есть четыре способа вывести сообщение об исключении:

🐟 print(e)
🐟 print(str(e))
🐟 print(e.message)
🐟 print(repr(e))

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

my_dict = {}
try:
b = my_dict["bad"]
except Exception as e:
print(f"Unexpected exception: {e}")

Unexpected exception: 'bad'

Это происходит потому, что str(e) и e выводят только сообщение об ошибке, но не ее тип. Чаще всего этого достаточно, но исключения существуют для того, чтобы информировать о ситуациях, которые не были ожидаемы.

Иногда люди используют print(e.message). Однако здесь возникают две проблемы: во-первых, мы все равно получаем только сообщение об ошибке, а не ее тип. Во-вторых, атрибут message не определен для всех исключений. Если мы не проверим наличие этого атрибута перед использованием, мы получим новое исключение:

AttributeError: 'KeyError' object has no attribute 'message'

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

try:
raise KeyError
except Exception as e:
print(f"Unexpected exception: {repr(e)}")

Unexpected exception: KeyError()

В случае с использованием словаря получим:

Unexpected exception: KeyError('bad')

что является более явным, чем все три предыдущих варианта
Еще про исключения:

repr(e) -- это, конечно хорошо, но ведь есть ещё лучше, а именно:

from traceback import print_exc
...
my_dict = {}
try:
b = my_dict["bad"]
except Exception as e:
print_exc()

что выведет:

Traceback (most recent call last):
File "<pyshell#1>", line 2, in <module>
KeyError: 'bad'

и при этом программа продолжит свою работу. А если ошибку надо выводить не в stdout, то можно сделать так:

from traceback import print_exc
from io import StringIO
...
try:
b = my_dict["bad"]
except Exception:
buffer = StringIO()
print_exc(file=buffer)
out_var = buffer.getvalue()
Дублирование объектов в множестве

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

Давайте создадим класс Client и добавим его в коллекцию (я также добавил метод repr для красивого вывода):

class Client:
def init(self, user_name):
self.user_name = user_name

def repr(self):
return self.user_name

fish1 = Client(user_name="catfish")

clients = set()
clients.add(fish1)

print(clients) # {catfish}

Замечательно, всё работает как задумано! Теперь попробуем добавить второго клиента и убедимся, что дублирования не происходит:

fish2 = Client(user_name="catfish")
clients.add(fish2)

print(clients) # {catfish}

Как это возможно? Мы добавили два абсолютно идентичных экземпляра в коллекцию, ожидая, что останется только один, но оба остались.

При добавлении объекта в коллекцию интерпретатор следует следующему правилу:

если a == b, то hash(a) == hash(b) должно быть обязательно выполнено.

То есть, интерпретатор сравнивает объекты не только напрямую, но также сравнивает их хеши.

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

print(fish1 == fish2) # False

По умолчанию все объекты в Python также имеют хеш-значение (хеш), которое рассчитывается из их идентификатора (id). Когда мы пытаемся добавить объект в коллекцию, мы используем магический метод hash этого объекта, который также определен по умолчанию. Часто люди думают, что хеш объекта совпадает с его адресом в памяти, но это не всегда так:

🦑 В Python 2.6 и более ранних версиях hash(x) = id()
🦑 В Python 2.6 и более поздних версиях: (https://bugs.python.org/issue5169) hash(x) == id(x)/16

То есть, нельзя полагаться на то, что hash(x) = id().

Однако можно полагаться на то, что в Python у объектов по умолчанию есть хеш-значение, которое зависит от их идентификатора (id), и что хеш объекта не меняется в течение его жизни.

Так как в нашем примере объекты разные и занимают разные ячейки памяти, мы получаем:

print(hash(fish1), hash(fish2)) # 8786876890805 8786876904409

Так что можно сделать, чтобы объекты, которые мы считаем одинаковыми, объединялись в коллекции? Нужно переопределить методы hash и eq, чтобы явно указать интерпретатору, как сравнивать объекты и как рассчитывать хеш.

Для этого внесем изменения в определение класса:

class Client:
def init(self, user_name):
self.user_name = user_name

def repr(self):
return self.user_name

def hash(self):
return hash(self.user_name)

def eq(self, other):
if self.user_name == other.user_name:
return True
else:
return False

Теперь, при добавлении объектов в коллекцию, всё работает так, как ожидалось:

clients = set()

fish1 = Client(user_name="catfish")
clients.add(fish1)
fish2 = Client(user_name="catfish")
clients.add(fish2)

print(clients) # {catfish}

Помните, чтобы добавить объект вашего собственного класса в коллекцию или словарь в Python, вам необходимо:

🦑 Переопределить и метод hash, и метод eq, иначе это не будет работать (https://docs.python.org/3/reference/datamodel.html#object.__hash__)
🦑 Если объекты идентичны, их хеши должны быть равными.
🦑 Хеш-функция должна содержать информацию, которая однозначно идентифицирует объект.
🦑 Хеш объекта не должен изменяться в течение его жизни (иначе могут возникнуть проблемы во время выполнения программы).
🖥Microsoft интегрировала Python в Excel

Все популярные библиотеки Python (pandas, statsmodels и matplotlib) стали доступны для создания графиков и диаграмм. Для реализации всего этого появилась новая функция "PY"
Please open Telegram to view this post
VIEW IN TELEGRAM
Топ популярных горячих клавиш, для PyCharm:

Ctrl + D
— Дублировать строку, когда пишешь схожие строки, теперь нет надобности набирать их сначала или выделять и копировать.
Ctrl + R — Решил переименовать класс? Изменит имя во всем проекте.
Ctrl + Shift + N — Поиск класса или метода по всему проекту.
Ctrl + Alt + M — Написали код, теперь захотели его обернуть в функцию, вот сочетание.
Ctrl + Alt + S — Перейти в настройки.
Ctrl + Y — Удалить строку.
Ctrl + B — Переместиться к данному классу.
Ctrl + F12 — Показывает структуру данных файла.
Alt + F7 — Посмотреть где используется данный класс, метод или функция.
Ctrl + Shift + U — Быстро изменить регистр слов.
Ctrl + Alt + L — Быстрое форматирование кода по стандарту PEP 8.
Ctrl + Shift + ↑ ↓ — Для быстрого перемещения строк или блоков.
Ctrl + W — Выделить текущий блок.
Forwarded from Хитрый Питон
Вышла первая бета Django 5.0, а значит значимого изменения состава релиза уже не будет и можно смотреть, что завезли:

1. Много поддержки асинхронности - в contrib.auth, возможность получить и обработать asyncio.CancelledError внутри вьюхи, если клиент разорвал соединение до того, как мы закончили обрабатывать запрос, поддержка асинхронки в куче декораторов, асинхронная отправка сигналов, новые асинхронные методы у моделек
2. На первом месте довольно спорная фича - возможность показывать количество фильтруемых объектов в боковом фильтре в админке. Там под капотом COUNT и естественно на более-менее приличных объемах данных тормозить будет нечеловечески. Благо можно глобально отключить
3. Упрощенная шаблонизация для форм из коробки, для тех кто работает с html-формами код станет читабельнее (хотя думается мне, что те кто работает с большими формами уже давно что-то подобное у себя реализовали)
4. Возможность задавать дефолты на уровне базы данных
5. `GENERATED`-поля в моделях, значение которых рассчитывается на уровне БД
6. В Choice-полях теперь можно использовать словарь, вместо кортежей

В общим никаких революций https://docs.djangoproject.com/en/5.0/releases/5.0/
Forwarded from Хитрый Питон
Новости nogil. Steering Council принял PEP 703 - опциональный gil из коробки. На discuss.python.org большой пост про это, я приведу только заинтересовавшие меня моменты:

Пока нет уверенности, что выпиливание gil не поломает совместимость со сторонними либами, не сделает интерпретатор медленнее на 10–15% и не усложнит его поддержку. Выбрали вполне ожидаемый подход - реализуем и посмотрим, как пойдет, потому что на прототипах и теоретических выкладках далеко не уедешь.

Как и предполагали раньше, все будет происходить в несколько этапов:

1. Возможность отключить gil на этапе сборки — это точно не продакшен решение, а инструмент для авторов библиотек и экспериментаторов
2. Финализация изменений API и ABI, после чего nogil-сборка будет уже считаться не экспериментальной но все еще не включаться по дефолту
3. Отключенный gil по умолчанию с возможностью включить при сборке

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

https://discuss.python.org/t/pep-703-making-the-global-interpreter-lock-optional-in-cpython-acceptance/37075