[TOC]

什么是魔术方法

定义

魔术方法,官方名称是special methods,因为带有双下划线,又被称为dunder methods双下划线方法。

当我们创建一个自定义类时,往往需要控制这个类的行为和操作。通过定义和实现这些魔术方法,我们可以在自定义类中实现类似于内置类型的行为和功能,使得我们的类更加灵活和易于使用。

示例

__init__用于在创建对象时进行初始化操作。它在对象创建之后立即被调用。

1
2
3
4
5
6
7
8
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

person1 = Person("Alice", 25)
print(person1.name) # 输出:Alice
print(person1.age) # 输出:25

__init__方法的第一个参数通常被命名为self,它代表正在被创建的对象自身。接下来的参数表示我们在创建对象时传递的参数。通过这些传递的参数,我们可以在初始化方法中对对象的属性进行赋值。

魔术方法类别

比较

符号 方法 tips
== __eq__ equal,无定义时,默认使用is逻辑。
!= __ne__ not equal,无定义时,把eq函数取反
> __gt__ greater than,无定义时,取反、报错
< __lt__ less than,无定义时,取反、报错
>= __ge__ greater equal,无定义时,取反、报错
<= __le__ less equal,无定义时,取反、报错
  1. 取反,当==方法被定义,而!=方法没有定义时,调用!=方法,会使用==方法,并将结果取反。其他同理。

  2. 一般情况下,当符号是<时,调用__lt__方法,但也有例外,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class Date():
    def __init__(self, year, month, date):
    self.year = year
    self.month = month
    self.day = day

    def __gt__(self, other):
    ...

    def __lt__(self, other):
    if self.year < other.year:
    return True
    if self.year == other.year:
    if self.month < other.month:
    return True
    if self.month == other.month:
    return self.day < other.day
    return False

    class NewDate(Date):
    pass

    date1 = Date(2022,10,1)
    date2 = NewDate(2023,10,1)

    print(date1 < date2)
    # 正常情况下,date1.__lt__(date2),调用lt函数,但这里的date2是子类,因为有可能被重写,所以实际上调用的是date2.__gt__(date1)
    # 这里的规则是,两边是不同类且是衍生类关系,首先使用衍生类的方法。两边是不同类且无衍生关系的时候,我们首先使用左边类的方法,如果左边没有lt方法,则将date1 < date2,视为date2 > date1,调用date2的gt方法,如果这两者都没有,就会报错,python认为两边没有定义date1 < date2的比较逻辑,不可比较。
  3. <=方法不等于<和==方法,即使写了后两者,没写前者也用不了<=方法。

  4. hash值,当我们自定义一个数据结构时,是有默认的__hash__方法的,如果我们定义了__eq__函数,那么默认的hash方法会被删除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    # 解释一下hash函数

    # 首先hash函数有关于可变对象和不可变对象
    # 如果一个对象是不可变的,那么它的__hash__()方法应该返回一个固定的哈希值,并且__eq__()方法应该比较对象的内容来判断相等性。
    # 如果一个对象是可变的,那么它的__hash__()方法通常不会被实现(默认返回None),并且对象的相等性比较通常会比较对象的身份(即id())而不是内容。
    # 例如:
    a = 1
    b = 1
    # int类型是不可变对象,所以a is b结果为true,这里不要认为a、b是两个不同的对象,因为在编程中,a和b都是引用,指向同一块内存。hash值用来标识唯一数据对象,a和b都是同一块内存,输入相同,输出的hash值也一样。

    # 自定义的数据对象例如:
    class Date():
    ...
    date1 = Date(2022,10,1)
    date2 = Date(2022,10,1)
    # date1 is date2的时候就会返回False,因为两个变量指向的是两块不同的内存。得出的hash值也不同。
    # 当定义了__eq__方法,hash方法就不存在,从常理来讲,我们认为date1和date2相等,是因为两个变量都指向2022/10/1这个日期,所以自定义的Date类型应该和int类型一样,都是不可变对象,但由于是自定义对象,date1和date2实际上是指向两块不同的内存,而hash值是确保对象的唯一标识的手段,也就是说,如果两个对象的哈希值不同,那么它们的对象肯定不相等。但如果两个对象的哈希值相同,系统会继续比较它们的值或调用 __eq__() 方法来确定它们是否相等。
    # 这就好比两个双胞胎,明明是两个不同的肉体(内存),却有着一样的外形(eq),你分辨不出,所以你通过名字标识了他们(hash),你就能知道是两个不同的个体。如果是两个名字一样的人(hash相同),但是外形不同(eq),那么你也知道是两个人。hash帮助了你在分别见到双胞胎的时候,叫名字不至于认错,这就是唯一标识。唯一标识的作用下面解释。

    # 用处
    # 在字典中,key值必须唯一,不能重复,这是为了快速索引。但Date创建的date1和date2对象是个双胞胎,如果在字典中:
    income = {} # 收入
    income[date1] = 100
    income[date2] = 100
    print(income)
    '''{2022/10/1: 100, 2022/10/1: 100}'''
    # 但我们要的是一个date
    # 所以可以在Date类中定义hash和eq方法,系统就会知道他俩相等。
    def __eq__(self, other):
    return self.year == other.year and self.month == other.month and self.day == other.day

    def __hash__(self):
    return 1 #例子,别学
    '''{2022/10/1: 100}'''
    # 这样就标识了2022/10/1这个日期是唯一的,日期就是不可变对象

    # 总结
    # 对于不可变对象(如整数、浮点数、字符串和元组),它们的哈希值是在对象创建时计算并保存的,因为它们的值是不可变的。这使得它们的哈希值唯一性和稳定性得到了保证。
    # 对于可变对象(如列表、集合和字典),由于其值可以进行修改,因此在对象创建后即使不同的修改操作会改变其哈希值,因此可变对象默认是不可哈希的,即不能被用作字典的键或集合的元素。
    # 尽管可变对象默认是不可哈希的,但可以通过自定义对象的 __hash__() 方法来使可变对象变为可哈希的。但一旦对象是可哈希的,并且作为字典键或集合元素使用时,就不应该再修改该对象的值,因为修改后会破坏数据结构的一致性。

    # hash函数要求
    # 必须返回一个整数
    # 两个对象相等的时候,hash值必须相等
    # 官方推荐写法
    def __hash__(self):
    return hash((self.year,self.month,self.day))

对象表示

方法 tips
__str__(self) 返回对象的字符串表示
__repr__(self) represent,返回对象的可打印字符串表示

区别:

str面向用户,要求可读性好。repr面向的是python的解释器,或者说开发人员,用来重新获得该对象,将对象转化为供解释器读取的形式。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

# 定制类的字符串形式,使用 str() 返回一个描述性好的字符串
def __str__(self):
return f'Point({self.x}, {self.y})'

# 定制类的字符串形式,使用 repr() 返回一个准确可重建对象的字符串
def __repr__(self):
return f'Point({self.x}, {self.y})'

# 创建一个 Point 对象
p1 = Point(3, 5)
print(str(p1)) # 输出:Point(3, 5)

# 使用 repr() 获取对象的字符串形式
repr_str = repr(p1)
print(repr_str)
# 使用 eval() 重新创建相同的对象
p2 = eval(repr_str)
# eval会把字符串当作代码运行
p3 = eval('Point(3,5)')

print(p2) # 输出:Point(3, 5)
print(p3) # 输出:Point(3, 5)

属性

方法 作用 tips
__getattr__(self,name) 访问对象某个属性不存在时,做的处理 不存在指getattribute方法返回值没有
__getattribute__(self,name) 访问对象属性存在时,返回属性值 可以加一写其他操作,注意递归
__setattr__(self,name,val) 设置一个对象属性
__delattr__(self,name) del o.data对象属性时被调用 对象删除时并不会调用
__dir__(self) 打印可访问的属性和方法 必须返回sequence
  • __getattr__(self,name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person:
def __init__(self, name):
self.name = name

def __getattr__(self, attr):
# 处理不存在的属性
return f"The attribute '{attr}' does not exist."

# 创建一个 Person 对象
person = Person("John")

# 访问已经存在的属性
print(person.name) # 输出:John

# 访问不存在的属性
print(person.age) # 输出:The attribute 'age' does not exist.

  • __getattribute__(self,name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A():
def __init__(self):
self.data = "abc"
self.counter = 0

def __getattribute__(self, name):
if name == "data":
self.counter += 1
return super().__getattribute__(name) #getattr(self,name)

# self.counter += 1实际上也是再访问a中的属性,所以会产生递归。
# return super().__getattribute__(name)是为了返回属性值,如果写成self.name又会触发递归,所以调用父类的getattribute方法,虽然A()没有表明继承,但实际上是有父类的,我们的getattribute实际上重写,父类的getattritubr方法就只是单纯返回属性调用,return属性值。
# getattr(self,name),该方法不是调用的__getattr__,而是先调用__getattritube__方法,如果不存在返回值,再调用__getattr__。它是一个获取属性的方法,它还有第三个参数,用来返回当属性不存在时,需返回的默认值,如果__getattr__已经被重写,则第三个参数不起作用。

# 创建一个 A对象ssss
a = A()

# 访问已经存在的属性
print(a.data)
print(a.counter)

描述器

方法 作用 tips
__get__(self, instance, owner) 通过实例访问属性时调用,定义属性的获取行为
__set__(self, instance, owner) 给属性赋值时调用,定义属性的设置行为
__delete__(self, instance, owner)