
Ceci est la quatrième collection de conseils et de programmation Python de mon flux @pythonetc .
Sélections précédentes:
Remplacement et surcharge
Il y a deux concepts faciles à confondre: le remplacement et la surcharge.
La substitution se produit lorsqu'une classe enfant définit une méthode déjà fournie par les classes parentes et la remplace ainsi. Dans certains langages, il est nécessaire de marquer explicitement la méthode de override
(en C #, le modificateur de override
est utilisé), et dans certains langages, cela se fait comme souhaité (annotation @Override
en Java). Python ne nécessite pas l'utilisation d'un modificateur spécial et ne fournit pas de marquage standard de ces méthodes (pour des raisons de lisibilité, quelqu'un utilise le décorateur @override
personnalisé, qui ne fait rien).
La surcharge est une autre histoire. Ce terme fait référence à une situation où plusieurs fonctions portent le même nom, mais avec des signatures différentes. La surcharge est possible en Java et C ++; elle est souvent utilisée pour fournir des arguments par défaut:
class Foo { public static void main(String[] args) { System.out.println(Hello()); } public static String Hello() { return Hello("world"); } public static String Hello(String name) { return "Hello, " + name; } }
Python ne prend pas en charge la recherche de fonctions par signature, uniquement par nom. Bien sûr, vous pouvez écrire du code qui analyse explicitement les types et le nombre d'arguments, mais cela semblera gênant, et cette pratique est préférable d'éviter:
def quadrilateral_area(*args): if len(args) == 4: quadrilateral = Quadrilateral(*args) elif len(args) == 1: quadrilateral = args[0] else: raise TypeError() return quadrilateral.area()
Si vous avez besoin d'indices de typing
, utilisez le module de typing
avec le décorateur @overload
:
from typing import overload @overload def quadrilateral_area( q: Quadrilateral ) -> float: ... @overload def quadrilateral_area( p1: Point, p2: Point, p3: Point, p4: Point ) -> float: ...
Vivification automatique
collections.defaultdict
vous permet de créer un dictionnaire qui renvoie une valeur par défaut si la clé demandée est manquante (au lieu de lancer une KeyError
). Pour créer un defaultdict
vous devez fournir non seulement une valeur par défaut, mais une fabrique de ces valeurs.
Vous pouvez donc créer un dictionnaire avec un nombre pratiquement infini de dictionnaires imbriqués, ce qui vous permet d'utiliser des constructions comme d[a][b][c]...[z]
.
>>> def infinite_dict(): ... return defaultdict(infinite_dict) ... >>> d = infinite_dict() >>> d[1][2][3][4] = 10 >>> dict(d[1][2][3][5]) {}
Ce comportement est appelé «auto-vivification», un terme qui vient de Perl.
Instanciation
L'instanciation d'objets implique deux étapes importantes. Tout d'abord, la méthode __new__
est appelée à partir de la classe, qui crée et renvoie un nouvel objet. Python appelle ensuite la méthode __init__
, qui définit l'état initial de cet objet.
Cependant, __init__
ne sera pas appelé si __new__
renvoie un objet qui n'est pas une instance de la classe d'origine. Dans ce cas, l'objet pourrait être créé par une autre classe, ce qui signifie que __init__
a déjà été appelé sur l'objet:
class Foo: def __new__(cls, x): return dict(x=x) def __init__(self, x): print(x)
Cela signifie également que vous ne devez pas instancier la même classe dans __new__
utilisant le constructeur normal ( Foo(...)
). Cela peut conduire à une exécution répétée de __init__
, voire à une récursion infinie.
Récursion infinie:
class Foo: def __new__(cls, x): return Foo(-x)
Double exécution __init__
:
class Foo: def __new__(cls, x): if x < 0: return Foo(-x) return super().__new__(cls) def __init__(self, x): print(x) self._x = x
La bonne façon:
class Foo: def __new__(cls, x): if x < 0: return cls.__new__(cls, -x) return super().__new__(cls) def __init__(self, x): print(x) self._x = x
L'opérateur [] et les tranches
En Python, vous pouvez remplacer l'opérateur []
en définissant la méthode magique __getitem__
. Ainsi, par exemple, vous pouvez créer un objet qui contient virtuellement un nombre infini d'éléments répétitifs:
class Cycle: def __init__(self, lst): self._lst = lst def __getitem__(self, index): return self._lst[ index % len(self._lst) ] print(Cycle(['a', 'b', 'c'])[100])
La chose inhabituelle ici est que l'opérateur []
prend en charge une syntaxe unique. En l'utilisant, vous pouvez obtenir non seulement [2]
, mais aussi [2:10]
, [2:10:2]
, [2::2]
et même [:]
. La sémantique de l'opérateur est: [start: stop: step], mais vous pouvez l'utiliser de toute autre manière pour créer des objets personnalisés.
Mais si vous appelez __getitem__
avec cette syntaxe, __getitem__
t-il comme paramètre d'index? C'est exactement pourquoi les objets slice existent.
In : class Inspector: ...: def __getitem__(self, index): ...: print(index) ...: In : Inspector()[1] 1 In : Inspector()[1:2] slice(1, 2, None) In : Inspector()[1:2:3] slice(1, 2, 3) In : Inspector()[:] slice(None, None, None)
Vous pouvez même combiner la syntaxe des tuples et des tranches:
In : Inspector()[:, 0, :] (slice(None, None, None), 0, slice(None, None, None))
slice
ne fait rien, ne stocke que les attributs start
, stop
et step
.
In : s = slice(1, 2, 3) In : s.start Out: 1 In : s.stop Out: 2 In : s.step Out: 3
Interrompre Asyncio Coroutine
Tout asyncio
coroutine exécutable peut être abandonné à l'aide de la méthode cancel()
. Dans ce cas, une CanceledError sera envoyée à la coroutine, en conséquence, celle-ci et toutes les coroutines qui lui sont associées seront interrompues jusqu'à ce que l'erreur soit détectée et supprimée.
CancelledError
est une sous-classe d' Exception
, ce qui signifie qu'elle peut être accidentellement interceptée en utilisant une combinaison de try ... except Exception
, conçue pour intercepter "toutes les erreurs". Pour attraper en toute sécurité un bug pour une coroutine, vous devez faire ceci:
try: await action() except asyncio.CancelledError: raise except Exception: logging.exception('action failed')
Planification de l'exécution
Pour planifier l'exécution de certains codes à un certain moment, asyncio
crée généralement une tâche qui s'exécute await asyncio.sleep(x)
:
import asyncio async def do(n=0): print(n) await asyncio.sleep(1) loop.create_task(do(n + 1)) loop.create_task(do(n + 1)) loop = asyncio.get_event_loop() loop.create_task(do()) loop.run_forever()
Mais la création d'une nouvelle tâche peut coûter cher, et il n'est pas nécessaire de le faire si vous ne prévoyez pas d'effectuer des opérations asynchrones (comme la fonction do
dans mon exemple). Au lieu de cela, vous pouvez utiliser les fonctions loop.call_later
et loop.call_at
, qui vous permettent de planifier un appel de rappel asynchrone:
import asyncio def do(n=0): print(n) loop = asyncio.get_event_loop() loop.call_later(1, do, n+1) loop.call_later(1, do, n+1) loop = asyncio.get_event_loop() do() loop.run_forever()