Python 装饰器在 class 中获取或设置字典值

Python decorator get or set dictionary value in class

我正在研究 class 表示具有大量关联数据的对象。我将这些数据存储在名为 metadata 的字典 class 属性中。表示可以是:

{'key1':slowToComputeValue, 'key2':evenSlowerToComputeValue}

值的计算在某些情况下 非常 慢,所以我想做的是,使用 "getter" 函数,首先尝试从元数据 字典。只有在出现 KeyError 时(即当 getter 试图为一个尚不存在的键获取一个值时)才应该计算该值(并添加到字典中以便下次 getter 时快速访问)被称为)。

我从一个简单的开始:

try:
    return self.metadata[requested_key]
except KeyError:
    #Implementation of function

由于class中有很多getter,我开始认为这前3行代码可以由装饰器处理。但是我在做这项工作时遇到了问题。问题是我需要将元数据字典从 class 实例传递给装饰器。我发现了一些教程和帖子,比如 this 其中一个表明可以将参数发送到封闭函数,但我遇到的困难是向它发送 class 实例化属性元数据(如果我发送一个字符串值,它就可以工作)。

我尝试的一些示例代码在这里:

def get_existing_value_from_metadata_if_exists(metadata):
    def decorator(function):
        @wraps(function)
        def decorated(*args, **kwargs):
            function_name = function.__name__
            if function_name in metadata.keys():
                return metadata[function_name]
            else:
                function(*args, **kwargs)
        return decorated
    return decorator

class my_class():
    @get_existing_value_from_metadata_if_exists(metadata)
    def get_key1(self):
        #Costly value calculation and add to metadata

    @get_existing_value_from_metadata_if_exists(metadata)
    def get_key2(self):
        #Costly value calculation and add to metadata

    def __init__(self):
        self.metadata = {}

我得到的错误通常是自我未定义的,但我尝试了参数放置、装饰器放置等的各种组合,但都没有成功。

所以我的问题是:

  1. 我怎样才能完成这项工作?
  2. 装饰器是否适合实现我想要做的事情?

是的,装饰器是一个很好的用例。例如 Django 已经包含了类似的东西,它叫做 cached_property.

基本上它所做的就是当第一次访问 属性 时,它将数据存储在实例的 dict(__dict__) 中,名称与函数相同。当我们稍后获取相同的 属性 时,它会简单地从实例字典中获取值。

一个cached_property是一个non-data descriptor。因此,一旦在实例的字典中设置了键,对 属性 的访问将始终从那里获取值。

class cached_property(object):
    """
    Decorator that converts a method with a single self argument into a
    property cached on the instance.

    Optional ``name`` argument allows you to make cached properties of other
    methods. (e.g.  url = cached_property(get_absolute_url, name='url') )
    """
    def __init__(self, func, name=None):
        self.func = func
        self.__doc__ = getattr(func, '__doc__')
        self.name = name or func.__name__

    def __get__(self, instance, cls=None):
        if instance is None:
            return self
        res = instance.__dict__[self.name] = self.func(instance)
        return res

你的情况:

class MyClass:
    @cached_property
    def key1(self):
        #Costly value calculation and add to metadata

    @cached_property
    def key2(self):
        #Costly value calculation and add to metadata

    def __init__(self):
        # self.metadata not required

使用 name 参数将现有方法转换为缓存 属性。

class MyClass:
    def __init__(self, data):
        self.data = data

    def get_total(self):
        print('Processing...')
        return sum(self.data)

    total = cached_property(get_total, 'total')

演示:

>>> m = MyClass(list(range(10**5)))

>>> m.get_total()
Processing...
4999950000

>>> m.total
Processing...
4999950000

>>> m.total
4999950000

>>> m.data.append(1000)

>>> m.total  # This is now invalid
4999950000

>>> m.get_total()  # This still works
Processing...
4999951000

>>> m.total
4999950000

根据上面的例子可以看出,只要知道内部数据还没有更新,就可以使用total,这样可以节省处理时间。但这并不会使 get_total() 变得多余,因为它可以根据数据得出正确的总数。

另一个例子可能是我们的 public 面向客户目前正在使用某种方法(比如 get_full_name()),但我们意识到将其用作 属性(只是 full_name),在这种情况下,保持方法不变但将其标记为已弃用并开始建议用户从现在开始使用新的 属性 是有意义的。

解决此问题的另一种方法是使用 class "properties",如下所示:

class MyClass():
    def __init__():
        self._slowToComputeValue = None
    @property
    def slowToComputeValue(self):
        if self._slowToComputeValue is None:
            self._slowToComputeValue = self.ComputeValue()
        return self._slowToComputeValue
    def ComputeValue(self):
        pass

现在您可以像访问 class 属性一样访问它:

myclass = MyClass()
print(myclass.slowToComputeValue)