Python中的Dunder/Magic方法

282 阅读7分钟

Python中的Dunder/Magic方法

Dunder方法是指前面和后面都有双下划线的名字,因此被称为dunder。它们也被称为魔法方法,可以帮助覆盖自定义类的内置函数的功能。

1.简介

为类实现dunder方法是多态性的一种很好的形式。如果你曾经在Python中创建了一个类并使用了init函数,那么你就已经在使用dunder方法了。

2.先决条件

在我们继续之前,必须具备以下条件。

  • 对使用Python的面向对象编程有基本了解。
  • 有在Python中处理类的经验。
  • 熟悉内置函数,如len、get、set等。

3.为什么我们需要Dunder方法?

考虑一个案例,我们有以下的类。

class point:
    x = 4
    y = 5

p1 = point()
print(p1)

打印语句会打印出类似<__main__.point object at 0x7fb992998d00> 的东西。但是,我们可能想让打印语句显示一些格式为(4,10) 。我们可以通过覆盖我们类的__str__ 方法来实现这个目的。

我们还可以覆盖其他的方法,如len, +, [] 等。我们将创建一个新的类并覆盖本文中的许多内置函数。

4.我们的自定义类

class softwares:
    names = []
    versions = {}

这个类将用于保存一个软件及其版本的列表。names 是一个存储软件名称的列表,versions 是一个字典,其中键是软件名称,值是版本号。默认情况下,所有的软件都以1的版本开始。

5.为我们的类提供Dunder方法

在继续前进之前,请确保你的缩进是正确的。下面要讨论的方法是属于我们创建的类的方法,必须适当地缩进。

5.1 init

如果你曾与类打过交道,这个方法你一定已经用过了。init 方法用于创建一个类的实例。

def __init__(self,names):
    if names:
        self.names = names.copy()
        for name in names:
            self.versions[name] = 1
    else:
        raise Exception("Please Enter the names")

上面定义的init 方法接受一个名字的列表作为参数,并将其存储在类的names 列表中。此外,它还填充了versions 字典。我们还在names 列表中设置了一个检查。

如果这个列表是空的,就会产生一个异常。下面是我们如何使用init 方法。

p = softwares(['S1','S2','S3'])
p1 = softwares([])

第一条语句可以正常工作,但第二行会引发一个异常,因为一个空的列表被作为参数传入。

5.2 str

当我们想在打印语句中使用我们类的实例时,str 方法很有用。正如前面所讨论的,它通常返回一个内存对象。但是我们可以重写str 方法以满足我们的要求。

def __str__(self):
    s ="The current softwares and their versions are listed below: \n"
    for key,value in self.versions.items():
        s+= f"{key} : v{value} \n"
    return s

上面的str 方法返回软件和它们的版本。确保该函数返回一个字符串。下面是我们如何调用该方法。

print(p)

5.3 Setitem

在字典中赋值时,setitem 方法被调用。

d = {}
d['key'] = value

我们可以在setitem 方法的帮助下给我们类的实例一个类似的功能。

def __setitem__(self,name,version):
    if name in self.versions:
        self.versions[name] = version
    else:
        raise Exception("Software Name doesn't exist")

上面的方法是要更新软件的版本号。如果没有找到该软件,它将引发一个错误。

在第3行中,我们使用字典内置的setitem 方法。

我们可以通过以下方式调用setitem 方法。

p['S1'] = 2
p['2'] = 2

第一行将更新软件S1的版本为2。但第二行将引发一个异常,因为软件2不存在。

5.4 getitem

getitem 方法和setitem 方法一样,主要的区别是,当我们使用字典的[] 操作符时,会调用getitem 方法。

d = {'val':key}
print(d['val'])

我们类的实例也可以被赋予类似的功能。

def __getitem__(self,name):
    if name in self.versions:
        return self.versions[name]
    else:
        raise Exception("Software Name doesn't exist")

上述方法实质上是返回软件的版本。如果没有找到该软件,它会引发一个异常。为了调用getitem 方法,我们可以写下面一行代码。

print(p['S1'])
print(p['1'])

第一行将打印S1的版本。但是,第二行会引发一个异常,因为1并不存在。

5.5 delitem

delitem ,就像setitemgetitem 方法一样。为了避免重复,我们将继续讨论实现和用例。

def __delitem__(self,name):
    if name in self.versions:
        del self.versions[name]
        self.names.remove(name)
    else:
        raise Exception("Software Name doesn't exist")

delitem 方法将软件从字典以及列表中删除。

它的使用方法如下。

del p['S1']

5.6 len

在 dictionary 中,len 方法返回 list 中的元素数或 dictionary 中的 key-value 对数。

我们也可以为我们的类定义一个len 方法。

def __len__(self):
    return len(self.names)

我们类的len 方法返回软体的数量。你可能已经注意到了,我们正在使用列表中内置的len 方法来返回软件的数量。

我们类的len 方法可以用在以下方面。

print(len(p))

5.7 包含

contains 方法是在使用in 操作符时使用的。其返回值必须是一个布尔值。

def __contains__(self,name):
    if name in self.versions:
        return True
    else:
        return False

该方法检查名称是否在字典中找到。我们将为此使用字典内置的contains 方法。

if 'S2' in p:
    print("Software Exists")
else:
    print("Software DOESN'T exist")

上面的代码在if块内打印语句,因为软件S2存在于versions 字典内。

5.8完整的代码

class softwares:
    names = []
    versions = {}
    
    def __init__(self,names):
        if names:
            self.names = names.copy()
            for name in names:
                self.versions[name] = 1
        else:
            raise Exception("Please Enter the names")
    
    def __str__(self):
        s ="The current softwares and their versions are listed below: \n"
        for key,value in self.versions.items():
            s+= f"{key} : v{value} \n"
        return s
    
    def __setitem__(self,name,version):
        if name in self.versions:
            self.versions[name] = version
        else:
            raise Exception("Software Name doesn't exist")
    
    def __getitem__(self,name):
        if name in self.versions:
            return self.versions[name]
        else:
            raise Exception("Software Name doesn't exist")
    
    def __delitem__(self,name):
        if name in self.versions:
            del self.versions[name]
            self.names.remove(name)
        else:
            raise Exception("Software Name doesn't exist")
    
    def __len__(self):
        return len(self.names)
    
    def __contains__(self,name):
        if name in self.versions:
            return True
        else:
            return False

6.一些更多的dunder方法

在看一些更多的dunder方法之前,让我们创建一个新的类。

class point:
    x = None
    y = None
    
    def __init__(self, x , y):
        self.x = x
        self.y = y
    
    def __str__(self):
        s = f'({self.x},{self.y})'
        return s

p1 = point(5,4)
p2 = point(2,3)

我们已经创建了一个点类,它基本上是一个二维的点。该类有一个init 方法和一个str 方法。我们还创建了该类的几个实例。

6.1 添加

当使用+ 操作符时,add 方法被调用。我们可以为我们的类定义一个自定义的add 方法。

p1 + p2 等于p1._add__(p2)

def __add__(self,p2):
    x = self.x + p2.x
    y = self.y + p2.y
    return point(x,y)

上面的方法添加了point 的第一个实例和point 的第二个实例的x和y坐标。它将创建一个新的point 的实例,然后返回它。

p3 = p1 + p2

上面的这行代码调用了add 方法。

6.2 iadd

iadd 方法和add 方法一样。它在使用+= 操作符时被调用。

def __iadd__(self,p2):
    self.x += p2.x
    self.y += p2.y
    return self

上面的方法只是通过添加p2 的坐标来更新一个实例的坐标。确保你返回self ,否则它将返回None,不会像预期的那样工作。

p1 += p2
print(p1)

上面的方法调用了iadd 方法。

6.3 其他运算符

  • __sub__(self,p2) ( - )
  • __isub__(self,p2) ( -= )
  • __mul__(self,p2) ( * )
  • __imul__(self,p2) ( *= )
  • __truediv__(self,p2)( \ )
  • __itruediv__(self,p2) ( \= )
  • __floordiv__(self,p2) ( \\ )
  • __ifloordiv__(self,p2) ( \= )

6.4 调用

当调用一个像func() 的函数时,我们是在调用call 方法。

如果我们为我们的类放置一个call 方法,我们就可以做以下事情。

p1()
p2()

下面是一个调用方法的例子。

def __call__(self):
    print(f"Called Point {self.x},{self.y}")

6.5 完整的代码

class point:
    x = None
    y = None
    
    def __init__(self, x , y):
        self.x = x
        self.y = y
    
    def __str__(self):
        s = f'({self.x},{self.y})'
        return s
    
    def __add__(self,p2):
        print("In add")
        x = self.x + p2.x
        y = self.y + p2.y
        return point(x,y)
    
    def __iadd__(self,p2):
        self.x += p2.x
        self.y += p2.y
        return self
    
    def __isub__(self,p2):
        self.x -= p2.x
        self.y -= p2.y
        return self
    
    def __imul__(self,p2):
        self.x *= p2.x
        self.y *= p2.y
        return self
    
    def __itruediv__(self,p2):
        self.x /= p2.x
        self.y /= p2.y
        return self
    
    def __ifloordiv__(self,p2):
        self.x //= p2.x
        self.y //= p2.y
        return self
    
    def __call__(self):
        print(f"Called Point {self.x},{self.y}")

7.总结

Dunder方法确实很神奇,可以帮助你提高你的类的功能。