不使用 "with" 语句块的情况下使用上下文管理器

22

下面是我的my_create方法的示例,以及该方法在使用时的示例。


@contextmanager
def my_create(**attributes):
    obj = MyObject(**attributes)
    yield obj
    obj.save()

with my_create(a=10) as new_obj:
     new_obj.b = 7

new_obj.a  # => 10
new_obj.b  # => 7
new_obj.is_saved()  # => True

对于Ruby/Rails用户来说,这可能看起来很熟悉。它类似于ActiveRecord::create方法,其中with块内的代码充当了一个块。

但是:

with my_create(a=10) as new_obj:
    pass

new_obj.a  # => 10
new_obj.is_saved()  # => True
在上面的例子中,我把一个空的“block”传递给我的my_create函数。虽然格式看起来有点奇怪,而且with块似乎没有必要,但事情按预期工作了(my_obj已被初始化并保存)。
我希望能够直接调用my_create,而不必设置一个passing with块。不幸的是,根据我当前的my_create实现,这是不可能的。
my_obj = create(a=10)
my_obj  # => <contextlib.GeneratorContextManager at 0x107c21050>

为了获得我想要的结果,我需要在GeneratorContextManager上调用__enter____exit__两个方法。

问题:

有没有一种方法可以编写我的my_create函数,使其可以被调用时带有可选的“块”作为参数?我不想向my_create传递可选的函数。我希望my_create可以可选地将执行权转移到代码块中。

解决方案不一定涉及withcontextmanager。例如,可以使用generatorfor循环实现与上述相同的结果,尽管语法变得更加不清晰。

目前我担心的是,可能并不存在一个足够易读且可用的解决方案,但我仍然很想看看每个人都能想出什么来。

一些澄清:

另一个示例是:

@contextmanager
def header_file(path):
    touch(path)
    f = open(path, 'w')
    f.write('This is the header')
    yield f
    f.close()

with header_file('some/path') as f:
    f.write('some more stuff')

another_f = header_file('some/other/path')

我总是想做上下文管理器的__enter____exit__部分。但并不总是要提供一个代码块。如果不必要,我就不想设置一个passing with块。

Ruby中可以实现这一点,并且非常容易。如果在Python中也能实现这一点就很酷了,因为我们已经很接近了(我们只需要设置一个passing with块)。我知道语言机制使它很难(技术上不可能?),但接近解决方案对我来说很有趣。


1
如果有人感兴趣,这个问题的灵感来自于我的这篇文章:http://wiki.c2.com/?BlocksInPython - chmod 777 j
你需要用try finally包装yield obj以实现异常安全。 - Neil G
不依赖于 @contextmanager 并在定义自己的 __enter____exit__ 的同时,提供一个可用于未给出块时的方法可能是一种选择。 - minmaxavg
5个回答

7
MyObject上添加一个新的方法,该方法可以创建并保存。
class MyObject:

    @classmethod
    def create(cls, **attributes):
        obj = cls(**attributes)
        obj.save()
        return obj

这是一种备用的初始化程序,即工厂,该设计模式在Python标准库和许多流行框架中都有先例。Django模型使用此模式,其中备用初始化程序Model.create(**args)可以提供通常Model(**args)不具备的其他功能(例如持久化到数据库)。

是否有一种方法可以编写my_create函数,使其可以带有可选的“块”作为“参数”进行调用?

没有。


1
好的,我刚刚编辑了以回答你字面上的问题。 - wim
4
不不不,Python并不是Ruby。在Python中传递代码块并不是一个常见的做法,而且yield在Python中意义完全不同于Ruby。不行。 - user2357112
@user2357112。实际上,yield在Python中的意思与Ruby中几乎相同。在执行某些代码(如循环、环境设置/拆卸)期间,将一些对象传递给其他代码块(例如Ruby代码块或您的for或with语句中的Python代码块),然后可能获取返回值并继续执行原始代码。 - chmod 777 j
@j0eb:不,yield的意思完全不同。在Ruby中,yield的意思是“调用块”。在Python中,yield的意思是函数是一个生成器函数,并且生成器的堆栈帧应该被暂停并从堆栈中删除,nextsend应该返回给yield给定的参数。 - user2357112
确实,存在生成器/函数的二分法,但我们并不是在问“Python函数和Python生成器有什么不同?”生成器与Ruby中使用yield的函数并没有太大区别。只是你基本上需要将生成器放在withfor块中才能发挥作用。当然,你也可以调用next,但那有点奇怪,通常不会这样做。 - chmod 777 j
显示剩余9条评论

3

好的,似乎有些混淆了,我被迫提供一个示例解决方案。以下是到目前为止我能想到的最好的解决方案。

class my_create(object):
    def __new__(cls, **attributes):
        with cls.block(**attributes) as obj:
            pass

        return obj

    @classmethod
    @contextmanager
    def block(cls, **attributes):
        obj = MyClass(**attributes)
        yield obj
        obj.save()

如果我们按照上述方式设计my_create,我们可以在不使用块的情况下正常使用它:
new_obj = my_create(a=10)
new_obj.a  # => 10
new_obj.is_saved()  # => True

我们可以用一个块稍微不同地称呼它。

with my_create.block(a=10) as new_obj:
    new_obj.b = 7

new_obj.a  # => 10
new_obj.b  # => 7
new_obj.saved  # => True

调用my_create.block有点类似于调用Celery任务Task.s,不想用块调用my_create的用户可以正常调用它,所以我会允许这样做。

然而,这个my_create的实现看起来有点奇怪,因此我们可以创建一个包装器,使其更像问题中context_manager(my_create)的实现。

import types

# The abstract base class for a block accepting "function"
class BlockAcceptor(object):
    def __new__(cls, *args, **kwargs):
        with cls.block(*args, **kwargs) as yielded_value:
            pass

        return yielded_value

    @classmethod
    @contextmanager
    def block(cls, *args, **kwargs):
        raise NotImplementedError

# The wrapper
def block_acceptor(f):
    block_accepting_f = type(f.func_name, (BlockAcceptor,), {})
    f.func_name = 'block'
    block_accepting_f.block = types.MethodType(contextmanager(f), block_accepting_f)
    return block_accepting_f

然后,my_create 变成了:
@block_acceptor
def my_create(cls, **attributes):
    obj = MyClass(**attributes)
    yield obj
    obj.save()

使用中:

# creating with a block
with my_create.block(a=10) as new_obj:
    new_obj.b = 7

new_obj.a  # => 10
new_obj.b  # => 7
new_obj.saved  # => True


# creating without a block
new_obj = my_create(a=10)
new_obj.a  # => 10
new_obj.saved  # => True

理想情况下,my_create 函数不需要接受 cls,而是由 block_acceptor 包装器处理,但是我现在没有时间进行这些更改。
Python风格?不是很好。有用吗?可能吧?
我仍然很感兴趣看看别人能想出什么。

3
我建议使用不同的函数来获取一个上下文管理器,该管理器在 __exit__ 上保存对象,并自动保存对象。没有一种简单的方法可以让一个函数完成这两件事(除了你说不想要的函数之外,没有其他“块”可以传递)。例如,您可以创建第二个函数,仅创建并立即保存对象,而无需运行任何其他代码以在其间运行:
def create_and_save(**args):
    obj = MyObject(**args)
    obj.save()
    return obj

所以,您可以使用两个函数使其工作。但更加Pythonic的方法可能是摆脱上下文管理器函数,并使MyObject类成为其自身的上下文管理器。您可以给它非常简单的__enter____exit__方法:

def __enter__(self):
    return self

def __exit__(self, exception_type, exception_value, traceback):
    if exception_type is None:
        self.save()

您的第一个示例将变为:

with MyObject(a=10) as new_obj:
     new_obj.b = 7

您还可以将我上面展示的create_and_save函数转换为一个classmethod:

@classmethod
def create_and_save(cls, **args):
    obj = cls(**args)
    obj.save()
    return obj

你的第二个例子将会是这样:

你的第二个例子将是:

new_obj = MyObject.create_and_save(a=10)

这两种方法都可以写在一个基类中,然后被其他类继承,所以不需要一遍遍地重写它们。


嘿,感谢您的回答。我已经更新了我的问题,并进行了“一些澄清”,因为似乎这更适合于一个单一的目的:“我想能够创建和保存一个对象问题”。 - chmod 777 j
2
你的 header_file 函数实际上是将上下文管理器放错了位置的另一个例子。文件已经是上下文管理器(在 __exit__ 中自动关闭)。因此,将 header_file 设为普通函数,返回一个已写入标题行的打开文件即可。然后,您可以在 with 语句中使用它(文件作为上下文管理器),或者只需调用该函数并在之后自己调用 close 关闭文件。 - Blckknght
谢谢回复。但是我之后必须自己调用close来关闭文件。现在我的文件没有被上下文管理,我只是手动管理上下文,因为我不想处理文件的任何额外操作,比如写入'some more stuff' - chmod 777 j
就好像,一旦我不想在“with”块内做任何事情,我就必须突然管理所有的上下文,或在代码中放置一个不祥的“with”-“pass”块。 - chmod 777 j
要么我必须打破DRY原则并重复所有方法,以便它们被包装为上下文管理器,要么它们执行完全相同的操作,只是它们没有那个使它们作为上下文管理器工作的小yield语句。 - chmod 777 j

1

稍作修改,你就可以接近你想要的东西,只是不能使用contextlib.contextmanager来实现:

creator = build_creator_obj()

# "with" contextmanager interface
with creator as obj:
     obj.attr = 'value'

# "call" interface
obj = creator(attr='value')

creator 是一个实现了 __enter____exit__ 的对象,用于第一种用法,并实现了 __call__ 用于第二种用法。

你也可以将 creator 的构造隐藏在某个持久化对象的 property 中,例如:

class MyDatabase():
    @property
    def create(self):
        return build_creator_obj()

db = MyDatabase()

# so that you can do either/both:
with db.create as obj:
  obj.attr = 'value'

obj = db.create(attr='value')

0

目前还没有不需要至少两个不同调用签名的解决方案。

这里有一个使用create()内部标志的解决方案:

class MyObject:
    def __init__(self, a, b):
        self.a = a
        self.b = b
        self.saved = True
        self.in_context = False

    def __enter__(self):
        if not self.in_context:
            raise TypeError(f"Context manager only supported with MyObject::create(..., in_context=True)")

        self.saved = False
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.saved = True
        self.in_context = False

    @classmethod
    def create(cls, a, b, in_context=False):
        product = cls(a, b)
        product.in_context = in_context
        return product

    def __eq__(self, other: object) -> bool:
        return isinstance(other, MyObject) and self.a == other.a and self.b == other.b

>>> with MyObject.create(1, 2, in_context=True) as product:
...     product.a=2
... 
>>> assert product == MyObject.create(2, 2)
>>> assert product.saved

>>> # We don't want this to work outside of MyObject::create()
>>> with product:
...     product.a=3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in __enter__
TypeError: Context manager only supported with MyObject::create(..., in_context=True)

>>> # Should fail since in_context is False
>>> with MyObject.create(1, 2) as product:
...     product.a=2
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in __enter__
TypeError: Context manager only supported with MyObject::create(..., in_context=True) 

然而,只要你不介意with MyObject()with MyObject.create()的行为相同,你可以完全不使用in_context来实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接