Коробка с питоном
523 subscribers
49 photos
128 links
Заметки от Python-разработчика: сниппеты, обзоры пакетов, новости и другая полезная информация.
Download Telegram
Коробка с питоном
Про __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
🔥5
Решил расширить канал ещё одной тематикой - занимательными задачками.
Пока что буду писать про те, которые встречались на тех. собеседованиях. Они не всегда будут адекватные, но что уж есть :)

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

>>> 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 #задачки
🔥17👏4😢2
Сегодня у нас простенькая задачка, а то пятница, все отдыхать хотят, я понимаю.

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

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 #задачки
🔥9👏5🤔4🤯1
Там в 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
🔥151