Python:实体变更的上下文管理器?

0 投票
2 回答
40 浏览
提问于 2025-04-14 16:44

我想建立一个代码模式,让我能做一些类似下面的事情:

# Foo has getter-only properties
foo: Foo = FooRepo.get(id="foo_id")
assert foo.bar == "original_bar"

# MutableFoo is a window onto the Foo, and when it __exit__s, it persists to the repo
with foo.mutable() as mutable_foo:
    mutable_foo.bar = "new_bar"

# We've updated the Foo, as well as whatever persistent version the FooRepo owns
assert foo.bar == "new_bar"

我对具体的代码模式并不执着。我喜欢这个模式的原因有:

  • 我们可以把 Foo 传递到很多代码的地方,只要不调用 mutable(),我们就可以把它当作不可变的,忽略持久化的问题。
  • 我们可以在 ContextManager 中以多种方式处理事务性。离开 ContextManager 后,我们可以把这个对象当作快照,这样使用起来更常见,也更简单。
  • 调用者基本上可以忽略持久化的事情。

我看到的一些挑战:

  • 需要一种优雅的方法来防止在 with 块外创建可变版本。
  • 同样需要一种方法来处理与 Foo 的接口,这样 MutableFoo 才能进行修改。(你能看出来我习惯用 Java 吗?没有内部类来访问私有成员让我有点困惑)
  • 需要一种优雅的方法来进行错误检查。由于持久化是在退出上下文时发生的,这可能会出现异常,我们需要妥善处理这些异常。

有没有人用 Python 建立过这种框架?你们喜欢什么解决方案?

2 个回答

1

你可以创建一个“不可变”的代理对象,这个对象会让设置值和删除值的方法抛出异常,也就是说不允许修改。你可以实现一个上下文管理器,这样在进入这个上下文时,可以返回原来的可变对象,以便在这个上下文中进行修改:

def protected(obj):
    class Protected(type(obj)):
        def __getattr__(self, name):
            return getattr(obj, name)

        def __setattr__(self, name, value):
            raise AttributeError("can't set attribute")

        def __delattr__(self, name):
            raise AttributeError("can't delete attribute")

        def __enter__(self):
            return obj

        def __exit__(self, exc_type, exc_val, exc_tb):
            pass

    return object.__new__(Protected)

这样,下面的代码就能通过检查:

class Foo:
    def __init__(self, bar):
        self.bar = bar

foo = protected(Foo('original_bar'))

with foo as mutable_foo:
    mutable_foo.bar = 'new_bar'

assert foo.bar == 'new_bar'

而下面的代码则会抛出一个 AttributeError 错误:

foo.bar = 'new_bar'
1

我觉得除非必要,否则最好不要使用包装类。用一个简单的布尔值就能搞定,感觉会简单很多。

class Foo:
    def __init__(self, bar):
        self._bar = bar
        self._mutable = False

    @property
    def bar(self):
        return self._bar

    @bar.setter
    def bar(self, value):
        if not self._mutable:
            raise RuntimeError('No touchy')
        self._bar = value

    @contextlib.contextmanager
    def mutable(self):
        self._mutable = True
        try:
            yield
        finally:
            self._mutable = False

foo = Foo('sup')
foo.bar = 'bro' # RuntimeError

with foo.mutable():
    foo.bar = 'bro'

因为所有的修改都是直接在Foo对象上进行的,所以不需要特别的操作,修改的内容会自动在你的FooRepo中反映出来。

撰写回答