This is just curiosity on my part. By creating dummy classes with just one method on them printing things, I discovered that writing
a.keys(), then getitem on
a using the resulting keys.
Why was it chosen to use a normal-named method,
keys, rather than a dunder like
Before even raising the question of whether it should be changed or not and what precautions and costs would be involved, I find it very surprising that a native syntax behavior depends on a method not part of the datamodel - which adresses mappings, but only “recommends” that they implement
keys, not presenting it as required.
Another point, for uses of the mixin class
__iter__ method is user-provided while the
keys method is mixin and actually a wrapper around
__iter__, so calling
keys instead of
__iter__ costs more execution time.
It can’t use
__iter__, because sequences also have that so things like
range would unpack successfully. Maybe it’d make sense to make a
__keys__ method, but then you’d need
__values__, presumably a
values() builtin (which would be shadowed an awful lot), all for no real gain.
keys is not a reserved method name since it’s not a dunder, so the exact same kind of bug could happen to a type having a
keys method which would return an iterable thing. Yes, it’s probably less frequent, but it’s not forbidden by the documentation whereas dunders are reserved.
I guess I see a point : for a range, the second part, the getitem part, would succeed since in that case the iterated values are valid subscripts (for 1-argument ranges that is).
foo(**range(15)) call would still fail since the “keys” are numbers and not strings. So I don’t think it would have enabled any unintended behavior.
I’m wondering whether all implementations work that way or if it’s only cpython.
Checking the language specification, it doesn’t mention the use of
keys(), just saying that it will look for mappings in general. It probably should.
An instance of builtin
dict is required internally, so it creates a new
dict instance and merges in the mapping. Ultimately the merge is implemented by
Objects/dictobject.c. This function is also used by the
update() method of a
dict when it’s merging in a mapping that has a
D.update([E, ]**F) -> None. Update D from dict/iterable E and F.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
In either case, this is followed by: for k in F: D[k] = F[k]
[lightly edited] It is possible that
a.__iter__ is used, just not directly.
a.keys() is intended to be an iterable live view of a, in which case it must have a reference to a, in which case
a.keys().__iter__ should produce the same names as a.iter and could actually invoke
a.__iter__. A perhaps better question is why access
a.keys() instead of directly. The answer, already given, is to avoid iterating sequences.