12 votes

Renvoi de la valeur lors de la sortie du gestionnaire de contexte python

Il s'agit peut-être d'une question stupide (et pas très pratique), mais je la pose parce que je n'arrive pas à m'y retrouver.

En cherchant à savoir si un return à l'intérieur d'un appel à un gestionnaire de contexte empêcherait __exit__ d'être appelé (non, ce n'est pas le cas), j'ai constaté qu'il semble courant de faire une analogie entre __exit__ y finally en un try/finally (par exemple ici : https://stackoverflow.com/a/9885287/3471881 ) parce que :

def test():
    try:
        return True
    finally:
        print("Good bye")

Exécuterait la même chose que :

class MyContextManager:

    def __enter__(self):
        return self

    def __exit__(self, *args):
        print('Good bye')

def test():
    with MyContextManager():
        return True

Cela m'a vraiment aidé à comprendre le fonctionnement des cm:s, mais après avoir joué un peu, j'ai réalisé que cette analogie ne fonctionnerait pas si nous retournions quelque chose plutôt que d'imprimer.

def test():
    try:
        return True
    finally:
        return False
test()    
--> False

Alors que __exit__ ne reviendront apparemment pas du tout :

class MyContextManager:

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False

def test():
    with MyContextManager():
        return True

test()
--> True

Cela m'a conduit à penser que peut-être vous ne pouvez pas retourner quoi que ce soit à l'intérieur. __exit__ mais vous pouvez :

class MyContextManager:

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return self.last_goodbye()

    def last_goodbye(self):
        print('Good bye')

def test():
    with MyContextManager():
        return True
test()
--> Good bye
--> True

Notez que cela n'a pas d'importance si nous ne retournons rien à l'intérieur de la balise test() fonction.

Cela m'amène à ma question :

  • Est-il impossible de retourner une valeur à partir de l'intérieur __exit__ et si oui, pourquoi ?

11voto

iBug Points 20818

Oui. Il est impossible de modifier la valeur de retour du contexte à partir de l'intérieur de l'application. __exit__ .

Si le contexte est quitté avec un return vous ne pouvez pas modifier la valeur de retour à l'aide de l'instruction context_manager.__exit__ . C'est différent d'un try ... finally ... car le code dans finally toujours appartient à la fonction parentale alors que context_manager.__exit__ fonctionne dans son propre champ d'application .

En fait, __exit__ peut retourner une valeur booléenne ( True o False ) et il sera compris par Python. Elle indique à Python si l'exception qui sort du contexte (le cas échéant) doit être supprimée (ne pas se propager à l'extérieur du contexte).

Voir cet exemple de la signification de la valeur de retour de la fonction __exit__ :

>>> class MyContextManager:
...  def __init__(self, suppress):
...   self.suppress = suppress
...  
...  def __enter__(self):
...   return self
...  
...  def __exit__(self, exc_type, exc_obj, exc_tb):
...   return self.suppress
... 
>>> with MyContextManager(True):  # suppress exception
...  raise ValueError
... 
>>> with MyContextManager(False):  # let exception pass through
...  raise ValueError
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError
>>>

Dans l'exemple ci-dessus, les deux ValueError provoquera un saut du contrôle hors du contexte. Dans le premier bloc, le __exit__ du gestionnaire de contexte renvoie True Python supprime donc cette exception et elle n'apparaît pas dans le REPL. Dans le second bloc, le gestionnaire de contexte retourne False Python laisse donc le code externe gérer l'exception, qui est imprimée par le REPL.

2voto

Guimoute Points 173

La solution consiste à stocker le résultat dans un attribut au lieu de le renvoyer, et à y accéder ultérieurement. Ceci si vous avez l'intention d'utiliser cette valeur dans plus d'une impression.

Par exemple, prenez ce simple gestionnaire de contexte :

class time_this_scope():
    """Context manager to measure how much time was spent in the target scope."""

    def __init__(self, allow_print=False):
        self.t0 = None
        self.dt = None
        self.allow_print = allow_print

    def __enter__(self):
        self.t0 = time.perf_counter()

    def __exit__(self, type=None, value=None, traceback=None):
        self.dt = (time.perf_counter() - self.t0) # Store the desired value.
        if self.allow_print is True:
            print(f"Scope took {self.dt*1000: 0.1f} milliseconds.")

On pourrait l'utiliser de cette façon :

with time_this_scope(allow_print=True):
    time.sleep(0.100)

>>> Scope took 100 milliseconds.

ou comme ça :

timer = time_this_scope()
with timer:
    time.sleep(0.100)
dt = timer.dt 

Non comme ce qui est montré ci-dessous puisque le timer n'est plus accessible car la portée se termine. Nous devons modifier la classe comme décrit ici et ajouter return self à la valeur __enter__ . Avant la modification, vous obteniez une erreur :

with time_this_scope() as timer:
    time.sleep(0.100)
dt = timer.dt 

>>> AttributeError: 'NoneType' object has no attribute 'dt'

Enfin, voici un exemple d'utilisation simple :

"""Calculate the average time spent sleeping."""
import numpy as np
import time

N = 100
dt_mean = 0
for n in range(N)
    timer = time_this_scope()
    with timer:
        time.sleep(0.001 + np.random.rand()/1000) # 1-2 ms per loop.
    dt = timer.dt
    dt_mean += dt/N
    print(f"Loop {n+1}/{N} took {dt}s.")
print(f"All loops took {dt_mean}s on average.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X