This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: @functools.lru_cache() not respecting typed=False
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.8
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: bbernard, rhettinger
Priority: normal Keywords:

Created on 2020-02-05 03:47 by bbernard, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Messages (2)
msg361404 - (view) Author: Benoit B (bbernard) Date: 2020-02-05 03:47
I don't know if I'm missing something, but there's a behavior of functools.lru_cache() that I currently don't understand.

As the documentation states:

"If typed is set to true, function arguments of different types will be cached separately. For example, f(3) and f(3.0) will be treated as distinct calls with distinct results."

For a function accepting only positional arguments, using typed=False doesn't seem to be working in all cases.

>>> import functools
>>> 
>>> @functools.lru_cache()      # Implicitly uses typed=False
>>> def func(a):
...     return a
>>> 
>>> func(1)
>>> func(1.0)
>>> 
>>> print(func.cache_info())
CacheInfo(hits=0, misses=2, maxsize=128, currsize=2)

Instead, I would have expected: CacheInfo(hits=1, misses=1, maxsize=128, currsize=2)

So it looks like 1 and 1.0 were stored as different values even though typed=False was used.

After analyzing the source code of _functoolsmodule.c::lru_cache_make_key(), I found what follows:

    if (!typed && !kwds_size) {
        if (PyTuple_GET_SIZE(args) == 1) {
            key = PyTuple_GET_ITEM(args, 0);
            if (PyUnicode_CheckExact(key) || PyLong_CheckExact(key)) {      <<< it appears that a 'float' would cause 'args' (a tuple) to be returned as the key, whereas an 'int' would cause 'key'
                /* For common scalar keys, save space by                        (an int) to be returned as the key. So 1 and 1.0 generate different hashes and are stored as different items.
                   dropping the enclosing args tuple  */
                Py_INCREF(key);
                return key;
            }
        }
        Py_INCREF(args);
        return args;
    }

At some point in the past, the above code section looked like this:

    if (!typed && !kwds) {
        Py_INCREF(args);
        return args;
    }
    
So no matter what the type of the argument was, it was working.
    
Am I somehow mistaken in my analysis or is this a bug?
msg361405 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2020-02-05 04:18
I understand the confusion, but this isn't a bug.

Specifying "typed=True" means that the cache is required to treat the calls f(1) and f(1.0) as distinct.

However, specifying or defaulting to "typed=False" means that the cache isn't required to do so, but it is still allowed to.

This flexibility allowed the tool to add a space saving path for *int*.  It comes at the expense of leaving equivalent int/float calls as distinct.  Most apps win here because it is typical to keep the type the same across calls.  Also, the saved space may allow users to choose a larger value for *maxsize*.

Note, similar liberties were taken with keyword argument ordering.  Formerly, f(a=1, b=2) was considered equivalent to f(b=1, a=1).  Now, they are treated as distinct.  The downside is a potential extra call.  The upside is that we save the huge overhead of sorting the keyword arguments.  Mostly, this is a net win, despite the visible change in cache utilization statistics.
History
Date User Action Args
2022-04-11 14:59:26adminsetgithub: 83735
2020-02-05 04:18:33rhettingersetstatus: open -> closed

nosy: + rhettinger
messages: + msg361405

resolution: not a bug
stage: resolved
2020-02-05 03:47:25bbernardcreate