Python 中容器的 @property 等价物

37 阅读4分钟

在 Python 中,当我们使用诸如 Boto 之类的 AWS 模块时,经常需要定义一个简化的包装类。在这个过程中,我们可能会使用 @property 来避免在库中使用特殊的“getter”和“setter”方法。这是 Python 中更符合风格的一种做法。

我们希望以一种类似于简单对象的方式来调用类中的方法,例如:

myclass.myprop = 5         # 将“5”发送到 myprop 的 setter 函数
result = myclass.myprop    # 调用 myprop 的 getter 函数并将结果存储起来

我们还会处理多组对象,例如名称/值对的标签,并希望能够像访问容器(例如字典或列表)一样访问它们。例如,对于标签来说:

myclass.tags["newkey"] = "newvalue"   # 运行应用标签到 AWS 的函数
result = myclass.tags["newkey"]       # 访问 AWS 以获取“newkey”标签的值

2、解决方案

从目前的状况来看,似乎可以通过从 dict 继承类来实现这一点,但我们可能忽略了一些东西。那么,创建这种接口最符合 Python 风格的方法是什么呢?

根据问题中给出的一个回答,我们可以使用 Silas Ray 的解决方案,但对其进行修改,以便这些类可用于定义多个类似于字典的对象。这并不完全干净,但我们可以发布修改后的代码和解释说明,以帮助其他人理解这个解决方案。

class FakeDict(object):

    def __init__(self, obj, getter, setter, remover, lister):
        self.obj = obj
        self.getter = getter
        self.setter = setter
        self.lister = lister
        self.remover = remover

    def __getitem__(self, key):
        return self.getter(self.obj, key)

    def __setitem__(self, key, value):
        self.setter(self.obj, key, value)

    def __delitem__(self, key):
        self.remover(self.obj, key)

    def _set(self, new_dict):
        for key in self.lister(self.obj):
            if key not in new_dict:
                self.remover(self.obj, key)
        for key, value in new_dict.iteritems():
            self.setter(self.obj, key, value)

class ProxyDescriptor(object):

    def __init__(self, name, klass, getter, setter, remover, lister):
        self.name = name
        self.proxied_class = klass
        self.getter = getter
        self.setter = setter
        self.remover = remover
        self.lister = lister

    def __get__(self, obj, klass):
        if not hasattr(obj, self.name):
            setattr(obj, self.name, self.proxied_class(obj, self.getter, self.setter, self.remover, self.lister))
        return getattr(obj, self.name)

    def __set__(self, obj, value):
        self.__get__(obj, obj.__class__)._set(value)

class AWS(object):

    def get_tag(self, tag):
        print("Ran get tag")
        return "fgsfds"
        # 调用 AWS 来获取标签

    def set_tag(self, tag, value):
        print("Ran set tag")
        # 调用 AWS 来设置标签

    def remove_tag(self, tag):
        print("Ran remove tag")
        # 调用 AWS 来移除标签

    def tag_list(self):
        print("Ran list tags")
        # 调用 AWS 来检索所有标签

    def get_foo(self, foo):
        print("Ran get foo")
        return "fgsfds"
        # 调用 AWS 来获取标签

    def set_foo(self, foo, value):
        print("Ran set foo")
        # 调用 AWS 来设置标签

    def remove_foo(self, tag):
        print("Ran remove foo")
        # 调用 AWS 来移除标签

    def foo_list(self):
        print("Ran list foo")
        # 调用 AWS 来检索所有标签

    tags = ProxyDescriptor('_tags', FakeDict, get_tag, set_tag, remove_tag, tag_list)
    foos = ProxyDescriptor('_foos', FakeDict, get_foo, set_foo, remove_foo, foo_list)


test = AWS()

tagvalue = test.tags["tag1"]
print(tagvalue)
test.tags["tag1"] = "value1"
del test.tags["tag1"]

foovalue = test.foos["foo1"]
print(foovalue)
test.foos["foo1"] = "value1"
del test.foos["foo1"]

解释:

  • tagsfoos 都是 ProxyDescriptor 的类级实例,它们只在类定义时实例化一次。它们被移动到最底部,以便它们可以引用它们上面的函数定义,这些函数用于定义各种字典操作的行为。

  • 大部分“魔术”发生在 ProxyDescriptor__get__() 方法中。任何带 test.tags 的代码都会运行描述符的 __get__() 方法,该方法简单地检查 test(作为 obj 传入)是否已经具有一个名为 _tags 的属性。如果它没有,它会创建一个——一个之前传给它的类的一个实例。这就是 FakeDict 的构造函数被调用并创建的地方。它最终被调用并为 AWS 的每个实例被引用的tags 创建一次。

  • 我们已经将四组函数通过描述符和 FakeDict 的构造函数传递——但是在 FakeDict 内部使用它们有点棘手,因为上下文已经改变了。如果我们在 AWS 类的一个实例中直接使用函数(如 test.get_tag),Python 会自动用所有者 test 填充 self 参数。但它们不是从 test 中调用的——当我们把它们传递给描述符时,我们传递了类级函数,它们没有 self 可以引用。为了解决这个问题,我们将 self 视为一个传统的参数。FakeDict 中的 obj 实际上代表着已经传给它们的 AWS 对象,所以我们可以简单地将它作为函数的第一个参数传入。

  • 造成这一切如此混乱的部分原因是 AWSProxyDescriptorFakeDict 之间有很多奇怪的循环引用。如果你对它感到困惑,请记住在 ProxyDescriptorFakeDict 中,objAWS 类的实例,该类已经传给它们,即使 FakeDict 的实例存在