I think the combination in Python of dynamic features with "static friendly" features like ABC's and type hints is really amazing. While keeping all its dynamism, the language has become rather "static friendly" if you want it. One rather surprising mechanism is the possibility of interfering in the process of checking if a class inherits from another class (checked with the issubclass function) or if an object is an instance of a class (checked with the isinstance function).
issubclass(Student, Person) and isinstance(p1, Person) will search for a __subclasscheck__ and an __instancecheck__ dunder method respectively. They don't check in the Person class, but in the class of the class (the metaclass). So if you are not using some new metaclass it will end up using the methods in the type metaclass (the type class is implemented in pure c, so the type.__dict__["__instancecheck__"] and type.__dict__["__subclasscheck__"] functions are functions in typeobject.c).
The __isinstance__ and __issubclass__ mechanism was added to expand, not to restrict, the cases when an object passes the isinstance, issubclass check, and should not be used for the contrary, for restricting (that is, making objects that comply with that type-check fail), which indeed should have little use. If you try to use it for restriction you'll get odd results, it will work for the issubclass check, but will fail for some isinstance checks.
This is because of an optimization applied to __instancecheck__ explained here
an optimization in isinstance's implementation that immediately returns True if type(obj) == given_class:
You can see here the confusing results that you'll get if used for restriction (I have no idea of why for the issubclass check they do not apply an "if cls == cls" optimization
# https://stackoverflow.com/questions/52168971/instancecheck-overwrite-shows-no-effect-what-am-i-doing-wrong
class Meta1(type):
def __instancecheck__(cls, inst):
"""Implement isinstance(inst, cls)."""
print("__instancecheck__")
#return super().__instancecheck__(inst)
return False
def __subclasscheck__(cls, sub):
"""Implement issubclass(sub, cls)."""
print("__subclasscheck__")
#return super().__subclasscheck__(sub)
return False
class Person1(metaclass=Meta1):
pass
class Worker1(Person1):
pass
p1 = Person1()
print(isinstance(p1, Person1))
# True
# it uses the optimization and our custom __isinstance__ is not even invoked
# type(p1)
print("------------------------")
p1 = Worker1()
print(isinstance(p1, Person1))
# False
# the optimization returns False, so it invokes our custom __isinstance__ that returns False
print("------------------------")
print(issubclass(Person1, Person1))
# False
# __subclasscheck__ is invoked and returns False
print("------------------------")
print(issubclass(Worker1, Person1))
# False
# __subclasscheck__ is invoked and returns False
print("------------------------")
This machinery was added to python in part to give "extra powers" to abstract classes. It makes easier performing "multiple duck type checks". Let's say you need an object that conforms to an "interface" defined by an abc, but you don't need it to inherit from that class, you just want it to have those methods (duck typing). Furthermore let's say you don't need just one method of the abc, but all or many of them. Checking beforehand if all those methods exist rather than calling methods in the object until one of them fails cause it's not implemented seems like a better option. The __subclasshook__ was born for that. You can define a @classmethod def __subclasshook__(cls, subclass) for that. abc's have ABCMeta as metaclass, and __isinstance__ and __issubclass__ have been defined in it, and both of them will invoke the __subclasshook__ of that abstract class if it exists. The source code for this is in _py_abc.py.
from abc import ABC, abstractmethod
class Movable(ABC):
@abstractmethod
def move_rigth(self, x):
pass
@abstractmethod
def move_left(self, x):
pass
@classmethod
def __subclasshook__(cls, C):
# Both isinstance() and issubclass() checks will call into this (through ABCMeta.__instancecheck__, ABCMeta.__subclasscheck__ )
if cls is Movable:
# Note that you generally check if the first argument is the class itself.
# That's to avoid that subclasses "inherit" the __subclasshook__ instead of using normal subclass-determination.
return hasattr(C, "move_right") and hasattr(C, "move_left")
return NotImplemented
class Button:
def move_right(self, x):
print("moving rigth")
def move_left(self, x):
print("moving left")
# Button is a Movable in duck-typing and structural typing terms
# but it does not strictly implement the Movable "interface" (it does not inherit from Movable)
# thanks to the __subclasshook__ both checks below are true
if issubclass(Button, Movable):
bt = Button()
bt.move_right(4)
bt.move_left(2)
bt = Button()
if isinstance(bt, Movable):
bt.move_right(4)
bt.move_left(2)
#moving rigth
#moving left
#moving rigth
#moving left
This discussion probably explains all this quite better than this post :-)
Additionally there's an alternative/complementary way to declare that a class complains with the "interface" defined by an abc, the ABCMeta.register method. With this you declare that specific classes complain with the "interface" rather than defining the conditions to comply with it, as you do with __subclasshook__. The collections.abc module is a very interesting sample of the use of these mechanisms.
No comments:
Post a Comment