可以更改 with 语句中引用的对象但保留上下文功能?

Posted

技术标签:

【中文标题】可以更改 with 语句中引用的对象但保留上下文功能?【英文标题】:Possible to change object referenced in with statement but retain context functionality? 【发布时间】:2022-01-11 21:20:52 【问题描述】:

我正在尝试找到一种方法来使用with 语句来打开几个大型数据库文件中的一个。出于这个问题的目的,我们可以假设我们希望确保文件在不再需要时关闭,因为底层库可以一次打开的数据库文件的数量是有限的。

使用特定类读取数据库文件,例如Database,它不会在实例化时读取所有数据库内容,而是仅读取有关数据库的元数据。此元数据用于确定数据库是否有效。 (仅供参考)只有在请求特定数据时,它才会进一步询问(打开的)文件。

这是一个如何在没有with 上下文的情况下进行编码的最小示例:

file='file1.dh'         # don't read into the extension; it's just an example
dataobj=Database(file)  # Database was created specifically to read files of .dh extension
if not dataobj.isvalid:
  dataobj.close()
  file='file2.dh'
  dataobj=Database(file)

# ... then proceed with remainder of calculation
# and finally...

databoj.close()

Database 类具有必要的__enter____exit__ 方法以允许它在with 构造中使用,这是可取的。例如(伪代码):

class Database():
  def __init__(self,file):
    self.read_metadata()
  def read_metadata(self):
    # ... read metadata from file
  def close(self):
    # ... do some necessary cleanup and disconnect from underlying database reader
  def __enter__(self):
    return self
  def __exit__(self,typ,value,traceback):
    self.close()

这个问题背后的真正动机是 - 因为即使从这些大文件中读取元数据也需要很长时间 - 非常不希望 (1) 打开找到的第一个有效文件不止一次,或者 (2)打开比需要更多的文件(即不应浪费打开多个文件的资源)。

希望一旦找到有效文件,我们可以继续在with 上下文中打开该文件。一般可以看出,这可以扩展到无限数量的文件,但这不是这个问题所必需的。

例如,使用with 语句,我们可以这样写

with Database('file1.dh') as dh:
  if dh.isvalid:
    file='file1.dh'
  else:
    file='file2.dh'
with Database(file) as dh:
  # ...proceed with calculation

这要求file1.dh在有效时打开两次

在仍然使用with 语句时可以避免吗?也请随意提出一些我可能不知道的建议(即,我不想排除不使用 with 但仍保留上下文管理器功能的可能方法)。

我将在工作中再投入一把扳手...目前,第二个文件“file2.dh”是未知的(它是计算出来的),首选保留它方式(即,如果不需要,甚至不知道第二个文件的身份,可以节省一些计算。如果发现第一个文件是有效的)。

我将在这里放置一个示例伪代码,从概念上讲,我想要实现的目标:

with FirstValidFile('file1.dh','file2.dh') as dh:
  # ... do all computation

如果file1.dh 无效,则file2.dh 替换dh,否则以file1.dh 继续计算。

我想下面的方法会起作用,但它是理想的,还是有其他方法?有没有办法在 Database 对象周围单独保留 with 构造,以便在出现异常时进行清理?

class FirstValidFile():
  def __init__(self,*potential_files):
    for file in potential_files:
      dh=Database(file)
      if dh.isvalid:
        self.dh=dh
        return
      dh.close()

  def __enter__(self):
    return self.dh
  def __exit__(self,a,b,c):
    self.dh.__exit__(a,b,c)

【问题讨论】:

【参考方案1】:

我只需添加一个标志并将其放入while 循环中。如果您愿意,也可以使用 break 语句进行无限循环,但我认为它的可维护性稍差。

file = 'file1.dh'
valid_file_found= False
while not valid_file_found:
    with Database(file) as dh:
        if dh.isvalid:
            # do calculations
            valid_file_found= True
        else:
            file = # calculate file name      

【讨论】:

哇。我真的把这一切都搞砸了。我更喜欢break/continue 方法,但这只是为了避免过度缩进。谢谢。【参考方案2】:

我认为这个问题的答案相当简单:您需要做的就是将确定所选数据库对象的所有初始代码放入一个函数中,然后返回“获胜者”。因为那是实际的上下文管理器对象,所以您将保留所需的属性。

作为一个简化的概念:

import random
from dataclasses import dataclass


@dataclass
class Database:

    name: str

    def __enter__(self):
        print("__enter__", self)
        return self

    def __exit__(self, *args):
        print("__exit__", self)

def chose_database(*args):
    return Database(random.choice(args))


with chose_database("foo", "bar", "baz") as db:
    print("the chosen db", db)

【讨论】:

感谢您的回答,但是决定使用哪个文件的函数是这里的症结所在。按照你的方式,你必须打开文件两次:一次是为了确定它是有效的,然后再一次。【参考方案3】:

在进一步考虑之后,我会提出一个答案,作为我在问题中建议的解决方案的一个转折点。使用Database 类的继承将__enter__ 的最终返回值完全耦合到所需的Database 对象,但通过super 函数。

class FirstValidFile(Database):
  def __init__(self,*potential_files):
    for file in potential_files:
      super().__init__(self,file)
      if self.isvalid:
        return
      self.close()

这满足所有要求。在实例化过程中仍有可能引发异常,但无论如何都存在。

我仍然有兴趣看看是否有人有更好的选择。

【讨论】:

以上是关于可以更改 with 语句中引用的对象但保留上下文功能?的主要内容,如果未能解决你的问题,请参考以下文章

让对象支持with语句

让对象支持with语句

如何用python 中with 用法

我可以将 with 语句与 MySQLdb.Connection 对象一起使用吗?

Python教程:with语句的用法

Python中with用法详解