web-dev-qa-db-fra.com

__init__ vs __enter__ dans les gestionnaires de contexte

Pour autant que je sache, les méthodes __init__() et __enter__() du gestionnaire de contexte sont appelées exactement une fois chacune, l'une après l'autre, ne laissant aucune chance à tout autre code d'être exécuté entre les deux. Quel est le but de les séparer en deux méthodes, et que dois-je mettre dans chacune?

Edit: désolé, ne faisait pas attention aux documents.

Edit 2: en fait, la raison pour laquelle je me suis perdu est parce que je pensais à @contextmanager décorateur. Un gestionnaire de contexte créé à l'aide de @contextmananger ne peut être utilisé qu'une seule fois (le générateur sera épuisé après la première utilisation), ils sont donc souvent écrits avec l'appel du constructeur à l'intérieur de l'instruction with; et si c'était la seule façon d'utiliser l'instruction with, ma question aurait eu un sens. Bien sûr, en réalité, les gestionnaires de contexte sont plus généraux que ce @contextmanager Peut créer; en particulier, les gestionnaires de contexte peuvent, en général, être réutilisés. J'espère avoir bien compris cette fois?

23
max

Pour autant que je sache, les méthodes __init__() et __enter__() du gestionnaire de contexte sont appelées une fois chacune exactement, l'une après l'autre, ne laissant aucune chance à tout autre code d'être exécuté entre les deux.

Et votre compréhension est incorrecte. __init__ est appelé lors de la création de l'objet, __enter__ quand il est entré avec l'instruction with, et ce sont 2 choses bien distinctes. Souvent, c'est pour que le constructeur soit directement appelé dans l'initialisation with, sans code intermédiaire, mais cela n'a pas à être le cas.

Considérez cet exemple:

class Foo:
    def __init__(self):
        print('__init__ called')
    def __enter__(self):
        print('__enter__ called')
        return self
    def __exit__(self, *a):
        print('__exit__ called')

myobj = Foo()

print('\nabout to enter with 1')
with myobj:
    print('in with 1')

print('\nabout to enter with 2')
with myobj:
    print('in with 2')

myobj peut être initialisé séparément et entré dans plusieurs blocs with:

Production:

__init__ called

about to enter with 1
__enter__ called
in with 1
__exit__ called

about to enter with 2
__enter__ called
in with 2
__exit__ called

De plus, si __init__ et __enter__ n'étaient pas séparés, il ne serait même pas possible d'utiliser ce qui suit:

def open_etc_file(name):
    return open(os.path.join('/etc', name))

with open_etc_file('passwd'):
    ...

puisque l'initialisation (dans open) est clairement distincte de l'entrée with.


Les managers créés par contextlib.manager sont à entrée unique, mais ils peuvent à nouveau être construits en dehors du bloc with. Prenons l'exemple:

from contextlib import contextmanager

@contextmanager
def tag(name):
    print("<%s>" % name)
    yield
    print("</%s>" % name)

vous pouvez l'utiliser comme:

def heading(level=1):
    return tag('h{}'.format(level))

my_heading = heading()
print('Below be my heading')
with my_heading:
     print('Here be dragons')

production:

Below be my heading
<h1>
Here be dragons
</h1>

Cependant, si vous essayez de réutiliser my_heading (et, par conséquent, tag), vous obtiendrez

RuntimeError: generator didn't yield
40
Antti Haapala

La réponse d'Anti Haapalas est parfaitement correcte. Je voulais juste développer un peu l'utilisation des arguments (comme myClass(* args)) car cela n'était pas clair pour moi (rétrospective je me demande pourquoi ...)

L'utilisation d'arguments pour initialiser votre classe dans une instruction with n'est pas différente de l'utilisation habituelle de la classe. Les appels se feront dans l'ordre suivant:

  1. __init__ (attribution de la classe)
  2. __enter__ (entrez le contexte)
  3. __exit__ (sortant du contexte)

Exemple simple:

class Foo:
    def __init__(self, i):
        print('__init__ called: {}'.format(i))
        self.i = i
    def __enter__(self):
        print('__enter__ called')
        return self
    def do_something(self):
        print('do something with {}'.format(self.i))
    def __exit__(self, *a):
        print('__exit__ called')

with Foo(42) as bar:
    bar.do_something()

Production:

__init__ called: 42
__enter__ called
    do something with 42
__exit__ called

Si vous voulez vous assurer que vos appels ne peuvent (presque) être utilisés que dans un contexte (par exemple pour forcer l'appel à __exit__), voir le post stackoverflow ici . Dans les commentaires, vous trouverez également une réponse à la question de savoir comment utiliser les arguments même alors.

6
SeparateReality