Объяснение Python '__enter__' и '__exit__'

365

Я видел это в чьем-то коде. Что это значит?

    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
источник
19
Хорошее объяснение здесь: effbot.org/zone/python-with-statement.htm
Манур
7
@ StevenVascellaro Редактирование кода вопроса, как правило, плохая идея, особенно когда в коде есть ошибки. Этот вопрос был задан с учетом Py2, и нет никаких оснований обновлять его до Py3.
августа

Ответы:

421

Использование этих магических методов ( __enter__, __exit__) позволяет вам реализовать объекты, которые можно легко использовать с withоператором.

Идея состоит в том, что он облегчает создание кода, который требует выполнения некоторого кода очистки (думайте о нем как о try-finallyблоке). Еще несколько объяснений здесь .

Полезным примером может быть объект соединения с базой данных (который затем автоматически закрывает соединение, как только соответствующий оператор with выходит из области видимости):

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

Как объяснено выше, используйте этот объект с withоператором (вам может потребоваться сделать from __future__ import with_statementэто вверху файла, если вы используете Python 2.5).

with DatabaseConnection() as mydbconn:
    # do stuff

PEP343 - У оператора with тоже хорошая запись.

ChristopheD
источник
20
Вероятно, __enter__должен возвращаться selfвсегда, так как тогда только другие методы класса могут быть вызваны в контексте.
ViFI
3
@ViFI В def __enter__(self)PEP 343 есть 4 примера, но никто этого не делает return self: python.org/dev/peps/pep-0343 . Почему ты так думаешь?
Горе
4
@Grief: по двум причинам, по моему мнению, 1) я не смогу вызывать другие методы для selfобъекта, как описано здесь: stackoverflow.com/questions/38281853/… 2) self.XYZ является лишь частью объекта self и возвращать ручку только к тому, что кажется мне неуместным с точки зрения обслуживания. Я бы предпочел вернуть дескриптор для завершения объекта, а затем предоставить открытые API-интерфейсы только для тех компонентов компонентов self, которые я хочу предоставить пользователю, как в with open(abc.txt, 'r') as fin: content = fin.read()
ViFI
4
Файловые объекты возврата selfиз __enter__, который как же вы можете обработать этот файл как fвнутриwith open(...) as f
holdenweb
2
Я должен был понять одну тонкость: если объект требует инициализации параметров, они должны быть в init , а не в self .
dfrankow
71

Если вы знаете, что такое контекстные менеджеры , вам больше ничего не нужно понимать __enter__и использовать __exit__магические методы. Давайте посмотрим на очень простой пример.

В этом примере я открываю myfile.txt с помощью функции open . Блок try / finally гарантирует, что даже в случае непредвиденного исключения myfile.txt будет закрыт.

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

Теперь я открываю тот же файл с заявлением:

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

Если вы посмотрите на код, я не закрыл файл и нет блока try / finally . Потому что с оператором автоматически закрывается myfile.txt . Вы даже можете проверить это, вызвав print(fp.closed)атрибут - который возвращает True.

Это потому, что файловые объекты (fp в моем примере), возвращаемые функцией open, имеют два встроенных метода __enter__и __exit__. Он также известен как менеджер контекста. __enter__метод вызывается в начале с блока и __exit__ метод вызывается в конце. Замечания: оператор with работает только с объектами, которые поддерживают протокол управления контекстами, т. Е. Имеют методы __enter__и __exit__методы. Класс, который реализует оба метода, известен как класс менеджера контекста.

Теперь давайте определим нашу собственную класс менеджера контекста .

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

Я надеюсь, что теперь у вас есть базовое понимание обоих __enter__и __exit__магических методов.

N Randhawa
источник
53

Мне было странно трудно найти документы __enter__и __exit__методы Python для Googling, поэтому, чтобы помочь другим, вот ссылка:

https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers
https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers
(детали одинаковы для обеих версий)

object.__enter__(self)
Введите контекст времени выполнения, связанный с этим объектом. withЗаявление будет связывать возвращаемое значение этого метода, к целевому (ы) , указанное в качестве пункта заявления, если таковые имеются.

object.__exit__(self, exc_type, exc_value, traceback)
Выход из контекста времени выполнения, связанного с этим объектом. Параметры описывают исключение, которое вызвало выход из контекста. Если контекст был закрыт без исключения, все три аргумента будут None.

Если предоставлено исключение, и метод желает подавить исключение (то есть предотвратить его распространение), он должен вернуть истинное значение. В противном случае исключение будет обработано нормально при выходе из этого метода.

Обратите внимание, что __exit__()методы не должны повторно вызывать переданное исключение; это ответственность звонящего.

Я надеялся на четкое описание __exit__аргументов метода. Этого не хватает, но мы можем вывести их ...

Предположительно exc_typeэто класс исключения.

В нем говорится, что вы не должны повторно поднимать переданное исключение. Это говорит нам о том, что один из аргументов может быть фактическим экземпляром Exception ... или, может быть, вы должны создать его экземпляр самостоятельно по типу и значению?

Мы можем ответить, посмотрев эту статью:
http://effbot.org/zone/python-with-statement.htm

Например, следующий __exit__метод проглатывает любую ошибку TypeError, но пропускает все остальные исключения:

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

... так ясно value, это исключение.

И предположительно tracebackэто объект трассировки Python .

Anentropic
источник
2
Согласен. Этот URL так сложно найти.
Сихао Сюй
может быть важно отметить этот скрытый
Tcll
43

В дополнение к приведенным выше ответам, чтобы проиллюстрировать порядок вызова, простой пример запуска

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

Производит вывод:

__init__
__enter__
body
__exit__
__del__

Напоминание: при использовании синтаксиса with myclass() as mcпеременная mc получает значение, возвращаемое __enter__(), в вышеприведенном случае None! Для такого использования необходимо определить возвращаемое значение, например:

def __enter__(self): 
    print('__enter__')
    return self
Юрий Фельдман
источник
3
И даже если последовательность определений переключается, порядок выполнения остается неизменным!
Шон,
1
Это было очень полезно. Спасибо.
Reez0
5

попробуйте добавить мои ответы (моя мысль об обучении):

__enter__и [__exit__]оба являются методами, которые вызываются при входе и выходе из тела « оператора with » ( PEP 343 ), и реализация обоих называется контекстным менеджером.

Оператор with предназначен для сокрытия управления потоком предложения try finally и делает код непостижимым.

синтаксис оператора with:

with EXPR as VAR:
    BLOCK

что переводится как (как упомянуто в 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)

попробуйте немного кода:

>>> 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'

и теперь попробуйте вручную (следуя синтаксису перевода):

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

результат на стороне сервера такой же, как и раньше

извините за мой плохой английский и мои неясные объяснения, спасибо ....

Wira Bhakti
источник
1

Это называется диспетчер контекста, и я просто хочу добавить, что аналогичные подходы существуют для других языков программирования. Сравнение их может помочь понять менеджер контекста в python. По сути, менеджер контекста используется, когда мы имеем дело с некоторыми ресурсами (файл, сеть, база данных), которые необходимо инициализировать и в какой-то момент свернуть (утилизировать). В Java 7 и выше у нас есть автоматическое управление ресурсами, которое принимает форму:

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

Обратите внимание, что Session необходимо реализовать AutoClosableили один из его (многих) подчиненных интерфейсов.

В C # мы используем операторы для управления ресурсами, которые принимают форму:

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

В котором Sessionследует реализовать IDisposable.

В python класс, который мы используем, должен реализовывать __enter__и __exit__. Так что он принимает форму:

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

И, как отмечали другие, вы всегда можете использовать оператор try / finally на всех языках для реализации одного и того же механизма. Это просто синтаксический сахар.

Рохола Занди
источник