Python3高级编程第2版-语法类级别以上

学习总结:Python高级编程第2版-语法最佳实践-类级别以上

  • 子类化内置类型
  • 访问超类中的方法
  • 高级属性访问模式

子类化内置类型

Python中的类都有一个共同的祖先object。当需要实现与某个内置类型相似的行为时,可以将这个内置类型子类化。

子类化dict

我们创建子类 distinctdict 继承 dict ,添加新元素时不允许多个键对应相同的键值,否则触发自定义异常DistinctError.

1
2
3
4
5
6
7
8
9
class DistinctError(ValueError):
"""如果向distinctdict添加重复value,则触发该异常"""

class distinctdict(dict):
def __setitem__(self, key, value):
if value in self.values():
if (key in self and self[key] != value) or key not in self:
raise DistinceError("This value already exists for different key")
super().__setitem__(key, value)

子类化list

我们创建子类Folder继承list,管理序列。

1
2
3
4
5
6
7
8
9
10
11
12
class Folder(list):
def __init__(self, name):
self.name = name

def dir(self, nesting=0):
offset = " "*nesting
print("%s%s/" % (offset, self.name))
for element in self:
if hasattr(element, 'dir'):
element.dir(nesting+1)
else:
print("%s %s" % (offset, element))

在交互式shell中测试,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> tree = Folder('project')
>>> tree.append('README.md')
>>> tree.dir()
project/
README.md
>>> src = Folder("src")
>>> src.append("script.py")
>>> tree.append(src)
>>> tree.dir()
project/
README.md
src/
script.py

访问超类中的方法

super是一个内置类,可用于访问某个对象的超类的属性。

Python官方文档将super作为内置函数给出,虽然它的用法与函数类似,但实际上它是一个内置类。

1
2
>>> super
<class 'super'>

旧的写法与新的写法

旧的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Mama:
def say_hello(self):
print("Mama: Hello")

def says(self):
print("do your homework")
self.say_hello()

class Sister(Mama):
def say_hello(self):
print("Sister: Hello")

def says(self):
Mama.says(self)
print("and clean your bedroom")

上述例子中Mama.says(self)表明调用超类中的方法,传入的是子类本身self, 下面是执行案例。

1
2
3
4
>>> Sister().says()
do your homework
Sister: Hello
and clean your bedroom

下面展示使用super的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Mama:
def say_hello(self):
print("Mama: Hello")
def says(self):
print("do your homework")
self.say_hello()

class Sister(Mama):
def say_hello(self):
print("Sister: Hello")

def says(self):
super(Sister, self).says() # 或者super().says()
print("and clean your bedroom")

super 可以在方法内部使用,也可以在方法外部使用,下面的例子是在方法外部使用。

1
2
3
4
5
6
>>> anita = Sister()
>>> anita.__class__
<class '__main__.Sister'>
>>> super(anita.__class__, anita).says()
do your homework
Sister: Hello

super 的第二个参数是可选的,如果只提供第一个参数,那么super返回的是一个未绑定的类型,这一点与classmethod 在一起使用时特别有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Pizza:
def __init__(self, toppings):
self.toppings = toppings

def __repr__(self):
return "Pizza with: " + " and ".join(self.toppings)

@classmethod
def recommend(cls):
return cls(["spam", "ham", "eggs"])

class VikingPizza(Pizza):
@classmethod
def recommend(cls):
recommended = super().recommend() # 书中是super(VikingPizza).recommend(),但是python3.7.9运行报错
recommended.toppings += ['spam'] * 5
return recommended

  • 多继承中super将变得非常难用
  • 需要理解何时避免使用 super
  • 需要理解方法解析顺序(Method Resolution Order, MRO)

理解Python的MRO

Python的解析方法基于C3,C3是为Dylan编程语言构建的MRO,参考文档位于 https://www.python.org/download/releases/2.3/mro/ 。描述了C3是如何构建一个类的线性化(优先级,祖先的有序列表)。

  • L[MyClass(Basel, Base2)] = MyClass + merge(L[Basel], L[Base2], Basel, Base2)
  • C的线性化是C加上父类的线性化和父类列表的合并的总和
  • merge算法负责删除重复项并保持正确的顺序

类的__mro__属性保存了线性化的计算结果,也可以调用 .mro()获取。

使用super易犯的错误

如果在多继承下使用了 super ,这是非常危险的,主要原因在于类的初始化。在Python中,基类不会在__init__()中被隐式的调用,所以需要开发人员来调用它们。

混用super与显示类调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A:
def __init__(self):
print("A", end=" ")
super().__init__()

class B:
def __init__(self):
print("B", end=" ")
super().__init__()

class C(A, B):
def __init__(self):
print("C", end=" ")
A.__init__(self)
B.__init__(self)

>>> C.mro()
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
>>> print("MRO:", [x.__name__ for x in C.mro()])
MRO: ['C', 'A', 'B', 'object']
>>> C()
C A B B <__main__.C object at 0x0000028B7F5FBD08>

出现上面输出的原因是C的实例调用了A的__init__()函数,使得super(A, self).__init__()调用了B.__init__()方法。

super 应被用到整个类的层次结构中,但不幸的是,有时这种层次结构位于第三方代码中,你无法确定外部包代码中是否使用了 super 。如果需要对第三方类进行子类化,最好总是查看其内部代码以及MRO中其他类的内部代码。

不同种类的参数

使用 super 的另一个问题是初始化过程中的参数传递。如果没有相同的签名,一个类怎么能调用其基类的__init__()代码呢?这会导致下列问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CommonBase:
def __init__(self):
print("CommonBase")
super().__init__()

class Base1(CommonBase):
def __init__(self):
print("Base1")
super().__init__()

class Base2(CommonBase):
def __init__(self, arg):
print("Base2")
super().__init__()

class MyClass(Base1, Base2):
def __init__(self, arg):
print("MyClass")
super().__init__(arg)

>>> MyClass()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init__() missing 1 required positional argument: 'arg'

创建MyClass会报错,原因是其与父类Bas1的__init__()签名不匹配。

解决这种问题的办法之一是将所有类的参数都写成*args, **kwargs这种形式,但这会导致代码很脆弱,任何参数都能被接受。

另一种方式是在MyClass中显示调用__init__(),但这会导致和上一小节相同的问题。

最佳实践

  • 避免使用多继承:使用设计模式来代替它
  • super的使用必须一致:在类的层次结构中,要么不用super,要么全用super
  • 调用父类时必须查看类的层次结构

高级属性访问模式

C++和JAVA中有private关键字保护属性,但是在Python中与其最接近的是名称修饰(name mangling),每当在一个属性前面加上__前缀,解释器就会立即将其重命名

1
2
3
4
5
6
7
8
9
10
11
>>> class A:
... __secret = 1
...
>>> a = A()
>>> a.__secret
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__secret'
>>> dir(a)
['_A__secret', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>>

可以看出 __secret属性被重命名为_A__secret

Python提供这一特性是为了避免继承中的名称冲突

在Python中,如果一个属性不是公有的,约定以前缀_开头,这是流行的写法。

Python中还有其他可用机制来构建类的公有代码和私有代码,应该使用descriptor和property这些OOP设计的关键特性来设计一个清晰的API。

descriptor - 描述符

  • 描述符允许我们自定义在引用一个对象的属性应该完成的事情。

  • 描述符是Python中复杂属性访问的基础。

  • 描述符在内部用于实现 property方法类方法静态方法super类型

  • 描述符是一个类,定义了另一个类的属性访问方式,即一个类可以将属性管理委托给另一个类。

描述符协议:描述符类基于下面三个特殊方法

  1. __set__(self, obj, type=None): 设置属性时将调用这一方法,称为setter
  2. __get__(self, obj, value): 获取属性时将调用这一方法,称为getter
  3. __delete__(self, obj): 对属性调用 del 或 调用delattr方法时将调用这一方法

数据描述符:实现了__set____get__的描述符

非数据描述符:只实现了__get__的描述符

在每次属性查找时,这个协议的方法实际上由对象的特殊方法__getattribute__()调用(不要与__getattr__()混淆,后者用于其他目的)。

每次通过点号,形式为(instance.attribute),或者getattr(instance, 'attribute')函数执行来执行这样的查找时,都会隐式地调用__getattribute__()方法,它按下列顺序查找属性:

  1. 验证该属性是否为对象的数据描述符
  2. 如果不是,查看该属性是否能在实例对象的__dict__中找到。
  3. 最后,查看该属性是否为对象的非数据描述符

一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class RevealAccess:
"""一个数据描述符"""
def __init__(self, initval=None, name="var"):
self.val = initval
self.name = name

def __get__(self, obj, objtype):
print("Get", self.name)
return self.val
def __set__(self, obj, val):
print("Set", self.name)
self.val = val

class MyClass:
x = RevealAccess(10, 'var "x"')
y = 5

>>> m = MyClass()
>>> m.x
Get var "x"
10
>>> m.x = 20
Set var "x"
>>> m.x
Get var "x"
20
>>> m.y
5

数据描述符和非数据描述符的区别很重要。

  • Python已经使用描述符协议将类函数绑定为实例方法

  • 描述符还支持了classmethod和staticmethod装饰器背后的机制

函数和lambda表达式是非数据描述符:

1
2
3
4
5
6
7
8
9
10
>>> def func(): pass
...
>>> hasattr(func, '__get__')
True
>>> hasattr(fun, '__set__')
False
>>> hasattr(lambda: None, '__get__')
True
>>> hasattr(lambda: None, '__set__')
False

因此,如果没有__dict__优先于非数据描述符,我们不可能在运行时在已经构建好的实例上动态覆写特定的方法。幸运的是,多亏了Python的描述符工作方式,由于这一工作方法,使得开发人员可以使用一种叫做猴子补丁(monkey-patching)的流行技术来改变实例的工作方式,而不需要子类化。

例子 - 延迟求值属性

将类属性的初始化延迟到被实例访问时。

  • 情况一:这些属性的初始化依赖全局上下文
  • 情况二:初始化的代价很大,在导入类的时候不知道是否会用到这个属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class InitOnAccess:
def __init__(self, klass, *args, **kwargs):
self.klass = klass
self.args = args
self.kwargs = kwargs
self._initialized = None

def __get__(self, instance, owner):
if self._initialized is None:
self._initialized = self.klass(*self.args, **self.kwargs)
print("initialized")
else:
print("cached")
return self._initialized

>>> class MyClass:
... value = InitOnAccess(list, "argument")
...
>>> m = MyClass()
>>> m.value
initialized
['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']
>>> m.value
cached
['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']

property

property提供了一个内置的描述符类型,它知道如何将一个属性链接到一组方法上。

property接受4个可选参数:fget、fset、fdel和doc。最后一个参数可以用来定义一个链接到属性的 docstring,就像是一个方法一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Rectangle:
def __init__(self, x1, y1, x2, y2):
self.x1, self.y1 = x1, y1
self.x2, self.y2 = x2, y2
@property
def width(self):
"""rectangle width"""
return self.x2 - self.x1
@width.setter
def width(self, value):
self.x2 = self.x1 + value
@property
def height(self):
"""rectangle height"""
return self.y2 - self.y1
@height.setter
def height(self, value):
self.y2 = self.y1 + value
def __repr__(self):
return "{}({}, {}, {}, {})".format(
self.__class__.__name__,
self.x1, self.y1, self.x2, self.y2
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
>>> rec = Rectangle(10, 10, 25, 24)
>>> rec.width, rec.height
(15, 14)
>>> rec.width = 100
>>> rec
Rectangle(10, 10, 110, 24)
>>> rec.height = 100
>>> rec
Rectangle(10, 10, 110, 110)
>>> help(Rectangle)
Help on class Rectangle in module __main__:

class Rectangle(builtins.object)
| Rectangle(x1, y1, x2, y2)
|
| Methods defined here:
|
| __init__(self, x1, y1, x2, y2)
| Initialize self. See help(type(self)) for accurate signature.
|
| __repr__(self)
| Return repr(self).
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| height
| rectangle height
|
| width
| rectangle width

允许开发人员使用__slots__属性为指定的类设置一个静态属性列表,并在类的每个实例中跳过__dict__字典的创建过程,它可以为属性很少的类解决内存空间,因为每个实例都没有创建__dict__

除此之外,它还可以有助于签名需要被冻结的类,但是对派生类就解除了这个限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class Frozen:
... __slots__ = ['ice', 'cream']
...
>>> '__dict__' in dir(Frozen)
False
>>> 'ice' in dir(Frozen)
True
>>> frozen = Frozen()
>>> frozen.ice = True
>>> frozen.cream = None
>>> frozen.icy = True
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Frozen' object has no attribute 'icy'