11. 符合Python风格的对象

发布时间:2024年01月21日

11. 符合Python风格的对象

一个库或框架是否符合Python风格, 要看它能不能让Python程序员以一种简单而自然的方式执行任务.
										
										--Martijn Faassen Python 和JavaScript 框架开发者①
                                        
(1: 摘自Faassen的题为'What is Pythonic?'的博客文章.)
得益于Python数据模型, 自定义类型的行为可以像内置类型那样自然.
实现如此自然的行为, 靠的不是继承, 而是鸭子类型: 只需按照预定行为实现对象所需的方法即可.

前几章分析了很多内置对象的行为.
本章会自己定义类, 让类的行为跟真正的Python对象一样.
你在开发应用程序时, 不一定要像本章的示例那样实现那么多特殊方法.
然而, 对库或框架来说, 程序员可能希望你定义的类能像Python内置的类一样.
满足这个预期也算得上是符合'Python风格'.
本章接续第1, 说明如何实现很多Python类型中常见的特殊方法.
本章包含以下话题:
? 把对象转换成其他类型的内置函数(例如repr(), bytes(), complex());
? 通过一个类方法实现备选构造函数;
? 扩展f字符串, 内置函数format()和str.format()方法使用的格式化微语言;
? 实现只读属性;
? 把对象变为可哈希的, 以便在集合中及作为dict的键使用;
? 利用__slots__节省内存.

本章将开发一个简单的二维欧几里得向量类型Vector2d, 
在这个过程中涵盖上述全部话在实现这个类型的中间阶段, 本章会讨论如下两个概念:
? 如何以及何时使用@classmethod装饰器和@staticmethod装饰器;
? Python中私有属性和受保护属性的用法, 约定和局限.
11.1 本章新增内容
本章引言部分内容换了, 2段又增加了几句话, 说清'符合Python 风格'是什么意思.
1版直到全书最后才讨论这个问题.

11.6节有更新, 增加了Python3.6引入的f字符串.
但那一节改动不大, 因为f字符串支持的格式化微语言与内置函数format()和str.format()方法一样, 
以前实现的_format_方法也适用于f字符串.

本章其他内容基本没变, 毕竟自Python 3.0以来, 特殊方法没什么变化, 而且核心理念在Python2.2中就出现了.

从对象表示形式函数开始讲起.
11.2 对象表示形式
每门面向对象语言至少都有一种获取对象字符串表示形式的标准方式.
Python提供了两种方式.
repr()
  以便于开发者理解的方式返回对象的字符串表示形式.
  Python 控制台或调试器在显示对象时采用这种方式.
str()
  以便于用户理解的方式返回对象的字符串表示形式.
  使用print()打印对象时采用这种方式.
  
1章讲过, 在背后支持repr()和str()的是特殊方法__repr__和__str__.

除此之外, 还有两个特殊方法(__bytes__和__format__)可为对象提供其他表示形式.
__bytes__方法与__str__方法类似, bytes()函数调用它获取对象的字节序列表示形式.
而__format__方法供f字符串, 内置函数format()和str.format()方法使用, 
通过调用obj.__format__(format_spec)以特殊的格式化代码显示对象的字符串表示形式.
本章将先讨论__bytes__方法, 随后再讨论__format__方法.
***-----------------------------------------------------------------------------------------***
如果你是Python 2用户, 那么请记住, 在Python3中,
__repr__, __str___和__format__都必须返回Unicode字符串(str 类型).
只有__bytes__方法应该返回字节序列(bytes类型).
***-----------------------------------------------------------------------------------------***
11.3 再谈向量类
本章将以一个与第1章类似的Vector2d类为例, 说明用于生成对象表示形式的众多方法.
本节和接下来的几节会逐渐实现这个类.
我们期望Vector2d实例具有的基本行为如示例11-1所示.
# 示例 11-1 Vector2d实例有多种表示形式
>>> v1 = Vector2d(3, 4)

# Vector2d实例的分量可以直接通过属性访问(无须调用读值方法).
>>> print(v1.x, v1.y)
3.0 4.0

# Vector2d实例可以拆包成变量元组.
>>> ×, y = v1
(3.0, 4.0)

# Vector2d实例的表示形式模拟源码构建实例的形式.
>>> v1
Vector2d(3.0, 4.0)

# 这里使用eval函数表明Vector2d实例的表示形式是对构造函数的准确表述. ②
# (注2: 使用eval函数克隆对象是为了说明repr方法.
# 使用copy.copy函数克隆实例更安全且更快速.)
>>> v1_clone = eval(repr(v1))
# Vector2d 实例支持使用==比较, 这样便于测试.
>>> v1 == v1_clone
True
# print 函数调用str函数, 对Vector2d来说, 输出的是一个有序对.
>>> print(v1)
(3.0, 4.0)

# bytes 函数调用__bytes__方法, 输出实例的二进制表示形式.
>>> octets = bytes(v1)
>>> octets
b'd\\x00\\x0O\\xO0\\x00\|x00\|xO0\|xO8@\\x00\\x00\|x00\\x00\|x00\|x00\|x10@'

# abs函数调用__abs__方法, 返回Vector2d实例的模.
>>> abs(v1)
5.0

# bool函数调用__bool__方法, 如果 Vector2d实例的模为零, 就返回False, 否则返回True.
>>> bool(v1), bool(Vector2d(0, 0))
(True, False)

示例11-1中的Vector2d类在vector2d_v0.py 文件中实现(参见示例11-2).
这段代码基于示例1-2, 支持+运算和*运算的方法将在第16章实现.
另外, 为了方便测试, 示例11-2增加了支持==运算符的方法.
现在, vector2d用到了几个特殊方法, 这些方法提供的操作是Python程序员预期设计良好的对象应当提供的.
# 示例 11-2 vector2d_v0.py:目前定义的都是特殊方法
from array import array
import math

class Vector2d:
    # typecode 是类属性, 在Vector2d实例和字节序列之间转换时使用.
    typecode = 'd'
    """
    typecode属性为'd', 该属性指定了数组中元素的类型为双精度浮点数.
    在数组初始化时, 通过使用该属性来创建数组, 因此数组中的元素将会是浮点数类型.
    """
    
    def __init__(self, x, y):
        # 在_init_方法中把x和y转换成浮点数, 
        # 尽早捕获错误, 以防调用Vector2d构造函数时传入不当参数.
        self.x = float(x)
        self.y = float(y)
    
    def __iter__(self):
        # 定义__iter__方法, 把Vector2d实例变成可迭代对象, 这样才能拆包(例如, ×, y = my_vector)
        # 这个方法的实现方式很简单, 直接调用生成器表达式依次产出分量. ③
        # (注3:这一行也可以写成yield self.x; yield.self.y.
        # 第17章将进一步讨论__iter__特殊方法, 生成器表达式和yield关键字.)
        return (i for i in (self.x, self.y))
        
    def __repr__(self):
        class_name = type(self).__name__
        # _repr_方法使用{1r}获取各个分量的表示形式, 然后插值, 构成一个字符串.
        # 因为Vector2d实例是可迭代对象, 所以*self会把×分量和y分量提供给format方法.
        return '{}({!r}, {!r})'.format(class_name, *self)
    
    def __str__(self):
        # 从可迭代的Vector2d实例中可以轻易得到一个元组, 显示为有序对.
        return str(tuple(self))
    
    def __bytes__(self):
        # 为了生成字节序列, 把typecode转换成字节序列, 然后......
        # ......迭代 Vector2d实例, 得到一个数组, 再把数组转换成字节序列.
        return (bytes([ord(self.typecode)]) + 
                bytes(array(self.typecode, self))) 
    
    def __eq__(self, other):
        # 为了快速比较所有分量, 把运算对象转换成元组.
        # 对Vector2d实例来说, 虽然可以这样做, 但仍有问题. 参见下面的警告栏.
        return tuple(self) == tuple(other)
    
    def __abs__(self):
        # 模是×分量和y分量构成的直角三角形的斜边长.
        return math.hypot(self.x, self.y)
    
    def __bool__(self):
        # bool_方法使用abs(self)计算模, 然后把结果转换成布尔值,
        # 因此, 0.0是False, 非零值是True.
        return bool(abs(self))
    
***-----------------------------------------------------------------------------------------***
示例11-2中的__eq__方法, 在两个运算对象都是Vector2d实例时没有向题,
不过拿Vector2d实例与其他具有相同数值的可迭代对象相比, 结果也是True(例如, Vector(3, 4)==[3, 4]).
这个行为既可以被视为特性, 也可以被视为bug.
16章在讲到运算符重载时将进一步讨论.
***-----------------------------------------------------------------------------------------***

我们已经定义了很多基本方法, 但是显然少了一个操作: 
使用bytes()函数生成的二进制表示形式重建Vector2d实例.
11.4 备选构造函数
现在可以把Vector2d实例转换成字节序列了.
同理, 我们也希望能从字节序列构建vector2d 实例.
在标准库中探索一番之后, 我们发现array.array 有个类方法.frombytes(2.10.1节介绍过)正好符合需求.
下面在vector2d_v1.py 文件中为Vector2d定义一个同名类方法, 如示例11-3所示.
# 示例11-3 vector2d_vl.py的一部分:这段代码只列出了fronbytes类方法, 
# 要添加到vector2d_v0.py(参见示例11-2)定义的Vector2d类中

# classmethod 装饰的方法可直接在类上调用.
@classmethod
def frombytes(cls, octets):  # 第一个参数不是self, 而是类自身(习惯命名为cls).
    # 从第一字节中读取typecode.
    typecode = chr(octets[0])  
    # 使用传入的octets字节序列创建一个memoryview, 然后使用typecode进行转换. ④
    # 注4: 2.10.2节简单介绍过memoryview, 说明了它的, cast方法.
    memv = memoryview(octets[1:]).cast(typecode)
    # 拆包转换后的memoryview, 得到构造函数所需的一对参数.
    return cls(*memv)

我们用的classmethod装饰器是Python特有的, 下面来讲解一下.
11.5 classmethod与staticmethod
Python官方教程没有提到classmethod装饰器, 也没有提到staticmethod.
学过Java面向对象编程的人可能觉得奇怪, 为什么Python提供两个这样的装饰器, 而不是只提供一个?

先来看classmethod.
示例11-3展示了它的用法: 定义操作类而不是操作实例的方法.
由子classmethod改变了调用方法的方式, 因此接收的第一个参数是类本身, 而不是实例.

classnethod最常见的用途是定义备选构造函数, 例如示例11-3中的frombytes.
注意, frombytes的最后一行使用cls参数构建了一个新实例, 即cls(*memv).

相比之下, staticmethod装饰器也会改变方法的调用方式, 使其接收的第一个参数没什么特殊的.
其实, 静态方法就是普通的函数, 只是碰巧位于类的定义体中, 而不是在模块层定义.
示例11-4对classmethod和staticmethod的行为做了对比.

# 示例11-4 比较classmethod 和staticmethod的行为
class Demo:
...     @classmethod  
...     def klassmeth(*args):  # 没有写cls参数时, 类名会被*args一起接收.
...         return args  # klassmeth返回全部位置参数. (包括cls)
    
...     @staticmethod
...     def statmeth(*args):
...         return args  # statmeth 也返回全部位置参数.


# 不管怎样调用Demo.klassmeth, 它的第一个参数始终是Demo类.
>>> Demo.klassmeth()  
(<class '__main__.Demo'>,)

>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')

# Demo.statmeth的行为与普通的函数一样.
Demo.statmeth()
()

Demo.statmeth('spam')
('spam',)

classmethod装饰器非常有用, 但是我从未见过不得不使用staticmethod的情况.
有些函数即使不直接处理类, 也与类联系紧密, 因此你会想把函数与类放在一起定义.
对于这种情况, 在类的前面或后面定义函数, 保持二者在同一个模块中基本上就可以了. 
现在, 我们对classmethod的作用已经有所了解(而且知道staticmethod不是特别有用).
下面继续讨论对象的表示形式, 说明如何支持格式化输出.

(5: 本书的技术审校之--Leonardo Rochael不同意我对staticmethod的贬低,
作为反驳, 他推荐阅读Julien. Dunyou写的一篇题为'The Definitive Guide on How to Use Static, 
Class or Abstract Methrods i Pytben'的博客文章.
Danjou的这篇文章写得很好, 我推荐阅读. 但是, 我对staticnethod的观点依然不变. 请读者自辦.)
11.6 格式化显示
f字符串, 内置函数format()和str.format()方法
会把各种类型的格式化方式委托给相应的.__format__(format_spec)方法.
format_spec是格式说明符, 它是:

? format(my_obj, format_spec)的第二个参数; 
? {}内代换字段中冒号后面的部分, 或者fnt.str.format()中的fnt.
请看以下示例.
# 巴西雷亚尔兑换美元的汇率
>>> brl= 1 / 4.82 
>>> brl
0.20746887966804978

# 格式说明符是'0.4f'
>>> format(brl,'0.4f')  
'0.2075'

# 格式说明符是'0.2f'. 代换字段中的rate部分不属于格式说明符,
# 只用于决定把.format()的哪个关键字参数传给代换字段.
>>> '1 BRL = {rate: 0.2f} USD'.format(rate=brl)
'1 BRL = 0.21 USD'

# 同样, 格式说明符是'0.2f'. 1/brl表达式不属于格式说明符.
>>> f'1 USD = {1 / brl: 0.2f} BRL'
'1 USD = 4.82 BRL'

2和第3个标号指出了一个重要知识点:
'{0.mass: 5.3e}'这样的格式字符串其实包含两部分,
冒号左边的'0.mass'在代换字段句法中是字段名, 在f字符串中可以是任意表达式;
冒号右边的'5.3e'是格式说明符.
格式说明符使用的表示法叫格式规范微语言(Format Specification Mini-Language).

*---------------------------------------------------------------------------------------------*
如果你对f字符串, format()和str.format()感到陌生, 
根据我的教学经验, 最好先学内置函数format()因为它只使用格式规范微语言.
学会这些表示法之后, 再阅读'Formatted String Literals''Format StringSyntax',
学习f字符串和str.format()方法使用的代换字段表示法{:}(包括转换标志!s, !r和!a).
f字符串出现之后, str.format()并没有被淘汰: 
f字符串适用于多数情况, 不过有时候格式化字符串不在渲染的地方而是在别处指定.
*---------------------------------------------------------------------------------------------*

格式规范微语言为一些内置类型提供了专用的表示代码.
例如, b和×分别表示二进制和十六进制的int类型, f表示小数形式的float类型, %表示百分数形式.
>>> format(42,'b')
'101010'
>>> format(2 / 3,'.1%')
166.7%

格式规范微语言是可扩展的, 各个类可以自行决定如何解释format_spec参数.
例如, datetime模块中的类的__format__方法所使用的格式代码与strftime()函数一样.
下面是内置函数format()和str.format()方法的几个示例.
>>> from datetime import datetime
>>> now = datetime.now()
>>> format(now, '%H:%M:%S')
18:49:05
>>> "It's now {:%I:%M %p}".format(now)
"It's now 06:49 PM"

如果一个类没有定义__format__, 那么该方法就会从object继承, 并返回str(my_object).
由于Vector2d类有__str__方法, 因此可以这样做.
>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'

然而, 如果传入格式说明符, 则object.__format__会抛出TypeError.
>>> format(v1, '.3f')
Traceback (most recent call last):
  ...
TypeError: non-empty format string passed to object.__format__

我们将实现自己的格式微语言来解决这个问题.
首先, 假设用户提供的格式说明符是用于格式化向量中各个float分量的.
我们想达到的效果如下所示.
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'

>>> format(v1, '.2f')
'(3.00, 4.00)

>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'

实现这种输出的__format__方法如示例11-5所示.
# 示例11-5 Vector2d.__format__方法, 第1版
# 在Vector2d类中定义
def __format__(self, fmt_spec=''):
    # 使用内置函数format 把 fnt_spec应用到向量的各个分量上, 
    # 构建一个可迭代的格式化字符串.
    components = (format(c, fmt_spec) for c in self)  # 遍历对象的属性(基于__iter__方法). 
    # 把格式化字符串代入公式'(x, y)'中.
    return '({}, {})'.format(*components)

然后, 在微语言中添加一个自定义的格式代码: 
如果格式说明符以'P'结尾, 就在极坐标中显示向量, <r, θ>, 其中r是模, θ(西塔)是弧度. 
格式说明符中的其他部分('p'前面)像往常那样解释.
*---------------------------------------------------------------------------------------------*
为自定义的格式代码选择字母时, 我会避免使用其他类型用过的字母.
根据格式规范微语言文档, 整数使用的代码是'bcdoxXn', 
浮点数使用的代码是eEfFgGn%', 字符串使用的代码是's'.

因此, 我为极坐标选的代码是'P'.
因为各个类会使用自己的方式解释格式代码, 
所以在自定义的格式代码中重复使用代码字母不会出错, 但是可能会让用户感到困惑.
*---------------------------------------------------------------------------------------------*
为了生成极坐标, 我们已经定义了计算模的__abs__方法, 但还要定义一个简单的angle方法,
使用math.atan2()函数计算角度. angle方法的代码如下所示.
# 在Vector2d类中定义
def angle(self):
    return math.atan2(self.y, self.x)

现在可以增强__format__方法, 计算极坐标了, 如示例11-6所示.
# Vector2d.__format__方法, 第2版, 现在能计算极坐标了
        
    def __format__(self, fmt_spec=''):
        # 如果格式代码以'p'结尾, 就使用极坐标.
        if fmt_spec.endswith('p'):
            # 从fnt_spec 中删除'p'后缀.
            fmt_spec = fmt_spec[:-1]
            # 构建一个元组来表示极坐标: (magnitude, angle).
            coords = (abs(self), self.angle())
            # 把外层格式设为一对尖括号.
            outer_fmt = '<{}, {}>'
        else:
            # 如果不以'p'结尾, 则使用self的×分量和y分量构建直角坐标.
            coords = self
            outer_fmt = '({}, {})'
        
        # 使用各个分量生成可迭代的对象, 构成格式化字符串.
        components = (format(c, fmt_spec) for c in coords)
        # 把格式化字符串代入外层格式.
        return outer_fmt.format(*components)
    
# 省略代码, 测试足以.
import math


class Vector2d:
    typecode = 'd'

    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def angle(self):
        return math.atan2(self.y, self.x)

    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'

        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)

运行示例11-6, 结果如下所示.
>>> format(Vector2d(1, 1), 'p')
<1.4142135623730951, 0.7853981633974483>

>>> format(Vector2d(1, 1), '.3ep')
"<1.414e+00, 7.854e-01>'

>>> format(Vector2d(1, 1), '0.5fp')
<1.41421, 0.78540>

如本节所示, 为用户定义的类型扩展格式规范微语言并不难.
下面换一个不仅仅事关对象外在表现的话题.
我们将把Vector2d变成可哈希的, 以便构建向量集合, 或者把向量当作dict的键使用.
11.7 可哈希的Vector2d
按照定义, 目前Vector2d实例不可哈希, 因此不能放入集合中.
>>> V1 = Vector2d(3, 4)
>>> hash(v1)
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Vector2d'

>>> set([v1])
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Vector2d"

为了把Vector2d实例变成可哈希的, 必须实现__hash__方法(还需要__eq__方法, 前面已经实现了).
此外, 还要让向量实例不可变(详见3.4.1).
目前, 可以为分量赋新值(例如v1.x = 7), Vector2d类的代码并不阻止这么做.
而我们想要的行为如下所示.
>>> v1.x, v1.y
(3.0, 4.0)

>>> v1.x = 7
Traceback (most recent call last):
  ...
AttributeError: can't set attribute

为此, 要把×分量和y分量设为只读特性, 如示例11-7所示.
# 示例11-7 vector2d_v3.py: 这里只给出了让Vector2d不可变的代码, 完整的代码清单见示例11-11

class Vector2d:
    typecode = 'd'
    
	def __init__(self, x, y)
        # 使用两个前导下划线(尾部没有下划线或有一个下划线), 把属性标记为私有的. ⑥
        # 注6: 私有属性的优缺点见11.10节.
        self.__x= float(x)
        self.__y = float(y)
    
    
    # @property 装饰器把读值方法标记为特性(property).  让方法支持点操作不加括号.
    @property
    def x(self):  # 读值方法与公开属性同名, 都是×.
        return self.__x  # 直接返回self.__x.
    
    
    @property  # 以同样的方式处理y特性.
    def y(self):
    	return self.__y
    
    def __iter__(self):
         # 需要读取×分量和y分量的方法可以保持不变, 仍然通过self.x和self.y读取公开特性,
         # 而不必读取私有属性, 因此该代码清单省略了这个类余下的代码.
		return (i for i in (self.x, self.y))
    
    # 其他方法可以参见前面的代码清单
    
***-----------------------------------------------------------------------------------------***
Vector.x和Vector.y是只读特性.
22章将讨论读写特性, 届时会深入说明@property装饰器.
***-----------------------------------------------------------------------------------------***
现在, 向量不会被意外修改, 有了一定的安全性, 接下来可以实现__hash__方法了.
这个方法应该返回一个int值, 理想情况下还要考虑对象属性的哈希值(__eq__方法也是如此),
因为相等的对象应该具有相同的哈希值.
特殊方法__hash__的文档建议根据元组的分量计算哈希值, 11-8所示.

# 示例11-8 vector2d_v3.py: 实现__hash__方法 
# 在Vector2d类中定义
    def __hash__(self):
        return hash((self.x, self.y))

实现__hash__方法之后, 向量就变成可哈希的了.
>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)

>>> hash(v1), hash(v2)
(1079245023883434373, 1994163070182233067)

>>> {v1, v2}
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}

*---------------------------------------------------------------------------------------------*
为了创建可哈希的类型, 不一定要实现特性, 也不一定要保护实例属性, 正确实现__hash__方法和__eq__方法即可.
但是, 可哈希对象的值绝不应该变化, 因此我们借机提到了只读特性.
*---------------------------------------------------------------------------------------------*
如果你定义的类型有标量数值, 那么可能还要实现__int__方法和__float__方法
(分别被int()构造函数和float()构造函数调用), 以便在某些情况下强制转换类型.

此外, 还有用于支持内置构造函数complex()的__complex__方法.
Vector2d或许应该提供__complex__方法, 这就给读者留作练习吧.
import math


class Vector2d:

    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __float__(self):
        return float(abs(self))  # 绝对值可转为浮点数

    def __complex__(self):
        return complex(self.x, self.y)  # 返回对应的复数


v1 = Vector2d(3, 4)
print(float(v1))  # 5.0
print(complex(v1))  # (3+4j)

11.8 支持位置模式匹配
目前, vector2d实例兼容关键字类模式(参见5.8.2).
在示例11-9, 所有关键字模式都能按预期匹配.
# 示例 11-9 匹配Vector2d对象的关键字模式 (需要在Python 3.10中操作)
def keyword_pattern_demo(v: Vector2d) -> None:

    match v:
        case Vector2d(x=0, y=0): 
            print(f'{v!r} is null')  
        case Vector2d(x=0):
        	print(f'{v!r} is vertical')
        case Vector2d(y=0):
        	print(f'{v!r} is horizontal')
        case Vector2d(x=x, y=y) if x==y:
        	print(f'{v!r} is diagonal')
        case _:
        	print(f'{v!r} is awesome')
            
然而, 如果使用如下位置模式:
case Vector2d(_, 0):  # 第一个_表示位置参数
    print(f'{v!r} is horizontal')
    
则会得到如下结果.
TypeError: Vector2d() accepts 0 positional sub-patterns (1 given)
# 类型错误: Vector2d() 接受0个位置子模式(给定1个)

为了让vector2d支持位置模式, 需要添加一个名为__match_args__的类属性,
按照在位置模式匹配中的使用顺序列出实例属性.
class Vector2d:
	__match_args__ = ('x', 'y')
    # ......, 等等
    
现在, 编写匹配vector2d对象的模式时可以少敲几次键盘了, 如示例11-10所示.
# 示例11-10 匹配Vector2d对象的位置模式(需要在Python3.10中操作)
def positional_pattern_demo(v: Vector2d)-> None:
    match v:
        case Vector2d(0,0):
            print(f'{v!r} is null')
        case Vector2d(0):
            print(f'{v!r} is vertical')
        case Vector2d(_, 0):
            print(f'{v!r} is horizontal')
        case Vector2d(x, y) if x==y:
            print(f'{v!r} is diagonal')
        case _:
            print(f'{v!r} is awesome')
            
# 省略代码, 测试足以.
import math


class Vector2d:
    __match_args__ = ('x', 'y')
    # ......, 等等
    typecode = 'd'

    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __repr__(self):
        class_name = type(self).__name__

        return '{}({!r}, {!r})'.format(class_name, *self)

    
def keyword_pattern_demo(v: Vector2d) -> None:
    match v:
        case Vector2d(x=0, y=0):
            print(f'{v!r} is null')
        case Vector2d(x=0):
            print(f'{v!r} is vertical')
        # case Vector2d(y=0):
        #     print(f'{v!r} is horizontal')
        case Vector2d(_, 0):
            print(f'{v!r} is horizontal')
        case Vector2d(x=x, y=y) if x == y:
            print(f'{v!r} is diagonal')

        case _:
            print(f'{v!r} is awesome')


obj = Vector2d(1, 0)
keyword_pattern_demo(obj)

__match_args__类属性不一定要把所有公开的实例属性都列出来.
如果一个类的__init__方法可能有全都赋值给实例属性的必需的参数和可选的参数,
那么__match_args__应当列出必需的参数, 而不必列出可选的参数.
现在, 暂停一下, 看看Vector2d类目前的代码.
11.9 第3版Vector2d的完整代码
前面一直在定义Vector2d类, 不过每次只给出了部分片段.
示例11-11是整理后的完整代码清单, 保存在vector2d_v3.py文件中, 包含我在开发时编写的doctest.
# 示例11-11 vector2d_v3.py: 完整版
"""
一个二维向量类

    >>> v1 = Vector2d(3, 4)
    >>> print(v1.x, v1.y)
    3.0 4.0
    >>> x, y = v1
    >>> x, y
    (3.0, 4.0)
    >>> v1
    Vector2d(3.0, 4.0)
    >>> v1_clone = eval(repr(v1))
    >>> v1 == v1_clone
    True
    >>> print(v1)
    (3.0, 4.0)
    >>> octets = bytes(v1)
    >>> octets
    b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
    >>> abs(v1)
    5.0
    >>> bool(v1), bool(Vector2d(0, 0))
    (True, False)


测试类方法``.frombytes()``::

    >>> v1_clone = Vector2d.frombytes(bytes(v1))
    >>> v1_clone
    Vector2d(3.0, 4.0)
    >>> v1 == v1_clone
    True


使用笛卡儿坐标测试``format()``::

    >>> format(v1)
    '(3.0, 4.0)'
    >>> format(v1, '.2f')
    '(3.00, 4.00)'
    >>> format(v1, '.3e')
    '(3.000e+00, 4.000e+00)'


测试``angle``方法::

    >>> Vector2d(0, 0).angle()
    0.0
    >>> Vector2d(1, 0).angle()
    0.0
    >>> epsilon = 10**-8
    >>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
    True
    >>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
    True


使用极坐标测试``format()``::

    >>> format(Vector2d(1, 1), 'p')  # doctest:+ELLIPSIS
    '<1.414213..., 0.785398...>'
    >>> format(Vector2d(1, 1), '.3ep')
    '<1.414e+00, 7.854e-01>'
    >>> format(Vector2d(1, 1), '0.5fp')
    '<1.41421, 0.78540>'

测试只读特性`x`和`y`::

    >>> v1.x, v1.y
    (3.0, 4.0)
    >>> v1.x = 123
    Traceback (most recent call last):
      ...
    AttributeError: can't set attribute 'x'


测试哈希::

    >>> v1 = Vector2d(3, 4)
    >>> v2 = Vector2d(3.1, 4.2)
    >>> len({v1, v2})
    2

"""

from array import array
import math

class Vector2d:
    __match_args__ = ('x', 'y')

    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
                bytes(array(self.typecode, self)))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __hash__(self):
        return hash((self.x, self.y))

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

    def angle(self):
        return math.atan2(self.y, self.x)

    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(*memv)

小结一下, 前两节说明了一些特殊方法, 要想得到功能完善的对象, 这些方法可能是必备的.

***-----------------------------------------------------------------------------------------***
当应用程序真正需要这些特殊方法时才应实现它们.
终端用户并不关心应用程序中的对象是否符合'Python风格'.

另外, 如果你的类是供其他Python程序员使用的库的一部分, 那么你肯定猜不到程序员会对你的对象做什么,
他们或许更希望你的代码符合'Python风格'.
***-----------------------------------------------------------------------------------------***

示例11-11中的Vector2d类只是为了教学, 我们为它定义了许多与对象表示形式有关的特殊方法.
不是每个用户定义的类都要这样做.
11.10节暂时不继续定义Vector2d类了, 
我们将讨论Python对私有属性(带两个下划线前缀的属性, 例如self.__x)的设计方式及其缺点.
11.10 Python私有属性和"受保护"的属性
Python不能像Java那样使用private修饰符创建私有属性,
但是它有一个简单的机制, 能避免子类意外覆盖'私有'属性.

举个例子. 
有人编写了一个名为Dog的类, 内部用到了mood实例属性, 但是没有将其开放.
现在, 你创建了Dog类的子类Beagle. 如果你在毫不知情的情况下又创建了名为mood的实例属性,
那么在继承的方法中就会把Dog类的mood属性覆盖. 
这是难以调试的问题.

为了避免这种情况, 如果以mood的形式(两个前导下划线, 尾部没有或最多有一个下划线)命名实例属性,
那么Python就会把属性名存入实例属性_dict_中, 而且会在前面加上一个下划线和类名.
因此, 对Dog类来说, __mood会变成_Dog__mood; 对Beagle类来说, __mood会变成_Beagle__mood.
这个语言功能叫'名称改写'(name mangling).

示例11-12以示例11-7中定义的Vector2d类为例对名称改写进行了说明.
# 示例11-12私有属性的名称会被'改写', 在前面加上和类名
>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{'_Vector2d__y': 4.0, '_Vector2d__x': 3.0}

>>> v1._Vector2d__x
3.0

名称改写是一种安全措施, 不能保证万无一失: 它的目的是避免意外访问, 不能防止故意做错事.
11-1也是一种保护装置.

safety-switch

11-1: 把手上的盖子是一种保护装置, 而不是安全装置:
它能避免意外触动把手, 但是不能防止有意转动.
如示例11-12的最后一行所示, 只要知道改写私有属性名称的机制,
任何人都能直接读取私有属性--这实际上对调试和序列化很有用.
此外, 编写v1._Vector2d__x=7这样的代码也能直接为Vector2d实例的私有分量赋值.
但是, 如果真在生产环境中这么做了, 那么出问题时可别抱怨.

(这样做存在以下两个问题:
破坏了对象的封装性:
  私有成员的作用是被对象所封装, 只能通过类的接口来访问和修改.
  如果直接通过这种方式来操作私有成员, 就违反了面向对象编程的封装原则.
不安全:
  私有成员的命名方式只是为了表明这是类内部使用的成员, 实际上还是可以被外部访问.
  如果直接通过'_Classname__private_var'的方式来访问私有成员, 
  就容易在不经意间修改了类的内部状态, 导致程序出现不可预料的行为.
  
因此, 在正式的生产环境中, 我们应该遵守类的封装原则, 通过类的接口来访问和修改成员.
如果需要修改私有成员, 可以通过公共的方法或属性来实现.
同时, 我们也应该编写完善的测试用例来测试类的功能和稳定性, 以确保代码的正确性和安全性. )
不是所有Python程序员都喜欢名称改写功能, 也不是所有人都喜欢self.__×这种头重脚轻的名称.
有些人不喜欢这种句法, 他们约定使用一个下划线前缀编写'受保护'的属性(例如self._x).
批评使用两个下划线这种改写机制的人认为, 应该使用命名约定来避免意外覆盖属性.
pip, virtualenv等项目的开发者lan Bicking指出:
  千万千万不要使用两个前导下划线, 这是很烦人的自私行为.
  如果担心名称冲突, 则应该明确使用一种名称改写方式(例如_MyThing_blahblah).
  这其实与使用双下划线一样, 不过自己定的规则比双下划线易于理解. 
  (7: 摘自'Paste StyleGuide'.)
  
Python解释器不会对使用单下划线的属性名做特殊处理,
不过这是很多Python程序员严格遵守的约定, 他们不会在类的外部访问这种属性.
⑧遵守使用一个下划线标记对象的私有属性很容易, 就像遵守使用全大写字母编写常量一样.
(8: 不过, 在模块中, 如果顶层名称使用一个前导下划线, 那么的确会有影响:  from mymod import * ,
mymod中前缀为一个下划线的名称不会被导入.
然而, 依旧可以使用fron mymod inport _privatefunc将其导入.
详见Python官方教程6.1'Moreon Modules'.)

Python文档的某些角落把使用一个下划线前缀标记的属性称为'受保护'的属性.
⑨使用self.x这种形式的'保护'属性的做法很常见, 但很少有人把这种属性叫作'受保护'的属性.
有些人甚至将其称为'私有'属性.
(9:gettext模块文档中就有一个例子.)

总之, Vector2d的分量都是'私有', 而且Vector2d实例都是'不可变'.
我用了两对引号, 因为并不能真正实现私有和不可变. 
(10: 如果这种说法令你感到沮丧, 让你觉得在这方面Python应该向Java看齐,
那么就不要阅读本章的'杂谈'. 我在'杂谈'中探讨了Java private修饰符的相对优势.)

下面继续定义Vector2d类.
11.11 节将讨论一个特殊的属性(不是方法), 
它会影响对象的内部存储, 对内存用量可能也有重大影响, 但是对对象的公开接口没什么影响.
这个属性是__slots__.
11.11 使用__slots__节省空间
默认情况下, Python把各个实例的属性存储在一个名为__dict__的字典中.
3.9节讲过, 字典消耗的内存很多--即使有一些优化措施.
但是, 如果定义一个名为__slots__的类属性, 以序列的形式存储属性名称,
那么Python将使用其他模型存储实例属性:
__slots__中的属性名称存储在一个隐藏的引用数组中, 消耗的内存比字典少.

下面通过几个简单的示例说明一下, 先看示例11-13.
# 示例11-13 使用__slots__的Pixel类
>>> class Pixel:
...     __slots__ = ('x', 'y')  # __slots__必须在定义类时声明, 之后再添加或修改均无效.
...     # 属性名称可以存储在一个元组或列表中, 不过我喜欢使用元组, 因为这可以明确表明__slots__无法修改.


# 创建一个Pixel实例, 因为__slots__的效果要通过实例体现.
>>> p = Pixel()
# 第一个效果: Pixel实例没有__dict__属性.
>>> p.__dict__
Traceback (most recent call last):
  ...
AttributeError: 'Pixel' object has no attribute '__dict__'

# 正常设定p.×属性和p.y属性.
>>> p.x = 10 
>>> p.y = 20
# 第二个效果: 设定不在__slots__中的属性抛出AttributeError.
>>> p.color = 'red'
Traceback (most recent call last):
AttributeError: 'Pixel' object has no attribute 'color'

目前还没有什么难以理解的.
现在, 在示例11-14中定义一个Pixel的子类, 看看__slots__违反直觉的一面.
# 示例11-14 OpenPixel是Pixel的子类

# OpenPixel自身没有声明任何属性.
>>> class OpenPixel(Pixel):
...		pass
    
>>> op = OpenPixel()

# 奇怪的事情发生了, OpenPixel实例有__dict__属性.
>>> op.__dict__ 
{}

# 即使设定属性×(在基类Pixel的__slots__属性中)......
>>> op.x = 8
# 也不存入实例的__dict__属性中......
>>> op.__dict__
()
# ......而是存入实例的一个隐藏的引用数组中.
>>> op.×
8

# 设定不在__slots__中的属性......
>>> op.color = 'green
# ......存入实例的__dict__属性中.
>>> op.__dict__
{'color': 'green'}

示例11-14表明, 子类只继承__slots__的部分效果.
为了确保子类的实例也没有__dict__属性, 必须在子类中再次声明__slots__属性.
如果在子类中声明__slots__= () (一个空元组), 则子类的实例将没有__dict__属性,
而且只接受基类的__slots__属性列出的属性名称.
如果子类需要额外属性, 则在子类的__slots__属性中列出来, 如示例11-15所示.
# 示例11-15 ColorPixel也是Pixel的子类
>>> class ColorPixel(Pixel):
...     __slots__ = ('color', )  # 其实, 超类的__slots__ 属性会被添加到当前类的__slots__属性中.
...     # 别忘了, 只有一项的元组, 因此在那一项后面要加上一个逗号.

>>> cp = ColorPixel()
# ColorPixel实例没有__dict__属性.
>>> cp.__dict__
Traceback (most recent call last):
  ...
AttributeError: 'ColorPixel' object has no attribute '__dict__"

>>> cp.x= 2
# 可以设定在当前类和超类的__slots__中声明的属性, 其他属性则不能设定.
>>> cp.color = 'blue'
>>> cp.flavor = 'banana'
Traceback (most recent call last):
  ...
AttributeError: 'ColorPixel' object has no attribute 'flavor'

然而, '节省的内存也可能被再次吃掉': 如果把'__dict__'这个名称添加到__slots__列表中,
则实例会在各个实例独有的引用数组中存储__slots__中的名称,
不过也支持动态创建属性, 存储在常规的__dict__中.
如果想使用@cached_property装饰器, 就要这么做(详见22.3.5).

当然, '__dict__'添加到__slots__中可能完全违背了初衷,
这取决于各个实例的静态属性和动态属性的数量及其用法.
粗心的优化甚至比提早优化还糟糕, 往往得不偿失.

此外, 还有一个实例属性可能需要注意, 即__weakref__.
为了让对象支持弱引用(6.6节简单提过), 必须有这个属性.
用户定义的类默认就有__weakref__属性.
然而, 如果类中定义了__slots__, 而且想把该类的实例作为弱引用的目标,
则必须把'__weakref__'添加到__slots__中.
下面来看为Vector2d添加_slots_的效果.
11.11.1 简单衡量__slot__节省的内存
示例11-16在Vector2d中实现了__slots__.
# 示例11-16 vector2d_v3_slots.py: 只为Vector2d类增加__slots__属性
class Vector2d:
    # __match_args__列出位置模式匹配可用的公开属性名称.
    __match_args__ = ('x', 'y')
    # 而__slots__列出的是实例属性名称. 这里列出的是私有属性.
    __slots__ = ('__x', '__y')
    
    typecode = 'd'
    # 方法与前面的版本一样

(__slot__为什么可以节省的内存:
在Python中, 每个对象都有一个字典用于存储它所有的属性和方法, 这对于动态添加属性非常有用.
但同时, 这也意味着每个对象都需要额外的内存来存储这些属性.
对于属性数量较少的对象来说, 这些额外的内存开销并不明显, 但对于属性数量众多的对象来说,
这一开销就会变得非常明显, 甚至会影响整个程序的性能.

而__slots__可以有效地解决这个问题, 它是一个特殊的属性, 用于限制一个类的实例可以有哪些属性.
通过将 __slots__属性设置为一个列表或元组, 我们可以明确地告诉Python, 在这个类的实例中,
只会存在__slots__列表中所列出的属性, 这些属性名字会被编译成固定位置的偏移量, 从而省去了实例字典的开销.

这样一来, 由于不需要为每个实例都分配一个字典, 因此可以显著地减少内存占用.
此外, 由于属性名被编译成偏移量, 因此访问属性的速度也可以更快.
需要注意的是. 使用__slots__会限制动态添加实例属性的能力, 
原因是实例只能具有__slots__列表中所列出的属性.
但是, 这种开销的省略和提高的速度通常可以补偿这种限制.)
为了衡量节省了多少内存, 我编写了mem_test.py脚本.
这个脚本在命令行中运行, 参数是两版Vector2d类所在的模块名.
我们使用列表推导式构建一个列表, 存储10000000个Vector2d实例.
第一次运行时, 使用vector2d_v3.Vector2d(来自示例11-7).
第二次运行时, 使用示例11-16中带__slots__的版本, 如示例11-17所示.
# mem_test.py
import importlib
import sys
import resource

NUM_VECTORS = 10**7

module = None
if len(sys.argv) == 2:
    module_name = sys.argv[1].replace('.py', '')
    module = importlib.import_module(module_name)
else:
    print(f'Usage: {sys.argv[0]} <vector-module-to-test>')

if module is None:
    print('Running test with built-in `complex`')
    cls = complex
else:
    fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
    print(fmt.format(module, module.Vector2d))
    cls = module.Vector2d

mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f'Creating {NUM_VECTORS:,} {cls.__qualname__!r} instances')

vectors = [cls(3.0, 4.0) for i in range(NUM_VECTORS)]

mem_final = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f'Initial RAM usage: {mem_init:14,}')
print(f'  Final RAM usage: {mem_final:14,}')

# 示例11-17 mem_test.py 使用指定模块中定义的Vector2d类创建10 000 000个实例
$ time python3 mem_test.py vector2d_v3
Selected Vector2d type: vector2d_v3. Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage: 6,983,680 
  Final RAM usage: 1,666,535,424

real 0m11.990s 
user 0m10.861s
sys 0m0.978s

$ time python3 mem_test.py vector2d_v3_slots
Selected Vector2d type: vector2d_v3_slots. Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage: 6,995,968
  Final RAM usage: 577,839,104

real 0m8.381s
user 0m8.006s
sys 0m0.352s

如示例11-17所示, 10 000 000个Vector2d实例使用__dict__属性时, RAM用量高达1.55 GiB,
而当Vector2d有__slots__属性之后, RAM用量降到了551 MiB.
而且, 带__slots__属性的版本运行速度也更快.
这个测试中使用的mem_test.py脚本其实只是加载一个模块, 检查内存用量和格式化结果,
源码放在fuentpython/example-code-2e中.
*---------------------------------------------------------------------------------------------*
处理数百万个具有数值数据的对象其实应该使用NumPy数组(参见2.10.3).
NumPy数组不仅节约内存, 还高度优化了数值处理函数(很多函数一次性处理整个数组).
我设计 Vector2d类的目的只是为了讨论特殊方法, 因为不想随意举个含糊不清的示例.
*---------------------------------------------------------------------------------------------*
11.11.2 总结__slots__的问题
如果使用得当, 则类属性__slots__能显著节省内存, 不过有几个问题需要注意.
? 每个子类都要重新声明__slots__属性, 以防止子类的实例有__dict__属性.
? 实例只能拥有__slots__列出的属性, 除非把'__dict__'加入__slots__中
  (但是这样做就失去了节省内存的功效).
? 有__slots__的类不能使用@cached_property装饰器, 除非把'__dict__'加入__slots__中.
? 如果不把'__weakref__'加入__slots__中, 那么实例就不能作为弱引用的目标.
class B:
    __slots__ = ('__dict__',)


b = B()
b.a = 1
print(b.a)  # 1

import weakref


class A:
    def __init__(self, x):
        self.x = x


a = A(1)
obj_a = weakref.ref(a)


class B:
    # __slots__ = ('x',)  # TypeError: cannot create weak reference to 'B' object
    # 不能创建'B'对象的弱引用
    __slots__ = ('x', '__weakref__')


b = B()
obj_b = weakref.ref(b)

本章最后一个话题讨论如何在实例和子类中覆盖类属性.
11.12 覆盖类属性
Python有一个很独特的功能: 类属性可为实例属性提供默认值.
Vector2d中有一个名为typecode的类属性. 
__bytes__方法两次用到了这个属性, 而且都故意使用self.typecode读取它的值.
因为Vector2d实例本身没有typecode属性, 所以self.typecode默认获取的是Vector2d.typecode类属性的值.

但是, 如果为不存在的实例属性赋值, 那么将创建一个新实例属性.
假如为typecode实例属性赋值, 那么同名类属性将不受影响.
然而, 一旦这样做, 实例读取的self.typecode是实例属性typecode, 也就是把同名类属性遮盖了.
借助这个功能, 可以为各个实例的typecode属性定制不同的值.

Vector2d.typecode属性的默认值是'd', 即转换成字节序列时使用8字节双精度浮点数表示向量的各个分量.
如果在转换之前把Vector2d实例的typecode 属性设为'f', 那么将使用4字节单精度浮点数表示各个分量,
如示例11-18所示.
# 示例11-18设定原本从类中继承的typecode属性, 自定义一个实例属性
>>> from vector2d_v3 import Vector2d
>>> v1 = Vector2d(1.1, 2.2)

>>> dumpd= bytes(v1)
>>> dumpd
b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@

# 默认的字节序列长度为17字节.
>>> len(dumpd) 
17

# 把v1实例的typecode属性设为'f'.
>>> v1.typecode = 'f'

>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\xOc@'

# 现在得到的字节序列是9字节长.
>>> len(dumpf)
9

# Vector2d.typecode属性的值不变, 只有v1实例的typecode属性使用'f'.
>>> Vector2d.typecode
'd'

**-------------------------------------------------------------------------------------------**
这里在讨论如何添加自定义的实例属性, 
因此示例11-18使用的是示例11-11中不带__slots__属性的Vector2d类.
**-------------------------------------------------------------------------------------------**
现在你应该知道为什么要在得到的字节序列前面加上typecode的值了: 为了支持不同的格式.

如果想修改类属性的值, 那么必须直接在类上修改, 不能通过实例修改.
如果想修改所有实例(自身没有typecode 属性)的typecode属性的默认值, 则可以像下面这样做.
>>> Vector2d.typecode = 'f'

# 示例11-19 ShortVector2d是Vector2d的子类, 只覆盖typecode的默认值
>>> from vector2d_v3 import Vector2d
# 把ShortVector2d定义为Vector2d的子类, 只覆盖typecode类属性.
>>> class ShortVector2d(Vector2d):
...     typecode = 'f'
...
# 为了演示, 创建一个ShortVector2d实例, 即sv.
>>> sv = Shortvector2d(1/11, 1/27)

# 查看sv的表示形式.
>>> sv
ShortVector2d(0.09090909090909091, 0.037037037037037035# 确认得到的字节序列长度为9字节, 而不是之前的17字节.
>>> len(bytes(sv))
9
              
这也说明了在Vecto2d.__repr__方法中为什么没有硬编码class_name的值,
而是使用type(self).__name__获取, 如下所示.
# 在Vector2d类中定义:
    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)
    
如果硬编码class_name的值, 那么仅为了修改class_name的值,
Vector2d的子类(例如Shortvector2d)就要覆盖__repr_方法.
从实例的类型中读取类名, __repr__方法可以放心继承.


'硬编码': 是一种编码方式, 它将常量和其他固定的值直接写入程序中, 
不使用变量或配置文件等外部数据源来存储这些值.
class Vector2d:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        class_name = type(self).__name__
        return f'{class_name}({self.x}, {self.y})'
        # 硬编码的格式, 只适用于固定的类, 被子类继承之后得到的结果不是想要的.
        #  return 'Vector2d(%r, %r)' % (self.x, self.y)


class ShortVector2d(Vector2d):
    def __init__(self, x: int, y: int):
        super().__init__(x=x, y=y)


sv = ShortVector2d(1, 2)
print(f'{sv!r}')  # ShortVector2d(1, 2)

由于ShortVector2d是Vector2d的子类, 它会自动继承__repr__方法,
这样它就会正确地返回类名和向量的坐标.
这也使得我们可以轻松地扩展Vector2d类, 并创建其他类似的子类, 而不需要重复相同的代码.
至此, 本章通过一个简单的类说明了如何利用数据模型处理Python的其他功能,
包括提供不同的对象表示形式, 实现自定义的格式代码, 公开只读属性, 以及通过hash()函数支持集合和映射.
11.13 本章小结
本章的目的是说明如何使用特殊方法和约定的结构来定义行为良好且符合Python风格的类.
vector2d_v3.py(参见示例11-11) vector2d_v0.py(参见示例11-2)更符合Python风格吗?
vector2d_v3.py中的Vector2d类用到的Python功能肯定更多, 
但是Vector2d类的第1版和最后一版相比哪个更符合Python风格要看其使用上下文.
Tim Peter 写的<<Python之禅>>说道:
简洁胜于复杂.

符合Python风格的对象应该正好符合所需, 而不是堆砌语言功能.
开发应用程序时, 应该集中精力满足终端用户的需求, 仅此而已.
编写供其他程序员使用的库时, 应该实现一些特殊方法, 提供Python程序员预期的行为.
例如, __eq__方法对业务需求可能没有必要, 但是方便程序员测试.

本章不断改写Vector2d类是为了提供上下文, 以便讨论Python的特殊方法和编程约定.
回看表1-1, 你会发现本章的几个代码清单说明了以下特殊方法.
? 字符串和字节序列表示形式的方法: __repr__, __str__, __format__和__bytes__.
? 把对象转换成数值的方法: __abs__, __bool__和__hash__.
? 支持测试和哈希的__eq__方法(外加__hash__).

为了转换成字节序列, 本章还实现了一个名为Vector2d.frombytes()的备选构造函数, 
顺便又讨论了@classmethod(十分有用)@staticmethod(不太有用, 使用模块层函数更简单) 两个装饰器.
frombytes方法的实现方式借鉴了array.array类中的同名方法.

我们了解到, 格式规范微语言可通过__format__方法扩展.
在__format__方法中, 我们要做的是解析format_spec.
这就是传给内置函数format(obj, format_spec)的参数, f字符串的代换字段'[: <<format_specn>>]',
以及str.format()方法处理的字符串.

为了把vector2d实例变成可哈希的, 
先让实例不可变, 至少要把×和y设为私有属性, 再以只读特性公开, 以防意外修改.
随后, 实现__hash__方法, 使用推荐的异或运算符计算实例属性的哈希值.

接着, 本章讨论了如何使用_slots__属性节省内存, 以及这么做要注意的问题.
__slots__属性有点儿棘手, 因此仅当处理特别多的实例(数百万个, 而不是几千个)时才建议使用.
如果真有这么多的数量, 那么使用pandas或许是最好的选择.

最后, 本章说明了如何通过访问实例属性(例如self.typecode)覆盖类属性.
我们先创建一个实例属性, 然后创建子类, 在类中覆盖类属性.

前面多次提到, 本章的示例受Python API的影响很大, 这是我长期研究Python标准对象的结果.
如果用一句话总结本章的内容, 那就是:
  要构建符合Python风格的对象, 就要观察真正的Python对象的行为.
                                                               --源自古老的中国谚语
11.14 延伸阅读
本章介绍了数据模型的几个特殊方法, 因此主要参考资料与第1章一样, 阅读那些资料能对这个话题有整体了解.
为方便起见, 这里再次给出之前推荐的4份资料, 同时再多加几份.
<<Python 语言参考手册>>中的第3'数据模型'
  本章用到的方法大部分见于3.3.1'基本定制'.
  
Python in a Nutshell, 3rd ed. (Alex Martelli, Anna Ravenscroft  Steve Holden )
  深入讲解特殊方法.
  
<<Python Cookbook (3)中文版>>
  通过经典实例演示现代Python编程实践. 尤其是第8'类与对象', 其中有好几种方案与本章讨论的话题有关.

<<Python参考手册 (4)>>
  详细说明了数据模型, 即使(4)只涵盖了Python 2.6和Python 3.
  基础概念都是一样的, 自Python 2.2统一内置类型和用户定义的类以来, 数据模型API没有任何变化. 

2015, 也就是写完本书第1版那一年, Hynek Schlawack 启动了attrs包的开发.
attrs文档中有这么一句话:
  attrs包把你从实现对象协议(那些双下划线方法)的苦差事中解放出来, 让你重拾编写类的乐趣.
  
5.10节提到过attrs包, 它功能强大, @dataclass之外的另一种选择.
5章中所述的数据类构建器和attrs包能自动为类配备几个特殊方法.
但是, 知道如何自己编写特殊方法才能理解那些包的作用, 
才能判断是否真正需要使用包, 才能在必要时覆盖包生成的方法.

本章涵盖了与对象表示形式有关的全部特殊方法, 唯有__index__和__fspath__没有讲到.
__index__将在12.5.2节讨论. 
本书不讲_fspath_, 如果你想学习, 请阅读'PEP 519 Adding a file system path protocol'.

意识到应该区分字符串表示形式的早期语言是Smalltalk.
1996, Bobby Woolf写了一篇题为'How to Display an Object as a String:
printString and displayString”的文章,
讨论了Smalltalk xprintString方法和displaystring方法的实现.
11.2节在说明repr()和str()的作用时, 从这篇文章中借用了言简意赅的表述,
'便于开发者理解的方式''便于用户理解的方式'.
*--------------------------------------------杂谈----------------------------------------------*
'特性有助于减少前期投入':

在Vector2d类的第1版中, x属性和y属性是公开的.
默认情况下, Python的所有实例属性和类属性都是公开的.
这对向量来说是合理的, 因为需要访问分量.
虽然这些向量是可迭代的对象, 而且可以拆包成一对变量, 
但我们还是希望能够通过my_vector.x和my_vector.y获取各个分量.

如果觉得应该避免意外更新×属性和y属性, 则可以实现特性(私有化属性), 
但是代码的其他部分没有变化, Vector2d的公开接口也不受影响, 这一点从doctest中可以得知.
我们依然能够访问my_vector.×和my_vector.y.

这表明可以先以最简单的方式定义类, 也就是使用公开属性, 
因为如果以后需要对读值方法和设值方法增加控制, 则可以通过特性实现, 
这样做对一开始通过公开属性的名称(例如×和y)与对象交互的代码没有影响.

Java语言采用的方式则截然相反: 
Java程序员不能先定义简单的公开属性, 等需要时再实现特性, 因为Java 语言没有特性.
因此, 在Java中编写读值方法和设值方法是常态, 就算这些方法没什么实际作用.
这是因为API不能从简单的公开属性变成读值方法和设值方法, 同时又不影响使用那些属性的代码.
此外, Martelli, Ravenscroft和Holden在Python in a Nutshell, 3rd ed. 一书中指出,
到处使用读值方法和设值方法是愚蠢的行为.
如果想编写如下代码:
  >>> my_object.set_foo(my_object.get_foo() + 1)
则这样做就行了.
  >>> my_object.foo += 1
  
维基的发明人和极限编程先驱WardCunningham建议问以下问题:
'做这件事最简单的方法是什么?' 意即, 我们应该把焦点放在目标上.
?提前实现设值方法和读值方法偏离了目标. 在Python中, 可以先使用公开属性, 然后等需要时再变成特性.
(11: 参见'Simplest Thing that Could Possibly Work:
A Conversation with Ward Cunningham, Part V'.)
'私有属性的安全性和保障性':
  Perl不会强制你保护隐私. 你应该待在客厅外, 因为你没有收到邀请, 而不 是因为里面有把枪.
 
                                                                            --Larry Wall
                                                                               Perl之父

Python和Perl在很多方面的做法截然相反, 但是Guido和Larry似乎都同意要保护对象的隐私.

这些年我教过许多Java程序员学习Python, 我发现很多人对Java提供的隐私保障推崇备至.
可事实是, Java的private修饰符和protected修饰符往往只是为了防止意外发生(一种安全措施).
只有使用SecurityManager部署Java应用程序时才能保障绝对安全, 防止恶意访问.
但是, 实际上很少有人这么做, 即便在企业中也少见.

下面通过一个Java类来证明这一点, 如示例11-20所示.
// 示例11-20 Confidential.java: 定义了一个名为secret的私有字段的Java类
public class Confidential {
    
    private String secret = "";
    
    public Confidential(String text){
    	this.secret = text.toUpperCase();
    }
}
示例11-20把text转换成大写后存入了secret字段.
转换成大写只是为了表明secret字段中的值全部是大写形式.

要使用Jython运行expose.py脚本才能真正说明问题.
该脚本使用内省(Java称之为'反射')获取私有字段的值.
示例11-21中是expose.py脚本的代码.
# 示例 11-21 expose.py: 一段Jython代码, 该代码会从另一个类中读取一个私有字段

#!/usr/bin/env jython
# 注意: 截至2020年年底, Jython仍只支持Python 2.7
import Confidential
message = Confidential('top secret text')
secret_field = Confidential.getDeclaredField('secret')
secret_field.setAccessible(True)  # 攻破防线
print 'message.secret =', secret_field.get(message)

运行示例11-21, 得到的结果如下所示.
$ jython expose.py
message.secret = TOP SECRET TEXT
我们从Confidential类的私有字段secret中读取到了字符串'TOP SECRET TEXT'.

这里没有什么黑魔法:expose.py脚本使用Java反射API获取私有字段'secret'的引用, 
然后调用'secret_field.setAccessible(True)'把它设为可读的.
显然, 使用Java代码也能做到这一点(不过所需的代码行数是这里的3倍多, 参见本书代码中的Expose.java文件).

如果这个Jython脚本或Java主程序(例如 Expose.class)在SecurityManager的监管下运行,
那么关键调用.setAccessible(True)就会失败.
但是在现实中, 很少有人使用SecurityManager部署Java应用程序, 浏览器以前支持的Java applet除外.
我的观点是, Java中的访问控制修饰符基本上也是安全措施, 不能保证万无一失, 至少在实践中是这样.
因此, 安心享受Python提供的强大功能, 放心去用吧.
*---------------------------------------------------------------------------------------------*
文章来源:https://blog.csdn.net/qq_46137324/article/details/135725600
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。