การอธิบาย '__enter__' ของ Python และ '__exit__'


362

ฉันเห็นสิ่งนี้ในรหัสของใครบางคน มันหมายความว่าอะไร?

    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

19
คำอธิบายที่ดีที่นี่: effbot.org/zone/python-with-statement.htm
Manur

7
@StevenVascellaro การแก้ไขรหัสของคำถามเป็นความคิดที่ไม่ดีโดยเฉพาะเมื่อมีข้อผิดพลาดในรหัส คำถามนี้ถูกถามโดยคำนึงถึง Py2 และไม่มีเหตุผลใดที่จะปรับปรุงเป็น Py3
jpaugh

คำตอบ:


420

การใช้วิธีการทางเวทย์มนตร์เหล่านี้ ( __enter__, __exit__) ช่วยให้คุณสามารถใช้วัตถุที่สามารถใช้งานได้ง่ายกับwithคำสั่ง

แนวคิดคือทำให้ง่ายต่อการสร้างโค้ดซึ่งต้องการโค้ด 'ที่ถูกลบ' บางตัว (คิดว่ามันเป็นtry-finallyบล็อก) บางคำอธิบายเพิ่มเติมได้ที่นี่

ตัวอย่างที่มีประโยชน์อาจเป็นวัตถุการเชื่อมต่อฐานข้อมูล (ซึ่งจะปิดการเชื่อมต่อโดยอัตโนมัติเมื่อคำสั่ง 'with'-statement ไม่สอดคล้องกัน):

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'มีการเขียนที่ดีเช่นกัน


20
อาจ__enter__จะกลับมาselfเสมอเนื่องจากวิธีการอื่น ๆ ของชั้นเรียนเท่านั้นที่สามารถเรียกใช้ในบริบท
ViFI

3
@ViFI มี 4 ตัวอย่างของการอยู่def __enter__(self)ใน PEP 343 ไม่มีใครไม่และreturn self: python.org/dev/peps/pep-0343 ทำไมคุณคิดอย่างงั้น?
ความเศร้าโศก

4
@ สรุป: ด้วยเหตุผลสองประการในความคิดของฉัน 1) ฉันจะไม่สามารถเรียกวิธีการอื่น ๆ บนselfวัตถุตามที่อธิบายไว้ที่นี่: stackoverflow.com/questions/38281853/… 2) self.XYZ เป็นเพียงส่วนหนึ่งของวัตถุตัวเองและ กลับมาจัดการเฉพาะที่ดูเหมือนว่าไม่เหมาะสมกับฉันจากมุมมองการบำรุงรักษา ฉันอยากจะกลับไปจัดการกับวัตถุที่สมบูรณ์แล้วให้สาธารณะ APIs เฉพาะselfวัตถุองค์ประกอบเหล่านั้นซึ่งฉันต้องการเปิดเผยให้ผู้ใช้เช่นใน with open(abc.txt, 'r') as fin: content = fin.read()
ViFI

4
วัตถุไฟล์กลับมาselfจาก__enter__ซึ่งเป็นวิธีที่คุณสามารถประมวลผลไฟล์fภายในwith open(...) as f
โฮลเด็นเว็บ

2
หนึ่งในความละเอียดอ่อนผมต้องเข้าใจว่าวัตถุต้องพารามิเตอร์ในการเริ่มต้นเหล่านั้นควรจะอยู่ในinitไม่ได้ด้วยตนเอง
dfrankow

70

หากคุณรู้ว่าผู้จัดการบริบทคืออะไรคุณไม่จำเป็นต้องเข้าใจ__enter__และ__exit__ใช้วิธีการเวทมนต์อีกต่อไป ให้ดูตัวอย่างที่ง่ายมาก

ในตัวอย่างนี้ฉันกำลังเปิดmyfile.txtด้วยความช่วยเหลือของฟังก์ชั่นเปิด การลอง / ในที่สุดบล็อกทำให้แน่ใจว่าแม้ว่าข้อยกเว้นที่ไม่คาดคิดเกิดขึ้น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) 

หากคุณดูรหัสฉันไม่ได้ปิดไฟล์ & ไม่มีการลอง /ปิดกั้นในที่สุด เพราะมีคำสั่งปิดโดยอัตโนมัติ myfile.txt คุณยังสามารถตรวจสอบได้โดยการเรียกprint(fp.closed)แอตทริบิวต์ - Trueซึ่งผลตอบแทน

นี้เป็นเพราะไฟล์วัตถุ (FP ในตัวอย่างของฉัน) ส่งกลับโดยเปิดฟังก์ชั่นมีสองตัวในวิธีการและ__enter__ __exit__เป็นที่รู้จักกันว่าผู้จัดการบริบท __enter__วิธีการที่เรียกว่าในช่วงเริ่มต้นของการมีบล็อกและ__exit__ วิธีการที่เรียกว่าในตอนท้าย หมายเหตุ: ด้วยคำสั่งทำงานได้เฉพาะกับวัตถุที่สนับสนุนโปรโตคอล mamangement บริบทเช่นพวกเขามี__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__เวทมนตร์


53

ฉันพบว่ามันยากที่จะค้นหา python docs สำหรับ__enter__และ__exit__method โดย 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คำสั่งจะผูกค่าตอบแทนของวิธีการนี้เพื่อเป้าหมาย (s) ที่ระบุไว้ในข้อที่เป็นของคำสั่งในกรณีใด ๆ

object.__exit__(self, exc_type, exc_value, traceback)
ออกจากบริบทรันไทม์ที่เกี่ยวข้องกับวัตถุนี้ พารามิเตอร์อธิบายข้อยกเว้นที่ทำให้บริบทถูกออก Noneถ้าบริบทที่ถูกออกโดยไม่ต้องมีข้อยกเว้นทั้งสามข้อโต้แย้งจะเป็น

หากมีการจัดหาข้อยกเว้นและวิธีการที่ต้องการที่จะระงับข้อยกเว้น (เช่นป้องกันไม่ให้ถูกเผยแพร่) ก็ควรส่งกลับค่าที่แท้จริง มิฉะนั้นข้อยกเว้นจะดำเนินการตามปกติเมื่อออกจากวิธีนี้

โปรดทราบว่า__exit__()วิธีการไม่ควรปรับปรุงข้อยกเว้นการส่งผ่านอีกครั้ง นี่เป็นความรับผิดชอบของผู้โทร

ฉันหวังว่าจะได้คำอธิบายที่ชัดเจนเกี่ยวกับ__exit__วิธีการโต้แย้ง สิ่งนี้ขาดไป แต่เราสามารถสรุปได้ ...

สมมุติว่าexc_typeเป็นคลาสของข้อยกเว้น

มันบอกว่าคุณไม่ควรเพิ่มข้อยกเว้นการส่งผ่านใหม่อีกครั้ง สิ่งนี้ชี้ให้เราเห็นว่าข้อโต้แย้งข้อใดข้อหนึ่งอาจเป็นข้อยกเว้นที่เกิดขึ้นจริง ... หรือบางทีคุณควรจะยกตัวอย่างตัวเองจากประเภทและค่า

เราสามารถตอบได้โดยดูที่บทความนี้:
http://effbot.org/zone/python-with-statement.htm

ตัวอย่างเช่น__exit__เมธอดต่อไปนี้จะกลืน TypeError ใด ๆ แต่ให้ข้อยกเว้นอื่น ๆ ทั้งหมดผ่าน:

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

... ชัดเจนว่าvalueเป็นตัวอย่างข้อยกเว้น

และน่าtracebackจะเป็นวัตถุการสืบค้นกลับของงูหลาม


2
ตกลง. URL นี้หายากมาก
Shihao Xu

อาจเป็นสิ่งสำคัญที่จะต้องทราบบิตที่ฝังอยู่ภายในการอ้างอิง PEP ที่สังเกตเห็นการใช้ ARG: python.org/dev/peps/pep-0343/#generator-decorator
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 ในที่สุดและทำให้โค้ดไม่สามารถถอดรหัสได้

ไวยากรณ์ของคำสั่ง 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

ผลลัพธ์ของฝั่งเซิร์ฟเวอร์เหมือนเมื่อก่อน

ขอโทษสำหรับภาษาอังกฤษที่ไม่ดีของฉันและคำอธิบายที่ไม่ชัดเจนของฉันขอบคุณ ....


1

สิ่งนี้เรียกว่าตัวจัดการบริบทและฉันต้องการเพิ่มวิธีการที่คล้ายกันที่มีอยู่สำหรับภาษาการเขียนโปรแกรมอื่น ๆ การเปรียบเทียบมันอาจมีประโยชน์ในการทำความเข้าใจกับ context manager ใน python โดยทั่วไปแล้ว context manager จะใช้เมื่อเราจัดการกับทรัพยากรบางอย่าง (ไฟล์, เครือข่าย, ฐานข้อมูล) ที่จำเป็นต้องเริ่มต้นใช้งานและในบางจุดอาจทำให้เกิดการฉีกขาด (กำจัด) ในJava 7ขึ้นไปเรามีการจัดการทรัพยากรอัตโนมัติที่ใช้รูปแบบของ:

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

โปรดทราบว่าเซสชันต้องใช้งานAutoClosableหรือหนึ่งในอินเตอร์เฟซย่อย (มาก)

ในC #เราได้ใช้คำสั่งสำหรับการจัดการทรัพยากรที่อยู่ในรูปแบบของ:

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

ซึ่งควรใช้SessionIDisposable

ในหลามชั้นที่เราใช้ควรใช้และ__enter__ __exit__ดังนั้นจึงใช้รูปแบบของ:

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

และตามที่คนอื่น ๆ ชี้ให้เห็นคุณสามารถใช้คำสั่ง try / สุดท้ายในทุกภาษาเพื่อใช้กลไกเดียวกัน นี่เป็นเพียงน้ำตาลวากยสัมพันธ์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.