Wyjaśnienie Pythona „__enter__” i „__exit__”

361

Widziałem to w czyimś kodzie. Co to znaczy?

    def __enter__(self):
        return self

    def __exit__(self, type, value, tb):
        self.stream.close()

from __future__ import with_statement#for python2.5 

class a(object):
    def __enter__(self):
        print 'sss'
        return 'sss111'
    def __exit__(self ,type, value, traceback):
        print 'ok'
        return False

with a() as s:
    print s


print s
zjm1126
źródło
19
Dobre wyjaśnienie tutaj: effbot.org/zone/python-with-statement.htm
Manur
7
@StevenVascellaro Edycja kodu pytania jest generalnie złym pomysłem, szczególnie gdy występują błędy w kodzie. To pytanie zostało zadane z myślą o Py2 i nie ma powodu, aby aktualizować je do Py3.
jpaugh

Odpowiedzi:

420

Korzystanie z tych magicznych metod ( __enter__, __exit__) pozwala na implementację obiektów, których można łatwo używać z withinstrukcją.

Chodzi o to, że ułatwia budowanie kodu, który wymaga wykonania kodu „wyczyszczenia” (traktuj go jako try-finallyblok). Więcej wyjaśnień tutaj .

Przydatnym przykładem może być obiekt połączenia z bazą danych (który następnie automatycznie zamyka połączenie, gdy odpowiednia instrukcja „z” wykracza poza zakres):

class DatabaseConnection(object):

    def __enter__(self):
        # make a database connection and return it
        ...
        return self.dbconn

    def __exit__(self, exc_type, exc_val, exc_tb):
        # make sure the dbconnection gets closed
        self.dbconn.close()
        ...

Jak wyjaśniono powyżej, użyj tego obiektu z withinstrukcją (możesz potrzebować zrobić to from __future__ import with_statementna górze pliku, jeśli korzystasz z Python 2.5).

with DatabaseConnection() as mydbconn:
    # do stuff

PEP343 - Polecenie „z” ma również niezły opis.

ChristopheD
źródło
20
Prawdopodobnie __enter__powinien selfzawsze zwracać, ponieważ w kontekście można wywoływać tylko inne metody klasy.
ViFI,
3
@ViFI Istnieją 4 przykłady def __enter__(self)PEP 343 i nikt tego nie robi return self: python.org/dev/peps/pep-0343 . Dlaczego tak myślisz?
Smutek
4
@ Skrót: Z dwóch powodów: Moim zdaniem 1) nie będę mógł wywoływać innych metod na selfobiekcie, jak wyjaśniono tutaj: stackoverflow.com/questions/38281853/... 2) self.XYZ jest tylko częścią self obiektu i zwracanie uchwytu tylko do tego, co wydaje mi się nieodpowiednie z punktu widzenia konserwacji. Wolałbym wolą wrócić do pełnej uchwyt obiektu, a następnie dostarczyć API publiczne tylko te elementy selfobiektu, który chcę wystawiać użytkownikowi jak w with open(abc.txt, 'r') as fin: content = fin.read()
ViFI
4
Powracają obiekty plików, selfz __enter__tego powodu możesz przetwarzać plik jak w fśrodkuwith open(...) as f
holdenweb 31.10.16
2
Jedną subtelność musiałem zrozumieć: jeśli obiekt wymaga parametrów do zainicjowania, powinny one być na init , a nie na siebie .
dfrankow
70

Jeśli wiesz, kim są menedżerowie kontekstu, nie potrzebujesz nic więcej do zrozumienia __enter__i __exit__magicznych metod. Zobaczmy bardzo prosty przykład.

W tym przykładzie otwieram plik myfile.txt za pomocą funkcji open . Blok try / wreszcie zapewnia, że ​​nawet jeśli wystąpi nieoczekiwany wyjątek, plik myfile.txt zostanie zamknięty.

fp=open(r"C:\Users\SharpEl\Desktop\myfile.txt")
try:
    for line in fp:
        print(line)
finally:
    fp.close()

Teraz otwieram ten sam plik z instrukcją:

with open(r"C:\Users\SharpEl\Desktop\myfile.txt") as fp:
    for line in fp:
        print(line) 

Jeśli spojrzysz na kod, nie zamknąłem pliku i nie ma bloku try / wreszcie . Ponieważ z instrukcją automatycznie zamyka plik myfile.txt . Możesz to nawet sprawdzić, wywołując print(fp.closed)atrybut - który zwraca True.

Jest tak, ponieważ obiekty plików (fp w moim przykładzie) zwrócone przez funkcję open mają dwie wbudowane metody __enter__i __exit__. Jest również znany jako menedżer kontekstu. __enter__Metoda jest wywoływana na początku za pomocą bloku, a __exit__ metoda jest wywoływana na końcu. Uwaga: z instrukcją działa tylko z obiektami, które obsługują protokół modyfikowania kontekstu, tj. Mają __enter__i __exit__metody. Klasa implementująca obie metody jest znana jako klasa menedżera kontekstu.

Teraz pozwala zdefiniować naszą własną klasę menedżera kontekstu .

 class Log:
    def __init__(self,filename):
        self.filename=filename
        self.fp=None    
    def logging(self,text):
        self.fp.write(text+'\n')
    def __enter__(self):
        print("__enter__")
        self.fp=open(self.filename,"a+")
        return self    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__")
        self.fp.close()

with Log(r"C:\Users\SharpEl\Desktop\myfile.txt") as logfile:
    print("Main")
    logfile.logging("Test1")
    logfile.logging("Test2")

Mam nadzieję, że teraz rozumiesz obie metody __enter__i __exit__metody magiczne.

N Randhawa
źródło
53

Znalezienie dokumentacji __enter__i __exit__metod Pythona przez Googlinga było dla mnie dziwnie trudne , więc aby pomóc innym tutaj, jest link:

https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers
https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers
(szczegóły są takie same dla obu wersji)

object.__enter__(self)
Wprowadź kontekst środowiska wykonawczego związany z tym obiektem. withInformacja będzie wiązać tej metody jest wartość powrotu do docelowego (ych), jak określona w klauzuli rachunku, jeśli w ogóle.

object.__exit__(self, exc_type, exc_value, traceback)
Wyjdź z kontekstu wykonawczego związanego z tym obiektem. Parametry opisują wyjątek, który spowodował wyjście z kontekstu. Jeśli kontekst został zamknięty bez wyjątku, wszystkie trzy argumenty będąNone .

Jeśli podano wyjątek, a metoda chce go wyłączyć (tzn. Uniemożliwić jego propagację), powinna zwrócić wartość true. W przeciwnym razie wyjątek zostanie przetworzony normalnie po wyjściu z tej metody.

Należy pamiętać, że __exit__()metody nie powinny ponownie przekazywać przekazanego wyjątku; to obowiązek osoby dzwoniącej.

Miałem nadzieję na jasny opis __exit__argumentów metody. Tego brakuje, ale możemy je wywnioskować ...

Prawdopodobnie exc_type jest to klasa wyjątku.

Mówi, że nie powinieneś ponownie podnosić przekazywanego wyjątku. Sugeruje to nam, że jednym z argumentów może być faktyczna instancja wyjątku ... a może powinieneś utworzyć ją samodzielnie z rodzaju i wartości?

Możemy odpowiedzieć, patrząc na ten artykuł:
http://effbot.org/zone/python-with-statement.htm

Na przykład poniższa __exit__metoda połyka dowolny błąd typu, ale przepuszcza wszystkie inne wyjątki:

def __exit__(self, type, value, traceback):
    return isinstance(value, TypeError)

... tak wyraźnie valuejest przypadek wyjątku.

I prawdopodobnie tracebackjest to obiekt śledzenia w Pythonie .

Anentropic
źródło
2
Zgodzić się. Ten adres URL jest bardzo trudny do znalezienia.
Shihao Xu
może być ważne, aby zanotować ten ukryty bit w odnośniku PEP, zauważając użycie arg: python.org/dev/peps/pep-0343/#generator-decorator
Tcll
43

Oprócz powyższych odpowiedzi w celu zilustrowania kolejności wywoływania, prosty przykład uruchomienia

class myclass:
    def __init__(self):
        print("__init__")

    def __enter__(self): 
        print("__enter__")

    def __exit__(self, type, value, traceback):
        print("__exit__")

    def __del__(self):
        print("__del__")

with myclass(): 
    print("body")

Daje wynik:

__init__
__enter__
body
__exit__
__del__

Przypomnienie: podczas korzystania ze składni with myclass() as mczmienna mc otrzymuje wartość zwracaną przez __enter__(), w powyższym przypadku None! W takim przypadku należy zdefiniować wartość zwracaną, na przykład:

def __enter__(self): 
    print('__enter__')
    return self
Jurij Feldman
źródło
3
I nawet jeśli sekwencja definicji zostanie zmieniona, kolejność wykonywania pozostaje taka sama!
Sean
1
To było bardzo pomocne. Dziękuję Ci.
Reez0
5

spróbuj dodać moje odpowiedzi (moja myśl o nauce):

__enter__i [__exit__]obie są metodami wywoływanymi przy wejściu i wyjściu z treści „ instrukcji with ” ( PEP 343 ), a implementacja obu nazywa się menedżerem kontekstu.

instrukcja with ma na celu ukrycie kontroli przepływu klauzuli try last i uczynienie kodu nieodgadnialnym.

składnia instrukcji with jest następująca:

with EXPR as VAR:
    BLOCK

co przekłada się na (jak wspomniano w PEP 343):

mgr = (EXPR)
exit = type(mgr).__exit__  # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
    try:
        VAR = value  # Only if "as VAR" is present
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        exit(mgr, None, None, None)

spróbuj trochę kodu:

>>> import logging
>>> import socket
>>> import sys

#server socket on another terminal / python interpreter
>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> s.listen(5)
>>> s.bind((socket.gethostname(), 999))
>>> while True:
>>>    (clientsocket, addr) = s.accept()
>>>    print('get connection from %r' % addr[0])
>>>    msg = clientsocket.recv(1024)
>>>    print('received %r' % msg)
>>>    clientsocket.send(b'connected')
>>>    continue

#the client side
>>> class MyConnectionManager:
>>>     def __init__(self, sock, addrs):
>>>         logging.basicConfig(level=logging.DEBUG, format='%(asctime)s \
>>>         : %(levelname)s --> %(message)s')
>>>         logging.info('Initiating My connection')
>>>         self.sock = sock
>>>         self.addrs = addrs
>>>     def __enter__(self):
>>>         try:
>>>             self.sock.connect(addrs)
>>>             logging.info('connection success')
>>>             return self.sock
>>>         except:
>>>             logging.warning('Connection refused')
>>>             raise
>>>     def __exit__(self, type, value, tb):
>>>             logging.info('CM suppress exception')
>>>             return False
>>> addrs = (socket.gethostname())
>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> with MyConnectionManager(s, addrs) as CM:
>>>     try:
>>>         CM.send(b'establishing connection')
>>>         msg = CM.recv(1024)
>>>         print(msg)
>>>     except:
>>>         raise
#will result (client side) :
2018-12-18 14:44:05,863         : INFO --> Initiating My connection
2018-12-18 14:44:05,863         : INFO --> connection success
b'connected'
2018-12-18 14:44:05,864         : INFO --> CM suppress exception

#result of server side
get connection from '127.0.0.1'
received b'establishing connection'

i teraz spróbuj ręcznie (zgodnie ze składnią tłumaczenia):

>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #make new socket object
>>> mgr = MyConnection(s, addrs)
2018-12-18 14:53:19,331         : INFO --> Initiating My connection
>>> ext = mgr.__exit__
>>> value = mgr.__enter__()
2018-12-18 14:55:55,491         : INFO --> connection success
>>> exc = True
>>> try:
>>>     try:
>>>         VAR = value
>>>         VAR.send(b'establishing connection')
>>>         msg = VAR.recv(1024)
>>>         print(msg)
>>>     except:
>>>         exc = False
>>>         if not ext(*sys.exc_info()):
>>>             raise
>>> finally:
>>>     if exc:
>>>         ext(None, None, None)
#the result:
b'connected'
2018-12-18 15:01:54,208         : INFO --> CM suppress exception

wynik po stronie serwera taki sam jak poprzednio

przepraszam za mój zły angielski i niejasne wyjaśnienia, dziękuję ....

Wira Bhakti
źródło
1

Nazywa się to menedżerem kontekstu i chcę tylko dodać, że podobne podejścia istnieją w przypadku innych języków programowania. Porównanie ich może być pomocne w zrozumieniu menedżera kontekstu w Pythonie. Zasadniczo menedżer kontekstu jest używany, gdy mamy do czynienia z niektórymi zasobami (plik, sieć, baza danych), które muszą zostać zainicjowane, aw pewnym momencie zerwane (usunięte). W Javie 7 i nowszych mamy automatyczne zarządzanie zasobami, które przyjmuje postać:

//Java code
try (Session session = new Session())
{
  // do stuff
}

Należy pamiętać, że Sesja musi zostać wdrożona AutoClosablelub jeden z (wielu) pod-interfejsów.

W języku C # używamy instrukcji do zarządzania zasobami, które mają postać:

//C# code
using(Session session = new Session())
{
  ... do stuff.
}

W którym Sessionnależy wdrożyćIDisposable .

W Pythonie klasa, której używamy, powinna implementować __enter__i __exit__. Przybiera więc postać:

#Python code
with Session() as session:
    #do stuff

I jak zauważyli inni, zawsze możesz użyć instrukcji try / wreszcie we wszystkich językach, aby zaimplementować ten sam mechanizm. To tylko cukier syntaktyczny.

Rohola Zandie
źródło