Trucs et astuces de ma chaîne Telegram @pythonetc, août 2019



C'est une nouvelle sélection de trucs et astuces sur Python et la programmation de mon canal Telegram @pythonetc.

Publications précédentes


Si 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 # 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(Ax) # 2 A().x # raises AttributeError 

Voir également comment fonctionne le décorateur de classonlymethod Django classonlymethod : https://github.com/django/django/blob/b709d701303b3877387020c1558a590713b09853/django/utils/decorators.py#L6


Les 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: # ← ← ← 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)) 

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() 

Source: https://habr.com/ru/post/fr466317/


All Articles