第9章–符合python风格的对象
这一章接续第 1 章,说明如何实现在很多 Python 类型中常见的特殊方法。
本章包含以下话题:
• 支持用于生成对象其他表示形式的内置函数(如 repr()、bytes(),等等)
• 使用一个类方法实现备选构造方法
• 扩展内置的 format() 函数和 str.format() 方法使用的格式微语言
• 实现只读属性
• 把对象变为可散列的,以便在集合中及作为 dict 的键使用
• 利用 slots 节省内存
我们将开发一个简单的二维欧几里得向量类型,在这个过程中涵盖上述全部话题。
在实现这个类型的中间阶段,我们会讨论两个概念:
• 如何以及何时使用 @classmethod 和 @staticmethod 装饰器
• Python 的私有属性和受保护属性的用法、约定和局限
9.1 对象表示形式
- repr() 以便于开发者理解的方式返回对象的字符串表示形式。
- str() 以便于用户理解的方式返回对象的字符串表示形式。
- format() __format__ 方法会被内置的 format() 函数和 str.format() 方法调用,使用特殊的格式
代码显示对象的字符串表示形式。 - bytes() bytes() 函数调用它获取对象的字节序列表示形式
9.4 classmethod和staticmethod
都表示修饰的函数是类的静态方法,只是classmethod修饰的函数第一个参数是类本身,而staticmethod的参数跟传进来的参数一致
只用classmethod就可以了
class Demo():
@classmethod
def clsmethod(*args):
return args
@staticmethod
def stamethod(*args):
return args
print(Demo.clsmethod()) #(<class '__main__.Demo'>,)
print(Demo.stamethod()) # ()
print(Demo.clsmethod(1,2)) # (<class '__main__.Demo'>, 1, 2)
print(Demo.stamethod(1,2)) # (1,2)
9.4-9.6
关于对象的只读属性,格式化显示、序列化、哈希化,可迭代化的例子可以见下面的例子
用@property让属性变为只读
格式规范微语言(https://docs.python.org/3/library/string.html#formatspec)是
可扩展的,方法是实现 format 方法,对提供给内置函数 format(obj, format_spec)
的 format_spec,或者提供给 str.format 方法的 ‘{:«format_spec»}’ 位于代换字段中的
«format_spec» 做简单的解析。
实现了 __hash__ 方法,使用推荐的异或运算符计算实例属性的散列值。
"""
A 2-dimensional vector class
>>> 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)
Test of ``.frombytes()`` class method:
>>> v1_clone = Vector2d.frombytes(bytes(v1))
>>> v1_clone
Vector2d(3.0, 4.0)
>>> v1 == v1_clone
True
Tests of ``format()`` with Cartesian coordinates:
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'
Tests of the ``angle`` method::
>>> 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
Tests of ``format()`` with polar coordinates:
>>> 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>'
Tests of `x` and `y` read-only properties:
>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 123
Traceback (most recent call last):
...
AttributeError: can't set attribute
Tests of hashing:
>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> hash(v1), hash(v2)
(7, 384307168202284039)
>>> len(set([v1, v2]))
2
"""
from array import array
import math
class Vector2d:
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) ^ hash(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)
9.7 python的私有属性
如果在类里定义一个变量以两个前下划线开始,尾部至多一个下划线结束,比如self.__num 那么python会把这个变量保存到类实例的__dict__属性中,并且在前面加一个下划线和类名,这个语言特性叫做 名称改写(name mangling)
class Person():
def __init__(self):
self.__name='jason'
self.age=35
class Student(Person):
def __init__(self):
super().__init__()
self.__name='mike'
self.age=20
jason=Person()
print(jason.__dict__) #{'_Person__name': 'jason', 'age': 35}
print(jason._Person__name) #jason
print(jason.age) # 35
mike=Student()
print(mike.__dict__) # {'_Person__name': 'jason', 'age': 20, '_Student__name': 'mike'}
print(mike._Person__name) # jason
print(mike._Student__name) # mike
print(mike.age) # 20
从例子可以看到,mike中有两个name,但只有一个age(说不定他换了35岁时候换了名字?),可见用双下划线开始的类内变量名的特殊处理是为了避免子类无意识中覆盖了父类的变量.
但名称改写只能避免无意的变量修改,子类依然可以故意修改父类的变量,修改mike._Person__name就可以
程序员们可能会约定在变量前加一个下划线表示这个变量是受保护的,不应被类的外部修改的(虽然实际还是可以修改的
9.8使用__slots__类属性节省空间
默认情况下,Python 在各个实例中名为__dict__ 的字典里存储实例属性。如 3.9.3 节所述,
为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不
多的实例,通过 __slots__ 类属性,能节省大量内存,方法是让解释器在元组中存储实例
属性,而不用字典。
继承自超类的 __slots__ 属性没有效果。Python 只会使用各个类中定义的
__slots__ 属性。
class Person():
__slots__ = ('name','age')
def __init__(self):
self.name='jason'
self.age=25
下面是书中做的一个内存消耗对比
使用__slot__需要注意的问题
- 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性。
- 实例只能拥有 __slots__ 中列出的属性,除非把 ‘__dict__’ 加入 __slots__ 中(这样做
就失去了节省内存的功效)。 - 如果不把 ‘__weakref__’ 加入 __slots__,实例就不能作为弱引用的目标。
- 在类中定义 __slots__ 属性之后,实例不能再有 __slots__ 中所列名称之外
的其他属性。这只是一个副作用,不是 __slots__ 存在的真正原因。不要使
用 __slots__ 属性禁止类的用户新增实例属性。slots 是用于优化的,不
是为了约束程序员。