Python程序设计 魔法函数

1.魔法方法

Python中有一些特殊方法,它们允许我们的类和Python更好地集成。在标准库参考(Standard Library Reference)中,它们被称为魔法方法(Magic Methods),是与Python的其他特性无缝集成的基础。

例如,我们用字符串来表示一个对象的值。Object 基类包含了__repr__() 和__str__() 的默认实现,它们提供了一个对象的字符串描述。遗憾的是,这些默认的实现不够详细。我们几乎总会想重写它们中的一个或两个。我们还会介绍__format__() ,它更加复杂一些,但是和上面两个方法的作用相同。

我们还会介绍其他的转换方法,尤其是__hash__() 、__bool__() 和__bytes__() 。这些方法可以把一个对象转换成一个数字、一个布尔值或者一串字节。例如,当我们实现了__bool__() ,我们就可以像下面这样在if语句中使用我们的对象:if someobject: 。

接下来,我们会介绍实现了比较运算符的几个特殊方法:__lt__() 、__le__() 、__eq__() 、__ne__() 、__gt__() 和__ge__() 。

当我们定义一个类时,几乎总是需要使用这些基本的特殊方法。

我们会在最后介绍__new__() 和__del__() ,因为它们的使用更加复杂,而且相比于其他的特殊方法,我们并不会经常使用它们。

我们会详细地介绍如何用这些特殊方法来扩展一个简单类。我们需要了解从object 继承而来的默认行为,这样,我们才能理解应该在什么时候使用重写,以及如何使用它。

说白了就是对象的 我是谁,我从哪里来,到哪里去

1.1__init__()

所有类的超类object,有一个默认包含pass的__init__()实现,这个函数会在对象初始化的时候调用,我们可以选择实现,也可以选择不实现,一般建议是实现的,不实现对象属性就不会被初始化,虽然我们仍然可以对其进行赋值,但是它已经成了隐式的了,编程时显示远比隐式的更好

1.2__new__()

__new__() 方法作为元类型的一部分,主要是为了控制如何创建一个类。这和之前的如何用 __new__() 控制一个不可变对象是完全不同的。

一个元类型创建一个类。一旦类对象被创建,我们就可以用这个类对象创建不同的实例。所有类的元类型都是type ,type() 函数被用来创建类对象。

另外,type() 函数还可以被用作显示当前对象类型。

下面是一个很简单的例子,直接使用 type() 作为构造器创建了一个新的但是几乎完全没有任何用处的类:

Useless= type("Useless",(),{})

一旦我们创建了这个类,我们就可以开始创建这个类的对象。但是,这些对象什么都做不了,因为我们没有定义任何方法和属性。

为了最大化利用这个类,在下面的例子中,我们使用这个新创建的Useless 类来创建对象。

>>> Useless()
<__main__.Useless object at 0x101001910>
>>> u=_
>>> u.attr= 1 
>>> dir(u)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
'__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', 
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', 
'__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'attr']

我们可以向这个类的对象中增加属性。至少,作为一个对象,它工作得很好。

这样的类定义与使用types.SimpleNamespace 或者像下面这样定义一个类的方式几乎相同。

class Useless:
   pass

1.3__repr__()

__repr__() 和 __str__() 方法

对于一个对象,Python提供了两种字符串表示。它们和内建函数repr() 、str() 、print() 及string.format() 的功能是一致的。

  • 通常,str() 方法表示的对象对用户更加友好。这个方法是由对象的__str__ 方法实现的。

  • repr() 方法的表示通常会更加技术化

这个方法是由__repr__() 方法实现的。

  • print() 函数会调用str() 来生成要输出的对象。
  • 字符串的format() 函数也可以使用这些方法。当我们使用{!r} 或者{!s} 格式时,我们实际上分别调用了__repr__() 或者__str__() 方法。

下面我们先来看一下这些方法的默认实现。

直接打印对象的实现方法,__str__ 是被print函数调用的,一般都是return一个什么东西,这个东西应该是以字符串的形式表现的。如果不是要用str()函数转换,我们可以直接print的对象都是实现了`__str__这个方法的,比如dict。看下面的例子。

下面是一个很简单的类。

class RectAngle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def __str__(self):
        return f'<RectAngle: {self.width}(w) * {self.height}(h)>'

我们定义了两个简单类,每个类包含4个属性。

下面是在命令行中使用RectAngle 类的结果。

>>> rect = RectAngle(4, 5)
>>> print(rect)
<RectAngle: 4(w) * 5(h)>
>>> rect
<__main__.RectAngle object at 0x000002A1547E5A58>

可以看到,__str__() print 方法打印的内容就看起来更加输入,但是在命令行里面里面的调试信息还是现实的为对象,当增加 __repr__() 方法之后,调试信息也会变得更加清楚

def __repr__(self):
    return f'<RectAngle: w{self.width} h{self.height}>'

在以下两种情况下,我们可以考虑重写__str__() 和__repr__() 。

  • 非集合对象 :一个不包括任何其他集合对象的“简单”对象,这类对象的格式化通常不会特别复杂。
  • 集合对象 :一个包含集合的对象,这类对象的格式化会更为复杂。

1.4__bool__()

Python中有很多关于真假性的定义。参考手册中列举了许多和False 等价的值,包括False 、0 、'' 、() 、[] 和{} 。其他大部分的对象都和True 等价。

通常,我们会用下面的语句来测试一个对象是否“非空”。

if some_object:
   process( some_object )

默认情况下,这个是内置的bool() 函数的逻辑。这个函数依赖于一个给定对象的__bool__() 方法。

默认的__bool__() 方法返回True 。我们可以通过下面的代码来验证这一点。

>>> x = object()
>>> bool(x)
True

对大多数类来说,这是完全正确的。大多数对象都不应该和False 等价。但是,对于集合,这样的行为并不总是正确的。一个空集合应该和False 等价,而一个非空集合应该返回True 。或许,应该给我们的Deck 集合对象增加一个类似的方法。

如果我们在封装一个列表,我们可能会写下面这样的代码。

    def __bool__(self):
        return bool(self.area)

这段代码将__bool__() 的计算委托给了内部的函数area 。

现在,我们就可以像下面这样使用Deck 。

r = RectAngle(4, 5)

if r:
    print(r)

2.比较运算符方法

Python有6个比较运算符。这些运算符分别对应一个特殊方法的实现。根据文档,运算符和特殊方法的对应关系如下所示。

  • x > y 调用x.__gt__(y) 。
  • x >= y 调用x.__ge__(y) 。

  • x < y 调用x.__lt__(y) 。

  • x <=y 调用x.__le__(y) 。
  • x == y 调用x.__eq__(y) 。
  • x != y 调用x.__ne__(y) 。

对于实际上使用了哪个比较运算符,还有一条规则。这些规则依赖于作为左操作数的对象定义需要的特殊方法。如果这个对象没有定义,Python会尝试改变运算顺序。

下面是两条基本的规则: 首先,运算符的实现基于左操作数:A < B相当于A.__lt__(B) 。 其次,相反的运算符的实现基于右操作数:A < B相当于B.__gt__(A) 。 如果右操作数是左操作数的一个子类,那这样的比较基本不会有什么异常发生;同时,Python会首先检测右操作数,以确保这个子类可以重载基类。

下面,我们通过一个例子看看这两条规则是如何工作的,我们定义了一个只包含其中一个运算符实现的类,然后把这个类用于另外一种操作。

下面是我们使用类中的一段代码。

class RectAngle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def __gt__(self, other):
        # other 是另外一个对象
        return self.area() < self.area()

这段代码基于 RectAngle(矩形) 的比较规则,主要是对比三角形的大小。

我们省略了比较方法,看看当缺少比较运算符时,Python将如何回退。这个类允许我们进行<比较。但是有趣的是,通过改变操作数的顺序,Python也可以使用这个类进行>比较。换句话说,x <y 和y >x 是等价的。这遵从了镜像反射法则;

当我们试图评估不同的比较运算时就会看到这种现象。下面,我们创建两个RectAngle 类,然后用不同的方式比较它们。

>>> r1 = RectAngle(4, 5)
>>> r2 = RectAngle(5, 6)
>>> r1 > r2
True
>>> r1 < r2
False
>>> r1 == r2
False

从代码中,我们可以看到,r1 < r2 调用了r1.__lt__(three) 。

但是,对于r1 > three ,由于没有定义__gt__() ,Python使用r2.__lt__(two) 作为备用的比较方法。

默认情况下,__eq__() 方法从object 继承而来,它比较不同对象的ID值。当我们用于==或!=比较对象时,结果如下。

>>> r1_2 = RectAngle(4, 5)
>>> r1_2 == r1
False

可以看到,结果和我们预期的不同。所以,我们通常都会需要重载默认的__eq__() 实现。

此外,逻辑上,不同的运算符之间是没有联系的。但是从数学的角度来看,我们可以基于两个运算符完成所有必需的比较运算。Python没有实现这种机制。相反,Python默认认为下面的4组比较是等价的。

x < y ≡ y > x

x ≤ y ≡ y ≥ x

x = y ≡ y = x

x ≠ y ≡ y ≠ x

这意味着,我们必须至少提供每组中的一个运算符。例如,我可以提供__eq__() 、__ne__() 、__lt__() 和__le__() 的实现。

2.1比较运算

当设计比较运算符时,要考虑两个因素。

  • 如何比较同一个类的两个对象。
  • 如何比较不同类的对象。

对于一个有许多属性的类,当我们研究它的比较运算符时,通常会觉得有很明显的歧义。或许这些比较运算符的行为和我们的预期不完全相同。

2.2同类比较

下面我们通过一个更完整的RectAngle 类来看一下简单的同类比较。

# encoding:utf-8
class RectAngle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def __lt__(self, other):
        return self.area() > other.area()

    def __le__(self, other):
        return self.area() <= other.area()

    def __gt__(self, other):
        # other 是另外一个对象
        return self.area() < other.area()

    def __ge__(self, other):
        return self.area() >= other.area()

    def __eq__(self, other):
        return self.area() == other.area()

    def __ne__(self, other):
        return self.area() != other.area()


r1 = RectAngle(4, 5)
r2 = RectAngle(5, 6)

print(r1 > r2)
print(r1 < r2)

现在我们定义了6个比较运算符。

我们也没有给出类内比较的代码,这个我们会在下一个部分中详细讲解。用上面定义的这个类,我们可以成功地比较不同的牌。下面是一个创建并比较3张牌的例子。

>>> r1 = RectAngle(4, 5)
>>> r2 = RectAngle(5, 6)
>>> r3 = RectAngle(4, 6)

用上面定义的RectAngle 类,我们可以进行像下面这样的一系列比较。

>>> r1 == r2
False
>>> r1.width == r3.width
True
>>> r1 < r3
False

这个类的行为与我们预期的一致。

2.3不同类比较

我们会继续以RectAngle 类为例来看看当两个比较运算中的两个操作数属于不同的类时会发生什么。新增一个三角形对象

class Triangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height / 2

    def __lt__(self, other):
        return self.area() > other.area()

    def __eq__(self, other):
        return self.area() == other.area()

下面我们将一个RectAngle 实例和一个Triangle 值进行比较。

>>> r1 = RectAngle(4, 5)
>>> t1 = Triangle(4, 5)
>>> r1 == t1
>>> r1 + t1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'RectAngle' and 'Triangle'
>>>

可以看到,这和我们预期的行为一致,RectAngle 的子类Triangle 没有实现必需的特殊方法,所以产生了一个TypeError 异常。

3.__call__()

该方法的功能类似于在类中重载 () 运算符,使得类实例对象可以像调用普通函数那样,以“对象名()”的形式使用。

class Example:

    def __call__(self, *args, **kwargs):
        return "hello world !"


e = Example()

print(e)
print(e())

4.__del__() 方法

__del__() 方法有一个让人费解的使用场景。

这个方法的目的是在将一个对象从内存中清除之前,可以有机会做一些清理工作。对于Python的垃圾回收机制而言,创建一个上下文比使用 __del__() 更加容易预判。

但是,如果一个Python对象包含了一些操作系统的资源,__del__() 方法是把资源从程序中释放的最后机会。例如,引用了一个打开的文件、安装好的设备或者子进程的对象,如果我们将资源释放作为__del__() 方法的一部分实现,那么我们就可以保证这些资源最后会被释放。

很难预测什么时候 __del__() 方法会被调用。它并不总是在使用del 语句删除对象时被调用,当一个对象因为命名空间被移除而被删除时,它也不一定被调用。Python文档中用不稳定来描述 __del__() 方法的这种行为,并且提供了额外的关于异常处理的注释:运行期的异常会被忽略,相对地,会使用 sys.stderr 打印一个警告。

基于上面的这些原因,通常更倾向于使用上下文管理器,而不是实现 __del__() 。

4.1 引用计数和对象销毁

CPython的实现中,对象会包括一个引用计数器。当对象被赋值给一个变量时,这个计数器会递增;当变量被删除时,这个计数器会递减。当引用计数器的值为0时,表示我们的程序不再需要这个对象并且可以销毁这个对象。对于简单对象,当执行删除对象的操作时会调用 __del__() 方法。

对于包含循环引用的复杂对象,引用计数器有可能永远也不会归零,这样就很难让 __del__() 被调用。

我们用下面的一个类来看看这个过程中到底发生了什么。

class RectAngle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def __del__(self):
        print("被删除 {0}".format(id(self)))

我们可以像下面这样创建和删除这个对象。

>>> p = RectAngle(4, 5)
>>> del p
被删除 2643407508256

我们先创建,然后删除了Noisy 对象,几乎是立刻就看到了__del__() 方法中输出的消息。这也就是说当变量x 被删除后,引用计数器正确地归零了。一旦变量被删除,就没有任何地方引用Noisy 实例,所以它也可以被清除。

下面是浅复制中一种常见的情形。

>>> ln = [RectAngle(4, 5), RectAngle(4, 6)]
>>> ln2= ln.copy()
>>> del ln
>>>

Python没有响应del 语句。这说明这些Noisy 对象的引用计数器还没有归零,肯定还有其他地方引用了它们,下面的代码验证了这一点。

>>> del ln2
被删除 2643407508592
被删除 2643407508480

4.2 循环引用和垃圾回收

下面是一种常见的循环引用的情形。一个父类包含一个子类的集合,同时集合中的每个子类实例又包含父类的引用。

下面我们用这两个类来看看循环引用。

class Parent:
    def __init__(self, *children):
        self.children = list(children)
        for child in self.children:
            child.parent = self

    def __del__(self):
        print("删除 {} {}".format(self.__class__.__name__, id(self)))


class Child:
    def __del__(self):
        print("删除 {} {}".format(self.__class__.__name__, id(self)))

一个Parent 的instance 包括一个children 的列表。

每一个Child 的实例都有一个指向Parent 类的引用。当向Parent 内部的集合中插入新的Child 实例时,这个引用就会被创建。

我们故意把这两个类写得比较复杂,所以下面让我们看看当试图删除对象时,会发生什么。

>>> p = Parent(Child(), Child())
>>> id(p)
2643407508984
>>> del p
>>>

Parent 和它的两个初始Child 实例都不能被删除,因为它们之间互相引用。

下面,我们创建一个没有Child 集合的Parent 实例。

>>> p= Parent()
>>> id(p)
2643407509096
>>> del p
删除 Parent 2643407509096
>>>

和我们预期的一样,这个Parent 实例成功地被删除了。

许多基本的特殊方法,它们是我们在设计任何类时的基本特性。这些方法已经包含在每个类中,只是它们的默认行为不一定能满足我们的需求。

我们几乎总是需要重载__repr__() 、__str__() 。这些方法的默认实现不是非常有用。

我们几乎不需要重载__bool__() 方法,除非我们想自定义集合。这是第6章“创建容器和集合”的主题。

我们常常需要重载比较运算符。默认的实现只适合于比较简单不可变对象,但是不适用于比较可变对象。我们不一定要重写所有的比较运算符

另外两个较为特殊的方法__new__() 和__del__() 有更特殊的用途。大多数情况下,使用__new__() 来扩展不可变类型。

基本的特殊方法和__init__() 方法几乎会出现在我们定义的所有类中。其他的特殊方法则有更特殊的用途,它们分为6个不同的类别。

  • 属性访问 :这些特殊方法实现的是表达式中object.attribute 的部分,它通常用在一个赋值语句的左操作数以及del 语句中。
  • 可调用对象 :一个实现了将函数作为参数的特殊方法,很像内置的len() 函数。
  • 集合 :这些特殊方法实现了集合的很多特性,包括sequence[index] 、mapping[index] 和set | set 。
  • 数字 :这些特殊方法提供了算术运算符和比较运算符。我们可以用这些方法扩展Python支持的数值类型。
  • 上下文 :有两个特殊方法被我们用来实现可以和with 语句一起使用的上下文管理器。
  • 迭代器 :有一些特殊方法定义了一个迭代器。没有必要一定要使用这些方法,因为生成器函数很好地实现了这种特性。但是,我们可以了解如何实现自定义的迭代器。

5. 重要的魔法方法

下面的表格列出了 Python 3中的重要魔法方法。

初始化和构建描述
__new__(mcs, other)在对象的实例化中被调用
__init__(self, other)使用 __new__ 调用
__del__(self)删除方法
一元运算符和函数描述
__pos__(self)+(正数)
__neg__(self)- (负数)
__abs__(self)abs() 函数
__invert__(self)~ 操作符
__round__(self,n)round() 取整
__floor__(self)math.floor() 函数
__ceil__(self)math.ceil() 函数
__trunc__(self)math.trunc() 函数
增强赋值描述   
__iadd__(self, other)+=
__isub__(self, other)-=
__imul__(self, other)*=
__ifloordiv__(self, other)//=
__idiv__(self, other)a /=b
__itruediv__(self, other)
__imod__(self, other)%=
__ipow__(self, other)**=
__ilshift__(self, other)<<=
__irshift__(self, other)>>=
__iand__(self, other)&=
__ior__(self, other)|=
__ixor__(self, other)^=
类型转化魔法方法描述
__int__(self)int()方法调用来将类型转换为
int__float__(self)float()方法调用以将类型转换为
float__complex__(self)Tcomplex() 方法调用以将类型转换为 complex
__oct__(self)oct() 方法调用以将类型转换为八进制
__hex__(self)hex() 方法调用以将类型转换为十六进制
__index__(self)在切片中自动调用
__trunc__(self)math.trunc ()方法调用
字符串魔法方法         描述
__str__(self)str()方法调用以返回类型的字符串表示形式
__repr__(self)repr()方法调用,以返回类型的机器可读表示形式
__unicode__(self)unicode() 方法调用以返回类型的 unicode 字符串
__format__(self, formatstr)string.format() 方法调用,以返回一个新样式的字符串
__hash__(self)hash() 方法调用以返回整数
__nonzero__(self)bool() 方法调用以返回 True 或 False
__dir__(self)dir() 方法调用以返回类的属性列表
__sizeof__(self)sys.getsizeof()方法调用,以返回对象的大小
属性操作魔法方法描述
__getattr__(self, name)当类的访问属性不存在时调用
__setattr__(self, name, value)在为类的属性赋值时调用
__delattr__(self, name)在删除类的属性时调用
运算魔法方法描述
__add__(self, other)使用 + 运算符在比较时调用
__sub__(self, other)使用 - 运算符在比较时调用
__mul__(self, other)使用 * 运算符在比较时调用
__floordiv__(self, other)使用 // 运算符在比较时调用
__divmod__(self, other)使用 / 运算符在比较时调用
__mod__(self, other)使用 % 运算符在比较时调用
__pow__(self, other[, modulo])使用 ** 运算符在比较时调用
__lt__(self, other)使用 < 运算符在比较时调用
__le__(self, other)使用 <= 运算符在比较时调用
__eq__(self, other)使用 == 运算符在比较时调用
__ne__(self, other)使用 != 运算符在比较时调用
__ge__(self, other)使用 >= 运算符在比较时调用

因此,您可以使用适当的魔法方法在自定义类中添加各种功能。

  • 42
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值