> On Apr 13, 2020, at 18:44, Caleb Donovick <[email protected]> wrote:
>
> I have built this data structure countless times. So I am in favor.
Maybe you can give a concrete example of what you need it for, then? I think
that would really help the proposal. Especially if your example needs a
per-instance rather than per-class factory function.
> > Why can’t you just subclass dict and override that?
>
> Because TypeError: multiple bases have instance lay-out conflict is one of my
> least favorite errors.
But defaultdict, being a subclass or dict, has the same problem in the same
situations, and (although I haven’t checked) I assume the same is true for the
OP’s dynamicdict.
> Perhaps `__missing__` could be a first class part of the getitem of protocol,
> instead of a `dict` specific feature. So that
> ```
> r = x[key]
> ```
> means:
> ```
> try:
> r = x.__getitem__(key)
> except KeyError as e: # should we also catch IndexError?
> try:
> missing = x.__missing__
> except AttributeError:
> raise e from None
> r = missing(key)
> ```
>
> Obviously this would come at some performance cost for non dict mappings so I
> don't know if this would fly.
Besides performance, I don’t think it fits with Guido’s conception of the
protocols as being more minimal than the builtin types—e.g., set has not just a
& operator, but also an intersection method that takes 0 or more arbitrary
iterables; the set protocol has no such method, so collections.abc.Set neither
specifies nor provides an intersection method). It’s a bit muddy of a
conception at the edges, but I think this goes over the line, and maybe have
been explicitly thought about and rejected for the same reason as
Set.intersection.
On the other hand, none of that is an argument or any kind against your method
decorator:
> So instead maybe there could have standard decorator to get the same behavior?
> ```
> def usemissing(getitem):
> @wraps(getitem)
> def wrapped(self, key):
> try:
> return getitem(self, key)
> except KeyError as e:
> try:
> missing = self.__missing__
> except AttributeError:
> raise e from None
> return missing(key)
> return wrapped
> ```
This seems like a great idea, although maybe it would be easier to use as a
class decorator rather than a method decorator. Either this:
def usemissing(cls):
missing = cls.__missing__
getitem = cls.__getitem__
def __getitem__(self, key):
try:
return getitem(self, key)
except KeyError:
return missing(self, key)
cls.__getitem__ = __getitem__
return cls
Or this:
def usemissing(cls):
getitem = cls.__getitem__
def __getitem__(self, key):
try:
return getitem(self, key)
except KeyError:
return type(self).__missing__(self, key)
cls.__getitem__ = __getitem__
return cls
This also preserves the usual class-based rather than instance-based lookup for
most special methods (including __missing__ on dict subclasses).
The first one has the advantage of failing at class decoration time rather than
at first missing lookup time if you forget to include a __missing__, but it has
the cost that (unlike a dict subclass) you can’t monkeypatch __missing__ after
construction time. So I think I’d almost always prefer the first, but the
second might be a better fit for the stdlib anyway?
I think either the method decorator or the class decorator makes sense for the
stdlib. The only question is where to put it. Either fits in nicely with things
like cached_property and total_ordering in functools. I’m not sure people will
think to look for it there, as opposed to in collections or something else in
the Data Types chapter in the docs, but that’s true of most of functools, and
at least once people discover it (from a Python-list or StackOverflow question
or whatever) they’ll learn where it is and be able to use it easily, just like
the rest of that module.
It’s simple, but something many Python programmers couldn’t write for
themselves, or would get wrong and have a hard time debugging, and it seems
like the most flexible and least obtrusive way to do it. (It does still need
actual motivating examples, though. Historically, the bar seems to be lower for
new decorators in functools than new classes in collections, but it’s still not
no bar…)
Additionally—I’m a lot less sure if this one belongs in the stdlib like
@usemissing, but if you were going to put this on PyPI as a mappingtools or
collections2 or more-functools or whatever—you could have an
@addmissing(missingfunc) decorator, to handle cases where you want to adapt
some third-party mapping type without modifying the code or subclassing:
from sometreelib import SortedDict
def missing(self, key):
# ... whatever ...
SortedDict = addmissing(missing)(SortedDict)
And if the common use cases are the same kinds of trivial functions as
defaultdict, you could also do this:
@addmissing(lambda self, key: key)
class MyDict…
> Alternatively, it could be implemented as part of one of the ABCs maybe
> something like:
> ```
> class MissingMapping(Mapping):
> # Could also give MissingMapping its own metaclass
> # and do the modification of __getitem__ there.
> def __init_subclass__(cls, **kwargs):
> super().__init_subclass__(**kwargs)
> cls.__getitem__ = usemissing(cls.__getitem__)
>
> @abstractmethod
> def __missing__(self, key): pass
> ```
Presumably you’d also want to precompose a MutableMissingMapping ABC. Most user
mappings are mutable, and I suspect that’s even more true for those that need
__missing__, given that most uses of defaultdict are things like building up a
multidict without knowing all the keys in advance.
As for the implementation, I think __init_subclass__ makes more sense than a
metaclass (presumably a subclass or ABCMeta). Since mixins are all about
composing, often with multiple inheritance, it’s hard to add metaclasses
without interfering with user subclasses. (Or even with your own future—imagine
if someone realizes MutableMapping needs its own metaclass, and MissingMapping
already has one; now there’s no way to write MutableMissingMapping.) Composing
ABCs with non-ABC mixins is already more of a pain than would be ideal, and I
think a new submetaclass would make it worse.
But at any rate, I’m not sure this is a good idea. None of the other ABCs hide
methods like this. For example, Mapping will give you a __contains__ if you
don’t have one, but if you do write one that does things differently, yours
overrides the default; here, there’d be no way to override the default
__getitem__ to do things differently, because that would just get wrapped and
replaced. That isn’t unreasonable behavior for a mixin in general, but I think
it is confusing for an ABC/mixin hybrid in collections.abc.
Also, a protocol or ABC is something you can check for compliance (at runtime,
or statically in mypy, or just in theory even if not in the actual code); is
there ever any point in asking whether an object complies with MissingMapping?
It’s something you can use an object as, but is there any way you can use a
MissingMapping differently from a Mapping? So I think it’s not an ABC because
it’s not a protocol.
Of course you could just make it a pure mixin that isn’t an ABC (and maybe
isn’t in collections.abc), which also solves all of the problems above (except
the optional one with the metaclass, but you already avoided that). But at that
point, are there any advantages over the method or class decorator?
_______________________________________________
Python-ideas mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at
https://mail.python.org/archives/list/[email protected]/message/N6K2PLYJ7ZWEAN6FZWUGNJH23JBQQM33/
Code of Conduct: http://python.org/psf/codeofconduct/