Home > database >  Python: Setting attribute that is a lambda that just returns a dictionary
Python: Setting attribute that is a lambda that just returns a dictionary

Time:02-19

While learning about decorator and decorator factories, I checked the source code of the @functools.lru_cache since it allows both usages using one single implementation. And I spotted something that is intriguing me. In the following statement wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed} extracted from this piece of code from the CPython implementation:

def lru_cache(maxsize=128, typed=False):
    """<docstring ommited for brevity>"""
    if isinstance(maxsize, int):
        # Negative maxsize is treated as 0
        if maxsize < 0:
            maxsize = 0
    elif callable(maxsize) and isinstance(typed, bool):
        # The user_function was passed in directly via the maxsize argument
        user_function, maxsize = maxsize, 128
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
        return update_wrapper(wrapper, user_function)
    elif maxsize is not None:
        raise TypeError(
            'Expected first argument to be an integer, a callable, or None')
    
    def decorating_function(user_function):
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
        return update_wrapper(wrapper, user_function)
    
    return decorating_function

What's the purpose of creating a lambda that only returns a dictionary instead of setting the dictionary directly to the attribute?

CodePudding user response:

After thinking a bit on the subject I came to this conclusion. But if there's another reason or you have a more complete explanation, please post an answer so I can accept it :)

Dictionaries are mutable objects in Python. This means that anyone that has a reference to a dictionary can change it. So I think that the usage of the lambda here is a trick to make this a read-only attribute. Take a look at the following example:

def f():
    pass

f.a = lambda: {"a": 1}

ref = f.a()
ref

Outputs:

{'a': 1}

And if you change the returned dictionary, it will have no impact on the following calls to the lambda because a new dictionary is created in every call:

ref["b"] = 2
f.a()

Outputs:

{'a': 1}

As you can see in the example, the returned dictionary is obviously still mutable, but changing it has no effect in the returned value by the lambda itself.

In the commit that added this line, I also found the following note that supports my theory:

The wrapped function is instrumented with a :func:cache_parameters function that returns a new :class:dict showing the values for maxsize and typed. This is for information purposes only. Mutating the values has no effect.

CodePudding user response:

The (probable) reason

The purpose for the lambda assigned to wrapper.cache_parameters in lru_cache() is to provide read access to the cache parameters passed to lru_cache() for later introspection.

Since the lambda is assigned to the object wrapper which is modified and then returned by update_wrapper(), it is callable on the resulting cache object, i.e. the passed function's wrapper.

One could have implemented the same functionality as a method on the wrapper class. But this would have implied the need to grant this class access to the cache parameters by e.g. storing them in instance attributes. This would, however, be a redundant storage of state, since those parameters are already stored in the closure created by lru_cache(). Hence, it suffices to create a static method on the resulting wrapper object to return the cache parameters from that closure, which is what the lambda does.

What's not the reason

Doing this does not make the resulting dict immutable. Any dict object is always mutable. And any pure function that returns a dict will not be influenced by the later change of the dict returned by it anyway. It also does not make the property cache_parameters of the wrapper object immutable. It still could be replaced by anything else:

from functools import lru_cache


@lru_cache()
def foo():
    pass


print("Wrapper's cache params:", cache_params := foo.cache_parameters())
cache_params['maxsize'] = 9999
print('Modified cache params:', cache_params)
print("Wrapper's cache params:", foo.cache_parameters())
foo.cache_parameters = lambda: {'maxsize': 9999, 'typed': True}
print("Wrapper's cache params:", foo.cache_parameters())

Alternative approach

One could also have implemented the cache properties as an immutable object, such as a collections.namedtuple:

...

        wrapper.cache_parameters = namedtuple('CacheParams', 'maxsize typed')(maxsize, typed)
...

But I assume that lru_cache is older than namedtuple and a subsequent change would break existing code. Also, it would not prevent the cache_parameters property to be overridden either.

  • Related