C'est une nouvelle sélection de trucs et astuces sur Python et la programmation de mon canal Telegram @pythonetc.
←
Publications précédentesVous ne pouvez pas muter les variables de fermeture en les affectant simplement. Python traite l'affectation comme une définition à l'intérieur d'un corps de fonction et ne fait pas du tout de fermeture.
Fonctionne bien, imprime
2
:
def make_closure(x): def closure(): print(x) return closure make_closure(2)
UnboundLocalError: local variable 'x' referenced before assignment
:
def make_closure(x): def closure(): print(x) x *= 2 print(x) return closure make_closure(2)()
Pour le faire fonctionner, vous devez utiliser
nonlocal
. Il dit explicitement à l'interpréteur de ne pas traiter l'affectation comme une définition:
def make_closure(x): def closure(): nonlocal x print(x) x *= 2 print(x) return closure make_closure(2)()
Parfois, pendant l'itération, vous voudrez peut-être savoir s'il s'agit de la première ou de la dernière étape d'élément de l'itération. Un moyen simple de gérer cela est d'utiliser un indicateur explicite:
def sparse_list(iterable, num_of_zeros=1): result = [] zeros = [0 for _ in range(num_of_zeros)] first = True for x in iterable: if not first: result += zeros result.append(x) first = False return result assert sparse_list([1, 2, 3], 2) == [ 1, 0, 0, 2, 0, 0, 3, ]
Vous pouvez également traiter le premier élément en dehors de la boucle, ce qui peut sembler plus clair mais conduit à une duplication de code dans une certaine mesure. Ce n'est pas non plus une chose simple à faire en travaillant avec des itérables abstraits:
def sparse_list(iterable, num_of_zeros=1): result = [] zeros = [0 for _ in range(num_of_zeros)] iterator = iter(iterable) try: result.append(next(iterator)) except StopIteration: return [] for x in iterator: result += zeros result.append(x) return result
Vous pouvez également utiliser
enumerate
et vérifier le
i == 0
(ne fonctionne que pour la détection du premier élément, pas le dernier), mais la solution ultime pourrait être un générateur qui retourne les
first
et
last
drapeaux avec l'élément d'un itérable:
def first_last_iter(iterable): iterator = iter(iterable) first = True last = False while not last: if first: try: current = next(iterator) except StopIteration: return else: current = next_one try: next_one = next(iterator) except StopIteration: last = True yield (first, last, current) first = False
La fonction initiale peut maintenant ressembler à ceci:
def sparse_list(iterable, num_of_zeros=1): result = [] zeros = [0 for _ in range(num_of_zeros)] for first, last, x in first_last_iter(iterable): if not first: result += zeros result.append(x) return result
Si vous souhaitez mesurer le temps entre deux événements, vous devez utiliser
time.monotonic()
au lieu de
time.time()
.
time.monotonic()
ne revient jamais en arrière même si l'horloge système est mise à jour:
from contextlib import contextmanager import time @contextmanager def timeit(): start = time.monotonic() yield print(time.monotonic() - start) def main(): with timeit(): time.sleep(2) main()
Les gestionnaires de contexte imbriqués ne savent généralement pas qu'ils sont imbriqués. Vous pouvez les faire connaître en créant des gestionnaires de contexte interne par l'extérieur:
from contextlib import AbstractContextManager import time class TimeItContextManager(AbstractContextManager): def __init__(self, name, parent=None): super().__init__() self._name = name self._parent = parent self._start = None self._substracted = 0 def __enter__(self): self._start = time.monotonic() return self def __exit__(self, exc_type, exc_value, traceback): delta = time.monotonic() - self._start if self._parent is not None: self._parent.substract(delta) print(self._name, 'total', delta) print(self._name, 'outer', delta - self._substracted) return False def child(self, name): return type(self)(name, parent=self) def substract(self, n): self._substracted += n timeit = TimeItContextManager def main(): with timeit('large') as large_t: with large_t.child('medium') as medium_t: with medium_t.child('small-1'): time.sleep(1) with medium_t.child('small-2'): time.sleep(1) time.sleep(1) time.sleep(1) main()

Si vous souhaitez transmettre des informations le long de la chaîne d'appels, vous utilisez généralement la manière la plus simple possible: vous les transmettez en tant qu'arguments de fonctions.
Cependant, dans certains cas, il peut être très gênant de modifier toutes les fonctions de la chaîne pour propager de nouvelles données. Au lieu de cela, vous souhaiterez peut-être configurer une sorte de contexte à utiliser par toutes les fonctions de la chaîne. Comment ce contexte peut-il être réalisé techniquement?
La solution la plus simple est une variable globale. En Python, vous pouvez également utiliser des modules et des classes en tant que détenteurs de contexte, car ils sont à proprement parler des variables globales. Vous le faites probablement quotidiennement pour des choses comme les enregistreurs.
Si votre application est multithread, une variable globale nue ne fonctionnera pas pour vous car elle n'est pas thread-safe. Vous pouvez avoir plus d'une chaîne d'appel en cours d'exécution en même temps, et chacune a besoin de son propre contexte. Le module de
threading
vous couvre, il fournit l'objet
threading.local()
qui est thread-safe. Stockez-y toutes les données en accédant simplement aux attributs:
threading.local().symbol = '@'
.
Pourtant, ces deux approches ne sont pas sécurisées par concurrence, ce qui signifie qu'elles ne fonctionneront pas pour la chaîne d'appels coroutine où les fonctions sont non seulement appelées mais peuvent également être attendues. Une fois qu'une coroutine
await
, une boucle d'événement peut exécuter une coroutine complètement différente à partir d'une chaîne complètement différente. Cela ne fonctionnera pas:
import asyncio import sys global_symbol = '.' async def indication(timeout): while True: print(global_symbol, end='') sys.stdout.flush() await asyncio.sleep(timeout) async def sleep(t, indication_t, symbol='.'): loop = asyncio.get_event_loop() global global_symbol global_symbol = symbol task = loop.create_task( indication(indication_t) ) await asyncio.sleep(t) task.cancel() loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather( sleep(1, 0.1, '0'), sleep(1, 0.1, 'a'), sleep(1, 0.1, 'b'), sleep(1, 0.1, 'c'), ))
Vous pouvez résoudre ce problème en définissant la boucle et en restaurant le contexte chaque fois qu'il passe d'une coroutine à l'autre. Vous pouvez le faire avec le module
contextvars
depuis Python 3.7.
import asyncio import sys import contextvars global_symbol = contextvars.ContextVar('symbol') async def indication(timeout): while True: print(global_symbol.get(), end='') sys.stdout.flush() await asyncio.sleep(timeout) async def sleep(t, indication_t, symbol='.'): loop = asyncio.get_event_loop() global_symbol.set(symbol) task = loop.create_task(indication(indication_t)) await asyncio.sleep(t) task.cancel() loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather( sleep(1, 0.1, '0'), sleep(1, 0.1, 'a'), sleep(1, 0.1, 'b'), sleep(1, 0.1, 'c'), ))