
C'est une nouvelle sélection de trucs et astuces sur Python et la programmation de mon canal Telegram @pythonetc.
←
Publications précédentesSi une instance d'une classe n'a pas d'attribut avec le nom donné, elle essaie d'accéder à l'attribut de classe avec le même nom.
>>> class A: ... x = 2 ... >>> Ax 2 >>> A().x 2
Il est assez simple pour une instance d'avoir un attribut qu'une classe n'a pas ou d'avoir l'attribut avec une valeur différente:
>>> 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'
Si ce n'est pas aussi simple, cependant, si vous voulez qu'une instance se comporte comme si elle n'avait pas d'attribut malgré que la classe l'ait. Pour y arriver, vous devez créer un descripteur personnalisé qui n'autorise pas l'accès à partir de l'instance:
class ClassOnlyDescriptor: def __init__(self, value): self._value = value self._name = None
Voir également comment fonctionne le décorateur de
classonlymethod
Django
classonlymethod
:
https://github.com/django/django/blob/b709d701303b3877387020c1558a590713b09853/django/utils/decorators.py#L6Les fonctions déclarées dans un corps de classe ne peuvent pas voir la portée de la classe. Cela a du sens puisque la portée de classe n'existe que lors de la création de classe.
>>> class A: ... x = 2 ... def f(): ... print(x) ... f() ... [...] NameError: name 'x' is not defined
Ce n'est généralement pas un problème: les méthodes sont déclarées dans une classe uniquement pour devenir des méthodes et être appelées plus tard:
>>> class A: ... x = 2 ... def f(self): ... print(self.x) ... >>> >>> >>> A().f() 2
De façon assez surprenante, il en va de même pour les compréhensions. Ils ont leurs propres étendues et ne peuvent pas accéder à l'étendue de classe également. Cela a vraiment du sens pour les compréhensions de générateur: ils évaluent les expressions une fois la création de classe terminée.
>>> class A: ... x = 2 ... y = [x for _ in range(5)] ... [...] NameError: name 'x' is not defined
Les compréhensions, cependant, n'ont pas accès à
self
. La seule façon de le faire fonctionner est d'ajouter une portée supplémentaire (oui, c'est moche):
>>> class A: ... x = 2 ... y = (lambda x=x: [x for _ in range(5)])() ... >>> Ay [2, 2, 2, 2, 2]
En Python,
None
est égal à
None
, il semble donc que vous pouvez vérifier
None
avec
==
:
ES_TAILS = ('s', 'x', 'z', 'ch', 'sh') def make_plural(word, exceptions=None): if exceptions == None:
C'est une mauvaise chose à faire cependant.
None
n'est en effet égal à
None
, mais ce n'est pas la seule chose qui existe. Les objets personnalisés peuvent également être égaux à
None
:
>>> class A: ... def __eq__(self, other): ... return True ... >>> A() == None True >>> A() is None False
La seule façon appropriée de comparer avec
None
est d'utiliser
is None
.
Les flotteurs Python peuvent avoir des valeurs NaN. Vous pouvez en obtenir un avec
math.nan
.
nan
n'est égal à rien, y compris lui-même:
>>> math.nan == math.nan False
De plus, l'objet NaN n'est pas unique, vous pouvez avoir plusieurs objets NaN différents provenant de différentes sources:
>>> float('nan') nan >>> float('nan') is float('nan') False
Cela signifie que vous ne pouvez généralement pas utiliser NaN comme clé de dictionnaire:
>>> d = {} >>> d[float('nan')] = 1 >>> d[float('nan')] = 2 >>> d {nan: 1, nan: 2}
typing
vous permet de définir le type des générateurs. Vous pouvez également spécifier quel type est fourni, quel type peut être envoyé dans un générateur et ce qui est retourné.
Generator[int, None, bool]
est un générateur qui donne des entiers, retourne une valeur booléenne et ne prend pas en charge
g.send()
.
Voici un exemple un peu plus compliqué.
chain_while
cède à partir d'autres générateurs jusqu'à ce que l'un d'eux renvoie quelque chose qui est un signal d'arrêt selon la fonction de
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, )))
Annoter une méthode d'usine n'est pas aussi simple qu'il y paraît. L'envie immédiate est d'utiliser quelque chose comme ceci:
class A: @classmethod def create(cls) -> 'A': return cls()
Cependant, ce n'est pas une bonne chose à faire. Le hic, c'est que
create
ne renvoie pas
A
, il retourne une instance de cls qui est
A
ou l' un de ses descendants. Regardez ce code:
class A: @classmethod def create(cls) -> 'A': return cls() class B(A): @classmethod def create(cls) -> 'B': return super().create()
Le résultat de la vérification mypy est une
error: Incompatible return value type (got "A", expected "B")
. Encore une fois, le problème est
super().create()
est annoté pour renvoyer
A
alors qu'il renvoie clairement
B
dans ce cas.
Vous pouvez résoudre ce problème en annotant cls avec 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()
Maintenant,
create
retourne l'instance de la classe
cls
. Cependant, ces annotations sont trop lâches, nous avons perdu l'information que
cls
est un sous-type de
A
:
AType = TypeVar('AType') class A: DATA = 42 @classmethod def create(cls: Type[AType]) -> AType: print(cls.DATA) return cls()
L'erreur est
"Type[AType]" has no attribute "DATA"
.
Pour résoudre ce problème, vous devez définir explicitement
AType
comme un sous-type de
A
avec l'argument
bound
de
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()