Python魔法方法汇总

发布时间 2023-03-27 17:16:38作者: Circle_Wang

  Python中对于一个类来说,有着非常多的魔法方法(以__xxx__方法是进行定义的),这些方法在Python解释器中会被特殊的事件所触发调用。比如比较对象大小,实例对象的创建等很多重要时刻,对应的魔法方法都会被解释器调用。但并不是当我们自己编写一个类的时候,这些魔法方法都需要被重写(object这个基类已经默认写好了这些魔法方法,通常情况下我们都不需要去重写)。不过了解这些魔法方法会加深你对Python解释器在特定时刻到底是如何执行的有很大帮助。

1、创建和访问相关的魔法方法

 1.1、__init__(self,参数1, 参数2, ...)

  在创建实例后,会立刻执行__init__方法,对实例进行初始化。这是我们创建自定义类时最常用的魔法方法,并且也是基本上都会重写的魔法方法。下面是一个最简单的例子:

class MyClass(object):
    def __init__(self, age, name="未命名"):
        self.name = name
        self.age = age
    
boy1 = MyClass(name="小明", age=14)
boy2 = MyClass(12)
print(f"boy1的名字是:{boy1.name}, 年龄: {boy1.age}")
print(f"boy2的名字是:{boy2.name}, 年龄: {boy2.age}")

  执行结果是:

   上例中我们在__init__方法中,设定了一个关键字参数age,一个带默认值的关键字参数name。在我们创建MyClass实例对象是就会进入到该方法中,例如第6行我们创建了一个姓名为"小明",年龄为14的对象。第7行我们并没有给出参数name的值,因此创建boy2时name为默认的“未命名”,而12对应的位置参数为age,因此该对象的age被赋值为12。

  __init__方法相信大家在实际编程中都经常使用了,但我这里需要注意的是__init__是用于对象初始化时调用的,可以理解为我们已经创建了一个空白的对象,而__init__方法仅仅是给这个空白对象上增加了属于他独特的部分(创建属性值...),因此__init__方法不应该也不需要有任何返回值的。(如果你在该方法中return xxx,会得到系统报错,提示 TypeError)

 1.2、__new__(cls, 参数1, 参数2, ...)

  前文提到__init__其实只是对一个“空白”对象进行属性的初始化,那我们可以推测总归会有一个方法会在__init__之前调用,并返回一个"空白"对象吧。没错!这个魔法方法就是__new__。该魔法方法永远是静态的,并且不需要显式的装饰(通常类的静态方法都需要@staticmethod进行装饰,才能作为静态方法进行使用)。__new__的第一个参数也是最重要的一个参数就是创建实例所需得类(按照惯例是一般采用cls作为指代),而其余的参数则会原封不动的传递给__init__,最后会必须要返回一个创建好的对象,并且这个对象一定要是cls这个类的对象(因为__new__返回的对象会交给__init__进行初始化;如果返回的不是cls类的对象,那么就不会触发cls类的__init__方法)。

  一般来说我们并不需要去自定义这个__new__方法,因为这个方法本质上只是创建一个空白的类对象(通过调用父类的__new__方法进行的)。如果我们非得要重写该方法,我们一般也需要调用父类的__new__方法来得到一个空白对象。例如下例子:

class MyClass(object):

    def __new__(cls, age, name="未命名1"):
        print("进入了__new__方法")
        instance = super().__new__(cls)
        return instance

    def __init__(self, age, name="未命名2"):
        print("进入了__init__")
        self.name = name
        self.age = age
    
boy1 = MyClass(name="小明", age=14)

  输出结果是:

  从这里结果我们可以看到,当我们创建实例时,首先进入的就是__new__方法,第5行中调用了父类的__new__方法创建了一个实例(注意本例中的父类是object,其__new__方法只接收cls一个参数),得到的实例对象被直接return了。与此同时,age,name参数被原封不动的传递给了__init__,也就是将"小明"传递给了__init__中的name参数,14传递给了__init__中的age参数。这里我们需要注意的是在__new__中我们将name属性默认定义为"未命名1",而在__init__中name属性的默认定义为"未命名2",大家可以猜想下如果我们使用MyClass(12),进行创建对象,那该对象的name属性是多少?结果如下:

boy2 = MyClass(12)
print(f"boy2的名字是:{boy2.name}, 年龄: {boy2.age}")

  结果如下:

  可以看到,此时最终的结果boy2的name其实是按照__init__中所指定的。通过debug可以知道,其实执行完__new__后,会回到MyClass(12)该行,并且只会将12传入给__init__函数。因此实际上__new__的参数根本不会影响到__init__的收集参数,就算你在__new__中把name的数值改了,也没有任何影响。(从静态方法的角度来看会更清晰,因为__new__是一个静态方法,里面执行的任何操作都不会影响到对象)。

 1.3、__getattr__(self,attr_name)

  当我们试图访问一个对象不存在的属性时就会触发__getattr__()方法的调用。在创建实例对象后,我们可以通过 对象.属性名 的方式访问这个对象的对应属性。如果某个对象并不具有这个属性那么就会触发AttributeError异常,此时如果该类自定义了__getattr__()方法,那么编译器将不会报异常,并开始执行__getattr__()。__getattr__()方法有两个位置参数,第一个是self,第二个是属性名,也就是你想访问的属性的属性名,注意attr_name的类型是字符串类型。

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    ## 当访问对象属性时,如果出现异常(AttributeError)会执行该函数
    def __getattr__(self, item):
        print(type(item))
        print(f"{item}属性访问异常, 不存在属性{item}")

boy1 = MyClass(name="小明", age=14)
boy1.day

  我们在根据上文的代码稍作调整,我们创建了一个类,并重写了其中的__getattr__()方法,当我们对boy1对象的day属性进行访问时,由于boy1对象种并没有这个属性,因此会进入到__getattr__(),打印出两句话,运行结果如下:

  __getattr__方法捕获了我们想要访问但不存在的属性名,一般是当"常规方法无法访问属性时"会执行这个魔法函数。

 1.4、__getattribute__(self,attr_name)

  该魔法方法其实和__getattr__非常类似,__getattr__是在“常规方法”无法访问属性时才会被执行,而_getattribute__()方法是无条件的执行,只要程序中需要访问属性时都会被执行。

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    ## 在访问对象属性时会被无条件调用
    def __getattribute__(self, __name: str):
        attr_value = object.__getattribute__(self, __name)  ## 注意此处必须执行采用执行基类的方法来得到属性值,不能使用self.name, 会产生死循环
        print(f"{__name}属性被访问, 其值为:{attr_value}")
        return attr_value

    def __getattr__(self, item):
        print(f"{item}属性访问异常, 不存在属性{item}")

boy1 = MyClass(name="小明", age=14)
boy1.age
boy1.day

  我们看上面这个例子,在我们自定义类中增加了__getattribute__()方法,当有属性被访问时,该方法打印一句话也就是第10行代码执行的内容。最终执行的结果如下:

  确实得到了我们想要的结果,这里我们需要注意两点。第一:__getattribute__()应该有返回值,而且是应该是对应的属性的值,否则我们永远也无法得到属性的值。第二点:我们在__getattribute__()函数体中一定不要出现使用常规的方法去访问属性,因为这会使得程序进入死循环。其实这就是上例中我们在第9行中做的操作。要记住只要我们使用 self.属性名 就是在访问属性,就会进入到__getattribute__()方法中,因此如果你将第9行的代码改为: attr_value = self.__name,那么上面的代码将会进入死循环直到栈溢出(大家可以自行尝试)。

  为了解决上面的问题,我们就需要调用基类的__getattribute__方法,或者使用父类的__getattribute__方法去访问,分别是以下两种代码:

  • super().__getattribute__(__name):调用父类的__getattribute__方法,并将被访问的属性名传入进去
  • object.__getattribute__(self, __name): 调用基类object的__getattribute__方法,不过需要传入self指定对象

  一般情况下,我们并不需要重写__getattribute__(),因为该方法极容易出现死循环。再提一句,其实上面的例子中当我们试图访问boy1.day时,我们首先进入的还是__getattribute__()方法只不过,当执行到9行时,由于self中不存在day属性,因此在第9行会抛出AttributeError,此时编译器才会去执行__getattr__方法。换句话说我们再理解下__getattr__方法,其实就是当__getattribute__()方法中抛出了AttributeError,才会去执行__getattr__。基于此我们可以写出以下代码,那么我们将永远无法访问到类中的属性了。

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    def __getattribute__(self, __name: str):
        raise AttributeError("所有属性均不存在")

    def __getattr__(self, item):
        print(f"{item}属性访问异常, 不存在属性{item}")

 

 1.5、__setattr__(self,attr_name,attr_value)

  该魔法方法当你为一个属性/对象进行赋值操作(xxx = xxx)时会被调用。其中attr_name会捕获我们想要对哪个属性进行赋值,attr_value会捕获赋值内容,我们看下面这个例子。

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    ## 当给对象属性进行赋值时,会进入到该函数
    def __setattr__(self, name, value):
        print(f"属性{name}正在执行__setattr__, 值为{value}")
        object.__setattr__(self, name, value)
    
boy1 = MyClass(name="小明", age=14)
boy1.age = 18

  这段代码的执行结果是:

  有些读者可能会出现疑惑,为什么会打印第一句和第二句?我们不是只对age进行了赋值18的操作么?应该只打印最后一句才对。其实第1句和第2句的打印时在对象初始化时,也就是上面代码的第4和第5行。第4和第5行是对象实例化时进行的初始化操作,而这个初始化操作是赋值操作,因此也会进入到__setattr__中,这就是为什么前两句会被打印的原因。

  下面我们继续讨论在上述类的定义下,我们是否可以给不存在的属性进行赋值操作呢?我们执行以下代码

boy1 = MyClass(name="小明", age=14)
boy1.day = 18
print(boy1.day)

  我们在第2行对boy1的day属性进行了赋值,并且在第3行对boy1对象的day属性进行了访问,并打印其值。要知道boy1是没有day属性的,那第2行会不会报错呢?最终结果如下:

  我们可以看到在代码的第2行中对boy1不存在的属性day进行赋值时可行的,并且赋值之后就可以对这个属性进行访问了。其实对对象的不存在的属性进行赋值本身就是被允许的(即使我们不重写__setattr__方法,也是被允许的)。不过需要注意的是对于不存在的属性必须要先进行赋值才能被访问,如果直接就访问不存在的属性,将会直接爆出AttributeError异常。

  其实__setattr__()与__getattribute__()一样,只要是赋值操作就会被调用,因此该方法也非常容易造成死循环(当你在__setattr__()中试图使用 self.attr = xxx 时就又会触发死循环),具体的解决死循环的方式与__getattribute__()一样,就是采用基类的方式对属性进行赋值,这也就是我在例子中第10行所采用的方法,使用 object.__setattr__(self, name, value) 对属性进行赋值。

2、类型转换相关的魔法方法

 2.1、__str__(self)

  这是也是一个最常重写的魔法方法,__str__方法会执行str(对象)时被调用,也就是将对象转化成字符串时,该魔法方法会被调用。该魔法方法必须有一个字符串类型的返回值。直接看例子吧。

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    def __str__(self):
        return f"我的名字是是{self.name}, 年龄是{self.age}"
    
boy1 = MyClass(name="小明", age=14)
boy2 = MyClass(name="小华", age=15)
print(boy1)
print(str(boy2))

  第10行和第11行我们创建了两个对象,并且在第12行和第13行打印了这两个对象,由于print会自动调用str(),所以最后输出的结果是:

 2.2、__bool__(self)

  这个魔法函数与__str__类似,会在执行bool(对象)时被调用,当然如果直接把对象放在 if 后面作为条件判断,也相当于是 if bool(对象) :, 我们可以看下面这个例子。

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    def __bool__(self):
        print(f"{self.name}的__bool__被调用")
        if self.age >= 18:
            return True
        else:
            return False

boy1 = MyClass(name="小明", age=14)
boy2 = MyClass(name="小华", age=19)

if boy1:
    print("进入了if")
else:
    print("没有进入if")
print(bool(boy2))

  我们重写了__bool__方法,当对象的age属性大于等于18时返回True,否则返回False,最后的执行结果如下:

  可以看到由于对象小明的age 时小于18的,因此if条件判断进入了else分支。同时我们还打印了对象小华执行bool()函数的结果,是True,这与我们想象的是一致的。

 2.3、__int__(self)、__float__(self)

  这两个魔法方法与前面两个方法一样,都是会在指定的转换函数是触发调用。从名字中就可以看出__int__会在int(对象)时调用,__float__会在float(对象)时被调用。需要注意的是这两个魔法方法的返回值都一定要符合各自的要求,比如__int__的返回值必须是一个int类型。

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

  
    def __int__(self):
        print("__int__被调用")
        return int(self.age)

boy1 = MyClass(name="小明", age=14)
boy2 = MyClass(name="小华", age=19)

print(int(boy1))
print(int(boy2))

  运行结果是:

3、比较与操作符重载的魔法方法

 3.1、__eq__(self, other)、__ne__(self,other)

  当使用==符号时,将会被调用对象的__eq__方法。需要注意的是,我们会率先调用==左边对象的__eq__方法,如果左边对象没有定义__eq__方法,才会调用右边对象的__eq__方法,我们看下面的例子.

class MyClass(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other) -> bool:
        print(self.name, "的__eq__被调用")
        if isinstance(other, MyClass) and self.age == other.age:
            return True
        else:
            return False

boy1 = MyClass(name="小华", age=14)
boy2 = MyClass(name="小明", age=14)
boy3 = object()
print(boy1 == boy2)
print(boy3 == boy2)

  我们在__eq__方法中判断两个对象的年龄是否相等,如果相等就返回Treu,否则返回False(如果判断的对象类型不是MyClass实例,则返回False)。结果如下:

  在16行时我们其实是在调用boy1.__eq__(boy2),而在第17行,由于boy3是空对象,没__eq__方法,那么我们将调用boy2.__eq__(boy3)。因此Python并没有要求我们必须要保证==的可交换性。

  对于__ne__,其实就是在执行 != 时调用的,一般来说我们不需要单独定义这个魔法方法。因为如果我们定义了__eq__方法,那么Python会自动执行__eq__方法,并对得到的结果取反返回。

 3.2、__lt__(self, other)、__le__(self, other)、__gt__(self, other)、__ge__(self, other)

   这几个魔法方法分别时在<,<=,>,>=,时被调用。通常来说我们不需要对上述所有魔法方法都进行定义,因为Python解释器会认为__lt__方法(<)是__ge__方法(>=)的取反,__gt__(>)是__le__(<=)方法的取反。而同样的Python解释器会认为__le__(<=)方法是由__lt__(<)和__eq__(==)分离得到的。这就意味着我们通常只需要定义__eq__、__lt__、__gt__就可以让上述的四种方法正常工作(除非你不想让他们具有这样的逻辑关系)。

  其实定义了这相对比较的魔法方法之后,我们就可以实现自定义的排序了,比如我们想使用 列表.sort()进行排序,那么会自动调用列表元素中的这些比较方法,比如下面这个例子。

class MyClass(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other) -> bool:
        print(self.name, "的__eq__被调用")
        if isinstance(other, MyClass) and self.age == other.age:
            return True
        else:
            return False
        
    def __lt__(self, other):
        if self.age < other.age:
            return True
        else:
            return False
    
    def __gt__(self, other):
        if self.age > other.age:
            return True
        else:
            return False

    def __repr__(self) -> str:
        return f"MyClass(name={self.name}, age={self.age})"

boy1 = MyClass(name="小华", age=13)
boy2 = MyClass(name="小明", age=14)
boy3 = MyClass(name="小中", age=14)
boy4 = MyClass(name="小五", age=12)
boy5 = MyClass(name="小星", age=10)
boy6 = MyClass(name="小利", age=16)
boy7 = MyClass(name="小国", age=21)
boys = [boy1, boy2,boy3,boy4,boy5,boy6,boy7]
boys.sort()
print(boys)

  我们完成了__eq__、__lt__、__gt__的定义,并且创建了好几个对象,放入到列表中,最后使用列表自带的sort()方法进行排序,最后打印排序后的结果。(我们在类的定义中重写了__repr__方法,主要是用于展示排序后的结果的,后面会讲到它的作用)。结果如下:

   可以看到打印的结果真的是按照年龄来进行排序的。

 3.3、一元操作符 __pos__(self)、__neg__(self)、__invert__(self)

   一元操作符的魔法方法分别对应的: +、-、~,其中+和-既是一元操作符,又是二元操作符,但Python解释器会根据表达式来自己判断执行。一元操作符的使用非常直接,比如+x就是在执行x.__pos__(),~x就是在执行x.__invert__()。比如我们考虑下面这个类,我们将返回字符串的逆序.

class ReversibleString(object):
    def __init__(self, s):
        self.s = s
    
    def __invert__(self):
        return self.s[::-1]

re = ReversibleString("我爱中国")
print(~re)

  我们在__invert__()方法中将字符串进行了反转,并返回了,最终执行结果式:

  我们需要注意的式~re得到的返回值是个字符串,而字符串是没有定义~操作的,因此我们执行~~re将会报错。如何解决这个问题呢?只需要让__invert__()返回的是RversibleString对象即可,如下改进:

class ReversibleString(object):
    def __init__(self, s):
        self.s = s
    
    def __invert__(self):
        return ReversibleString(self.s[::-1])
    
    def __str__(self) -> str:
        return self.s

re = ReversibleString("我爱中国")
print(~re)
print(~~re)

  最终的返回结果是:

 3.4、二元操作符 __add__(self, other)等

  Python允许我们重载二元操作符,例如: +,-,*,/,|,%等等,理论上所有的python中的二元操作符都可以被重载。二元方法的调用顺序是从左至右,也就是如果x具有对应的二元方法则会被优先使用,比如x + y,实际上在执行 x.__add__(y)。如下面这个例子:

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    def __add__(self, other):
        print(f"{self.name}的__add__被调用")
        sum_age = self.age + other.age
        return sum_age

boy1 = MyClass(name="小明", age=14)
boy2 = MyClass(name="小华", age=19)

print(boy1 + boy2)
print(boy2 + boy1)

  我们重写了MyClass的__add__方法,由此我们可以使用 对象+对象 的方式(如果不重写该方法会爆出异常),我们在__add__中打印了到底是谁在执行__add__方法,并且架构两个对象的年龄求和的结果返回了,执行结果如下:

   可以看到,其实+只执行了左边的__add__方法,并不会执行+右边的对象,因此我们在重写这个方法时候,需要注意是否要保证二元运算符的可交换性。

  其实对于每一个二元操作符,Python都提供了3种魔术方法,第一种就是上面提到的,普通方法。第二种是即席方法,如果重写了即席方法,那么将会在执行 +=的时候被调用。+ 运算的即席方法叫做__iadd__(),例如: x += y 等价于在执行x.__iadd__(y)。一般情况下即席方法都是修改self,再返回self,但这并不是必须的。如果我们没有定义即席方法,只定义了__add__方法,那是否会报错呢?看如下例子:

boy1 = MyClass(name="小明", age=14)
boy2 = MyClass(name="小华", age=19)

boy1 += boy2
print(type(boy1))

  运行的结果是:

   我们可以看到boy1的类型变成了int,如果打印boy1会得到33,这是为什么呢?其实就是当我们只定义了普通方法,没有定义即席方法,那么+=操作就等价于,x = x.__add__(y),即将x + y的结果返回给左边的对象。此处的boy1其实接收的就是boy1.__add__(boy2)的返回值。

  第三种魔法函数是取反方法,当+左边的对象并没有提供__add__方法并且操作对象类型不同时,将会执行右边对象的__radd__()方法。换句话说 x + y 中如果x没有提供__add__()并且x与y不是同一类型对象,那么该条语句会执行y.__radd__(x)。我们看下面这个例子:

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    def __radd__(self, other):
        print(f"{self.name}的__radd__被调用")
        return self.age

class B(object):
    pass

boy1 = B()
boy2 = MyClass(name="小明", age=14)
print(boy1 + boy2)

  注意我们这里定义了两个类B类和MyClass类,这两个类都没有重写__add__方法。因此满足触发__radd__()调用的条件,boy1和boy2的类型是不同的,并且boy1没有__add__方法。最终的结果如下:

  由此我们上面介绍了所有的关于+ 运算的三种魔法方法,其实其余的二元运算也是一样的,都具有普通方法,取反方法,即席方法。具体的名称如下图所示:

 

4、类似集合的魔法方法

 4.1、__getitem__(self,index)

  当对对象使用下标查询时(对象[1])会进入到该方法。该方法会收集index,至于如何处理就是自己定义了,上例子。

class MyClass(object):
    def __init__(self, name):
        self.name = name

    ## 可以使用对对象使用下标: 对象[1]
    def __getitem__(self,index):
        print(f"{self.name}的__getitem__被调用, index为{index}")

boy1 = MyClass("小明")
boy1[1]
boy1[20]
boy1[200]

  结果如下:

  可以看到当我们定义完成__getitem__方法后就可以使用中括号+数字来进行操作了,但是要注意,只定义__getitem__方法只能完成查询操作,并不能实现对下标进行赋值,即执行 对象[1] == xxx 依然会报错。

 4.2、__setitem__(self,key, value)

  上一小节说明了如何使用下标访问,如果我们重写__setitem__方法,那么我们就可以实现使用中括号+下标进行赋值了,直接上例子。

class MyClass(object):
    def __init__(self, name):
        self.name = name
        self.friends = dict()

    ## 可以给指定对象下标进行赋值: 对象[1] = xxx
    def __setitem__(self, key, value):
        print(f"{self.name}的__setitem__被调用, key={key}, value={value}")
        self.friends[key] = value

boy1 = MyClass("小明")
boy1[1] = "李华"
boy1["邻居"] = "小花"
print(boy1.friends)

  我们这里看12和13行,我们并没有要求中括号中必须放入的是int类型的数值,理论上我们可以放任何东西,因为都会传入给__setitem__方法,并以key来接收中括号中的内容。最后的执行结果如下。

 4.3、__contains__(self,ele)

  这一个方法会在使用 in 时被触发,比如 boy1 in boy2 ,相当于在执行boy2.__contains__(boy1),直观上在查询boy1是否在boy2中,我们可以通过自定义__contains__方法从而实现这个过程,上例子:

class MyClass(object):
    def __init__(self, name):
        self.name = name
        self.friends = dict()

    def __setitem__(self, key, value):
        print(f"{self.name}的__setitem__被调用, key={key}, value={value}")
        self.friends[key] = value

    def __contains__(self, ele):
        print(f"{self.name}的 __contains__被调用")
        if ele in self.friends.values():
            return True
        else:
            return False

boy1 = MyClass("小明")
boy1["同学1"] = "李华"
boy1["同学2"] = "山炮"
boy1["邻居1"] = "小花"
boy1["邻居2"] = "静香"
print("胖虎" in boy1)
print("山炮" in boy1)

  这里我们自定义的__contains__方法实际上就是看ele是否在对象的friends属性(这是个字典)中,运行的结果是:

   我相信这个例子很直观的说明了__contains__方法的使用场景吧。

5、其他常用魔法方法

  下面介绍一些常见定义的魔法方法

 5.1、__len__(self)

  当调用len(对象)时会执行这个魔法方法,返回的时对象的"长度"(返回值需要是>=0的数值)。这本来没有什么好说的,但是该函数又与bool()和IF条件判断有关。因为在没有定义__bool__函数时,如果使用 if 对象 或者 bool(对象)语句的时候,将也会进入该魔法方法(返回值如果时0则为False,否则为True),上例子:

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age

    def __len__(self):
        return 100
    
boy1 = MyClass(name="小明", age=14)
print(bool(boy1))
if boy1:
    print("进入了if")
else:
    print("没有进入if")

  运行结果如下:

 5.2、__repr__(self)

  这个魔法方法非常重要(但也经常被忽略),__repr__用于确定对象在终端的显示方式(注意在终端中显示一个对象不是直接调用str(对象)的)。一个对象在终端默认显示是类似于<__main__.Object at xxxx>这样的,用类型+地址表示一个对象,比如你把object放入到一个[]中,你打印列表,得到的就是[<.....>, <.....>。。。 ]。但这种表示对于程序员来说也是没有多大价值的(除了显示出对象地址),程序员其实更想直到这个对象是如何建立的,因此一般__repr__都会直接返回初始化该对象的语句(这并不是必须的要求),例如我们在3.2小节中做的一样。直接看例子吧:

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age
 
    def __repr__(self) -> str:
        return f"MyClass(name={self.name}, age={self.age})"
    
boy1 = MyClass(name="小明", age=14)
boy2 = MyClass(name="小华", age=14)
boy3 = MyClass(name="小褚", age=14)
boys = []
boys.append(boy1)
boys.append(boy2)
boys.append(boy3)
print(boys)

  我们将三个对象放入到列表中,并打印这个列表,可以得到如下结果:

   大家可以试试如果删除__repr__打印的结果又会是什么呢?我们需要注意的是__repr__与__str__的差别,后者是当调用str(对象)时才会被执行,而__repr__则是显示在终端上的(你在DeBUG时对象显示的就是__repr__的内容),因此可以大致理解为__repr__显示的内容其实是给程序员看的,而__str__是给用户看的。

 5.3、__hash__(self)

  这个魔法方法在对象传递给散列函数时被调用,也就是放入集合中,hash表中时会调用(例如将对象add进一个set时)。__hash__方法需要返回一个整型值,可以为负数,这个整型值通常时为了唯一的标识一个对象使用,不同对象返回理论上应该返回不同的整型值才对(因此Cpython中是根据对象的内存地址转化的整型值返回,当我们没有定义__hash__方法时就是调用的这个默认的方法,返回一大串数字)。然而如果我们自定义了__eq__方法(但是没有定义__hash__方法),那么默认的__hash__方法会被隐式的变为None(即无法调用)。

  在python中只有可哈希化的对象才能放入set中或者称为字典的键,在上述两种情况下,哈希值(__hash__返回的值称为哈希值)用于确定一个对象是否是set对象成员以及将某个对象与字典的键进行比较从而进行查找。例如下面的例子:

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age
    
    def __hash__(self):
        print(f"{self.name}的__hash__被执行")
        return 0
    
    def __repr__(self) -> str:
        return f"MyClass(name={self.name}, age={self.age})"
    
    def __eq__(self, other) -> bool:
        if self.age == other.age:
            return True
        else:
            return False
    
boy1 = MyClass(name="小明", age=14)
boy2 = MyClass(name="小华", age=14)
boy3 = MyClass(name="小褚", age=14)
boys = set()
boys.add(boy1)
boys.add(boy2)
boys.add(boy3)
print(boys)

  我们重写了__hash__和__eq__方法,当年龄相等时,__eq__将返回True,而__hash__则一直返回0,在set或者字典的键判断两个对象是否相等,使用的逻辑是: obj1.__hash__() == obj.__hash__() and (obj1.id() == obj.id() or obj1.__eq__(obj2)),如果上述表达式返回的是True则认为两个对象相等,否则不相等(其中object.id()是得到对象的内存地址)。因此上述执行结果如下:

  其实这里里面有蛮多细节,关于散列表也就是set集合是如何储存一个对象的,这里仅仅简单说明一下(没有数据结构基础的不用了解)。

  • 首先调用放入对象object2的__hash__()得到哈希值,并根据hash值得到该对象在hash表(可以认为是一维列表)中的下标位置。
  • 如果hash表(可以认为是一维列表)中的下标位置处已经存在对象object1,则通过:obj1.__hash__() == obj2.__hash__() and (obj1.id() == obj2.id() or obj1.__eq__(obj2))的逻辑判断两个对象是否相等,如果相等则不放入该对象,如果不相等则将该对象"链接"到object2的后面。(其实这里的object2是一个链表的头节点,object1需要比较完整个链表的所有节点,才能被放入)

 5.4、__format__(self,spec_str)

  我们在字符串输出时,常常会采用"".format()的方式进行格式化,而__format__魔法方法,则是将对象放入"".format(对象)中时会被执行的,而spec_str参数则会保留下{}中:后的格式化要求(在字符串格式化中在冒号后面可以进行一些标准定义,比如{:^10}表示居中显示宽度为10),直接上例子:

class MyClass(object):

    def __init__(self, age, name):
        self.name = name
        self.age = age
    
    def __format__(self, spec_str) -> str:
        print(f"{self.name}的__format__被执行, spec_str={spec_str}")
        return self.name
    
boy1 = MyClass(name="小明", age=14)
print("{} 测试, {:.s}, {:^d}".format(boy1, boy1, boy1))

  这里我们直接将boy1传入到format中,而此处的:s,:^d都是我乱写的,只是为了展示__format__会接收到这些要求至于是否使用,全屏自定义,上面例子的运行结果是:

 6、总结

  本文总结有19种不同的魔法方法,但其实Python种定义的魔法方法有上百种,大多数魔法方法都是你不知道的地方发挥着作用。我们并不需要也不必须要去全部重写或者部分重写,这更像是Python给我们设定的语法糖一样,如果你的功能有魔法方法可以帮助你实现,那么可以使用这些魔法方法。在笔者看来了解这些魔法方法可以帮助编程者更加深入的了解Python在你不知道的地方到底执行了什么,有助于我们对python这一门动态语言的理解。