Tips and tricks from my Telegram-channel @pythonetc, August 2019
It is a new selection of tips and tricks about Python and programming from my Telegram-channel @pythonetc.
← Previous publications
If an instance of a class doesn«t have an attribute with the given name, it tries to access the class attribute with the same name.
>>> class A:
... x = 2
...
>>> A.x
2
>>> A().x
2
It«s fairly simple for an instance to have attribute that a class doesn«t or have the attribute with the different value:
>>> 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'
If it«s not that simple, however, if you want an instance behave like it doesn«t have an attribute despite the class having it. To make it happen you have to create custom descriptor that doesn«t allow access from the instance:
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
See also how the Django classonlymethod
decorator works: https://github.com/django/django/blob/b709d701303b3877387020c1558a590713b09853/django/utils/decorators.py#L6
Functions declared in a class body can«t see the class scope. It makes sense since the class scope only exists during class creation.
>>> class A:
... x = 2
... def f():
... print(x)
... f()
...
[...]
NameError: name 'x' is not defined
That is usually not a problem: methods are declared inside a class only to become methods and be called later:
>>> class A:
... x = 2
... def f(self):
... print(self.x)
...
>>>
>>>
>>> A().f()
2
Somewhat surprisingly, the same is true for comprehensions. They have their own scopes and can«t access the class scope as well. That really make sense for generator comprehensions: they evaluate expressions after the class creation is already finished.
>>> class A:
... x = 2
... y = [x for _ in range(5)]
...
[...]
NameError: name 'x' is not defined
Comprehensions, however, have no access to self
. The only way to make it work is to add one more scope (yep, that«s ugly):
>>> class A:
... x = 2
... y = (lambda x=x: [x for _ in range(5)])()
...
>>> A.y
[2, 2, 2, 2, 2]
In Python, None
is equal to None
so it looks like you can check for None
with ==
:
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))
This is a wrong thing to do though. None
is indeed is equal to None
, but it«s not the only thing that is. Custom objects may be equal to None
too:
>>> class A:
... def __eq__(self, other):
... return True
...
>>> A() == None
True
>>> A() is None
False
The only proper way to compare with None
is to use is None
.
Python floats can have NaN values. You can get one with math.nan
. nan
is not equal to anything including itself:
>>> math.nan == math.nan
False
Also, NaN object is not unique, you can have several different NaN objects from different sources:
>>> float('nan')
nan
>>> float('nan') is float('nan')
False
That means that you generally can«t use NaN as a dictionary key:
>>> d = {}
>>> d[float('nan')] = 1
>>> d[float('nan')] = 2
>>> d
{nan: 1, nan: 2}
typing
allows you to define type for generators. You can additionally specify what type is yielded, what type can be sent into a generator and what is returned. Generator[int, None, bool]
is a generator that yields integers, returns boolean value and doesn«t support g.send()
.
Here is slightly more complicated example. chain_while
yields from other generators until one of them returns something that is a signal to stop according to the condition
function:
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,
)))
Annotating a factory method is not as simple as it may seem. The immediate urge is to use something like this:
class A:
@classmethod
def create(cls) -> 'A':
return cls()
However, that is not a right thing to do. The catch is, create
doesn«t return A
, it returns an instance of cls that is A
or any of its descendants. Look at this code:
class A:
@classmethod
def create(cls) -> 'A':
return cls()
class B(A):
@classmethod
def create(cls) -> 'B':
return super().create()
The mypy check result is error: Incompatible return value type (got "A", expected "B")
. Again, the problem is super().create()
is annotated to return A
while it clearly returns B
in this case.
You can fix that by annotating cls with 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()
Now create
returns the instance of the cls
class. However, this annotations are too loose, we lost the information that cls
is a subtype of A
:
AType = TypeVar('AType')
class A:
DATA = 42
@classmethod
def create(cls: Type[AType]) -> AType:
print(cls.DATA)
return cls()
The error is "Type[AType]" has no attribute "DATA"
.
To fix that you have to explicitly define AType
as a subtype of A
with the bound
argument of TypeVar
:
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()