Inheritance: For Good or For Worse

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 by e.g. the built-in get() method.

自写示例:

# 字典的 [] 操作符取值会自动调用 __getitem__() 方法,但字典内置的 get() 方法并没有直接关联 __getitem__() 方法

In [53]: class DoppelDict(dict): 
    ...:     def __getitem__(self, key): 
    ...:         return key * 3 
    ...:

In [54]: dd = DoppelDict(one=10)

In [55]: dd['one']                                                                                                                                           
Out[55]: 'oneoneone'

In [56]: dd['two']                                                                                                                                           
Out[56]: 'twotwotwo'

In [57]: dd.get('one')                                                                                                                                       
Out[57]: 10

# 虽然 DoppelDict 继承了内置的 dict 类型,但其重写的 __getitem__() 方法并没有被它其它的内置方法(如: get() 方法)自动调用

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

在自己做测试时,出现了一些没有想到的结果。。。:

# iPython 的结果:
In [60]: class AnswerDict(dict): 
    ...:     def __getitem__(self, key): 
    ...:         return 42 
    ...:                                                                                                                                                     

In [61]: ad = AnswerDict(a='foo')                                                                                                                            

In [62]: ad                                                                                                                                                  
Out[62]: {'a': 42}


# 终端直接调用python3 的结果:
Python 3.6.3 (v3.6.3:2c5fed86e0, Oct  3 2017, 00:32:08) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 
>>> class AnswerDict(dict):
...     def __getitem__(self, key):
...         return 42
... 
>>> ad = AnswerDict(a='foo')
>>> ad
{'a': 'foo'}


# 同样都是直接输入 ad ,但得到的结果却不一样。。。

 

 

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

posted @ 2020-02-25 02:58  neozheng  阅读(194)  评论(0编辑  收藏  举报