A new selection of Python tips and programming from my @pythonetc feed.
←
Previous collections
If the class instance does not have an attribute with the given name, then it tries to access the class attribute with the same name.
>>> class A: ... x = 2 ... >>> Ax 2 >>> A().x 2
An instance can easily have an attribute that the class does not have, or have an attribute with a different value:
>>> class A: ... x = 2 ... def __init__(self): ... self.x = 3 ... self.y = 4 ... >>> A().x 3 >>> Ax 2 >>> A().y 4 >>> Ay AttributeError: type object 'A' has no attribute 'y'
If you want the instance to behave as if it does not have an attribute, although the class has it, then you will have to create a custom handle that prohibits access from this instance:
class ClassOnlyDescriptor: def __init__(self, value): self._value = value self._name = None
See also how the
classonlymethod
Django decorator
classonlymethod
:
https://github.com/django/django/blob/b709d701303b3877387020c1558a590713b09853/django/utils/decorators.py#L6
Functions declared in the class body are not accessible to the scope of this class. This is because this scope exists only during the creation of the class.
>>> class A: ... x = 2 ... def f(): ... print(x) ... f() ... [...] NameError: name 'x' is not defined
This is usually not a problem: methods are declared inside the class just to become methods and be called later:
>>> class A: ... x = 2 ... def f(self): ... print(self.x) ... >>> >>> >>> A().f() 2
Surprisingly, the same is true for comprehensions. They have their own scope and they also do not have access to the scope of the classes. This is very logical from the point of view of generator comprehensions: the code in them is executed when the class is already created.
>>> class A: ... x = 2 ... y = [x for _ in range(5)] ... [...] NameError: name 'x' is not defined
However, comprehensions do not have access to
self
. The only way to provide access to
x
is to add another scope (yeah, stupid solution):
>>> class A: ... x = 2 ... y = (lambda x=x: [x for _ in range(5)])() ... >>> Ay [2, 2, 2, 2, 2]
In Python,
None
equivalent to
None
, so it might seem like you can check for
None
with
==
:
ES_TAILS = ('s', 'x', 'z', 'ch', 'sh') def make_plural(word, exceptions=None): if exceptions == None:
But that will be a mistake. Yes,
None
is equal to
None
, but not only that. Custom objects can also be
None
:
>>> class A: ... def __eq__(self, other): ... return True ... >>> A() == None True >>> A() is None False
The only correct way to compare with
None
is to use
is None
.
Floating point numbers in Python can have NaN values. For example, such a number can be obtained using
math.nan
.
nan
not equal to anything, including itself:
>>> math.nan == math.nan False
In addition, a NaN object is not unique; you can have several different NaN objects from different sources:
>>> float('nan') nan >>> float('nan') is float('nan') False
This means that, in general, you cannot 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 types for generators. Additionally, you can specify which type is generated, which is passed to the generator, and which is returned using
return
. For example,
Generator[int, None, bool]
generates integers, returns Booleans, and does not support
g.send()
.
But the example is more complicated.
chain_while
data from other generators until one of them returns a value that is a stop signal in accordance with 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, )))
Setting annotations for a factory method is not as easy as it might seem. Just want to use something like this:
class A: @classmethod def create(cls) -> 'A': return cls()
But it will be wrong. The trick is that
create
returns not
A
, it returns
cls
, which is
A
or one of its descendants. Take a look at the code:
class A: @classmethod def create(cls) -> 'A': return cls() class B(A): @classmethod def create(cls) -> 'B': return super().create()
The result of the mypy check is the error
error: Incompatible return value type (got "A", expected "B")
. Once again, the problem is that
super().create()
annotated as returning
A
, although in this case it returns
B
This can be fixed if annotating
cls
using
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
now returns an instance of the
cls
class. However, these annotations are too vague, we have lost 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()
We
"Type[AType]" has no attribute "DATA"
error
"Type[AType]" has no attribute "DATA"
.
To fix it, you must explicitly define
AType
as a subtype of
A
For this,
TypeVar
with the
bound
argument is used.
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()