> 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.

Making __missing__ a first class part of how __getitem__ seems more
analogous to __getattr__ and __getattribute__ than the intersection
method.  Is there any other dunder that is only implicitly called on
builtin types?

I mostly agree with everything else you said.



On Tue, Apr 14, 2020 at 11:34 AM Steele Farnsworth <swfarnswo...@gmail.com>
wrote:

> 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/B5CWXREBLOB6B7EDFHC3LISKUOMWFSDX/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to