On Sep 30, 2019, at 23:46, Ben Rudiak-Gould <benrudiak@gmail.com> wrote:
On Mon, Sep 30, 2019 at 10:08 AM Andrew Barnert via Python-ideas <python-ideas@python.org> wrote:
Also, what we’re checking for really is subtyping.
Is it? Subtyping in type theory satisfies some axioms, one of which is transitivity. The addition of the ABCs broke transitivity:
Python subtyping isn’t perfect. The very fact that you’re allowed to disable inherited methods by assigning them to None breaks this; you don’t need ABCs for that: def spam(x: object): hash(x) spam([]) Even though [] is-a object, and objects have the __hash__ method, this raises a TypeError because lists don’t have the __hash__ method. That violates substitutability. [] is-a object is wrong. The Hashable ABC correctly reflects that incorrect relationship:
issubclass(list, object) True issubclass(object, collections.abc.Hashable) True issubclass(list, collections.abc.Hashable) False
ABC membership is a subtype relationship in some sense, and ordinary Python subclassing is a subtype relationship in some sense, but they aren't quite the same sense,
But in this case, they actually match. Hashable is correctly checking for structural subtyping, and the problem is that list isn’t actually a proper subtype of object, not that object isn’t a proper subtype of Hashable.
and merging them creates an odd hybrid system in which I'm no longer sure which subclass relationships should hold, let alone which do.
Let’s say instead of ABCs that test structural subtyping, we added a bunch of callable-like predicates to do the tests. You would have the exact same problem here: >>> issubclass(list, object) True >>> collections.ishashable(object) True >>> collections.ishashable(list): False The problem is with list is-a object, not with the way you test.
For example:
class A(collections.abc.Hashable): ... __hash__ = None ... issubclass(A, collections.abc.Hashable) True
This one is a direct consequence of the fact that you can lie to ABCs—if you inherit from an ABC you are treated as a subtype even if you don’t qualify, and the check isn’t perfect. You are explicitly, and obviously, lying to the system here. Should ABCs check whether the required methods are actually methods (with a Method ABC, or by calling _get__ and checking callable on the result, or whatever)? Or at least not None? I don’t know. Maybe. But that wouldn’t eliminate the ability to lie to them, because they have the register method. Even if you couldn’t inherit from an ABC to lie to it, you could still register with it, and there is no check at all there, and that’s definitely working as designed. And allowing you to lie isn’t really a bug; it’s a consenting-adults feature that can be misused but can also be useful for migrating legacy code. Of course register isn’t used only, or even primarily, for lying—Sequence can’t be tested structurally (at least not if you want to distinguish Sequence indexing from Mapping lookup when both protocols use the same dunder method), so list and tuple register with Sequence. But I believe range also used to register with Sequence to lie even before it became a proper sequence in 3.2, because it was “close enough” to being a Sequence and often used in real-life code that used sequences and worked. Registration can also be used for “file-like object” code that was written to the vague 2.x definition and worked fine in practice, but didn’t actually meet the ABCs in the io module which are no longer vague about what it means (because they require methods your legacy code never used). And so on. If ABCs had been in Python since 2.2, I’m not sure this feature would be a good idea, but adding it after the fact, I think it was.
hash(A()) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'A'
I didn't know what the issubclass call would return before I keyed in the example, and I can't decide what it should return. In contrast, I have no trouble deciding that the equivalent test implemented as a predicate ought to return False, since instances of A are in fact not hashable.
What about this: class A: __hash__ = None ishashable.register(A) ishashable(A) Would you be surprised if this returned true? If that registration were never useful, it would be a bizarre feature, a pointless bug magnet. But if it were useful for lots of legacy code and added for that reason, and you deliberately misused the feature like this, would you say the bug is in ishashable, in the predicate system, or in your code? Also, how would you write the issequence and is ismapping predicates?