However, in practice it's not used that way. This specific LWN article concerns the following code from functools.py:
_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
It is used like this: def decorating_function(user_function):
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
...
def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
...
def cache_info():
"""Report cache statistics"""
with lock:
return _CacheInfo(hits, misses, maxsize, cache_len())
The _lru_cache_wrapper is then replaced by a C extension in _functoolsmodule.c which only uses the cache_info_type as: static PyObject *
lru_cache_cache_info(lru_cache_object *self, PyObject *unused)
{
return PyObject_CallFunction(self->cache_info_type, "nnOn",
self->hits, self->misses, self->maxsize_O,
PyDict_GET_SIZE(self->cache));
}
This is basically the same code, just in C.There is no reason for this to expose a tuple API. And yet it does.
>>> @functools.lru_cache(maxsize=None)
... def fib(i):
... if i <= 1: return 1
... return fib(i-1)+fib(i-2)
...
>>> for i in range(10):
... print(i, fib(i))
...
0 1
1 1
2 2
3 3
4 5
5 8
6 13
7 21
8 34
9 55
>>> fib.cache_info()
CacheInfo(hits=17, misses=10, maxsize=None, currsize=10)
>>> for x in fib.cache_info():
... print(x)
...
17
10
None
10
A spot check shows no uses of namedtuple in the Python standard library which are used to keep backwards compatibility with tuples, though I only looked at a few.So while I agree with you, it appears that the core developers do not agree with us.
Regardless, through conversation I am confident that at least one of the core devs thinks about namedtuple in that manner. Not as namedtuple's only purpose, but as one major benefit. Further, the core devs are not a single entity, but many people who don't always agree with each other.
Edit: note Raymond's comment elsewhere on this page.
For the specific case under discussion, ncoghlan added cache_info() on 30 Nov 2010 - https://github.com/python/cpython/commit/234515afe594e5f9ca1...
Previously, "Performance statistics stored in f.cache_hits and f.cache_misses." With the change "Significant statistics (maxsize, size, hits, misses) are available through the f.cache_info() named tuple."
There was no time when it was a tuple, much less in a tuple in a public release.
I'll look first at the namedtuples which have been present since at least 2.7.10.
The named tuples in "urllib/parse", "difflib", "inspect", "sched", and "doctest" were originally tuples. The decimal module's uses a DecimalTuple as a return value from to_tuple(), so I didn't check that history.
ssl uses a DefaultVerifyPaths added by tiran on 9 Jun 2013 - https://github.com/python/cpython/commit/6d7ad13a458afdf2cbd... . It did not replace a tuple. I assume _ASN1Object is the same way.
Now for the namedtuples in recent version control:
dis.py:_Instruction was added in https://github.com/python/cpython/commit/b39fd0c9b8dc6683924... . It did not replace a tuple.
aifc.py:_aifc_params replaces a tuple.
crypt.py:_Method replaced a class with a namedtuple in https://github.com/python/cpython/commit/daa5799cb8866785543... .
nntplib.py:GroupInfo and ArticleInfo added in https://github.com/python/cpython/commit/69ab95105f5105b8337... . It did not replace a tuple.
pkgutil.py:ModuleInfo replaces a tuple
platform.py:uname_result replaces a tuple
sndhdr.py:SndHeader replaces a tuple
sunau.py:_sunau_params replaces a tuple
tokenize.py:TokenInfo replaces a tuple
typing.py:nm_tpl ... is above my paygrade to understand. I believe it's required to be a namedtuple since that what it's trying to implement.
wave.py:_wave_params replaces a tuple.
Finally, urllib/robotparser.py appears to contain a bug in the following:
req_rate = collections.namedtuple('req_rate',
'requests seconds')
entry.req_rate = req_rate
entry.req_rate.requests = int(numbers[0])
entry.req_rate.seconds = int(numbers[1])
As I read it, this should fail as the req_rate is immutable. This appears to be written by someone who thinks that it's a lightweight object. The commit is https://github.com/python/cpython/commit/960e848f0d32399824d... . It did not replace a tuple.In conclusion, 8 of the current uses of namedtuple are not to emulate a tuple. 13 of the current uses replace a tuple.
If a given function was in python2.7 then it was easier. I checked if the code used to be a tuple.
FWIW, that is a norm in the standard library that predates named tuples.
For example, sys.version_info and time.localtime() return structseq instances. Those both have a named tuple API. The are tuple subclasses with named fields. Accordingly, they are indexable, iterable, hashable, space-efficient, and have a self-documenting __repr__().
The two examples you gave are examples of that good use.
The time.localtime() used to return a tuple. See https://github.com/python/cpython/blob/770e4042db8a1c277b5d9... . The change to StructTimeType is an example of how to do that sort of migration.
Similarly, sys.version_info() used to return a tuple. See https://github.com/python/cpython/blob/193a3f6d37c1440b049f7... . Again, a good example of how to migrate.
However, the "this" in the "There is no reason for this to expose a tuple API" which you quoted refers specifically to the namedtuple that cache_info() returns. That did not replace a tuple, it replaced two attributes, as you can see at https://github.com/python/cpython/commit/234515afe594e5f9ca1... .
And returning a simple type with a couple of attributes while also (and needlessly) treating it as a tuple was never a norm in Python before namedtuples, if only because it was a lot of work.
Thus the desire for types.SimpleNamespace as others have noted.
Though we might have better design choices today than yesterday, there's also a downside to code churn. Case in point the annoyance of trying to modernize threading from isAlive to is_alive.
Even when writing new code, it's often wiser to stick with a "worse" standard than to use a new design.
I disagree with raymondh's statement that "that is a norm in the standard library that predates named tuples."