目录

Python风格对象

内容概要:

  • 如何使用特殊方法和约定的结构,定义行为良好且符合 Python 风格的类
  • 符合 Python 风格的对象应该正好符合所需,而不是堆砌语言特性

1. 对象表示形式

|特殊方法|调用函数|作用| |: —|: —|: —| |__str__|str()|以用户理解的方式返回对象的字符串表示形式| |__repr__|repr()|以开发者理解的方式返回对象的字符串表示形式| |__bytes__|bytes()|获取对象的字节序列表示形式| |__format__|format() str.format()|使用特殊的格式代码显示对象的字符串表示形式| |__int__ |int()|在某些情况下用于强制转换类型| |__float__|float()|在某些情况下用于强制转换类型| |__complex__|complex()|对象的复数形式|

Python 3:

  • __repr__、 __str__ 和 __format__ 都必须返回 Unicode 字符串(str 类型)
  • __bytes__ 方法应该返回字节序列(bytes 类型)

__index__:

  • 作用: 强制把对象转换成整数索引
  • 应用:
    • 特定的序列切片场景中使用,以及满足 NumPy 的一个需求
    • 在实际编程中,不用实现 __index__ 方法,除非决定新建一种数值类型, 并想把它作为参数传给 __getitem__ 方法
  • 参考:

2. 类的两个装饰器

classmethod:

  • 作用: 定义操作类,而不是操作实例的方法
  • 参数: 类方法的第一个参数是类本身,而不是实例
  • 用途: 最常见的用途是定义备选构造方法

staticmethod:

3. 字符串格式化

格式字符串句法:

  • 作用: 字符串格式化使用的语法,又称代换字段表示法
  • 文档: Format String Syntax https://docs.python.org/3/library/string.html#formatspec
  • 语法: {字段名称: 格式说明符}
    • 字段名称: 与格式说明符无关,用于决定把 .format() 的哪个参数传给代换字段
    • 格式说明符: 使用的表示法叫格式规范微语言(Format Specification Mini-Language)
  • 附注:
    • format() 函数,只使用格式规范微语言
    • str.format() 使用格式字符串句法

格式规范微语言

  • 文档: https://docs.python.org/3/library/string.html#formatspec
  • 特性:
    • 为一些内置类型提供了专用的表示代码
      • 浮点数使用的格式代码 ’eEfFgGn%’, f 表示 float 类型,% 表示百分数形式
      • 整数使用的格式代码有 ‘bcdoxXn’,b 和 x 分别表示二进制和十六进制的 int 类型
      • 字符串使用的是 ’s’
    • 是可扩展的,方法是实现 __format__ 方法,对提供给内置函数 format(obj, format_spec) 的 format_spec,或者提供给 str.format 方法的 ‘{: «format_spec»}’ 位于代换字段中的 «format_spec» 做简单的解析
1
2
3
4
5
6
7
# datetime 模块中的类的 __format__ 方法使用的格式代码与 strftime() 函数一样
>>> from datetime import datetime
>>> now = datetime.now()
>>> format(now, '%H: %M: %S')  # %H 等是 datetime __format__ 扩展的规则
'18: 49: 05'
>>> "It's now {: %I: %M %p}".format(now)
"It's now 06: 49 PM"

4. 可散列对象

需实现方法:

  1. __hash__:
  • 实现:
    • 应该返回一个整数
    • 还要考虑对象属性的散列值,因为相等的对象应该具有相同的散列值
    • 最好使用位运算符异或( ^)混合各分量的散列值
  • 文档: https://docs.python.org/3/reference/datamodel.html
  1. __eq__: 检测相等性,若 a == b 为真,则 hash(a) == hash(b) 也为真
  2. 对象不可变:
  • 实现: 要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只 需正确地实现 __hash__ 和 __eq__ 方法即可。但是,实例的散列值绝不应该变 化以保证散列值不可变,因此需要实现只读特性保证对象不可变
  • 方法:
    • 将属性值保存在私有属性中,再以只读特性公开
    • 使用 @property 装饰器把读取私有属性的读值方法标记为特性,读值方法与公开属性同名

私有属性

  • 定义: 两个前导下划线,尾部没有或最多有一个下划线命名的实例属性
  • 特性:
    • Python 会把属性名存入实例的 __dict__ 属性中,而且会在前面加上一个下划线和类名
    • 又称为名称改写,eg: __mood 会变成 _Dog__mood
  • 目的: 避免子类意外覆盖“私有”属性,不能防止故意做错事
  • 附注: Python 解释器不会对使用单个下划线的属性名做特殊处理

5. __slots__

实例属性:

  • 默认 Python 在实例中名为 __dict__ 的字典里存储实例属性
  • __slots__: 让解释器在元组中存储实例属性,而不用字典
  • 继承自超类的 __slots__ 属性没有效果,Python 只会使用各个类中定义的 slots 属性

__slots__:

  • 语法: 创建一个类属性 __slots__ eg: __slots__ = ('__x', '__y')
  • 类型: 值为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性
  • 作用: 告诉解释器,这个类中的所有实例属性都在这儿了,Python 会在各个实例中使用类似元组的 结构存储实例变量,从而避免使用消耗内存的 __dict__ 属性
  • 特性:
    • 定义 __slots__ 属性之后,实例不能再有 __slots__ 中所列名称之外的其他属性
    • 如果把 __dict__ 添加到 __slots__中,实例会在元组中保存各个实例的属性, 此外还支持动态创建属性,这些属性存储在常规的 __dict__ 中
    • 把 ‘__dict__’ 添加到 __slots__ 中可能完全违背了初衷,这取决于各个实例的 静态属性和动态属性的数量及其用法
  • 应用:
    • 处理列表数据时 __slots__ 属性最有用,例如模式固定的数据库记录,以及特大型数据集
    • 如果要处理数百万个数值对象,应该使用 NumPy 数组
      • NumPy 数组能高效使用内存,而且提供了高度优化的数值处理函数,其中很多都一次操作整个数组

__slots__问题:

  1. 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性
  2. 实例只能拥有 __slots__ 中列出的属性,除非把 ‘__dict__’ 加入 __slots__ 中
  3. 如果不把 ‘__weakref__’ 加入 __slots__,实例就不能作为弱引用的目标
  4. 不要使用 __slots__ 属性禁止类的用户新增实例属性,__slots__ 是用于优化的,不是为了约束程序员
  5. 仅当权衡当下的需求并仔细搜集资料后证明确实有必要时,才应该使用 __slots__ 属性

__weakref__:

  • 为了让对象支持弱引用,必须有这个属性
  • 用户定义的类中默认就有 __weakref__ 属性
  • 如果类中定义了 __slots__ 属性,而且想把实例作为弱引用的目标, 那么要把 ‘__weakref__‘添加到 __slots__ 中

6. 符合 Python 风格的对象

6.1 __slots__

1
2
3
4
5
6
from array import array
import math

class Vector2d:
    __slots__ = ('__x', '__y')
    typecode = 'd'

6.2 可散列与公开只读属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    def __init__(self, x, y):
        self.__x = float(x) # 把 x 和 y 转换成浮点数,尽早捕获错误
        self.__y = float(y)

    # 实现对象不可变
    @property           # 使用 @property 装饰器把读取私有属性的读值方法标记为特性
    def x(self):         #  读值方法与公开属性同名,都是 x
        return self.__x #  使用两个前导下划线,把属性标记为私有的

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

    def __eq__(self, other):  # 若 a == b 为真,则 hash(a) == hash(b) 也为真
        return tuple(self) == tuple(other)  

    def __hash__(self):   # 通过 self.x 和 self.y 读取公开特性
        return hash(self.x) ^ hash(self.y) # 位运算符异或 (^) 混合各分量的散列值

6.3 对象表示形式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    def __iter__(self):   # 把 Vector2d 实例变成可迭代的对象,这样才能拆包
        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)) #  从可迭代的 Vector2d 实例中可以轻松地得到一个元组

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
                bytes(array(self.typecode, self))) # 迭代 Vector2d 实例,得到一个数组

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

    def __bool__(self):  
        return bool(abs(self)) # 可以直接使用 abs()

6.4 自定义格式代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    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)

__format__

  • 如果类没有定义 __format__ 方法
    • 会从 object 继承的方法会返回 str(my_object)
    • 如果传入格式说明符, object.__format__ 方法会抛出 TypeError
1
2
3
4
5
6
7
8
>>> v1 = Vector2d(3, 4)
>>> format(v1) # 等同于调用 Vector2d 类的 \_\_str\_\_
'(3.0, 4.0)'

>>> format(v1, '.3f')
Traceback (most recent call last):
...
TypeError:  non-empty format string passed to object.__format__

6.5 备选构造方法

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

7. 覆盖类属性

  1. 类属性可用于为实例属性提供默认值
  2. 实例属性会覆盖同名类属性
  3. 类属性是公开的,因此会被子类继承,可以通过创建子类,用于定制类属性

延伸阅读

Python:

NumPy:

Pandas:

blog:

实用工具

书籍:

Python 语言参考手册中

《 Python 技术手册(第 2 版)》

《 Python Cookbook(第 3 版)中文版》

《 Python 参考手册(第 4 版)》

附注

特性:

  • 可以先以最简单的方式定义类,也就是使用公开属性
  • 如果以后需要对读值方法和设值方法增加控制,那就可以实现特性
  • 这样做对一开始通过公开属性的名称与对象交互的代码没有影响
  • Java:
    • 没有特性
    • API 不能从简单的公开属性变成读值方法和设值方法,同时又不影响使用那些属性的代码

私有属性的安全性和保障性

  • Java 的 private 和 protected 修饰符往往只是为了防止意外 (即一种安全措施)。只有使用安全管理器部署应用时才能保障绝对安全,防止恶意访 问;但是,实际上很少有人这么做,即便在企业中也少见
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import importlib
import sys
import resource

NUM_VECTORS = 10**7

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

fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
print(fmt.format(module, module.Vector2d))

mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print('Creating {:,} Vector2d instances'.format(NUM_VECTORS))

vectors = [module.Vector2d(3.0, 4.0) for i in range(NUM_VECTORS)]

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