Heim >Backend-Entwicklung >Python-Tutorial >Kontextmanager in Python

Kontextmanager in Python

高洛峰
高洛峰Original
2017-03-01 14:16:441114Durchsuche

In Python werden Objekte, die die Methode __enter__ aufrufen, bevor sie in den Codeblock eintreten, und die Methode __exit__ aufrufen, nachdem sie den Codeblock verlassen, als Kontextmanager verwendet. In diesem Artikel werden wir den Kontextmanager in Python eingehend analysieren und einen Blick darauf werfen im Kontext. Die Rolle und Verwendung von Managern:

1.

Wenn Sie beispielsweise Python-Code schreiben, fügen Sie häufig eine Reihe von Operationen in einen Anweisungsblock ein:

(1) Wenn eine bestimmte Bedingung wahr ist – führen Sie diesen Anweisungsblock aus

(2) Wenn eine bestimmte Bedingung wahr ist – Schleife zum Ausführen dieses Anweisungsblocks

Manchmal müssen wir einen bestimmten Zustand aufrechterhalten, wenn das Programm im Anweisungsblock ausgeführt wird und wenn wir den verlassen Anweisungsblock Beenden Sie dann diesen Zustand.

Die Aufgabe des Kontextmanagers besteht also tatsächlich darin, den Codeblock vor der Ausführung vorzubereiten und nach der Ausführung des Codeblocks zu bereinigen.

Kontextmanager ist eine in Python 2.5 hinzugefügte Funktion, die Ihren Code lesbarer machen und weniger Fehler verursachen kann. Schauen wir uns als Nächstes an, wie man es verwendet.

2. Wie verwende ich den Kontextmanager?

Das Anschauen von Code ist der beste Weg, um zu lernen. Sehen wir uns an, wie wir normalerweise eine Datei öffnen und „Hallo Welt“ schreiben.

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

In den Zeilen 1-2 geben wir den Dateinamen an und wie man ihn öffnet (schreibt).

Zeile 3 öffnet die Datei, Zeilen 4-5 schreiben „Hello world“ und Zeile 6 schließt die Datei.

Wenn das nicht ausreicht, warum brauchen Sie dann einen Kontextmanager? Aber wir haben ein kleines, aber wichtiges Detail übersehen: Was passiert, wenn wir nie zu Zeile 6 kommen, um die Datei zu schließen?

Zum Beispiel ist die Festplatte voll, sodass eine Ausnahme ausgelöst wird, wenn wir versuchen, in Zeile 4 in die Datei zu schreiben, und Zeile 6 überhaupt keine Chance zur Ausführung hat.

Natürlich können wir Try-finally-Anweisungsblöcke zum Verpacken verwenden:

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

Der Code im finally-Anweisungsblock unabhängig davon die try-Anweisung Was auch immer im Block passiert, wird ausgeführt. Somit ist gewährleistet, dass die Datei geschlossen wird. Gibt es dabei ein Problem? Natürlich nicht, aber wenn wir etwas Komplexeres tun, als „Hallo Welt“ zu schreiben, können try-finally-Anweisungen hässlich werden. Wenn wir beispielsweise zwei Dateien öffnen möchten, eine zum Lesen und eine zum Schreiben, und Kopiervorgänge zwischen den beiden Dateien ausführen möchten, kann die with-Anweisung sicherstellen, dass beide gleichzeitig geschlossen werden können.

OK, lassen Sie uns die Dinge aufschlüsseln:

(1) Erstellen Sie zunächst eine Dateivariable mit dem Namen „writer“.

(2) Führen Sie dann einige Vorgänge am Writer aus.

(3) Schließen Sie abschließend den Writer.

Ist das nicht eleganter?

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

Lassen Sie uns etwas tiefer gehen: „with“ ist ein neues Schlüsselwort und erscheint immer bei Kontextmanagern. „open(filename, mode)“ erschien im vorherigen Code. „as“ ist ein weiteres Schlüsselwort, das auf den von der Funktion „open“ zurückgegebenen Inhalt verweist und ihn einer neuen Variablen zuweist. „Writer“ ist ein neuer Variablenname.

2-3 Zeilen, einrücken, um einen neuen Codeblock zu öffnen. In diesem Codeblock können wir jede beliebige Operation am Writer ausführen. Auf diese Weise verwenden wir den „offenen“ Kontextmanager, der dafür sorgt, dass unser Code sowohl elegant als auch sicher ist. Es bewältigt die Try-Endlich-Aufgabe hervorragend.

Die Open-Funktion kann sowohl als einfache Funktion als auch als Kontextmanager verwendet werden. Dies liegt daran, dass die Öffnungsfunktion eine Dateitypvariable zurückgibt und dieser Dateityp die zuvor verwendete Schreibmethode implementiert. Wenn Sie ihn jedoch als Kontextmanager verwenden möchten, müssen Sie auch einige spezielle Methoden implementieren, die ich als Nächstes besprechen werde. im Abschnitt vorgestellt.

3. Benutzerdefinierter Kontextmanager

Lassen Sie uns einen „offenen“ Kontextmanager schreiben.

Um einen Kontextmanager zu implementieren, müssen zwei Methoden implementiert werden – eine ist für die Vorbereitungsoperation beim Betreten des Anweisungsblocks verantwortlich und die andere ist für die Nachwirkungsoperation beim Verlassen des Anweisungsblocks verantwortlich. Gleichzeitig benötigen wir zwei Parameter: Dateiname und Öffnungsmethode.

Die Python-Klasse enthält zwei spezielle Methoden mit den Namen: __enter__ und __exit__ (doppelte Unterstriche als Präfix und Suffix).

Wenn ein Objekt als Kontextmanager verwendet wird:

(1) Die Methode __enter__ wird aufgerufen, bevor der Codeblock eingegeben wird.

(2) Die Methode __exit__ wird nach dem Verlassen des Codeblocks aufgerufen (auch wenn im Codeblock eine Ausnahme auftritt).

Hier ist ein Beispiel für einen Kontextmanager, der beim Betreten und Verlassen eines Codeblocks druckt.

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

Beachten Sie etwas:

(1) Es werden keine Parameter übergeben.
(2) Das Schlüsselwort „as“ wird hier nicht verwendet.
Wir werden die Parametereinstellungen der __exit__-Methode später besprechen.
Wie übergeben wir Parameter an eine Klasse? Tatsächlich können Sie in jeder Klasse die Methode __init__ verwenden. Hier werden wir sie umschreiben, um zwei notwendige Parameter (Dateiname, Modus) zu erhalten.

Wenn wir den Anweisungsblock betreten, wird die Öffnungsfunktion verwendet, genau wie im ersten Beispiel. Und wenn wir den Anweisungsblock verlassen, wird alles, was in der Funktion __enter__ geöffnet wurde, geschlossen.

Hier ist unser Code:

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

Mal sehen, was sich ändert:

(1) 3 Zeile -5 , empfängt zwei Parameter über __init__.

(2) Zeilen 7-9, öffnen Sie die Datei und kehren Sie zurück.

(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中文网!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn