Maison  >  Article  >  développement back-end  >  Gestionnaires de contexte en Python

Gestionnaires de contexte en Python

高洛峰
高洛峰original
2017-03-01 14:16:441095parcourir

En Python, les objets qui appellent la méthode __enter__ avant d'entrer dans le bloc de code et appellent la méthode __exit__ après avoir quitté le bloc de code sont utilisés comme gestionnaires de contexte. Dans cet article, nous analyserons en profondeur le gestionnaire de contexte en Python et y jetterons un œil. au contexte. Le rôle et l'usage des managers :

1. Qu'est-ce qu'un gestionnaire de contexte ?

Par exemple, lorsque vous écrivez du code Python, vous placez souvent une série d'opérations dans un bloc d'instructions :

(1) Lorsqu'une certaine condition est vraie – exécutez ce bloc d'instructions

(2) Lorsqu'une certaine condition est vraie – boucle pour exécuter ce bloc d'instructions

Parfois, nous devons maintenir un certain état lorsque le programme s'exécute dans le bloc d'instructions et en quittant le bloc d'instructions Puis terminez cet état.

Donc, en fait, la tâche du gestionnaire de contexte est de préparer le bloc de code avant l'exécution et de nettoyer après l'exécution du bloc de code.

Le gestionnaire de contexte est une fonctionnalité ajoutée à Python 2.5, qui peut rendre votre code plus lisible et contenir moins d'erreurs. Voyons ensuite comment l'utiliser.

2. Comment utiliser le gestionnaire de contexte ?

Regarder le code est la meilleure façon d'apprendre. Voyons comment nous ouvrons habituellement un fichier et écrivons « 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()

Lignes 1-2, nous précisons le nom du fichier et comment l'ouvrir (écrire).

La ligne 3 ouvre le fichier, les lignes 4 à 5 écrivent "Hello world" et la ligne 6 ferme le fichier.

Si cela ne suffit pas, pourquoi avez-vous besoin d'un gestionnaire de contexte ? Mais nous avons oublié un petit détail important : que se passe-t-il si nous n'arrivons jamais à la ligne 6 pour fermer le fichier ?

Par exemple, le disque est plein, donc une exception est levée lorsque nous essayons d'écrire dans le fichier sur la ligne 4, et la ligne 6 n'a aucune chance de s'exécuter.

Bien sûr, nous pouvons utiliser des blocs d'instructions try-finally pour l'empaquetage :

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

Le code dans le bloc d'instructions final indépendamment de l'instruction try Tout ce qui se passe dans le bloc sera exécuté. Il est donc garanti que le dossier sera fermé. Y a-t-il un problème à faire cela ? Bien sûr que non, mais lorsque nous faisons quelque chose de plus complexe que d'écrire « Hello World », les déclarations try-finally peuvent devenir laides. Par exemple, si nous voulons ouvrir deux fichiers, un pour la lecture et un pour l'écriture, et effectuer des opérations de copie entre les deux fichiers, alors l'instruction with peut garantir que les deux peuvent être fermés en même temps.

OK, décomposons les choses :

(1) Tout d'abord, créez une variable de fichier nommée "writer".

(2) Ensuite, effectuez quelques opérations sur l'écrivain.

(3) Enfin, fermez l'écrivain.

N'est-ce pas plus élégant ?

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

Creusons un peu plus, « with » est un nouveau mot-clé et apparaît toujours avec les gestionnaires de contexte. "open(filename, mode)" est apparu dans le code précédent. "as" est un autre mot-clé qui fait référence au contenu renvoyé par la fonction "open" et l'assigne à une nouvelle variable. "writer" est un nouveau nom de variable.

2-3 lignes, indentez pour ouvrir un nouveau bloc de code. Dans ce bloc de code, nous pouvons effectuer n'importe quelle opération sur le rédacteur. De cette façon, nous utilisons le gestionnaire de contexte « ouvert », qui garantit que notre code est à la fois élégant et sûr. Il accomplit avec brio la tâche d’essai final.

La fonction open peut être utilisée aussi bien comme fonction simple que comme gestionnaire de contexte. En effet, la fonction open renvoie une variable de type de fichier, et ce type de fichier implémente la méthode d'écriture que nous avons utilisée auparavant, mais si vous souhaitez l'utiliser comme gestionnaire de contexte, vous devez également implémenter certaines méthodes spéciales, dont je parlerai ensuite. introduit dans la section.

3. Gestionnaire de contexte personnalisé

Écrivons un gestionnaire de contexte « ouvert ».

Pour implémenter un gestionnaire de contexte, deux méthodes doivent être implémentées : l'une est responsable de l'opération de préparation lors de l'entrée dans le bloc d'instructions, et l'autre est responsable de l'opération consécutive lors de la sortie du bloc d'instructions. En même temps, nous avons besoin de deux paramètres : le nom du fichier et la méthode d'ouverture.

La classe Python contient deux méthodes spéciales, nommées : __enter__ et __exit__ (doubles traits de soulignement comme préfixe et suffixe).

Lorsqu'un objet est utilisé comme gestionnaire de contexte :

(1) La méthode __enter__ sera appelée avant de saisir le bloc de code.

(2) La méthode __exit__ est appelée après avoir quitté le bloc de code (même si une exception est rencontrée dans le bloc de code).

Voici un exemple de gestionnaire de contexte qui s'imprime lors de la saisie et de la sortie d'un bloc de code.

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

Notez quelque chose :

(1) Aucun paramètre n'est transmis.
(2) Le mot-clé "as" n'est pas utilisé ici.
Nous discuterons plus tard des paramètres de la méthode __exit__.
Comment transmettre des paramètres à une classe ? En fait, dans n'importe quelle classe, vous pouvez utiliser la méthode __init__, et ici nous allons la réécrire pour recevoir deux paramètres nécessaires (nom de fichier, mode).

Lorsque nous entrons dans le bloc d'instructions, la fonction open sera utilisée, tout comme dans le premier exemple. Et lorsque nous quittons le bloc d'instructions, tout ce qui est ouvert dans la fonction __enter__ sera fermé.

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

Voyons ce qui change :

(1) 3 Ligne -5 , reçoit deux paramètres via __init__.

(2) Lignes 7 à 9, ouvrez le fichier et revenez.

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


Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn