>  기사  >  백엔드 개발  >  Python의 컨텍스트 관리자

Python의 컨텍스트 관리자

高洛峰
高洛峰원래의
2017-03-01 14:16:441091검색

Python에서는 코드 블록에 들어가기 전에 __enter__ 메서드를 호출하고 코드 블록을 떠난 후에 __exit__ 메서드를 호출하는 객체를 컨텍스트 관리자로 사용합니다. 이번 글에서는 Python의 컨텍스트 관리자를 심층적으로 분석하여 살펴보겠습니다. 컨텍스트에서 관리자의 역할과 사용법:

1. 컨텍스트 관리자란 무엇입니까?

예를 들어 Python 코드를 작성할 때 명령문 블록에 일련의 작업을 넣는 경우가 많습니다.

(1) 특정 조건이 true인 경우 - 이 명령문을 실행합니다.

(2) 특정 조건이 true인 경우 – 이 문장 블록을 반복해서 실행

문장 블록에서 프로그램이 실행 중일 때, 종료할 때 특정 상태를 유지해야 하는 경우가 있습니다. 문 블록 그런 다음 이 상태를 종료합니다.

그래서 실제로 컨텍스트 관리자의 작업은 실행 전에 코드 블록을 준비하고 코드 블록이 실행된 후 정리하는 것입니다.

컨텍스트 관리자는 Python 2.5에 추가된 기능으로, 코드를 더 읽기 쉽게 만들고 오류를 줄일 수 있습니다. 다음으로 사용법을 살펴보겠습니다.

2. 컨텍스트 관리자를 사용하는 방법은 무엇입니까?

우리가 일반적으로 파일을 열고 "Hello World"를 어떻게 쓰는지 살펴보는 것이 가장 좋은 학습 방법입니다.

filename = 'my_file.txt'
mode = 'w' # Mode that allows to write to the file
writer = open(filename, mode)
writer.write('Hello ')
writer.write('World')
writer.close()

1-2행에서는 파일 이름과 파일 열기(쓰기) 방법을 지정합니다.

3행에서는 파일을 열고, 4~5행에서는 "Hello world"를 쓰고, 6행에서는 파일을 닫습니다.

이것으로 충분하지 않다면 왜 컨텍스트 관리자가 필요한가요? 그러나 우리는 작지만 중요한 세부 사항을 간과했습니다. 파일을 닫기 위해 6번째 줄에 도달하지 못하면 어떻게 될까요?

예를 들어 디스크가 가득 차서 4행에서 파일에 쓰려고 하면 예외가 발생하고 6행에서는 전혀 실행할 기회가 없습니다.

물론 패키징을 위해 try-finally 문 블록을 사용할 수 있습니다.

writer = open(filename, mode)
try:
  writer.write('Hello ')
  writer.write('World')
finally:
  writer.close()

try와 관계없이 finally 문 블록의 코드 문 블록 무슨 일이 일어나든 실행됩니다. 따라서 파일이 닫히는 것이 보장됩니다. 이렇게 해도 문제가 없나요? 물론 그렇지는 않습니다. 하지만 "Hello world"를 작성하는 것보다 더 복잡한 작업을 수행하면 try-finally 문이 보기 흉해질 수 있습니다. 예를 들어, 두 개의 파일(하나는 읽기용, 다른 하나는 쓰기용)을 열고 두 파일 간에 복사 작업을 수행하려는 경우 with 문을 사용하면 두 파일을 동시에 닫을 수 있습니다.

자, 분석해 보겠습니다.

(1) 먼저 "writer"라는 파일 변수를 만듭니다.

(2) 그런 다음 작가에 대해 몇 가지 작업을 수행합니다.

(3) 마지막으로 작가를 닫습니다.

이게 더 우아하지 않나요?

with open(filename, mode) as writer:
  writer.write('Hello ') 
  writer.write('World')

좀 더 자세히 살펴보겠습니다. "with"는 새로운 키워드이며 항상 컨텍스트 관리자와 함께 나타납니다. "open(filename, mode)"는 이전 코드에 나타났습니다. "as"는 "open" 함수에서 반환된 내용을 참조하고 이를 새 변수에 할당하는 또 다른 키워드입니다. "writer"는 새로운 변수 이름입니다.

2-3줄, 들여쓰기하여 새 코드 블록을 엽니다. 이 코드 블록에서는 작성기에서 모든 작업을 수행할 수 있습니다. 이런 식으로 우리는 코드가 우아하고 안전하다는 것을 보장하는 "개방형" 컨텍스트 관리자를 사용합니다. try-final 작업을 훌륭하게 수행합니다.

open 기능은 간단한 기능과 컨텍스트 관리자로 모두 사용할 수 있습니다. 이는 open 함수가 파일 형식 변수를 반환하고 이 파일 형식이 이전에 사용한 쓰기 메서드를 구현하기 때문입니다. 하지만 이를 컨텍스트 관리자로 사용하려면 다음에 설명할 몇 가지 특수 메서드도 구현해야 합니다. 섹션에서 소개되었습니다.

3. 사용자 정의 컨텍스트 관리자

"개방형" 컨텍스트 관리자를 작성해 보겠습니다.

컨텍스트 관리자를 구현하려면 두 가지 방법을 구현해야 합니다. 하나는 명령문 블록에 들어갈 때 준비 작업을 담당하고, 다른 하나는 명령문 블록을 떠날 때 여파 작업을 담당합니다. 동시에 파일 이름과 열기 방법이라는 두 가지 매개변수가 필요합니다.

Python 클래스에는 __enter__ 및 __exit__(접두사 및 접미사로 이중 밑줄 사용)라는 두 가지 특수 메서드가 포함되어 있습니다.

객체가 컨텍스트 관리자로 사용되는 경우:

(1) 코드 블록을 입력하기 전에 __enter__ 메서드가 호출됩니다.

(2) __exit__ 메서드는 코드 블록을 떠난 후에 호출됩니다(코드 블록에서 예외가 발생하더라도).

다음은 코드 블록에 들어가고 나갈 때 인쇄하는 컨텍스트 관리자의 예입니다.

class PypixContextManagerDemo:
 
  def __enter__(self):
    print 'Entering the block'
 
  def __exit__(self, *unused):
    print 'Exiting the block'
 
with PypixContextManagerDemo():
  print 'In the block'
 
#Output:
#Entering the block
#In the block
#Exiting the block

참고:

(1) 매개변수가 전달되지 않습니다.
(2) 여기서는 "as" 키워드를 사용하지 않습니다.
나중에 __exit__ 메소드의 매개변수 설정에 대해 논의하겠습니다.
클래스에 매개변수를 어떻게 전달하나요? 실제로 모든 클래스에서 __init__ 메서드를 사용할 수 있으며 여기서는 두 가지 필수 매개변수(파일 이름, 모드)를 받도록 다시 작성하겠습니다.

Statement 블록에 들어가면 첫 번째 예와 마찬가지로 open 함수가 사용됩니다. 그리고 명령문 블록을 벗어나면 __enter__ 함수에서 열린 모든 것이 닫힙니다.

코드는 다음과 같습니다.

class PypixOpen:
 
  def __init__(self, filename, mode):
    self.filename = filename
    self.mode = mode
 
  def __enter__(self):
    self.openedFile = open(self.filename, self.mode)
    return self.openedFile
 
  def __exit__(self, *unused):
    self.openedFile.close()
 
with PypixOpen(filename, mode) as writer:
  writer.write("Hello World from our new Context Manager!")

변경된 내용을 살펴보겠습니다.

(1) 3-5 OK, 매개변수 2개 __init__을 통해 수신됩니다.

(2) 7~9행, 파일을 열고 돌아옵니다.

(3)12行,当离开语句块时关闭文件。

(4)14-15行,模仿open使用我们自己的上下文管理器。

除此之外,还有一些需要强调的事情:

4.如何处理异常

我们完全忽视了语句块内部可能出现的问题。

如果语句块内部发生了异常,__exit__方法将被调用,而异常将会被重新抛出(re-raised)。当处理文件写入操作时,大部分时间你肯定不希望隐藏这些异常,所以这是可以的。而对于不希望重新抛出的异常,我们可以让__exit__方法简单的返回True来忽略语句块中发生的所有异常(大部分情况下这都不是明智之举)。

我们可以在异常发生时了解到更多详细的信息,完备的__exit__函数签名应该是这样的:

def __exit__(self, exc_type, exc_val, exc_tb)

这样__exit__函数就能够拿到关于异常的所有信息(异常类型,异常值以及异常追踪信息),这些信息将帮助异常处理操作。在这里我将不会详细讨论异常处理该如何写,以下是一个示例,只负责抛出SyntaxErrors异常。

class RaiseOnlyIfSyntaxError:
 
  def __enter__(self):
    pass
 
  def __exit__(self, exc_type, exc_val, exc_tb):
    return SyntaxError != exc_type


捕获异常:
当一个异常在with块中抛出时,它作为参数传递给__exit__。三个参数被使用,和sys.exc_info()返回的相同:类型、值和回溯(traceback)。当没有异常抛出时,三个参数都是None。上下文管理器可以通过从__exit__返回一个真(True)值来“吞下”异常。例外可以轻易忽略,因为如果__exit__不使用return直接结束,返回None——一个假(False)值,之后在__exit__结束后重新抛出。

捕获异常的能力创造了有意思的可能性。一个来自单元测试的经典例子——我们想确保一些代码抛出正确种类的异常:

class assert_raises(object):
  # based on pytest and unittest.TestCase
  def __init__(self, type):
    self.type = type
  def __enter__(self):
    pass
  def __exit__(self, type, value, traceback):
    if type is None:
      raise AssertionError('exception expected')
    if issubclass(type, self.type):
      return True # swallow the expected exception
    raise AssertionError('wrong exception type')

with assert_raises(KeyError):
  {}['foo']

5. 谈一些关于上下文库(contextlib)的内容

contextlib是一个Python模块,作用是提供更易用的上下文管理器。

(1)contextlib.closing

假设我们有一个创建数据库函数,它将返回一个数据库对象,并且在使用完之后关闭相关资源(数据库连接会话等)

我们可以像以往那样处理或是通过上下文管理器:

with contextlib.closing(CreateDatabase()) as database:
  database.query()

contextlib.closing方法将在语句块结束后调用数据库的关闭方法。

(2)contextlib.nested

另一个很cool的特性能够有效地帮助我们减少嵌套:

假设我们有两个文件,一个读一个写,需要进行拷贝。

以下是不提倡的:

with open('toReadFile', 'r') as reader:
  with open('toWriteFile', 'w') as writer:
    writer.writer(reader.read())

可以通过contextlib.nested进行简化:

with contextlib.nested(open('fileToRead.txt', 'r'),
            open('fileToWrite.txt', 'w')) as (reader, writer):
  writer.write(reader.read())

在Python2.7中这种写法被一种新语法取代:

with open('fileToRead.txt', 'r') as reader, \
    open('fileToWrite.txt', 'w') as writer:
    writer.write(reader.read())
contextlib.contextmanager

对于Python高级玩家来说,任何能够被yield关键词分割成两部分的函数,都能够通过装饰器装饰的上下文管理器来实现。任何在yield之前的内容都可以看做在代码块执行前的操作,而任何yield之后的操作都可以放在exit函数中。

这里我举一个线程锁的例子:

下面是线程安全写函数的例子:

import threading
 
lock = threading.Lock()
 
def safeWriteToFile(openedFile, content):
  lock.acquire()
  openedFile.write(content)
  lock.release()

接下来,让我们用上下文管理器来实现,回想之前关于yield和contextlib的分析:

@contextlib.contextmanager
def loudLock():
  print 'Locking'
  lock.acquire()
  yield
  print 'Releasing'
  lock.release()
 
with loudLock():
  print 'Lock is locked: %s' % lock.locked()
  print 'Doing something that needs locking'
 
#Output:
#Locking
#Lock is locked: True
#Doing something that needs locking
#Releasing

特别注意,这不是异常安全(exception safe)的写法。如果你想保证异常安全,请对yield使用try语句。幸运的是threading。lock已经是一个上下文管理器了,所以我们只需要简单地:

@contextlib.contextmanager
def loudLock():
  print 'Locking'
  with lock:
    yield
  print 'Releasing'

因为threading.lock在异常发生时会通过__exit__函数返回False,这将在yield被调用是被重新抛出。这种情况下锁将被释放,但对于“print ‘Releasing'”的调用则不会被执行,除非我们重写try-finally。

如果你希望在上下文管理器中使用“as”关键字,那么就用yield返回你需要的值,它将通过as关键字赋值给新的变量。下面我们就仔细来讲一下。

6.使用生成器定义上下文管理器
当讨论生成器时,据说我们相比实现为类的迭代器更倾向于生成器,因为它们更短小方便,状态被局部保存而非实例和变量中。另一方面,正如双向通信章节描述的那样,生成器和它的调用者之间的数据流可以是双向的。包括异常,可以直接传递给生成器。我们想将上下文管理器实现为特殊的生成器函数。事实上,生成器协议被设计成支持这个用例。

@contextlib.contextmanager
def some_generator(<arguments>):
  <setup>
  try:
    yield <value>
  finally:
    <cleanup>

contextlib.contextmanager装饰一个生成器并转换为上下文管理器。生成器必须遵循一些被包装(wrapper)函数强制执行的法则——最重要的是它至少yield一次。yield之前的部分从__enter__执行,上下文管理器中的代码块当生成器停在yield时执行,剩下的在__exit__中执行。如果异常被抛出,解释器通过__exit__的参数将之传递给包装函数,包装函数于是在yield语句处抛出异常。通过使用生成器,上下文管理器变得更短小精炼。

让我们用生成器重写closing的例子:

@contextlib.contextmanager
def closing(obj):
  try:
    yield obj
  finally:
    obj.close()

再把assert_raises改写成生成器:

@contextlib.contextmanager
def assert_raises(type):
  try:
    yield
  except type:
    return
  except Exception as value:
    raise AssertionError(&#39;wrong exception type&#39;)
  else:
    raise AssertionError(&#39;exception expected&#39;)

这里我们用装饰器将生成函数转化为上下文管理器!

更多Python中的上下文管理器相关文章请关注PHP中文网!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.