Подборка @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. У них собственные области видимости и они тоже не имеют доступа к областям видимости классов. Это очень логично с точки зрения конструкторов генераторов: они оценивают выражения после завершения создания класса.
>>> 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()