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

Possible to change object referenced in with statement but retain context functionality?

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

使用特定的 class 读取数据库文件,例如 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 class 有必要的 __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

我想下面的方法可行,但它是否理想,或者有其他方法吗?有没有办法单独保留 with 围绕 Database 对象构造,以便在出现异常时进行清理?

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)

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

作为一个简化的概念:

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)

在进一步考虑之后,我会建议一个答案作为我在问题中建议的解决方案的转折。使用 Database class 的继承将 __enter__ 的最终 return 值完全耦合到所需的 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()

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

我仍然很想知道是否有人有更好的选择。

我只想添加一个标志并将其放入 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