Коробка с питоном
541 subscribers
45 photos
119 links
Заметки от Python-разработчика: сниппеты, обзоры пакетов, новости и другая полезная информация.

Автор: @kiriharu
Download Telegram
Как работает bool()?

Python может принимать любой объект в булевом контексте. Чтобы определить, является ли выражение истинным или ложным, применяется функция bool(x), которая должна вернуть булево значение (True или False).

По умолчанию, объект попытается вызвать метод __bool__, который должен вернуть булево значение. Если реализованного метода нет, то Python попытается вызвать __len__, который должен вернуть 0 или значение больше ноля - при ноле результатом будет False, соответственно при значении больше ноля - True. Если ни один метод не реализован, то автоматически вернется True.

Резюмируя, получается такая цепочка: __bool____len__True
#std
Немного фактов про str() и repr()

- repr() вызывает метод __repr__ объекта, а метод str(), собственно __str__.
- repr() вызывается интерактивной оболочкой, при попытке вывести объект.
- Если метод __str__ в объекте не определен, то вызывается __repr__.

А что вы можете сказать про str() и repr()? Пишите ниже, в комментарии 👇
#std
Коробка с питоном
Немного фактов про str() и repr() - repr() вызывает метод __repr__ объекта, а метод str(), собственно __str__. - repr() вызывается интерактивной оболочкой, при попытке вывести объект. - Если метод __str__ в объекте не определен, то вызывается __repr__. …
Когда использовать __str__ или __repr__?

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

Таким образом имеем, что __repr__ должен использоваться для предоставлении информации об объекте разработчику, а __str__ должен быть читаемым и использоваться для представления информации пользователю.
#std
В канале недавно уже был пост про namedtuple, но мне ещё есть что про него рассказать.

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

>>> lst = ['Mining', 'Excavating', 'Boiling']
>>> Skills = namedtuple('Skills', lst)
>>> skills = Skills(1, 2, 3)
>>> skills
Skills(Mining=1, Excavating=2, Boiling=3)

Ещё мы можем создать кортеж через метод _make():

>>> skills = Skills._make([1, 2, 3])
Skills(Mining=1, Excavating=2, Boiling=3)

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

Именованные кортежи неизменяемые, но мы можем использовать метод _replace(), чтобы изменить данные. Под капотом он использует метод _make для создания нового кортежа, поэтому, по сути, мы просто пересоздадим кортеж:

>>> skills._replace(Mining=100)
Skills(Mining=100, Excavating=2, Boiling=3)
#std
Типизация именованных кортежей.

Один из комментаторов указал, что namedtuple() объявлен устаревшим и в 3.10 его поддержка прекратится. Что же делать? Использовать NamedTuple, если в вашем проекте питон выше версии 3.6.

Именованный кортеж можно создать 2 путями - через наследование NamedTuple (класс Skills) или старым способом (AnotherSkills). Оба способа содержат аннотации типов, повышающие (как минимум) читаемость кода.
Все методы, которые были в namedtuple (_make(), _replace(), _asdict()) работают:

>>> s = Skills(0)
>>> s._asdict()
{'mining': 0, 'excavating': 1}

Теперь мы можем посмотреть на аннотации:

s.__annotations__
{'mining': <class 'int'>, 'excavating': <class 'int'>}

Так же, ничего не мешает добавлять новые методы или оверрайтить существующие. В примере у Skills переопределен метод для str():

>>>str(s)
'Какой качок, уровень копания: 0\nуровень лесорубства: 100'

GitHub
#std
Про применение all()

Стандартная библиотека Python полна полезных вещей, но полезные вещи есть даже среди встроенных функций. Одна из таких встроенных функций - это all().

Функция all() возвращает True, если все элементы истинные (или объект пустой).

К примеру, all() очень удобно использовать с генераторами. Например, мы можем проверить, являются ли все октеты в IP-адресе числами:

>>> all(i.isdigit() for i in '127.0.0.b'.split('.'))
False
>>> all(i.isdigit() for i in '127.0.0.1'.split('.'))
True
#std
Поиск N максимальных или минимальных элементов.

Если вы хотите создать список из N максимальных или минимальных элементов, модуль heapq вам поможет в этом.
У этого модуля есть две функции: nlargest() и nsmallest(), которые, соответственно ищут максимальные и минимальные элементы. Например:

>>> nums = [10, 20, 30, 40, 55, 632, -3, 98321, 82, 0, 8]
>>> heapq.nlargest(4, nums)
[98321, 632, 82, 55]
>>> heapq.nsmallest(4, nums)
[-3, 0, 8, 10]

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

currency = [
dict(name="Etherium", price=3323),
dict(name="Bitcoin", price=45538),
dict(name="ZCash", price=132),
dict(name="Litecoin", price=184),
dict(name="OmiseGo", price=8.933)
]

>>> heapq.nsmallest(2, currency, key=lambda s: s['price'])
[{'name': 'OmiseGo', 'price': 8.933}, {'name': 'ZCash', 'price': 132}]
>>> heapq.nlargest(2, currency, key=lambda s: s['price'])
[{'name': 'Bitcoin', 'price': 45538}, {'name': 'Etherium', 'price': 3323}]
#std
Определяем наиболее часто встречающиеся элементы в последовательности.

Допустим, у нас есть некоторая последовательность из слов (words), и мы хотим узнать, какие элементы в ней встречаются чаще остальных.

Для этого можно использовать класс Counter из модуля collections в котором есть метод most_common(), который и выдаст список элементов, которые встречаются чаще остальных.

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

>>>counts['test']
2

Кстати, эта задача часто встречается на собеседованиях и просят её реализовать без модуля collections. Как бы вы её решили? Ответы можете писать в комментарии, обсудим.
#std
Ручной проход по списку

Задачка: нужно пройти по списку не используя цикл for.

Решение у этой задачи достаточно простое - можно использовать функцию next() и ловить исключение StopIteration. На скриншоте можно увидеть пример с использованием цикла while.

Здесь мы получаем итератор - это такой объект, который облегчает навигацию по коллекциям. Дальше в цикле вызываем next(), который получает следующий элемент и так до тех пор пока элементы не закончатся - индикатором этого будет вызов исключения StopIteration.
#std
Я заметил, что редко использую функции из itertools, в основном когда нужно произвести какие-то "красивые" шаманства. Теперь будем вспоминать саму библиотеку вместе, естественно с примерами :)
Сам модуль itertools это набор из эффективных и быстрых по памяти инструментов, возвращающие итераторы. Сами по себе итераторы можно комбинировать с такими функциями как map(), list() лямбдами или же использовать их с помощью цикла for.

А сегодня мы начнем c самых простых - c бесконечных итераторов. Всего по документации их три: count(), cycle() и repeat().
Ниже я буду указывать аргументы по умолчанию и возможные передаваемые типы как тайп-хинты. Надеюсь ни у кого не возникнет проблем с пониманием.

#itertools #std
Коробка с питоном
Я заметил, что редко использую функции из itertools, в основном когда нужно произвести какие-то "красивые" шаманства. Теперь будем вспоминать саму библиотеку вместе, естественно с примерами :) Сам модуль itertools это набор из эффективных и быстрых по памяти…
count(start: int | float = 0, step: int | float = 1) -> Iterator[int | float]

Создает итератор, который возвращает равномерно распределенные значения, начиная с числа, указанного в аргументе start. Само значение-шаг указывается в переменной step:

>>> first = count(10, 2)
>>> next(first)
10
>>> next(first)
12
>>> next(first)
14
>>> second = count(0.1, 0.1)
>>> next(second)
0.1
>>> next(second)
0.2
>>> next(second)
0.30000000000000004
>>> next(second)
0.4

Небольшая загадка для вас - почему вместо 0.3 получили такое число? Пишите в комменты :)

#itertools #std
Коробка с питоном
Я заметил, что редко использую функции из itertools, в основном когда нужно произвести какие-то "красивые" шаманства. Теперь будем вспоминать саму библиотеку вместе, естественно с примерами :) Сам модуль itertools это набор из эффективных и быстрых по памяти…
cycle(p: Iterable) -> Iterator

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

>>> first = cycle([1, 2])
>>> next(first)
1
>>> next(first)
2
>>> next(first)
1
>>> next(first)
2

Например, вот так при помощи функций islice и cycle мы можем сгенерировать список из 20 элементов из необходимой нам коллекции:

>>> list(islice(cycle([1, 2, 3, 4]), None, 20))
[1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]

#itertools #std
Коробка с питоном
Я заметил, что редко использую функции из itertools, в основном когда нужно произвести какие-то "красивые" шаманства. Теперь будем вспоминать саму библиотеку вместе, естественно с примерами :) Сам модуль itertools это набор из эффективных и быстрых по памяти…
repeat(object: T, times: Optional[int] = None) -> Iterable[T]

Создает итератор который возвращает объект снова и снова. Можно указать параметр times, который вернет объект заданное количество раз и завершит работу итератора:

>>> first = repeat("hehe", 3)
>>> next(first)
'hehe'
>>> next(first)
'hehe'
>>> next(first)
'hehe'
>>> next(first)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

Очень часто repeat используют чтобы по быстрому сгенерировать коллекцию из элементов:

>>> list(repeat(10, 10))
[10, 10, 10, 10, 10, 10, 10, 10, 10, 10]

#itertools #std
Создание временных файлов

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

Для решения этих проблем в Python есть модуль tempfile. Нас интересует 2 функции - это TemporaryFile и NamedTemporaryFile.

TemporaryFile позволяет создать безымянный временный файл. Вот так можно создать временный текстовой файл, открыть его на запись и чтение (за это отвечает первый аргумент "w+t", подробнее можно прочитать здесь):

from tempfile import TemporaryFile
with TemporaryFile("w+t") as t:
t.write("Hello, boxwithpython!")
t.seek(0)
data = t.read()

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

from tempfile import 
NamedTemporaryFile
with NamedTemporaryFile("w+t") as t:
t.write("Hello, boxwithpython!")
print(t.name) # /tmp/tmpljhsktjt

#std
Shielded execution в asyncio

Допустим, есть следующий обработчик, который производит оплату:

async def handler(request):
await service.pay(request.user)
return web.Response(text="payed")

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

Поможет нам в этом asycio.shield(). Он защищает задачу от отмены, даже в случае возникновения ошибки. Выглядит это следующим образом:

async def handler(request):
await asyncio.shield(service.pay(request.user))
return web.Response(text="payed")

#asyncio #std
Про __slots__

Python, аналогично другим динамическим языкам, таким как JavaScript, предоставляет возможность манипулирования объектами в рантайме, в том числе позволяет добавлять, изменять и удалять атрибуты. Цена этого – понижение скорости доступа к атрибутам и дополнительные расходы памяти.

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

Слоты задаются через атрибут __slots__ в классе:

class SlotsClass:
slots = ('foo', 'bar')

>>> obj = SlotsClass()
>>> obj.foo = 5
>>> obj.foo
# 5
>>> obj.another_attribute = 'test'
Traceback (most recent call last):
File "python", line 5, in <module>
AttributeError: 'SlotsClass' object has no attribute 'another_attribute'

Теперь мы не можем добавлять новые атрибуты к нашим объектам. Скорость доступа к атрибутам повышается на 25-30%, потому что при доступе к ним их больше не надо вычислять.
В свою очередь, память экономится из-за того, что у класса не создается __dict__, который как раз хранил атрибуты.

#std #slots
Коробка с питоном
Про __slots__ Python, аналогично другим динамическим языкам, таким как JavaScript, предоставляет возможность манипулирования объектами в рантайме, в том числе позволяет добавлять, изменять и удалять атрибуты. Цена этого – понижение скорости доступа к атрибутам…
__slots__ и наследование

Важно помнить, что при попытке унаследовать класс с __slots__ подкласс их унаследует, но так же и создаст __dict__ для новых атрибутов:

class SlotsClass:
__slots__ = ('foo', 'bar')

class ChildSlotsClass(SlotsClass):
pass

>>> obj = ChildSlotsClass()
>>> obj.__slots__
# ('foo', 'bar')
>>> obj.foo = 5
>>> obj.test = 3
>>> obj.__dict__
# {'test': 3}

Это стандартное и понятное поведение. Чтобы избежать создания __dict__, можно снова переопределить __slots__ в подклассе:

class SlotsClass:
__slots__ = ('foo', 'bar')

class ChildSlotsClass(SlotsClass):
__slots__ = ('baz',)

>>> obj = ChildSlotsClass()
>>> obj.foo = 5
>>> obj.baz = 6
>>> obj.something_new = 3

AttributeError: 'ChildSlotsClass' object has no attribute 'something_new'

А что с множественным наследованием?

class ClassA:
__slots__ = ('foo', 'bar',)

class ClassB:
__slots__ = ('baz',)

class C(ClassA, ClassB):
pass

TypeError: multiple bases have instance lay-out conflict

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

#std #slots
Решил расширить канал ещё одной тематикой - занимательными задачками.
Пока что буду писать про те, которые встречались на тех. собеседованиях. Они не всегда будут адекватные, но что уж есть :)

А начнём, как полагается с классики. Надо объяснить следующее поведение:

>>> a = 256
>>> b = 256
>>> a is b
True # ???

>>> a = 257
>>> b = 257
>>> a is b
False # ???


Вопрос в том, что здесь творится с ссылками. Разберём самую первую часть. Пробуем получить id объектов:

>>> a = 256
>>> b = 256
>>> id(a), id(b)
(2214170730704, 2214170730704)


На вопрос, почему у них одинаковые идентификаторы ответит деталь реализации PyLong_FromLong (для искушенных читать можно читать отсюда), которая указывает, что интерпретатор хранит массив целочисленных объектов для всех чисел в диапазоне от -5 до 256. Поэтому, когда мы создаем переменную с числом в этом диапазоне он просто отдает ссылку на уже существующий объект.
Микрооптимизация, при чём очень важная - так уж получилось что числа из этого диапазона используются чаще всего.
В Java есть похожая оптимизация, там такой диапазон составляет от -128 до 127, но есть нюансы.

Второй вопрос отпадает сам собой (будут разные ссылки), но что будет если мы создадим файл с следующим содержимым и запустим его:

a = 257
b = 257
print(a is b) # True


А вот это уже нюанс работы нашего REPL.
Каждая написанная нами строка в нём разбирается отдельно. Но при запуске через файл Python имеет возможность применить дополнительные оптимизации для констант, так как он видит сразу весь код - в этом и различие.

А какие ещё неочевидные моменты вы знаете с REPL или int'ами? Пишите в комменты, обсудим :)

#std #задачки
Сегодня у нас простенькая задачка, а то пятница, все отдыхать хотят, я понимаю.

Есть следующий код:

def test():
try:
return 1
finally:
return 2


Вопрос - что вернется при вызове test()? Все и так на этом моменте понимают, что вернётся 2 (ну не просто так же мы собрались, верно?), но почему?

Ответ, как обычно, есть в документации. Возвращаемое функцией значение определяется последним выполненным return.
Вторым важным аспектом является то, что finally исполняется всегда, поэтому мы и получаем его return.

raise, кстати, тоже работать не будет:

def test():
try:
raise ValueError()
finally:
return 3

test() # 3


#std #задачки
Там в Python 3.12 добавили нашумевший PEP 659, а у меня пет-проект один давно не обновлялся, и так уж звёзды сошлись, что я сижу второй день обновляю его на 3.12

Задача - есть функционал, который под капотом имеет некоторый класс следующего вида:

class BaseFunction:
serialize_to: None

def serialize(self, data: dict) -> serialize_to:
pass # тут мы используем наш serialize_to

@dataclass
class ModelA:
x: str

class FunctionA(BaseFunction):
serialize_to: ModelA


Мы определяем новые классы наследуясь от BaseFunction, переопределяем в них serialize_to и вызываем serialize который делает нам инстанс serialize_to.

Ну прямо дженерик напрашивается! Тем более в 3.12 их завезли, красивые:

class BaseFunction[T]:
def serialize(self, data: dict) -> T:
pass # тут мы используем наш serialize_to

class FunctionA(BaseFunction[ModelA]):
pass


Встаёт вопрос, а как нам получить наш тип из дженерика?

Для начала, получим __orig_bases__[0] - он вернёт нам классы, от которых мы наследовались. Так как нам нужен только наш первый класс, мы указываем [0]:

>>> FunctionA.__orig_bases__
__main__.BaseFunction[__main__.ModelA]


Ещё можно это сделать с помощью get_original_bases из types, но его добавили только в 3.12 (почему я об этом сказал - узнаете ниже).

Теперь надо получить получить сам тип в дженерике. В этом нам поможет typing.get_args, который получает все аргументы типа. Дополнительно укажем, что нам нужен первый тип:

>>> get_args(FunctionA.__orig_bases__[0])[0]
<class '__main__.ModelA'>


Теперь в методе serialize класса BaseFunction[T] можно написать штуку, которая автоматически сериализует наши данные:

def serialize(self, data: dict) -> T:
type_from_generic = get_args(self.__class__.__orig_bases__[0])[0]
return type_from_generic(**data)


Проверяем:

>>> f = FunctionA()
>>> f.serialize(data={"x": 1})
ModelA(x=1)

Вы восхитительны!

Кстати, эта же штука должна работать ещё вроде как аж с 3.8, так как в нём именно был добавлен __orig_bases__ (PEP 560), ну и под капотом у новых дженериков используется...

>>> FunctionA.__mro__
(<class '__main__.FunctionA'>, <class '__main__.BaseFunction'>, <class 'typing.Generic'>, <class 'object'>)


Ага, typing.Generic :)

#рецепт #std