On Sep 30, 2019, at 02:38, Paul Moore email@example.com wrote:
It's (in my view) sad that the simple hasattr test is no longer sufficient, and in particular that if you want robustness, Python has changed to the point where a pseudo subclass check is the "right" way to check for an object that has certain properties. But I guess this is the case, and therefore the omission of a Subscriptable ABC is something that may need to be addressed.
The thing is, hasattr was _never_ the right way for some protocols, because the standard idiom to prevent inheriting a handful of special methods like __hash__ has always been to override it with __hash__ = None, and there have always been a few classes that did this for good reasons. I’m pretty sure it’s been like that since new-style classes were added, types and classes were unified, and all protocols were given dunder methods.
And how else would you do this? You could add custom syntax (as C++ did with `= delete`), but you’d need some way to record somewhere that this method should not be looked up in the mro.
Maybe rather than proliferating ABCs like this, exposing the internal function in the ABC that does the "robust" version of the hasattr test would be more flexible?
I’m pretty sure I suggested this around 3.4, and it was rejected, but I can’t remember why.
At the time there _was_ no internal function, and the ABCs weren’t all consistent about handling None. Now the function exists, and is used consistently, but it’s private. (Actually, I think what exists is a function that checks N methods at once, while a public function you’d probably want to fit the API of hasattr… but that would be trivial.)
But meanwhile, as far as I know, nobody’s asked for that function in the years since 3.5—and, AFAIK, this is only the second time (after Reversible) that someone has asked for a new implicit ABC that wasn’t related to a brand-new protocol (like all the async stuff). So, I’m not sure we need to worry about proliferating too many of these too quickly. In theory there are dozens of dunder methods and vastly more possible combinations of them that you could come up with names for and ask for, but in practice that hasn’t happened, so why worry?
I guess that depends on whether you find the "subclass of an ABC" approach acceptable. To me, it feels too much like "object oriented everywhere" languages like Java, which was always an issue I had with ABCs, but as Steven said, that ship has probably sailed by now.
But Python has always (or at least since 2.2) been even more objects-everywhere under the hood than Java. Python has types for classes, functions, bound methods, all kinds of things that aren’t even accessible at runtime except through reflection APIs in Java. The difference is that Python allows duck-typing the interfaces of those types. You can stick anything that meets the descriptor protocol by returning something that meets the callable protocol in place of a method, and it works.
Python never had a good way to check for its protocols until implicit ABCs. And notice that implicit protocol ABCs like Iterable are not simulating inheritance-based subtyping like Java’s interfaces, they’re simulating structure subtyping like Go’s protocols. (And even the explicit ABCs are often there to simulate something you just can’t spell implicitly, like the distinction between Sequence and Mapping even though they use the same dunder method.)
I think the main discomfort is that Python allows both kinds of ABCs, spells them the same way, and gives you a bunch of both kinds in the stdlib, so it _feels_ like you’re doing Java-style interfaces even though you usually aren’t. Spelling them both the same way is weird, and I hated the idea when I first saw it, but in practice it turns out to work really well. (Having half of those ABCs also be mixins to add useful methods is a lot more weird and wrong theoretically—and even more useful practically.)
Also, what we’re checking for really is subtyping. And if we’d done that by having collections, numbers, etc. sprout a whole bunch of new isspam functions instead of types to check with isinstance, imagine what a nightmare extending that to annotations would have been. We would have had to come up with some kind of syntax to let you write an arbitrary predicate or something as an annotation, so you could write something like `def f(things: isiterable(things) and all(isnumber(thing) for thing in things)` instead of `def d(things: Iterable[Number])`. (Of course if you hate static typing, that might have made you happy, because coming up with a way to statically check those annotations would have been a decades-long project like the one C++ is still fighting over for bounding its generics…)