Подборка @pythonetc, август 2019

от автора

Новая подборка советов про Python и программирование из моего авторского канала @pythonetc.

Предыдущие подборки

Если у экземпляра класса нет атрибута с заданным именем, то он пытается обратиться к атрибуту класса с тем же именем.

>>> class A: ...     x = 2 ... >>> A.x 2 >>> A().x 2


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

>>> class A: ...     x = 2 ...     def __init__(self): ...         self.x = 3 ...         self.y = 4 ... >>> A().x 3 >>> A.x 2 >>> A().y 4 >>> A.y AttributeError: type object 'A' has no attribute 'y'

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

class ClassOnlyDescriptor:     def __init__(self, value):         self._value = value         self._name = None  # see __set_name__      def __get__(self, instance, owner):         if instance is not None:             raise AttributeError(                 f'{instance} has no attribute {self._name}'             )          return self._value      def __set_name__(self, owner, name):         self._name = name   class_only = ClassOnlyDescriptor   class A:     x = class_only(2)   print(A.x)  # 2 A().x       # raises AttributeError 

См. также, как работает Django-декоратор classonlymethod: https://github.com/django/django/blob/b709d701303b3877387020c1558a590713b09853/django/utils/decorators.py#L6

Функциям, объявленным в теле класса, область видимости этого класса недоступна. Это сделано потому, что эта область видимости существует только в ходе создания класса.

>>> class A: ...     x = 2 ...     def f(): ...         print(x) ...     f() ... [...] NameError: name 'x' is not defined

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

>>> class A: ...     x = 2 ...     def f(self): ...         print(self.x) ... >>> >>> >>> A().f() 2

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

>>> class A: ...     x = 2 ...     y = [x for _ in range(5)] ... [...] NameError: name 'x' is not defined

Однако у comprehensions нет доступа к self. Единственный способ обеспечить доступ к

x

заключается в добавлении ещё одной области видимости (ага, дурацкое решение):

>>> class A: ...     x = 2 ...     y = (lambda x=x: [x for _ in range(5)])() ... >>> A.y [2, 2, 2, 2, 2] 

В Python None эквивалентно None, так что может показаться, что проверять на None можно с помощью ==:

ES_TAILS = ('s', 'x', 'z', 'ch', 'sh')   def make_plural(word, exceptions=None):     if exceptions == None:  # ← ← ←         exceptions = {}      if word in exceptions:         return exceptions[word]     elif any(word.endswith(t) for t in ES_TAILS):         return word + 'es'     elif word.endswith('y'):         return word[0:-1] + 'ies'     else:         return word + 's'  exceptions = dict(     mouse='mice', )  print(make_plural('python')) print(make_plural('bash')) print(make_plural('ruby')) print(make_plural('mouse', exceptions=exceptions)) 

Но это будет ошибкой. Да, None равно None, но не только оно. Пользовательские объекты тоже могут быть равны None:

>>> class A: ...     def __eq__(self, other): ...             return True ... >>> A() == None True >>> A() is None False

Единственный правильный способ сравнения с None заключается в использовании is None.

Числа с плавающей запятой в Python могут иметь значения NaN. Например, такое число можно получить с помощью math.nan. nan не равно ничему, включая себя:

>>> math.nan == math.nan False

Кроме того, NaN-объект не уникален, у вас может быть несколько разных NaN-объектов из разных источников:

>>> float('nan') nan >>> float('nan') is float('nan') False

Это означает, что, в целом, вы не можете использовать NaN в качестве ключа словаря:

>>> d = {} >>> d[float('nan')] = 1 >>> d[float('nan')] = 2 >>> d {nan: 1, nan: 2} 

typing позволяет определять типы для генераторов. Дополнительно можно указать, какой тип генерируется, какой передаётся генератору и какой возвращается с помощью

return

. Например, Generator[int, None, bool] генерирует целые числа, возвращает булевы и не поддерживает g.send().

А вот пример посложнее. chain_while проксирует данные от других генераторов до тех пор, пока один из них не вернёт значение, которое является сигналом остановки в соответствии с функцией condition:

from typing import Generator, Callable, Iterable, TypeVar  Y = TypeVar('Y') S = TypeVar('S') R = TypeVar('R')   def chain_while(     iterables: Iterable[Generator[Y, S, R]],     condition: Callable[[R], bool], ) -> Generator[Y, S, None]:     for it in iterables:         result = yield from it         if not condition(result):             break   def r(x: int) -> Generator[int, None, bool]:     yield from range(x)     return x % 2 == 1   print(list(chain_while(     [         r(5),         r(4),         r(3),     ],     lambda x: x is True, ))) 

Задать аннотации для фабричного метода не так просто, как может показаться. Сразу хочется использовать нечто подобное:

class A:     @classmethod     def create(cls) -> 'A':         return cls()

Но это будет неправильно. Хитрость в том, что create возвращает не A, он возвращает cls, который является A или одним из его потомков. Взгляните на код:

class A:     @classmethod     def create(cls) -> 'A':         return cls()   class B(A):     @classmethod     def create(cls) -> 'B':         return super().create()

Результатом проверки mypy является ошибка error: Incompatible return value type (got "A", expected "B"). Повторюсь, проблема в том, что super().create() аннотирован как возвращающий A, хотя в этом случае он возвращает B.

Это можно исправить, если аннотировать cls с помощью TypeVar:

AType = TypeVar('AType') BType = TypeVar('BType')   class A:     @classmethod     def create(cls: Type[AType]) -> AType:         return cls()   class B(A):     @classmethod     def create(cls: Type[BType]) -> BType:         return super().create()

Теперь create возвращает экземпляр класса cls. Однако эти аннотации слишком расплывчаты, мы потеряли информацию о том, что cls является подтипом A:

AType = TypeVar('AType')   class A:     DATA = 42     @classmethod     def create(cls: Type[AType]) -> AType:         print(cls.DATA)         return cls()

Получаем ошибку "Type[AType]" has no attribute "DATA".

Чтобы её исправить, нужно явно определить AType как подтип A. Для этого используется TypeVar с аргументом bound.

AType = TypeVar('AType', bound='A') BType = TypeVar('BType', bound='B')   class A:     DATA = 42     @classmethod     def create(cls: Type[AType]) -> AType:         print(cls.DATA)         return cls()   class B(A):     @classmethod     def create(cls: Type[BType]) -> BType:         return super().create() 


ссылка на оригинал статьи https://habr.com/ru/company/mailru/blog/466315/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *