I've implemented the class as a stand-alone module here: https://github.com/swfarnsworth/dynamicdict
It could in theory be made significantly more concise if `defdict_type` were the base for this class instead of `PyDict_Type`. On Tue, Apr 14, 2020 at 1:32 PM Andrew Barnert via Python-ideas < python-ideas@python.org> wrote: > On Apr 13, 2020, at 18:44, Caleb Donovick <donov...@cs.stanford.edu> > 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 -- python-ideas@python.org > To unsubscribe send an email to python-ideas-le...@python.org > https://mail.python.org/mailman3/lists/python-ideas.python.org/ > Message archived at > https://mail.python.org/archives/list/python-ideas@python.org/message/N6K2PLYJ7ZWEAN6FZWUGNJH23JBQQM33/ > Code of Conduct: http://python.org/psf/codeofconduct/ >
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-le...@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/D2AMDKMQNKL7TQ4HXL6CKDA5U3W5XMNP/ Code of Conduct: http://python.org/psf/codeofconduct/