Python魔法方法漫游指南:类的表示
2495 views, 2021/07/14 updated Go to Comments
使用字符串等信息来表示类是一个相当实用的特性。比方说你在调试代码时,会频繁使用 print()
等函数来获取对象信息,其背后就是隐式调用了将类转化为字符串的魔法方法。相对应的,还有另一部分魔法方法用于自定义在使用内建函数时类的行为。
基础方法
Python 中将对象转换为字符串有两个类似的魔法方法,即 __str__
和 __repr__
。
它两有什么区别呢?让我们先看结论:
__str__
注重可读性,比如展示给用户。__repr__
注重明确性,比如展示给开发中的程序员。
举个栗子。假设有一个表示当前时间的类:
from datetime import datetime class MyDate: def __init__(self): self.date = datetime.now() f = MyDate() print(f) print(f.__repr__()) print(f.__str__()) # 输出: # <__main__.MyDate object at 0x000002510231AFA0> # <__main__.MyDate object at 0x000002510231AFA0> # <__main__.MyDate object at 0x000002510231AFA0>
打印的结果不明确,得不到我想要的跟时间有关的信息。
增加 __str__
方法后:
from datetime import datetime class MyDate: def __init__(self): self.date = datetime.now() def __str__(self): return self.date.__str__() f = MyDate() print(f) print(f.__repr__()) print(f.__str__()) # 输出: # 2021-06-30 19:49:56.620427 # <__main__.MyDate object at 0x00000251026C3CA0> # 2021-06-30 19:49:56.620427
打印 f
或者 f.__str__()
均能够显示格式化后的时间信息,但是无法得知具体的类型。
如果只实现 __repr__
,则有:
from datetime import datetime class MyDate: def __init__(self): self.date = datetime.now() def __repr__(self): return self.date.__repr__() f = MyDate() print(f) print(f.__repr__()) print(f.__str__()) # 输出: # datetime.datetime(2021, 6, 30, 19, 53, 14, 797960) # datetime.datetime(2021, 6, 30, 19, 53, 14, 797960) # datetime.datetime(2021, 6, 30, 19, 53, 14, 797960)
三种打印方式均被 __repr__
覆盖,不仅显示了时间信息,也可得知具体的类型。
如果两种魔法方法同时实现:
from datetime import datetime class MyDate: def __init__(self): self.date = datetime.now() def __str__(self): return self.date.__str__() def __repr__(self): return self.date.__repr__() f = MyDate() print(f) print(f.__repr__()) print(f.__str__()) # 输出: # 2021-06-30 19:54:57.350076 # datetime.datetime(2021, 6, 30, 19, 54, 57, 350076) # 2021-06-30 19:54:57.350076
总的来说两者中可优先实现 __repr__
,有需要再实现 __str__
。
此外,还有两个常用的方法 __dir__
和 __dict__
。
__dir__
定义了调用 dir()
时的行为,返回对象的属性、方法的列表:
>>> a = 1 >>> a.__dir__() ['__repr__', '__hash__', '__getattribute__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__add__', '...']
而 __dict__
则会输出所有实例属性组成的字典:
class Bar: def __init__(self, a, b): self.a = a self.b = b b = Bar(1, 2) print(b.__dict__) # 输出: # {'a': 1, 'b': 2}
bytes 和 format 和 bool
理解了前面的内容,再来说类似的方法就简单了。
__bytes__
方法实现了通过 bytes()
获取对象字节序列的表示形式。而 __format__
方法被内置的 format()
或 str.format()
调用,获取对象的格式化后的字符串表示形式。
看例子:
from datetime import datetime class MyDate: def __init__(self): self.date = datetime.now() def __bytes__(self): return b'This is a bytes result' def __format__(self, format_spec): return 'The time is: ' + format(self.date, format_spec) f = MyDate() print(bytes(f)) print(format(f, '%H:%M:%S')) # 输出: # b'This is a bytes result' # The time is: 10:15:36
而 __bool__
就更简单了,它负责实现内置的 bool()
方法:
class Foo: def __bool__(self): return False f = Foo() print(bool(f)) # 输出: # False
如果类没有实现 __bool__
,那么调用 bool()
会检查类的 __len__
,非零则返回 True
。
如果连 __len__
也没实现,则会直接返回 True
。
hash可哈希
__hash__
这个稍微复杂点,放到最后来说。
Hash
,一般称作散列或哈希。
哈希算法是用来解决数据与数据之间对应关系的一种算法。它可以将任意长度的输入变换为固定长度的输出,该输出被称为哈希值。简单来说,就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。而实现了哈希算法的对象,就被称为可哈希的或者可散列的。
Python 中的不可变类型通常都是可哈希的,比如数字、字母、字符串、元组等。
可变类型通常是不可哈希的,比如列表、字典、集合等。
Python 的三种数据结构:set
、 frozenset
和 dict
都是要求其键值是可哈希的,因为要保证键的唯一性。
如果自定义对象需要实现可哈希,那么就必须实现 __hash__
方法。
我们自定义一个矢量类作为例子:
class Vector: # 用于哈希算法的属性就像一个id # 改变id会导致对象的身份混乱 # 因此将其标识为只能读取的私有变量 def __init__(self, x, y): self.__x = x self.__y = y @property def x(self): return self.__x @property def y(self): return self.__y # 根据官方文档建议 # 哈希算法最好作用于输入值的元组上 # 以使得哈希值更加随机 def __hash__(self): return hash((self.__x, self.__y)) # 实现__hash__ 必须同时实现 __eq__ def __eq__(self, other): return self.__x == other.__x and self.__y == other.__y # 格式化打印输出 def __repr__(self): return f'(x: {self.__x}, y: {self.__y})' # 注意 v1 和 v2 的矢量值相同 # 因此哈希函数计算结果也相同 # 那么在集合中则会被归为同一个元素 v1 = Vector(1,2) v2 = Vector(1,2) v3 = Vector(2,3) s = set([v1, v2, v3]) print(s) # 输出: # {(x: 2, y: 3), (x: 1, y: 2)}
可以看到这个自定义的类实现了可哈希化,并且顺利的放到了集合 set
中。
需要注意的是,如果类实现了 __hash__
,那么它也必须同时实现 __eq__
,因为键的唯一性是由它两一起参与验证的。并且你还需要保证 x==y
和 hash(x) == hash(y)
是等效的。
本系列文章开源发布于 Github,传送门:Python魔法方法漫游指南
看完文章想吐槽?欢迎留言告诉我!