4、可散列的Vector
到现在为止,我们的Vector是不可散列的,也就是说没有对应的哈希值:
>>>v = Vector(3, 4)
>>>hash(v)
TypeError: unhashable type: 'Vector'
要想将Vector实例变成可散列的,必须使用__hash__、以及__eq__方法,而且要保证向量不可变。
想要实现hash很简单,只要在类内增加__hash__方法以及__eq__方法即可,官方文档中说到:
当class没有定义__eq__()方法时,那么它也不应该定义__hash__()方法。
也就是说,如果要定义__hash__方法,同时也要定义__eq__方法。
4.1 可散列的实现(hash)
对象比较结果相同所需的唯一特征属性是其具有相同的哈希值;官方文档建议的做法是把参与比较的对象打包为一个元组并对该元组做哈希运算,例如:
def __hash__(self):
return hash((self.name, self.nick, self.color))
在流畅的python书中,作者提出最好使用位运算符异或(^)混合各分量的散列值:
def __hash__(self):
return hash(self.x) ^ hash(self.y)
此处采用书中的做法,在类定义中添加上面的代码以后,Vector就变成可散列的了:
>>>v = Vector(3, 4)
>>>hash(v)
7
4.2 不可变性的实现(只读特性)
尽管在添加了__hash__方法以后,Vector实现了可散列,但是仍然可以为每一个分量赋值新值:
>>>v.x = 4
>>>v
Vector(4,4)
接下来我们要把x和y设置为只读:
class Vector():
typecode = 'd'
def __init__(self, x=0, y=0):
self.__x = x
self.__y = y
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
# 下面的代码和前面一致,此处省略
首先将x转换为私有变量__x,然后创建了同名函数x,这样调用self.x的时候调用的是对应的方法,方便后续的读值。@property装饰器把读值方法标记为特性,后续会进行介绍。
5、python的私有属性
我们的Vector暂时告一段落,但这仅仅是一个开始,你可能还需要很多的方法,比如向量的范数、向量的叉乘等等,所以后续的工作仍需要自己的探索。接下来,我们讨论一下私有属性:
如果你用过别的语言,你可能会发现很多语言使用private修饰符创建私有属性,python对在属性前加__两个下划线表明私有属性,尽管这种方式简单却容易出现属性覆盖的情况:
class Flower():
def __init__(self, color='red'):
self.__color = color # 父类的私有属性
class Rose(Flower):
def __init__(self, color='white'):
super().__init__(self)
self.__color = color # 子类的同名私有属性
def read_color(self): # 对私有属性进行读取
return self.__color
rose = Rose() # 创建一个实例
>>>rose.__color
AttributeError: 'Rose' object has no attribute '__color'
>>>rose.read_color()
'white'
由于都存在私有属性__color,但是子类的私有属性将父类的同名私有属性进行了覆盖。为了避免这种情况,如果以__color的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,Python会把属性名存入实例的__dict__属性中,而且会在前面加上一个下划线和类名。因此对于Flower类来说,__color会变成_Flower__color,而对于Rose来说,__color会变成_Rose__color,这个语言特性叫名称改写(name mangling)。
>>>rose.__dict__
{'_Flower__color': <__main__.Rose at 0x1d1966b50c8>, '_Rose__color': 'white'}
名称改写可以防止私有属性被覆盖,但是却可能是一个巨大的bug,因为只要知道私有属性名的机制,任何人都可以读取甚至修改私有属性:
print(rose._Rose__color) # 知道命名机制后就可以直接读取私有属性
rose._Rose__color = 'black' # 知道命名机制后就可以直接修改私有属性
rose.read_color()
# 返回
white
'black'
不是所有Python程序员都喜欢名称改写功能,也不是所有人都喜欢self.__x这种不对称的名称。有些人不喜欢这种句法,他们约定使用一个下划线前缀编写“受保护”的属性(如self._x),尽管 Python解释器不会对使用单个下划线的属性名做特殊处理,但是这是这些程序员自己的约定,就像我们在定义常量的变量名的时候必须使用大写一样。
6、使用__slots__类属性节省空间(选读)
默认情况下,Python在各个实例中名为__dict__的字典里存储实例属性。不过我们前面也学过,为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果你的实例内包含了几百万个属性,为了节省内存,可以使用__slots__类属性,让解释器在元组中存储实例属性,而不用字典。不过__slots__只能在单独类内使用,无法继承。
定义__slots__的方式是,直接使用__slots__这个名字创建一个类属性,它的值为各个实例属性名构成的可迭代对象(推荐使用不可变的元组)。例如前面我们的Rose:
class Rose(Flower):
__slots__ = ('__color',)
def __init__(self, color='white'):
super().__init__(self)
self.__color = color # 子类的同名私有属性
def read_color(self): # 对私有属性进行读取
return self.__color
rose = Rose() # 创建一个实例
>>>rose.__dict__
AttributeError: 'Rose' object has no attribute '__dict__'
一旦创建了__slots__这个类属性以后,实例中将不会存在__dict__对象。
需要注意的是:__slots__只是一种事后优化的办法,却不能作为控制你的枷锁,如果你的属性不多的话,大可以忽略它,还是想强调一下,在使用过程中,应该注意这些问题:
- 每个子类都要定义__slots__属性,因为解释器会忽略继承的__slots__属性。
- 实例只能拥有__slots__中列出的属性,除非把__dict__加入__slots__中(这样做就失去了节省内存的功效)。
- 如果不把__weakreaf__加入__slots__,实例就不能作为弱引用的目标。
7、覆盖类属性(选读)
这个设计内部的实现方式,但是对于我们实际使用区分不大,大家简单读一下即可:
Python有个很独特的特性:类属性可用于为实例属性提供默认值。啥是类属性?
class Myclass():
a = 3 # 类属性
def Myfunc(self):
self.b = 3 # 实例属性
c = 4 # 局部变量
print(self)
print(self.__class__)
def say():
print("hello!")
obj = Myclass()
obj.Myfunc()
Myclass.say()
obj.say()
# 返回
<__main__.Myclass object at 0x00000249A8C22B48>
<class '__main__.Myclass'>
hello!
TypeError: say() takes 0 positional arguments but 1 was given
这里再回顾一下类的基本知识:
- (1)如果变量定义在类下面而不是类的方法下面,那这个变量既是类的属性也是类实例的属性,例如a
- (2)如果变量定义在类的方法下面,如果加了self,那这个变量就是类实例的属性,不是类的属性,例如b;如果没有加self,这个变量只是这个方法的局部变量,既不是类的属性也不是类实例的属性,例如c。
- (3)如果在类中定义函数时加了self,那这个函数是类实例的方法,而不是类的方法,类实例的方法传入的第一个参数就是类实例本身self。调用时应使用实例.方法,例如obj.Myfunc(),而不能是Myclass.Myfunc()。
- (4)如果在类中定义函数时候没有加self,那这个函数就只是类的方法,而不是类实例的方法。调用时直接类名.方法,例如Myclass.say(),如果使用obj.say()就会提示多给一个参数。
这里的self就是指的类实例本身,不是类哦,self.__class__才是对应的类。
在前面举到的Vector中有个typecode类属性,使用时均采用self.typecode读取它的值。因为Vector实例本身没有typecode属性,所以self.typecode默认获取的是Vector.typecode类属性的值。但是一旦创建同名实例属性,则原始类属性将会被覆盖。
v = Vector(3, 4)
vbd = bytes(v)
print(v.typecode, len(vbd), ': ', vbd)
# 返回
d 17 : b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
v.typecode = 'f'
vbf = bytes(v)
print(v.typecode, len(vbf), ': ', vbf)
# 返回
f 9 : b'f\x00\x00@@\x00\x00\x80@'
一开始的时候,v不存在实例化属性typecode,所以实际使用的是Vector.typecode;而一旦创建同名实例属性以后,就会把同名类属性遮盖。
如果想修改类属性的值,则需要在类上修改或者通过继承覆盖:
# 1 通过类进行修改:
Vector.typecode = 'f'
# 2 通过子类继承修改(推荐):
class anothorVector(Vector):
typecode = 'f'