1. python中的对象
python中万物都是对象,构造这些对象的都是由继承自object
的类型创建而得.而得益于python数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所需的方法即可.
面向对象惯用法部分我们会用一个贯穿始终的例子--自定义向量,来解释python中的类与对象.这边先将这个类的最基本形式写出来
from math import hypot,atan2 from array import array class Vector2D: # 在 Vector2d 实例和字节序列之间转换时使用 typecode = 'd' __slots__ = ('__x', '__y') @classmethod def frombytes(cls, octets): # 使用传入的`octets`字节序列创建一个 memoryview, # 然后使用 typecode 转换。 # 拆包转换后的 memoryview,得到构造方法所需的一对参数。 typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(*memv) def __init__(self, x=0, y=0): # 把 x 和 y 转换成浮点数,尽早捕获错误, # 以防调用 Vector2d 函数时传入不当参数。 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): # 定义 `__iter__` 方法,把 Vector2d 实例变成可迭代的对象 # 这样才能拆包(例如`x, y = my_vector`) # 这个方法的实现方式很简单,直接调用生成器表达式一个接一个产出分量. return (i for i in (self.x, self.y)) def __repr__(self): # `__repr__` 方法使用 `{!r}` 获取各个分量的表示形式,然后插值,构成一个字符串; #因为Vector2d 实例是可迭代的对象,所以 `*self` 会把` x `和` y `分量提供给 `format` 函数 class_name = type(self).__name__ return '{}({!r}, {!r})'.format(class_name, *self) def __str__(self): return str(tuple(self)) def __bytes__(self): # 为了生成字节序列,我们把 typecode 转换成字节序列, #然后迭代 Vector2d 实例,得到一个数组,再把数组转换成字节序列. return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) def __format__(self, fmt_spec=''): # 使用内置的 format 函数把 fmt_spec 应用到向量的各个分量上,构建一个可迭代的格式化字符串。 # 再把格式化字符串代入公式 '(x, y)' 中。 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) def __eq__(self, other): return tuple(self) == tuple(other) def __hash__(self): return hash(self.x) ^ hash(self.y) def __abs__(self): # 模是 x 和 y 分量构成的直角三角形的斜边长 return hypot(self.x, self.y) def __bool__(self): # __bool__ 方法使用 abs(self) 计算模,然后把结果转换成布尔值, # 因此,0.0 是 False,非零值是 True。 return bool(abs(self)) def __add__(self, other): x = self.x + other.x y = self.y + other.y return type(self)(x, y) def __mul__(self, scalar): return type(self)(self.x * scalar, self.y * scalar) def angle(self): return atan2(self.y, self.x) def __complex__(self): return complex(self.x, self.y)
v1 = Vector2D(3, 4) print(v1.x, v1.y)
x, y = v1 x,y
v1
Vector2D(3.0, 4.0)
v1_clone = eval(repr(v1)) v1 == v1_clone
print(v1)
(3.0, 4.0)
octets = bytes(v1) octets
abs(v1)
v1 = Vector2D(3, 4) print(v1.x, v1.y)
0
v1 = Vector2D(3, 4) print(v1.x, v1.y)
1
v1 = Vector2D(3, 4) print(v1.x, v1.y)
2
1.1. 对象表示形式
每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式.Python提供了两种方式.
repr()
以便于开发者理解的方式返回对象的字符串表示形式.
str()
以便于用户理解的方式返回对象的字符串表示形式.
估计你也猜到了,我们要实现接口__repr_
和__str__
特殊方法,为repr()
和str()
提供支持.
为了给对象提供其他的表示形式,还会用到另外两个特殊方法:
__bytes__
__bytes__
方法与__str__
方法类似:bytes()
函数调用它获取对象的字节序列表示形式.__format__
__format__
方法会被内置的format()
函数和str.format()
方法调用,使用特殊的格式代码显示对象的字符串表示形式.
1.1.1. 格式化显示
内置的format()
函数和str.format()
方法把各个类型的格式化方式委托给相应的.__format__(format_spec)
方法.format_spec
是格式说明符,它是:
format(my_obj, format_spec)
的第二个参数,或者str.format()
方法的格式字符串,{}里代换字段中冒号后面的部分
v1 = Vector2D(3, 4) print(v1.x, v1.y)
3
v1 = Vector2D(3, 4) print(v1.x, v1.y)
4
v1 = Vector2D(3, 4) print(v1.x, v1.y)
5
v1 = Vector2D(3, 4) print(v1.x, v1.y)
6
v1 = Vector2D(3, 4) print(v1.x, v1.y)
7
v1 = Vector2D(3, 4) print(v1.x, v1.y)
8
v1 = Vector2D(3, 4) print(v1.x, v1.y)
9
格式规范微语言为一些内置类型提供了专用的表示代码.比如,b
和x
分别表示二进制和十六进制的int
类型,f
表示小数形式的float
类型,而%
表示百分数形式
x, y = v1 x,y
0
x, y = v1 x,y
1
x, y = v1 x,y
2
x, y = v1 x,y
3
格式规范微语言是可扩展的,因为各个类可以自行决定如何解释format_spec
参数.例如,datetime
模块中的类,它们的 __format__
方法使用的格式代码与strftime()
函数一样.下面是内置的format()
函数和str.format()
方法的几个示例:
x, y = v1 x,y
4
x, y = v1 x,y
5
x, y = v1 x,y
6
x, y = v1 x,y
7
如果类没有定义__format__
方法,从object
继承的方法会返回str(my_object)
我们为Vector2D
类定义了__str__
方法,因此可以这样做.
我们实现自己的微语言来解决这个问题.首先,假设用户提供的格式说明符是用于格式化向量中各个浮点数分量的.
要在微语言中添加一个自定义的格式代码:
如果格式说明符以'p'结尾,那么在极坐标中显示向量,即<r, θ >
,其中r
是模,θ
(西塔)是弧度;其他部分('p' 之前的部分)像往常那样解释.
为自定义的格式代码选择字母时,我会避免使用其他类型用过的字母.在格式规范微语言中我们看到,
- 整数使用的代码有
'bcdoxXn'
- 浮点数使用的代码有
'eEfFgGn%'
- 字符串使用的代码有
's'
因此,我为极坐标选的代码是'p'
(polar coordinates).各个类使用自己的方式解释格式代码,在自定义的格式代码中重复使用代码字母不会出错,但是可能会让用户困惑.
对极坐标来说,我们已经定义了计算模的__abs__
方法,因此还要定义一个简单的angle
方法,使用math.atan2()
函数计算角度
这样便可以增强__format__
方法,计算极坐标.
x, y = v1 x,y
8
x, y = v1 x,y
9
v1
0
v1
1
v1
2
v1
3
v1
4
v1
5
v1
6
v1
7
v1
8
v1
9
Vector2D(3.0, 4.0)
0
1.2. 静态方法和类方法
Python使用classmethod
和staticmethod
装饰器声明类方法和静态方法.学过Java面向对象编程的人可能觉得奇怪,为什么Python
提供两个这样的装饰器,而不是只提供一个?java中只有静态方法.
先来看classmethod
它的用法:定义操作类,而不是操作实例的方法.classmethod
改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例.classmethod
最常见的用途是定义备选构造方法.
再看staticmethod
装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值.其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义.
classmethod
装饰器非常有用,但是我从未见过不得不用staticmethod
的情况.如果想定义不需要与类交互的函数,那么在模块中定义就好了.有时,函数虽然从不处理类,但是函数的功能与类紧密相关,因此想把它放在近处.即便如此,在同一模块中的类前面或后面定义函数也就行了.
下面的例子是静态方法与类方法的对比
Vector2D(3.0, 4.0)
1
Vector2D(3.0, 4.0)
2
Vector2D(3.0, 4.0)
3
Vector2D(3.0, 4.0)
4
Vector2D(3.0, 4.0)
5
Vector2D(3.0, 4.0)
6
Vector2D(3.0, 4.0)
7
Vector2D(3.0, 4.0)
8
Vector2D(3.0, 4.0)
9
1.3. 备选构造方法
我们可以把Vector2D
实例转换成字节序列了;同理,也应该能从字节序列转换成Vector2D
实例.
在标准库中探索一番之后,我们发现array.array
有个类方法 .frombytes
正好符合需求.
v1_clone = eval(repr(v1)) v1 == v1_clone
0
Vector2D(3.0, 4.0)
v1_clone = eval(repr(v1)) v1 == v1_clone
2
v1_clone = eval(repr(v1)) v1 == v1_clone
3
v1_clone = eval(repr(v1)) v1 == v1_clone
4
Vector2D(3.0, 4.0)
1.4. 可散列的对象
如果对象不是可散列的,那么就不能放入集合(set)中,而要可散列必须保证3点:
- 必须实现
__hash__
方法 - 必须实现
__eq__
方法 - 要让向量不可变
1.5. *使用特性让对象的分量只读
property
装饰器可以把读值方法标记为特性.
我们让这些向量不可变是有原因的,因为这样才能实现__hash__
方法.这个方法应该返回一个整数,理想情况下还要考虑对象属性的散列值(__eq__
方法也要使用),因为相等的对象应该具有相同的散列值.Vector2d.__hash__
方法的代码十分简单--使用位运算符异或(^
)混合各分量的散列值.
要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性.只需正确地实现 __hash__
和 __eq__
方法即可.但是,实例的散列值绝不应该变化,因此我们借机提到了只读特性.
1.6. Python的私有属性和"受保护的"属性
Python不能像Java
那样使用private
修饰符创建私有属性,但是Python
有个简单的机制,能避免子类意外覆盖"私有"属性.
举个例子:有人编写了一个名为Dog
的类,这个类的内部用到了mood
实例属性,但是没有将其开放.现在,你创建了Dog
类的子类:Beagle
.如果你在毫不知情的情况下又创建了名为mood
的实例属性,那么在继承的方法中就会把Dog
类的mood
属性覆盖掉.这是个难以调试的问题.
为了避免这种情况,如果以__mood
的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,Python会把属性名存入实例的__dict__
属性中,而且会在前面加上一个下划线和类名.因此,对Dog
类来说,__mood
会变成_Dog__mood
;对Beagle
类来说,会变成_Beagle__mood
.这个语言特性叫名称改写(name mangling).
名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事,只要知道改写私有属性名的机制,任何人都能直接读取私有属性——这对调试和序列化倒是有用.此外,只要编写v1._Vector__x = 7
这样的代码,就能轻松地为 Vector2D实例的私有分量直接赋值.如果真在生产环境中这么做了,出问题时可别抱怨.
不是所有Python程序员都喜欢名称改写功能,也不是所有人都喜欢 self.__x
这种不对称的名称。有些人不喜欢这种句法,他们约定使用一个下划线前缀编写"受保护"的属性(如self._x
)。批评使用两个下划线这种改写机制的人认为,应该使用命名约定来避免意外覆盖属性.Ian Bicking有一句话,那句话的完整表述如下:
绝对不要使用两个前导下划线,这是很烦人的自私行为.如果担心名称冲突,应该明确使用一种名称改写方式(如
_MyThing_blahblah
).这其实与使用双下划线一样,不过自己定的规则比双下划线易于理解.
Python解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在类外部访问这种属性.遵守使用一个下划线标记对象的私有属性很容易,就像遵守使用全大写字母编写常量那样容易.
Python文档的某些角落把使用一个下划线前缀标记的属性称为"受保护的"属性.使用self._x
这种形式保护属性的做法很常见,但是很少有人把这种属性叫作"受保护的"属性.有些人甚至将其称为"私有"属性.
总之,Vector2D的分量都是"私有的",而且Vector2D实例都是"不可变的".我用了两对引号,这是因为并不能真正实现私有和不可变.
1.7. 使用__slots__类属性节省空间
默认情况下,Python在各个实例中名为__dict__
的字典里存储实例属性.为了使用底层的散列表提升访问速度,字典会消耗大量内存.如果要处理数百万个属性不多的实例,通过__slots__
类属性,能节省大量内存,方法是让解释器在元组中存储实例 属性,而不用字典.
继承自超类的__slots__
属性没有效果.Python只会使用各个类中定义的__slots__
属性.
定义__slots__
的方式是,创建一个类属性,使用__slots__
这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性.我喜欢使用元组,因为这样定义的__slots__
中所含的信息不会变化.
在类中定义__slots__
属性的目的是告诉解释器:"这个类中的所有实例属性都在这儿了!"这样,Python会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的__dict__
属性.如果有数百万个实例同时活动,这样做能节省大量内存.
在类中定义__slots__
属性之后,实例不能再有__slots__
中所列名称之外的其他属性.这只是一个副作用,不是__slots__
存在的真正原因.不要使用__slots__
属性禁止类的用户新增实例属性.__slots__
是用于优化的,不是为了约束程序员.
然而,"节省的内存也可能被再次吃掉":如果把__dict__
这个名称添加到__slots__
中,实例会在元组中保存各个实例的属性,此外还支持动态创建属性,这些属性存储在常规的__dict__
中.当然,把 __dict__
添加到__slots__
中可能完全违背了初衷,这取决于各个实例的静态属性和动态属性的数量及其用法.粗心的优化甚至比提早优化还糟糕.
此外,还有一个实例属性可能需要注意,即__weakref__
属性,为了让对象支持弱引用,必须有这个属性.用户定义的类中默认就有__weakref__
属性.可是,如果类中定义了__slots__
属性,而且想把实例作为弱引用的目标,那么要把 __weakref__
添加到__slots__
中.
综上,__slots__
属性有些需要注意的地方,而且不能滥用,不能使用它限制用户能赋值的属性.处理列表数据时__slots__
属性最有用,例如模式固定的数据库记录,以及特大型数据集.
1.7.1. __slots__的问题
总之,如果使用得当,__slots__
能显著节省内存,不过有几点要注意.
- 每个子类都要定义
__slots__
属性,因为解释器会忽略继承的__slots__
属性。 - 实例只能拥有
__slots__
中列出的属性,除非把__dict__
加入__slots__
中(这样做就失去了节省内存的功效). - 如果不把
__weakref__
加入__slots__
,实例就不能作为弱引用的目标.
如果你的程序不用处理数百万个实例,或许不值得费劲去创建不寻常的类,那就禁止它创 建动态属性或者不支持弱引用.与其他优化措施一样,仅当权衡当下的需求并仔细搜集资料后证明确实有必要时,才应该使用__slots__
属性.
1.7.2. 覆盖类属性
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字节单精度浮点数表示各个分量.
现在你应该知道为什么要在得到的字节序列前面加上typecode
的值了:为了支持不同的格式.如果想修改类属性的值,必须直接在类上修改,不能通过实例修改.如果想修改所有实例(没有typecode
实例变量)的typecode
属性的默认值,可以这么做:
v1_clone = eval(repr(v1)) v1 == v1_clone
6
然而,有种修改方法更符合Python风格,而且效果持久,也更有针对性.类属性是公开的,因此会被子类继承,于是经常会创建一个子类,只用于定制类的数据属性.
v1_clone = eval(repr(v1)) v1 == v1_clone
7
v1_clone = eval(repr(v1)) v1 == v1_clone
8
(3.0, 4.0)
print(v1)
0
print(v1)
1
print(v1)
2
print(v1)
3
print(v1)
4
print(v1)
5
print(v1)
6
1.7.3. 使用具名元组构建节省空间的类[3.6]
在python 3.6后我们可以为类中的字段做类型标注了,利用这一特性,typing.NamedTuple
新增了一个用法,我们可以继承它用它构造一个节省空间的类,它有NamedTuple
的所有接口.
同时我们也可以像对其他类定义一样给它添加方法
print(v1)
7
print(v1)
8
print(v1)
9
(3.0, 4.0)
0
(3.0, 4.0)
1
1.8. 数据类[3.7]
新加的数据类在标准库dataclasses
中.它的作用是为类快速定义一些魔术方法,它使用装饰器语法,有两种形式:
带参数的装饰器
(3.0, 4.0)
2其中参数含义为:
init
自动生成__init__
默认为True,用于给字段赋值repr
自动生成__repr__
默认为True,用于展示eq
自动生成__eq__
默认为True,用于判断两个对象是否相等order
自动生成__gt__
,默认为False,这样就可以判断大小和排序了,实际上也相当于定义了__lt__
,__le__
.frozen
自动为对对象的修改操作抛出一个异常.默认为False
,相当于定义了__setattr__
和__delattr__
,如果设置为True,相当于这个数据是不可变类型.unsafe_hash
根据eq
和frozen
的值生成__hash__
,默认为False,表示__hash__()=None
eq
和frozen
都是True,自动生成__hash__
eq
为True,frozen
为False,则__hash__()=None
eq
为False,则__hash__
继承自其父类,如果其父类为object,那他就会有一个id
不带参数的装饰器
(3.0, 4.0)
3相当于是带参数用法全部使用默认值.
(3.0, 4.0)
4
(3.0, 4.0)
5
(3.0, 4.0)
6
1.8.1. 细化字段定义
dataclasses提供了dataclasses.field(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, compare=True, metadata=None)
来作为除了类型注解外的字段定义补充,
default
用于定义默认值default_factory
用于定义一个创建默认值的工厂函数,该工厂函数没有参数init
: 为True则这个字段强制出现在类的__init__
参数中repr
: 为True则这个字段强制出现在__repr__
结果中.compare
: 为True则这个字段出现在自动生成的__gt__
比较重.hash
: 对应unsafe_hash
metadata
: 这可以是映射或None,没有人被视为一个空的dict.此值包含在MappingProxyType()中以使其为只读,并在Field对象上公开.Data Classes根本不使用它,它是作为第三方扩展机制提供的.多个第三方可以各自拥有自己的密钥,以用作元数据中的命名空间.
(3.0, 4.0)
7
(3.0, 4.0)
8
(3.0, 4.0)
9
octets = bytes(v1) octets
0
octets = bytes(v1) octets
1
print(v1)
6
octets = bytes(v1) octets
3
octets = bytes(v1) octets
4
1.8.2. 转化为其他数据类型
- 转化为字典
dataclasses
提供了一个方法可以简单的把数据类型的实例转化为字典
dataclasses.asdict(instance, *, dict_factory=dict)
octets = bytes(v1) octets
5
octets = bytes(v1) octets
6
octets = bytes(v1) octets
7
octets = bytes(v1) octets
8
- 转化为元组
dataclasses
提供了一个方法可以简单的把数据类型的实例转化为元组
dataclasses.astuple(instance, *, tuple_factory=tuple)
octets = bytes(v1) octets
9
abs(v1)
0
abs(v1)
1
abs(v1)
2
更多的用法可以看模块文档.
1.9. 用于构建,解构,反射对象的工具
1.9.1. __new__构造运算符
也就是面向对象编程中常提到的构造方法了
这是一旦被调用就会执行的运算符,也是正常情况下一个实例第一个执行的运算符.该方法会返回一个对应对象的实例.我们来看看他的特性.
例: 建立一个可以记录调用次数的类
abs(v1)
3
abs(v1)
4
abs(v1)
5
abs(v1)
6
abs(v1)
7
1.9.2. __init__实例初始化
最常见的运算符重载应用就是__init__
方法了,即实例初始化方法.该方法无返回值.
这个方法我们在将继承的时候就有过接触,所以不多说,主要看看他和__new__
的关系.
__new__
运算符返回的是一个对象,这个对象就是类对象
abs(v1)
8
abs(v1)
9
v1 = Vector2D(3, 4) print(v1.x, v1.y)
00
v1 = Vector2D(3, 4) print(v1.x, v1.y)
01
v1 = Vector2D(3, 4) print(v1.x, v1.y)
02
v1 = Vector2D(3, 4) print(v1.x, v1.y)
03
v1 = Vector2D(3, 4) print(v1.x, v1.y)
04
v1 = Vector2D(3, 4) print(v1.x, v1.y)
05
v1 = Vector2D(3, 4) print(v1.x, v1.y)
06
v1 = Vector2D(3, 4) print(v1.x, v1.y)
07
v1 = Vector2D(3, 4) print(v1.x, v1.y)
08
v1 = Vector2D(3, 4) print(v1.x, v1.y)
09
v1 = Vector2D(3, 4) print(v1.x, v1.y)
10
1.9.3. __del__析构运算符
析构运算符__del__
定义当对象实例被删除或者释放时的操作,继续修改那个例子
v1 = Vector2D(3, 4) print(v1.x, v1.y)
11
v1 = Vector2D(3, 4) print(v1.x, v1.y)
12
v1 = Vector2D(3, 4) print(v1.x, v1.y)
13
v1 = Vector2D(3, 4) print(v1.x, v1.y)
14
v1 = Vector2D(3, 4) print(v1.x, v1.y)
15
v1 = Vector2D(3, 4) print(v1.x, v1.y)
16
v1 = Vector2D(3, 4) print(v1.x, v1.y)
02
v1 = Vector2D(3, 4) print(v1.x, v1.y)
18
v1 = Vector2D(3, 4) print(v1.x, v1.y)
19
1.9.4. __dir__()反射实现的所有属性,包括特殊方法
v1 = Vector2D(3, 4) print(v1.x, v1.y)
20
v1 = Vector2D(3, 4) print(v1.x, v1.y)
21
1.9.5. __class__反射对象所属的类
v1 = Vector2D(3, 4) print(v1.x, v1.y)
22
v1 = Vector2D(3, 4) print(v1.x, v1.y)
23
1.9.6. __sizeof__()反射对象所占的内存空间
v1 = Vector2D(3, 4) print(v1.x, v1.y)
24
v1 = Vector2D(3, 4) print(v1.x, v1.y)
25
1.10. 类作为对象
python中类也是对象,Python数据模型为每个类定义了很多属性.
cls.__class__
构造类对象的对象(元类)
cls.__bases__
由类的基类组成的元组.
cls.__qualname__
和cls.__name__
Python 3.3新引入的属性,其值是类或函数的限定名称,即从模块的全局作用域到类的点分路径.内部类
ClassTwo
的__qualname__
属性,其值是字符串'ClassOne.ClassTwo',而__name__
属性的值是'ClassTwo'
.cls.__subclasses__()
这个方法返回一个列表,包含类的直接子类.这个方法的实现使用弱引用,防止在超类和子类(子类在
__bases__
属性中储存指向超类的强引用)之间出现循环引用.这个方法返回的列表中是内存里现存的子类.cls.__mro__
记录类的继承顺序
cls.mro()
构建类时,如果需要获取储存在类属性
__mro__
中的超类元组,解释器会调用这个方法.元类可以覆盖这个方法,定制要构建的类解析方法的顺序.
还没有评论,来说两句吧...