__init__ срещу __enter__ в контекстните мениджъри

Доколкото разбирам, __init__() и __enter__() методите на контекстния мениджър се извикват точно веднъж, един след друг, без да се оставя никакъв шанс за друг код да бъде изпълнен между тях. Каква е целта на разделянето им на два метода и какво трябва да поставя във всеки?

Редактиране: съжалявам, не обърнах внимание на документите.

Редактиране 2: всъщност причината, поради която се обърках, е, че мислех за @contextmanager декоратор. Контекстният мениджър, създаден с помощта на @contextmananger, може да се използва само веднъж (генераторът ще бъде изчерпан след първото използване), така че често те се пишат с извикването на конструктора вътре в израза with; и ако това беше единственият начин да се използва израз with, въпросът ми щеше да има смисъл. Разбира се, в действителност мениджърите на контекста са по-общи от това, което @contextmanager може да създаде; по-специално контекстните мениджъри могат като цяло да се използват повторно. Надявам се този път да съм разбрал правилно?


person max    schedule 21.09.2016    source източник
comment
Бъркате създаване на контекстен мениджър с въвеждане на контекст. Двете са различни и можете да използвате един и същ контекстен мениджър повече от веднъж.   -  person Martijn Pieters    schedule 21.09.2016


Отговори (2)


Доколкото разбирам, __init__() и __enter__() методите на контекстния мениджър се извикват точно веднъж, един след друг, без да се оставя никакъв шанс за друг код да бъде изпълнен между тях.

И вашето разбиране е неправилно. __init__ се извиква, когато обектът е създаден, __enter__, когато е въведен с оператор with и това са 2 доста различни неща. Често конструкторът се извиква директно при with инициализация, без намесен код, но това не трябва да е така.

Помислете за този пример:

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 може да се инициализира отделно и да се въведе в множество with блокове:

Изход:

__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

Освен това, ако __init__ и __enter__ не бяха разделени, не би било възможно дори да се използва следното:

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

with open_etc_file('passwd'):
    ...

тъй като инициализацията (в рамките на open) е ясно отделена от записа with.


Мениджърите, създадени от contextlib.manager, са с единичен вход, но отново може да се изгради извън блок with. Вземете примера:

from contextlib import contextmanager

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

можете да използвате това като:

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

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

изход:

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

Въпреки това, ако се опитате да използвате повторно my_heading (и, следователно, tag), ще получите

RuntimeError: generator didn't yield
person Antti Haapala    schedule 21.09.2016
comment
О, човече, някак си във всички примери, които си спомних, извикването на конструктора се случва в оператора with (напр. with Foo()...). Сега всичко има смисъл. Мерси - person max; 21.09.2016
comment
О, чакай, но какво да кажем за @contextmanager? Тъй като разчита на генератор, не го ли изтощава при първото използване и по този начин прави невъзможно повторното използване на обекта? - person max; 21.09.2016
comment
Така че е правилно да се каже, че само подмножество от полезни контекстни мениджъри могат да бъдат произведени с @contextmanager декоратор; останалите изискват изрично писане на клас, който отговаря на API на контекстния мениджър? - person max; 21.09.2016
comment
Моля, внимавайте с формулировките. __init__ всъщност не е конструкторът, а инициаторът в Python. __new__ е конструкторът. Можете да видите в сигнатурата на метода на __init__, че ако self е подаден като първи параметър, конструкторът вече е бил извикан или в противен случай няма да имате екземпляр за себе си. - person CheradenineZK; 25.02.2021

Отговорът на Antti Haapalas е напълно добър. Просто исках да разясня малко използването на аргументи (като myClass(* args)), тъй като това беше малко неясно за мен (ретроспективно се питам защо....)

Използването на аргументи за инициализиране на вашия клас в оператор with не се различава от използването на класа по обичайния начин. Обажданията ще се извършват в следния ред:

  1. __init__ (разпределение на класа)
  2. __enter__ (въведете контекст)
  3. __exit__ (напускане на контекста)

Прост пример:

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()

Изход:

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

Ако искате да сте сигурни, че вашите обаждания могат (почти) да се използват само в контекст (напр. за принудително извикване до __exit__), вижте публикацията на stackoverflow тук. В коментарите ще намерите и отговор на въпроса как да използвате аргументи дори тогава.

person SeparateReality    schedule 26.11.2017