Python魔法方法漫游指南:类的表示

328 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 的三种数据结构:setfrozensetdict 都是要求其键值是可哈希的,因为要保证键的唯一性。

如果自定义对象需要实现可哈希,那么就必须实现 __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==yhash(x) == hash(y) 是等效的。


本系列文章开源发布于 Github,传送门:Python魔法方法漫游指南

看完文章想吐槽?欢迎留言告诉我!




本文作者: 杜赛
发布时间: 2021年07月14日 - 09:25
最后更新: 2021年07月14日 - 09:25
转载请保留原文链接及作者
本站使用 Github 评论后端。鉴于国内复杂的网络环境,如遇评论无法加载、发表等问题,请尝试刷新页面,或更换上网姿势。