📜  使用 Magic 或 Dunder 方法自定义Python类

📅  最后修改于: 2022-05-13 01:55:30.032000             🧑  作者: Mango

使用 Magic 或 Dunder 方法自定义Python类

魔术方法确保了一致的数据模型,该模型保留了内置类的继承特性,同时提供了自定义的类行为。这些方法可以丰富类设计,增强语言的可读性。

因此,在本文中,我们将了解如何使用魔法方法,它是如何工作的,以及Python中可用的魔法方法。让我们浏览每个部分:

  • 魔术方法语法
  • 常用魔法方法
  • 二元运算符的魔术方法
  • 一元运算符的魔术方法
  • 其他一些魔术方法

魔术方法语法

两边用两个下划线包裹的方法称为魔术方法。魔术方法背后的动机是重载 Python 的内置方法及其运算符。在这里,_syntax 防止程序员为自定义方法定义相同的名称。每种魔术方法都有其用途。让我们考虑一个检查等价的例子。

例子:

Python3
class EquivalenceClass(object):
    def __eq__(self, other):
        return type(self) == type(other)
  
print(EquivalenceClass() == EquivalenceClass())
print(EquivalenceClass() == 'MyClass')


Python3
class InitClass(object):
    def __init__(self):
        print('Executing the __init__ method.')
  
ic = InitClass()


Python3
class Square(object):
    def __init__(self, number = 2):
        self._number = number
  
    def square(self):
        return self._number**2
  
s = Square()
print('Number: % i' % s._number)
print('Square: % i' % s.square())


Python3
class Students(object):
    def __init__(self, idNo, grade):
        self._idNo = idNo
        self._grade = grade
  
    def __new__(cls, idNo, grade):
        print("Creating Instance")
        instance = super(Students, cls).__new__(cls)
        if 5 <= grade <= 10:
            return instance
        else:
            return None
  
    def __str__(self):
        return '{0}({1})'.format(self.__class__.__name__, self.__dict__)
  
  
stud1 = Students(1, 7)
print(stud1)
  
stud2 = Students(2, 12)
print(stud2)


Python3
class MyClass(object):
    def __del__(self):
        print('Destroyed')
  
MyClass()
'Immutable String - not assigned to a variable'


Python3
class MyString(object):
    def __str__(self):
        return 'My String !'
  
print(str(MyString()))


Python3
class HelloClass(object):
    def __str__(self):
        return 'George'
  
print('Hello, % s' % HelloClass())


Python3
class MyEquivalence(object):
    def __eq__(self, other):
        print('MyEquivalence:\n'
              '% r\n % r' %(self, other))
        return self is other
  
class YourEquivalence(object):
    def __eq__(self, other):
        print('Your Equivalence:\n'
              '% r\n % r' %(self, other))
        return self is other
  
eq1 = MyEquivalence()
eq2 = YourEquivalence()
# checking for equivalence where eq1 is at the left side
print(eq1 == eq2)
# checking for equivalence where eq2 is at the left side
print(eq2 == eq1)


Python3
class MyEquivalence(object):
    def __eq__(self, other):
        print('MyEquivalence:\n'
              '% r\n % r' %(self, other))
        return self is other
  
class MySubEquivalence(MyEquivalence):
    def __eq__(self, other):
        print('MySubEquivalence:\n'
              '% r\n % r' %(self, other))
        return self is other
  
eqMain = MyEquivalence()
eqSub = MySubEquivalence()
  
# eqMain at the right side
print(eqMain == eqSub)
  
# eqSub at the right side
print(eqSub == eqMain)


Python3
import time
class ObjectCreationTime(object):
    def __init__(self, objName):
        self._created = time.time()
        self._objName = objName
  
    def __lt__(self, other):
        print('Creation Time:\n'
              '% s:% f\n % s:% f' %(self._objName, self._created,
                                 other._objName, other._created))
        return self._created < other._created
  
    def __gt__(self, other):
        print('Creation Time:\n'
              '% s:% f\n % s:% f' %(self._objName, self._created,
                                 other._objName, other._created))
        return self._created > other._created
  
obj1 = ObjectCreationTime('obj1')
obj2 = ObjectCreationTime('obj2')
print(obj1 < obj2)
print(obj1 > obj2)


Python3
class Count(object):
    def __init__(self, count):
        self._count = count
    def __add__(self, other):
        total_count = self._count + other._count
        return Count(total_count)
    def __str__(self):
        return 'Count: % i' % self._count
  
  
  
c1 = Count(2)
c2 = Count(5)
c3 = c1 + c2
print(c3)


Python3
class Count(object):
    def __init__(self, count):
        self._count = count
  
    def __add__(self, other):
        total_count = self._count + other._count
        return Count(total_count)
  
    def __radd__(self, other):
        if other == 0:
            return self
        else:
            return self.__add__(other)
  
    def __str__(self):
        return 'Count:% i' % self._count
      
c2 = Count(2)
c3 = 0 + c2
print(c3)


Python3
class inPlace(object):
    def __init__(self, value):
        self._value = value
    def __iadd__(self, other):
        self._value = self._value + other._value
        return self._value
    def __str__(object):
        return self._value
  
inP1 = inPlace(5)
inP2 = inPlace(3)
inP1 += inP2
print(inP1)


Python3
class unaryOp(object):
    def __init__(self, value):
        self._value = value
    def __pos__(self):
        print('__pos__ magic method')
        return(+self._value)
     
up = unaryOp(5)
print(+up)


Python3
class unaryOp(object):
    def __init__(self, value):
        self._value = value
    def __neg__(self):
        print('__neg__ magic method')
        return(-self._value)
  
up = unaryOp(5)
print(-up)


Python3
class invertClass(object):
    def __init__(self, value):
        self._value = value
    def __invert__(self):
        return self._value[::-1]
    def __str__(self):
        return self._value
  
invrt = invertClass('Hello, George')
invertedValue = ~invrt
print(invertedValue)


Python3
class RectangleClass(object):
    def __init__(self, area, breadth):
        self._area = area
        self._breadth = breadth
          
    def __len__(self):
        return int(self._area / self._breadth)
  
rc = RectangleClass(90, 5)
print(len(rc))


Python3
class RectangleClass(object):
    def __init__(self, area, breadth):
        self._area = area
        self._breadth = breadth
          
    def __len__(self):
        return int(self._area / self._breadth)
  
## use python interactive terminal to check object representation.
RectangleClass(90, 5)


Python3
class RectangleClass(object):
    def __init__(self, area, breadth):
        self._area = area
        self._breadth = breadth
          
    def __len__(self):
        return int(self._area / self._breadth)
  
    def __repr__(self):
        """object representation"""
        return 'RectangleClass(area =% d, breadth =% d)' %\
               (self._area, self._breadth)
           
RectangleClass(90, 5)   
RectangleClass(80, 4)


Python3
import datetime
  
class DateClass(object):
    def __init__(self, startDate, endDate):
        self.startDate = startDate
        self.endDate = endDate
  
    def __contains__(self, item):
        """ check whether a date is between the given range and
        return true or false"""
        return self.startDate <= item <= self.endDate
  
dtObj = DateClass(datetime.date(2019, 1, 1), datetime.date(2021, 12, 31))
result = datetime.date(2020, 6, 4) in dtObj
print("Whether (2020, 6, 4) is within the mentioned date range? ", result)
  
result = datetime.date(2022, 8, 2) in dtObj
print("Whether (2022, 8, 2) is within the mentioned date range? ", result)



输出
True
False

__eq__方法接受两个参数——self 和 object——来检查相等性。重要的是要理解,当使用 ==运算符比较两个对象时调用 __eq__ 方法。让我们来看看Python中一些常见的魔术方法。

常用魔法方法

在Python中,我们有各种各样的魔法方法——每一种都有它的用途。下面我们就梳理一下,几种常见的魔法方法:

  • 创建
  • 破坏
  • 类型转换
  • 比较

创建

纠缠于创建的魔术方法在创建类实例时执行。相关的两个魔术方法是__init____new__方法。

__init__ 方法

对象的 __init__ 方法在实例创建后立即执行。在这里,该方法采用一个位置参数——self——和任意数量的可选或关键字参数。让我们看一个简单的例子:

例子:

Python3

class InitClass(object):
    def __init__(self):
        print('Executing the __init__ method.')
  
ic = InitClass()


输出
Executing the __init__ method.

在这里,需要注意的要点是,您没有调用 __init__ 方法。相反, Python解释器调用对象实例化。让我们考虑一个例子,它接受一个可选参数:

Python3

class Square(object):
    def __init__(self, number = 2):
        self._number = number
  
    def square(self):
        return self._number**2
  
s = Square()
print('Number: % i' % s._number)
print('Square: % i' % s.square())


输出
Number: 2
Square: 4

在这里我们可以注意到,在没有可选参数的情况下,__init__ 方法使用了默认值 (2)。让我们检查一些关于 __init__ 方法的事实:

  • __init__ 方法为对象提供初始数据,而不是创建对象。
  • 它只返回无;返回非 None 会引发 TypeError。
  • 它自定义类的实例化。

接下来,我们将继续__new__方法。

__new__ 方法

__new__ 方法创建并返回一个类的实例。 __new__ 方法的主要参数是必须实例化的类,其余是类调用期间提到的参数。让我们通过一个例子来探索:

例子:

Python3

class Students(object):
    def __init__(self, idNo, grade):
        self._idNo = idNo
        self._grade = grade
  
    def __new__(cls, idNo, grade):
        print("Creating Instance")
        instance = super(Students, cls).__new__(cls)
        if 5 <= grade <= 10:
            return instance
        else:
            return None
  
    def __str__(self):
        return '{0}({1})'.format(self.__class__.__name__, self.__dict__)
  
  
stud1 = Students(1, 7)
print(stud1)
  
stud2 = Students(2, 12)
print(stud2)


输出
Creating Instance
Students({'_idNo': 1, '_grade': 7})
Creating Instance
None

在大多数情况下,我们不需要定义 __new__ 方法。如果我们去实现一个 __new__ 方法,那么引用超类是必须的。另一个需要注意的要点是,实例化类的 __init__ 方法只有在 __new__ 方法返回同一类的实例时才会执行。

破坏

__del__ 方法

__del__ 方法在销毁类的实例时被调用——通过垃圾收集器的直接删除或内存恢复。让我们检查以下代码:

Python3

class MyClass(object):
    def __del__(self):
        print('Destroyed')
  
MyClass()
'Immutable String - not assigned to a variable'


输出
Destroyed

当我们创建一个对象而不将它们分配给一个变量时会发生什么?垃圾收集器将保留未引用变量的对象记录,并在另一个程序语句执行时将其删除。在这里,我们创建了一个 MyClass 对象,但没有将其分配给变量。在执行程序语句(不可变字符串 - 未分配给变量)时,垃圾收集器会销毁 MyClass 对象。

当我们直接删除对象时,也会发生同样的情况;但是在这里删除会立即发生。只需尝试以下代码。

x = MyClass()
del x

类型转换

类型转换是指将一种数据类型转换为另一种数据类型; Python提供了几种神奇的方法来处理转换。

  • __str__ 方法
  • __int__、__float__ 和 __complex__ 方法
  • __bool__ 方法

__str__ 方法

__str__ 方法需要一个位置参数——self——它返回一个字符串。当对象传递给str()构造函数时调用它。让我们考虑一个例子:

Python3

class MyString(object):
    def __str__(self):
        return 'My String !'
  
print(str(MyString()))


输出
My String!

让我们看一下调用 __str__ 方法的另一种情况。该场景是在格式字符串中使用 %s ,这反过来又调用了 __str__ 方法。

Python3

class HelloClass(object):
    def __str__(self):
        return 'George'
  
print('Hello, % s' % HelloClass())


输出
Hello, George

__int__、__float__ 和 __complex__ 方法

__int__ 方法在调用 int 构造函数时执行,它返回一个 int;它将复杂对象转换为原始 int 类型。同样, __float__ 和 _complex__ 方法分别在将对象传递给 float 和 complex 构造函数时执行。

__bool__ 方法

Python中的 __bool__ 魔术方法采用一个位置参数并返回真或假。其目的要么是检查对象是真还是假,要么是显式转换为布尔值。

比较

当我们检查等价(==,!=)或关系(<,和> =)时,会调用比较魔术方法。 Python中的每个运算符都映射到其相应的魔术方法。

二元等式

1. __eq__ 方法
__eq__ 方法在使用 ==运算符比较两个对象时执行。它需要两个位置参数——自我和检查相等性的对象。
在大多数情况下,如果定义了左侧的对象,则首先检查其等价性。让我们看一个例子:

Python3

class MyEquivalence(object):
    def __eq__(self, other):
        print('MyEquivalence:\n'
              '% r\n % r' %(self, other))
        return self is other
  
class YourEquivalence(object):
    def __eq__(self, other):
        print('Your Equivalence:\n'
              '% r\n % r' %(self, other))
        return self is other
  
eq1 = MyEquivalence()
eq2 = YourEquivalence()
# checking for equivalence where eq1 is at the left side
print(eq1 == eq2)
# checking for equivalence where eq2 is at the left side
print(eq2 == eq1)


输出
MyEquivalence:
<__main__.MyEquivalence object at 0x7fa1d38e16d8>
<__main__.YourEquivalence object at 0x7fa1d1ea37b8>
False
Your Equivalence:
<__main__.YourEquivalence object at 0x7fa1d1ea37b8>
<__main__.MyEquivalence object at 0x7fa1d38e16d8>
False

如果一个对象是另一个对象的直接子类,则排序规则不适用。让我们通过一个例子来检验:

Python3

class MyEquivalence(object):
    def __eq__(self, other):
        print('MyEquivalence:\n'
              '% r\n % r' %(self, other))
        return self is other
  
class MySubEquivalence(MyEquivalence):
    def __eq__(self, other):
        print('MySubEquivalence:\n'
              '% r\n % r' %(self, other))
        return self is other
  
eqMain = MyEquivalence()
eqSub = MySubEquivalence()
  
# eqMain at the right side
print(eqMain == eqSub)
  
# eqSub at the right side
print(eqSub == eqMain)


输出
MyEquivalence:
<__main__.MyEquivalence object at 0x7fa1d38e16d8>
<__main__.YourEquivalence object at 0x7fa1d1ea37b8>
False
Your Equivalence:
<__main__.YourEquivalence object at 0x7fa1d1ea37b8>
<__main__.MyEquivalence object at 0x7fa1d38e16d8>
False

2. __ne__ 方法
__ne__ 魔术方法在使用 !=运算符时执行。大多数情况下,我们不需要定义 __ne__ 方法;使用 !=运算符后, Python解释器将执行 __eq__ 方法并反转结果。

相对比较 – __lt__ & __le__, __gt__ & __ge__ 方法

分别在使用 < 和 <=运算符时调用 __lt__ 和 __le__ 方法。并且,分别在使用 > 和 >=运算符时调用 __gt__ 和 __ge__ 方法。但是,没有必要使用所有这 4 种方法;使用 __lt__ 和 __gt__ 方法将达到目的。只需检查以下几点即可了解为什么我们不需要所有这些方法:
1. __ge__ 和 __le__ 方法可以分别替换为 __lt__ 和 __gt__ 方法的逆。
2. __lt__ 和 __eq__ 方法的析取可以代替 __le__ 方法,类似地,__ge__ 方法的 __gt__ 和 __eq__ 方法。

让我们看一下下面的例子。在这里,我们将根据对象的创建时间来比较对象。

Python3

import time
class ObjectCreationTime(object):
    def __init__(self, objName):
        self._created = time.time()
        self._objName = objName
  
    def __lt__(self, other):
        print('Creation Time:\n'
              '% s:% f\n % s:% f' %(self._objName, self._created,
                                 other._objName, other._created))
        return self._created < other._created
  
    def __gt__(self, other):
        print('Creation Time:\n'
              '% s:% f\n % s:% f' %(self._objName, self._created,
                                 other._objName, other._created))
        return self._created > other._created
  
obj1 = ObjectCreationTime('obj1')
obj2 = ObjectCreationTime('obj2')
print(obj1 < obj2)
print(obj1 > obj2)


输出
Creation Time:
obj1:1590679265.753279
obj2:1590679265.753280
True
Creation Time:
obj1:1590679265.753279
obj2:1590679265.753280
False

二元运算符的魔术方法

让我们看看Python为二元运算符提供的3种神奇方法。

  • 香草法
  • 反向法
  • 就地方法

香草法

考虑一个表达式,x + y;在 vanilla 方法中,此表达式映射到 x.__add__(y)。

让我们考虑另一个表达式,y – x。在这里,表达式映射到 y.__sub__(x)。类似地,a * b 映射到 a.__mul__(b),a / b 映射到 a.__truediv__(b),依此类推。需要注意的一点,左侧对象的方法被调用,并将右侧对象作为参数传入。在 x + y 的情况下,调用 x 的 __add__ 方法并将 y 作为参数传递。让我们用一个例子来检验。

Python3

class Count(object):
    def __init__(self, count):
        self._count = count
    def __add__(self, other):
        total_count = self._count + other._count
        return Count(total_count)
    def __str__(self):
        return 'Count: % i' % self._count
  
  
  
c1 = Count(2)
c2 = Count(5)
c3 = c1 + c2
print(c3)


输出
Count: 7

反向法

在 Vanilla 方法中,左侧对象的方法在执行二元运算符时被调用。但是,如果左侧对象没有二元运算符映射的方法,则调用 reverse 方法;它检查要映射的右侧对象的方法。让我们看看下面的例子:

Python3

class Count(object):
    def __init__(self, count):
        self._count = count
  
    def __add__(self, other):
        total_count = self._count + other._count
        return Count(total_count)
  
    def __radd__(self, other):
        if other == 0:
            return self
        else:
            return self.__add__(other)
  
    def __str__(self):
        return 'Count:% i' % self._count
      
c2 = Count(2)
c3 = 0 + c2
print(c3)


输出
Count:2

由于 0 没有对应的 __add__ 方法, Python解释器会调用 __radd__ 方法 - c2.__radd__(0)。类似地,如果 __sub__ 方法没有定义,它会调用 __rsub __。

就地方法

计算和赋值操作都是在使用就地方法时执行的。映射到就地方法的一些运算符是 +=、-=、*= 等。就地方法名称以 i 开头。例如,语句 x += y 将映射到 x.__iadd__(y),依此类推。让我们看一下下面的例子:

Python3

class inPlace(object):
    def __init__(self, value):
        self._value = value
    def __iadd__(self, other):
        self._value = self._value + other._value
        return self._value
    def __str__(object):
        return self._value
  
inP1 = inPlace(5)
inP2 = inPlace(3)
inP1 += inP2
print(inP1)


输出
8

一元运算符的魔术方法

  • __pos__ 方法
  • __neg__ 方法
  • __invert__ 方法

__pos__ 方法

__pos__ 方法是使用 +运算符调用的。我们已经看到 +运算符也用作二元运算符。不用担心, Python解释器会根据情况知道使用哪一个 - 一元或二元。
__pos__ 方法接受一个位置参数——self——执行操作并返回结果。让我们通过一个例子来检验:

Python3

class unaryOp(object):
    def __init__(self, value):
        self._value = value
    def __pos__(self):
        print('__pos__ magic method')
        return(+self._value)
     
up = unaryOp(5)
print(+up)


输出
__pos__ magic method
5

__neg__ 方法

__neg__ 方法是使用 –运算符调用的。该运算符也用作二元运算符,但解释器会根据情况确定要映射的魔术方法。 __neg__ 魔术方法接受一个位置参数——self——,操作并返回结果。让我们检查以下示例:

Python3

class unaryOp(object):
    def __init__(self, value):
        self._value = value
    def __neg__(self):
        print('__neg__ magic method')
        return(-self._value)
  
up = unaryOp(5)
print(-up)


输出
__neg__ magic method
-5

__invert__ 方法

最后一个一元运算运算符是 __invert__ 方法,使用 ~运算符调用。语句 ~x 等价于 x.__invert__()。让我们考虑一个例子:

Python3

class invertClass(object):
    def __init__(self, value):
        self._value = value
    def __invert__(self):
        return self._value[::-1]
    def __str__(self):
        return self._value
  
invrt = invertClass('Hello, George')
invertedValue = ~invrt
print(invertedValue)


输出
egroeG, olleH

其他一些魔术方法

让我们讨论一些其他的魔术方法:

  • __len__ 方法
  • __repr__ 方法
  • __contains__ 方法

重载 __len__ 方法

len() 方法调用 __len__ 魔术方法。它接受一个位置参数并返回对象的长度。让我们看看下面的代码:

Python3

class RectangleClass(object):
    def __init__(self, area, breadth):
        self._area = area
        self._breadth = breadth
          
    def __len__(self):
        return int(self._area / self._breadth)
  
rc = RectangleClass(90, 5)
print(len(rc))


输出
18

__repr__ 方法的重要性

__repr__ 魔术方法有助于在Python交互式终端中表示对象。它需要一个位置参数——自我。
让我们看一下,在不重载 __repr__ 方法的情况下,如何在Python交互式终端中表示对象。

Python3

class RectangleClass(object):
    def __init__(self, area, breadth):
        self._area = area
        self._breadth = breadth
          
    def __len__(self):
        return int(self._area / self._breadth)
  
## use python interactive terminal to check object representation.
RectangleClass(90, 5)


输出
<__main__.RectangleClass object at 0x7f9ecaae9710>

我们可以看到,它返回了对象在内存中的地址,这并没有多大用处。

让我们看看如何重载 __repr__ 方法以返回有用的对象表示。

Python3

class RectangleClass(object):
    def __init__(self, area, breadth):
        self._area = area
        self._breadth = breadth
          
    def __len__(self):
        return int(self._area / self._breadth)
  
    def __repr__(self):
        """object representation"""
        return 'RectangleClass(area =% d, breadth =% d)' %\
               (self._area, self._breadth)
           
RectangleClass(90, 5)   
RectangleClass(80, 4)   


输出
RectangleClass(area=90, breadth=5)
RectangleClass(area=80, breadth=4)

__contains__ 魔法方法

__contains__ 方法在 'in' 表达式执行时被调用。它接受两个位置参数——self 和 item——如果 item 存在则返回 true,否则返回 false。让我们通过一个例子来检验:

Python3

import datetime
  
class DateClass(object):
    def __init__(self, startDate, endDate):
        self.startDate = startDate
        self.endDate = endDate
  
    def __contains__(self, item):
        """ check whether a date is between the given range and
        return true or false"""
        return self.startDate <= item <= self.endDate
  
dtObj = DateClass(datetime.date(2019, 1, 1), datetime.date(2021, 12, 31))
result = datetime.date(2020, 6, 4) in dtObj
print("Whether (2020, 6, 4) is within the mentioned date range? ", result)
  
result = datetime.date(2022, 8, 2) in dtObj
print("Whether (2022, 8, 2) is within the mentioned date range? ", result)


输出
Whether (2020, 6, 4) is within the mentioned date range?  True
Whether (2022, 8, 2) is within the mentioned date range?  False
概括

因此,我们可以得出结论,魔术方法是一种一致的数据模型,可以自定义类行为并增强可读性,而不会丢失其继承的特性。但是,在提供自定义功能之前,请确保是否需要自定义。