1. Subclassing Built-In Types Is Tricky
Since Python 2.2, subclassing built-in types such as list or dict can be done but there is a major caveat: the code of the built-ins (written in C) does not call special methods overridden by user-defined classes.
A good short description of the problem is in the documentation for PyPy, shown as below:
Officially, CPython has no rule at all for when exactly overridden method of subclasses of built-in types get implicitly called or not. As an approximation, these methods are never called by other built-in methods of the same object. For example, an overridden __getitem__() in a subclass of dict will not be called e.g. the built-in get() method.
Example 12-1. Our __setitem__ override is ignored by the __init__ and __update__ methods of the built-in dict.
>>> class DoppelDict(dict): ... def __setitem__(self, key, value): ... super().__setitem__(key, [value] * 2) ... >>> dd = DoppelDict(one=1) # The __init__ method inherited from dict clearly ignored that __setitem__ was overridden: the value of 'one' is not duplicated. >>> dd {'one': 1} >>> dd['two'] = 2 # The [] operator calls our __setitem__ and works as expected: 'two' maps to the duplicated value [2, 2] >>> dd {'one': 1, 'two': [2, 2]} >>> dd.update(three=3) # The update method from dict does not use our version of __setitem__ either: the value of 'three' was not duplicated. >>> dd {'one': 1, 'two': [2, 2], 'three': 3}
This built-in behavior is a violation of a basic rule of object-oriented programming: the search for methods should always start from the class of the target instance(self), even when the call happens inside a method implemented in a superclass. In this sad state of affairs, the __missing__ method works as documented only because it's handled as a special case.
This problem is not limited to calls within an instance, but also happens with overridden methods of other classes that should be called by the built-in methods.
Example 12-2. The __getitem__ of AnswerDict is bypassed by dict.update
>>> class AnswerDict(dict): ... def __getitem__(self, key): ... return 42 ... >>> ad = AnswerDict(a='foo') >>> ad {'a': 'foo'} >>> ad['a'] # ad['a'] returns 42, as expected. 42 >>> d = {} >>> d.update(ad) >>> d['a'] # The dict.update method ignored our AnswerDict.__getitem__ 'foo' >>> d {'a': 'foo'} """ Subclassing built-in types like dict or list or str directly is error-prone because the built-in methods mostly ignore user-defined override. Instead of subclassing the built-ins, derive your classes from the collections module using UserDict, UserList, and UserString, which are designed to be easily extended. """
If you subclass collections.UserDict instead of dict, the issues exposed in Eamples 12-1 and 12-2 are both fixed. See Example 12-3.
Example 12-3. DoppelDict2 and AnswerDict2 work as expected because they extend UserDict and not dict.
>>> import collections >>> >>> class DoppelDict2(collections.UserDict): ... def __setitem__(self, key, value): ... super().__setitem__(key, [value] * 2) ... >>> dd = DoppelDict2(one=1) >>> dd {'one': [1, 1]} >>> dd['two'] = 2 >>> dd {'one': [1, 1], 'two': [2, 2]} >>> dd.update(three=3) >>> dd {'one': [1, 1], 'two': [2, 2], 'three': [3, 3]} >>> >>> >>> class AnswerDict2(collections.UserDict): ... def __getitem__(self, key): ... return 42 ... >>> ad = AnswerDict2(a='foo') >>> ad['a'] 42 >>> d = {} >>> d.update(ad) >>> d['a'] 42 >>> d {'a': 42} """ The problem described in this section applies only to method delegation within the C language implementation of the built-in types, and only affects user-defined classes derived directly from those types. If you subclass from a class coded in Python such as UserDict or MutableMapping, you will not be troubled by this. """
2. Multiple Inheritance and Method Resolution Order
I often check the __mro__ of classes interactively when I am studing them. Example 12-8 has some examples using familiar classes.
Example 12-8. Inspecting the __mro__ attributes in several classes.
>>> bool.__mro__ (<class 'bool'>, <class 'int'>, <class 'object'>) >>> >>> def print_mro(cls): ... print(', '.join(c.__name__ for c in cls.__mro__)) ... >>> print_mro(bool) bool, int, object >>> >>> import numbers >>> print_mro(numbers.Integral) Integral, Rational, Real, Complex, Number, object >>> >>> import io >>> print_mro(io.BytesIO) BytesIO, _BufferedIOBase, _IOBase, object >>> print_mro(io.BytesIO) BytesIO, _BufferedIOBase, _IOBase, object """ bool inherits methods and attributes from int and object. """
end...