Ceci est la troisième collection de conseils et de programmation Python de mon
flux @pythonetc .
Sélections précédentes:
Méthode d'usine
Si vous créez de nouveaux objets dans
__init__
, il est plus judicieux de les passer prêts en tant qu'arguments et d'utiliser la méthode d'usine pour créer l'objet. Cela séparera la logique métier de la mise en œuvre technique de la création d'objets.
Dans cet exemple,
__init__
accepte l'
host
et le
port
comme arguments pour créer une connexion à la base de données:
class Query: def __init__(self, host, port): self._connection = Connection(host, port)
Option de refactoring:
class Query: def __init__(self, connection): self._connection = connection @classmethod def create(cls, host, port): return cls(Connection(host, port))
Cette approche présente au moins les avantages suivants:
- Le déploiement est simple. Dans les tests, vous pouvez faire une
Query(FakeConnection())
. - Une classe peut avoir autant de méthodes d'usine que vous le souhaitez. Vous pouvez créer une connexion non seulement en utilisant l'
host
et le port
, mais également en clonant une autre connexion, en lisant le fichier de configuration, en utilisant la connexion par défaut, etc. - Des méthodes d'usine similaires peuvent être transformées en fonctions asynchrones, ce qui est absolument impossible à faire avec
__init__
.
super ou suivant
La fonction
super()
vous permet de référencer la classe de base. Cela peut être très utile dans les cas où la classe dérivée souhaite ajouter quelque chose à l'implémentation de la méthode, plutôt que de la remplacer complètement.
class BaseTestCase(TestCase): def setUp(self): self._db = create_db() class UserTestCase(BaseTestCase): def setUp(self): super().setUp() self._user = create_user()
Le nom super ne signifie rien de «super». Dans ce contexte, cela signifie «plus haut dans la hiérarchie» (par exemple, comme dans le mot «surintendant»). En même temps,
super()
ne fait pas toujours référence à la classe de base; il peut facilement renvoyer une classe enfant. Il serait donc plus correct d'utiliser le nom
next()
, car la classe suivante est renvoyée selon le MRO.
class Top: def foo(self): return 'top' class Left(Top): def foo(self): return super().foo() class Right(Top): def foo(self): return 'right' class Bottom(Left, Right): pass
N'oubliez pas que
super()
peut produire des résultats différents en fonction de l'origine de la méthode.
>>> Bottom().foo() 'right' >>> Left().foo() 'top'
Espace de noms personnalisé pour créer une classe
Une classe est créée en deux grandes étapes. Tout d'abord, le corps de la classe est exécuté, comme le corps d'une fonction. Dans la deuxième étape, l'espace de noms résultant (qui est renvoyé par
locals()
) est utilisé par la métaclasse (la valeur par défaut est
type
) pour créer un objet de la classe.
class Meta(type): def __new__(meta, name, bases, ns): print(ns) return super().__new__( meta, name, bases, ns ) class Foo(metaclass=Meta): B = 2
Ce code affiche
{'__module__': '__main__', '__qualname__':'Foo', 'B': 3}
.
Évidemment, si vous introduisez quelque chose comme
B = 2; B = 3
B = 2; B = 3
, alors la métaclasse ne verra que
B = 3
, car seule cette valeur est en
ns
. Cette limitation provient du fait que la métaclasse ne commence à fonctionner qu'après l'exécution du corps.
Cependant, vous pouvez intervenir dans la procédure d'exécution en glissant votre propre espace de noms. Un dictionnaire simple est utilisé par défaut, mais vous pouvez fournir votre propre objet, semblable à un dictionnaire, si vous utilisez la méthode
__prepare__
partir de la métaclasse.
class CustomNamespace(dict): def __setitem__(self, key, value): print(f'{key} -> {value}') return super().__setitem__(key, value) class Meta(type): def __new__(meta, name, bases, ns): return super().__new__( meta, name, bases, ns ) @classmethod def __prepare__(metacls, cls, bases): return CustomNamespace() class Foo(metaclass=Meta): B = 2 B = 3
Résultat d'exécution du code:
__module__ -> __main__ __qualname__ -> Foo B -> 2 B -> 3
Ainsi,
enum.Enum
protégé contre la
duplication .
matplotlib
matplotlib
est une bibliothèque Python complexe et flexible pour la cartographie. De nombreux produits le prennent en charge, notamment Jupyter et Pycharm. Voici un exemple de rendu d'une fractale simple à l'aide de
matplotlib
:
https://repl.it/@VadimPushtaev/myplotlib (voir l'image titre de cette publication).
Prise en charge des fuseaux horaires
Python fournit une bibliothèque
datetime
puissante pour travailler avec les dates et les heures. Il est curieux que les objets
datetime
aient une interface spéciale pour supporter les fuseaux horaires (à savoir, l'attribut
tzinfo
), mais ce module a un support limité pour l'interface mentionnée, donc une partie du travail est assignée à d'autres modules.
Le plus populaire d'entre eux est le
pytz
. Mais le fait est que
pytz
n'est pas entièrement conforme à l'interface
tzinfo
. Ceci est indiqué au tout début de la documentation de
pytz
: "Cette bibliothèque diffère de l'API Python documentée pour les implémentations de tzinfo."
Vous ne pouvez pas utiliser les objets de
pytz
tzinfo
comme
tzinfo
. Si vous essayez de le faire, vous risquez d'obtenir un résultat complètement fou:
In : paris = pytz.timezone('Europe/Paris') In : str(datetime(2017, 1, 1, tzinfo=paris)) Out: '2017-01-01 00:00:00+00:09'
Notez le décalage +00: 09. Pytz doit être utilisé comme ceci:
In : str(paris.localize(datetime(2017, 1, 1))) Out: '2017-01-01 00:00:00+01:00'
De plus, après toute opération arithmétique, vous devez appliquer la
normalize
à vos objets
datetime
pour éviter de modifier les décalages (par exemple, à la frontière de la période d'heure d'été).
In : new_time = time + timedelta(days=2) In : str(new_time) Out: '2018-03-27 00:00:00+01:00' In : str(paris.normalize(new_time)) Out: '2018-03-27 01:00:00+02:00'
Si vous avez Python 3.6, la documentation recommande d'utiliser
dateutil.tz
au lieu de
pytz
. Cette bibliothèque est entièrement compatible avec
tzinfo
, elle peut être passée en tant qu'attribut et vous n'avez pas besoin d'utiliser la
normalize
. Certes, cela fonctionne plus lentement.
Si vous voulez savoir pourquoi
pytz
ne prend pas en charge l'API
datetime
ou si vous souhaitez voir plus d'exemples, lisez
cet article.
Magic StopIteration
Chaque fois que
next(x)
appelé, il retourne une nouvelle valeur de l'itérateur
x
jusqu'à ce qu'une exception soit levée. S'il s'avère que c'est
StopIteration
, alors l'itérateur est épuisé et ne peut plus fournir de valeurs. Si le générateur est itéré, à la fin du corps, il lancera automatiquement
StopIteration
:
>>> def one_two(): ... yield 1 ... yield 2 ... >>> i = one_two() >>> next(i) 1 >>> next(i) 2 >>> next(i) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
StopIteration
peut être géré automatiquement par les outils qui appellent
next
:
>>> list(one_two()) [1, 2]
Mais le problème est que toute StopIteration manifestement non attendue qui a surgi dans le corps du générateur sera silencieusement prise comme un signe de la fin du générateur, et non comme une erreur, comme toute autre exception:
def one_two(): yield 1 yield 2 def one_two_repeat(n): for _ in range(n): i = one_two() yield next(i) yield next(i) yield next(i) print(list(one_two_repeat(3)))
Ici, le dernier
yield
est une erreur: une exception
StopIteration
levée arrête l'itération de la
list(...)
. On obtient le résultat
[1, 2]
. Cependant, dans Python 3.7, ce comportement a changé. Alien
StopIteration
remplacé par
RuntimeError
:
Traceback (most recent call last): File "test.py", line 10, in one_two_repeat yield next(i) StopIteration The above exception was the direct cause of the following exception: Traceback (most recent call last): File "test.py", line 12, in <module> print(list(one_two_repeat(3))) RuntimeError: generator raised StopIteration
Vous pouvez utiliser
__future__ import generator_stop
pour activer le même comportement depuis Python 3.5.