Python中的魔法方法(magic methods 或 special methods)-1

了解魔法方法是什么

在 Python 中,特殊方法也称为 magic methodsspecial methods。后一个术语是指 Python 用于命名其特殊方法和属性的特定命名约定。约定是在手头的名称中使用双前导和尾随下划线,因此它看起来像 .method()
双下划线将这些方法标记为某些 Python 功能的核心。它们有助于避免与您自己的方法和属性发生名称冲突。一些流行和众所周知的魔术方法包括:

Special Method 特殊方法描述
__init__()在 Python 类中提供初始化操作
__str__() and __repr__()为对象提供字符串表示形式
.__call__() 使类的实例可调用
__len__() 支持 len() 函数

这只是冰山一角。所有这些方法都支持 Python 及其面向对象的基础设施的核心特定功能。

控制对象创建过程

在 Python 中创建自定义类时,您实现的第一个也是最常见的方法可能是 .__init__()。此方法用作初始值设定项,因为它允许您为类中定义的任何实例属性提供初始值。

__new__() 特殊方法在类实例化过程中也有一个作用。此方法负责在调用类构造函数时创建给定类的新实例。不过,.__new__() 方法在实践中不太常实现。
在以下部分中,您将了解这两种方法如何工作的基础知识,以及如何使用它们来自定义类的实例化。

使用 .__init__() 初始化对象

每当调用给定类的构造函数时,Python 都会调用 .init() 方法。.__init__() 的目标是初始化类中的任何实例属性。请考虑以下 Point 类:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


point = Point(21, 42)
point.x

point.y

当您通过调用类构造函数 Point() 创建点实例时,Python 会在后台使用您传递给构造函数的相同参数自动调用 .__init__()。您不必自己调用 .__init__()。你只需要依赖 Python 的隐式行为。请注意 .x 和 .y 属性如何保存您在调用构造函数时传入的值。

使用 .__new__() 创建对象

当您调用类构造函数来创建类的新实例时,Python 会隐式调用 .__new__() 方法作为实例化过程的第一步。此方法负责创建并返回基础类的新空对象。然后,Python 将刚刚创建的对象传递给 .__init__() 进行初始化。
.__new__() 的默认实现对于大多数实际用例来说已经足够了。因此,在大多数情况下,您可能不需要编写 .__new__() 的自定义实现。
但是,您会发现此方法在某些高级使用案例中很有用。例如,您可以使用 .__new__() 创建不可变类型的子类,例如 int、float、tuple 和 str。请考虑以下从内置 float 数据类型继承的类示例:

 class Storage(float):
     def __new__(cls, value, unit):
         instance = super().__new__(cls, value)
         instance.unit = unit
         return instance

在此示例中,您将注意到 .__new__() 是一个类方法,因为它获取当前类 (cls) 而不是当前实例 (self) 作为参数。
然后,运行三个步骤。首先,通过内置的 super() 函数对 float 类调用 .__new__() 来创建当前类 cls 的新实例。此调用会创建一个新的 float 实例,并使用 value 作为参数对其进行初始化。然后,您可以通过动态附加 .unit 属性来自定义新实例。最后,您返回新实例以满足 .__new__() 的默认行为。

>>> disk = Storage(1024, "GB")

>>> disk
1024.0
>>> disk.unit
'GB'

>>> isinstance(disk, float)
True

您的 Storage 类按预期工作,允许您使用 instance 属性来存储要测量存储空间的单元。.unit 属性是变的,因此您可以随时更改其值。但是,您无法更改 disk 的数值,因为它与父类型 float 一样是不可变的。

将对象表示为字符串

您将在 Python 代码中执行的一项常见任务是显示数据或生成输出。在某些情况下,您的程序可能需要向用户显示有用的信息。在其他情况下,您可能需要向使用您的代码的其他程序员显示信息。
这两种类型的输出可能非常不同,因为它们各自的目标受众可能有不同的需求。用户的信息可能不需要突出显示实现详细信息。它可能只需要用户友好且清晰。
相反,面向程序员受众的信息可能需要在技术上是合理的,并提供代码实现的详细信息。
Python 可以涵盖这两种情况。如果要提供用户友好的输出,则可以使用 .__str__() 方法。另一方面,当您需要提供对开发人员友好的输出时,您可以使用 .__repr__() 方法。这些方法支持 Python 对象的两种不同的字符串表示形式。
.__init__() 一起,.__str__().__repr__() 方法可以说是自定义类中最常用的特殊方法。在以下部分中,您将了解这两种有用的方法。

使用 .str() 的用户友好字符串表示

.__str__() 特殊方法返回手头对象的人类可读字符串表示形式。Python 在调用内置 str() 函数时调用此方法,并将类的实例作为参数传递。
当您将实例用作 print() 和 format() 函数的参数时,Python 也会调用此方法。该方法旨在提供应用程序最终用户可理解的字符串。为了说明如何使用 .str() 方法,请考虑以下 Person 类:

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

    def __str__(self):
        return f"I'm {self.name}, and I'm {self.age} years old."

此类有两个属性:人员的姓名和年龄。在 .str() 方法中,您将返回 person 的用户友好字符串表示形式。此表示形式包括描述性消息中的 name 和 age。
下面是类的使用示例:

>>> from person import Person

>>> jane = Person("Jane Doe", 25)

>>> str(jane)
"I'm Jane Doe, and I'm 25 years old."

>>> print(jane)
I'm Jane Doe, and I'm 25 years old.

当你使用 Person 的实例作为 str() 或 print() 的参数时,你会在屏幕上得到对象的字符串表示。此表示形式是专门为应用程序或代码的用户设计的。

使用 .repr() 的开发人员友好型字符串表示

.__repr__() 方法返回面向开发人员的对象的字符串表示形式。理想情况下,返回的字符串的制作方式应使您可以从字符串构造对象的相同实例。
要深入了解 .__repr__() 的工作原理,请返回 Person 类并更新它,如下面的代码片段所示:

class Person:
    # ...

    def __repr__(self):
        return f"{type(self).__name__}(name='{self.name}', age={self.age})"

.__repr__() 中,使用 f-string 来构建对象的字符串表示形式。此表示的第一部分是类的名称。然后,使用一对括号将构造函数的参数及其相应的值括起来。
运行以下代码

from person import Person

john = Person("John Doe", 35)

john


repr(john)

在此示例中,您将创建一个 Person 实例。然后,您可以直接通过 REPL 访问实例。标准 REPL 自动使用 .__repr__() 方法来显示有关对象的即时反馈。

在最后一个示例中,您使用了内置的 repr() 函数,该函数归结为对您作为参数传入的对象调用 .__repr__()

请注意,您可以复制并粘贴生成的字符串表示形式(不带引号)以重现当前对象。这种字符串表示形式对于使用您的代码的其他开发人员来说非常方便。从这种表示形式中,他们可以获得有关当前对象如何构造的关键信息。

支持自定义类中的运算符重载

Python 有多种类型的运算符,这些运算符是特殊符号、符号组合或指定某种计算类型的关键字。在内部,Python 支持具有特殊方法的运算符。例如,如前所述,.__add__() 特殊方法支持加号运算符 (+)。

在实践中,您将利用运算符后面的这些方法来实现所谓的运算符重载。
注意:要更深入地了解运算符重载,请查看自定义 Python 类中的运算符和函数重载。

运算符重载是指为运算符提供额外的功能。您可以使用大多数内置类型及其特定的受支持运算符来执行此操作。但是,这并不是您可以使用支持 Python 运算符的特殊方法执行的全部操作。您还可以使用这些方法在自定义类中支持某些运算符。

在以下部分中,您将了解支持 Python 运算符的特定特殊方法,包括算术运算符、比较运算符、隶属度运算符、按位运算符和增强运算符。首先,您将从算术运算符开始,这可以说是最常用的运算符。

Arithmetic Operators 算术运算符
算术运算符是用于对数值执行算术运算的运算符。在大多数情况下,它们来自数学,因此,Python 用通常的数学符号来表示它们。

在下表中,您将找到 Python 中的算术运算符及其支持的 magic 方法的列表:

Operator 算子Supporting Method 支持方法
+.__add__(self, other)
-.__sub__(self, other)
*.__mul__(self, other)
/.__truediv__(self, other)
//.__floordiv__(self, other)
%.__mod__(self, other)
**.__pow__(self, other[, modulo])
请注意,所有这些方法都采用名为 other 的第二个参数。在大多数情况下,此参数应与 self 相同类型或兼容类型。如果不是这种情况,则可能会收到错误。

为了说明如何重载其中一些运算符,请返回到 Storage 类。假设您希望确保在添加或减去此类的两个实例时,两个操作数具有相同的单位:

class Storage(float):
    def __new__(cls, value, unit):
        instance = super().__new__(cls, value)
        instance.unit = unit
        return instance

    def __add__(self, other):
        if not isinstance(other, type(self)):
            raise TypeError(
                "unsupported operand for +: "
                f"'{type(self).__name__}' and '{type(other).__name__}'"
            )
        if not self.unit == other.unit:
            raise TypeError(
                f"incompatible units: '{self.unit}' and '{other.unit}'"
            )

        return type(self)(super().__add__(other), self.unit)

.__add__() 方法中,首先检查 other 是否也是 Storage 的实例。如果没有,则引发 TypeError 异常,并显示相应的错误消息。接下来,检查两个对象是否具有相同的单位。如果没有,则再次引发 TypeError 。如果两项检查都通过,则返回通过添加两个值并附加单位创建的 Storage 新实例。
以下是此类的使用:

>>> from storage import Storage

>>> disk_1 = Storage(500, "GB")
>>> disk_2 = Storage(1000, "GB")
>>> disk_3 = Storage(1, "TB")

>>> disk_1 + disk_2
1500.0

>>> disk_2 + disk_3
Traceback (most recent call last):
    ...
TypeError: incompatible units: 'GB' and 'TB'

>>> disk_1 + 100
Traceback (most recent call last):
    ...
TypeError: unsupported operand for +: 'Storage' and 'int'

在此示例中,当您添加两个具有相同单位的对象时,您将获得正确的值。如果添加两个具有不同单位的对象,则会收到 TypeError ,告知您单位不兼容。最后,当您尝试添加具有内置数值的 Storage 实例时,您也会收到错误,因为内置类型不是 Storage 实例。
作为练习,您是否希望在 Storage 类中实现 .__sub__() 方法?查看下面的可折叠部分以获取可能的实现:

class Storage(float):
    def __new__(cls, value, unit):
        instance = super().__new__(cls, value)
        instance.unit = unit
        return instance

    def __add__(self, other):
        if not isinstance(other, type(self)):
            raise TypeError(
                "unsupported operand for +: "
                f"'{type(self).__name__}' and '{type(other).__name__}'"
            )
        if not self.unit == other.unit:
            raise TypeError(
                f"incompatible units: '{self.unit}' and '{other.unit}'"
            )

        return type(self)(super().__add__(other), self.unit)

    def __sub__(self, other):
        if not isinstance(other, type(self)):
            raise TypeError(
                "unsupported operand for -: "
                f"'{type(self).__name__}' and '{type(other).__name__}'"
            )
        if not self.unit == other.unit:
            raise TypeError(
                f"incompatible units: '{self.unit}' and '{other.unit}'"
            )

        return type(self)(super().__sub__(other), self.unit)

请注意,.__add__() 和 .__sub__() 有几行共同的行,这会导致代码重复。您可以通过使用帮助程序方法提取通用逻辑来解决此问题。
在上面的示例中,您在内置 float 类型上重载了加法运算符 (+)。您还可以使用 .__add__() 方法和任何其他运算符方法来支持自定义类中的相应运算符。
以下 Distance 类的示例:

class Distance:
    _multiples = {
        "mm": 0.001,
        "cm": 0.01,
        "m": 1,
        "km": 1_000,
    }

    def __init__(self, value, unit="m"):
        self.value = value
        self.unit = unit.lower()

    def to_meter(self):
        return self.value * type(self)._multiples[self.unit]

    def __add__(self, other):
        return self._compute(other, "+")

    def __sub__(self, other):
        return self._compute(other, "-")

    def _compute(self, other, operator):
        operation = eval(f"{self.to_meter()} {operator} {other.to_meter()}")
        cls = type(self)
        return cls(operation / cls._multiples[self.unit], self.unit)

在 Distance 中,您有一个名为 ._multiples 的非公共类属性。此属性包含长度单位及其相应转换因子的字典。在 .__init__() 方法中,您可以根据用户的定义初始化 .value 和 .unit 属性。请注意,您已使用 str.lower() 方法规范化单位字符串并强制使用小写字母。在 .to_meter() 中,使用 ._multiples 表示以米为单位的当前距离。您将使用此方法作为 .__add__() 和 .__sub__() 方法中的帮助程序。这样,您的类将能够正确地增加或减少不同单位的距离。
.__add__().__sub__() 方法分别支持加法和减法运算符。在这两种方法中,都使用 ._compute() 帮助程序方法来运行计算。在 ._compute() 中,你将 other 和 operator 作为参数。然后,使用内置的 eval() 函数计算表达式并获得当前操作的预期结果。接下来,使用内置的 type() 函数创建 Distance 的实例。最后,返回类的新实例,其中计算的值转换为当前实例的单位。
注意:内置的 eval() 函数暗示了一些安全风险,在代码中使用此函数时应注意这些风险。
以下是 Distance 类在实践中的工作原理:

>>> from distance import Distance

>>> distance_1 = Distance(200, "m")
>>> distance_2 = Distance(1, "km")

>>> total = distance_1 + distance_2
>>> total.value
1200.0
>>> total.unit
'm'

>>> displacement = distance_2 - distance_1
>>> displacement.value
0.8
>>> displacement.unit
'km'

.__add__() 为例。Python 在左侧操作数上调用此方法。如果该操作数未实现该方法,则操作将失败。如果左侧操作数实现该方法,但其实现的行为不符合您的预期,则可能会遇到问题。值得庆幸的是,Python 拥有右侧版本的运算符方法 (.__r*__()) 来解决这些问题。

例如,假设您要编写一个 Number 类,该类支持在其对象与整数或浮点数之间进行加法。在这种情况下,您可以执行如下操作:

class Number:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        print("__add__ called")
        if isinstance(other, Number):
            return Number(self.value + other.value)
        elif isinstance(other, int | float):
            return Number(self.value + other)
        else:
            raise TypeError("unsupported operand type for +")

    def __radd__(self, other):
        print("__radd__ called")
        return self.__add__(other)

当您在加法表达式中使用 Number 的实例作为左侧运算符时,.add() 方法有效。相反,当你使用 Number 的实例作为右侧操作数时,.radd() 有效。请注意,方法名称开头的 r 代表 right。

以下是 Number 类的工作原理:

>>> from number import Number

>>> five = Number(5)
>>> ten = Number(10)

>>> fifteen = five + ten
__add__ called
>>> fifteen.value
15

>>> six = five + 1
__add__ called
>>> six.value
6

>>> twelve = 2 + ten
__radd__ called
__add__ called
>>> twelve.value
12

在此代码段中,您将创建两个 Number 实例。然后,在 Python 按预期调用 .__add__() 的附加功能中使用它们。接下来,在表达式中使用 5 作为左侧运算符,该表达式将类与 int 类型混合在一起。在这种情况下,Python 会再次调用 Number.__add__()。

最后,在加法中使用 ten 作为右侧操作数。这一次,Python 隐式调用 Number.__radd__(),这又回退到调用 Number.__add__()。
以下是 .__r*__() 方法的摘要:

Operator 算子Right-Hand Method 右手法
+.__radd__(self, other)
-.__rsub__(self, other)
*.__rmul__(self, other)
/.__rtruediv__(self, other)
//.__rfloordiv__(self, other)
%.__rmod__(self, other)
**.__rpow__(self, other[, modulo])

当左侧操作数不支持相应的操作并且操作数的类型不同时,Python 会调用这些方法。

Python 也有一些一元运算符。它称它们为一元,因为它们在单个操作数上操作:

算子支持方法描述
-.__neg__(self) .返回带有相反符号的目标值
+.__pos__(self) 提供对否定的补充,而不执行任何转换

您还可以在 Python 的内置数值类型中重载这些运算符,并在您自己的类中支持它们。

比较运算符方法

您还会发现比较运算符后面有特殊方法。例如,当您执行 5 < 2 之类的操作时,Python 会调用 .__lt__() 魔术方法。以下是所有比较运算符及其支持方法的摘要:

算子支持方法
<.__lt__(self, other)
<= .__le__(self, other)
== .__eq__(self, other)
!= .__ne__(self, other)
>= .__ge__(self, other)
>.__gt__(self, other)
为了举例说明如何在自定义类中使用其中一些方法,假设您正在编写一个 Rectangle 类来创建多个不同的矩形。您希望能够比较这些矩形。下面是一个支持相等 (==)、小于 (<) 和大于 (>) 运算符的 Rectangle 类:
class Rectangle:
    def __init__(self, height, width):
        self.height = height
        self.width = width

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

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

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

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

您的类有一个 .area() 方法,该方法使用矩形的高度和宽度计算矩形的面积。然后,您有 3 种必需的特殊方法来支持预期的比较运算符。请注意,它们都使用矩形的面积来确定最终结果。

以下是您的类在实践中的工作原理

>>> from rectangle import Rectangle

>>> basketball_court = Rectangle(15, 28)
>>> soccer_field = Rectangle(75, 110)

>>> basketball_court < soccer_field
True
>>> basketball_court > soccer_field
False
>>> basketball_court == soccer_field
False

成员资格运算符

Python 有两个运算符,可用于确定给定值是否在值集合中。运算符在 in 和 not in。他们支持称为成员资格测试的检查。
例如,假设您要确定某个数字是否出现在数字列表中。您可以执行如下操作:

>>> 2 in [2, 3, 5, 9, 7]
True

>>> 10 in [2, 3, 5, 9, 7]
False

在第一个示例中,数字 2位于数字列表中,因此您得到 True。在第二个示例中,数字 10 不在列表中,您得到 False。not in 运算符的工作方式类似于 in,但是一种否定。使用此运算符,您可以了解给定值是否不在集合中。
支持成员身份运算符 in 和 not in 的特殊方法是 .__contains__()。通过在自定义类中实现此方法,您可以控制其实例在成员身份测试中的行为方式。.__contains__() 方法必须采用表示要检查的值的参数。
为了说明如何在自定义类中支持成员资格测试,假设您要实现一个基本的堆栈数据结构,该结构支持通常的 push 和 pop 操作以及 in 和 not in 运算符。
下面是 Stack 类的代码:

class Stack:
    def __init__(self, items=None):
        self.items = list(items) if items is not None else []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def __contains__(self, item):
        for current_item in self.items:
            if item == current_item:
                return True
        return False

在此示例中,您将定义 Stack 并提供 .push() 和 .pop() 方法。前一种方法将新项追加到堆栈顶部,而后者则删除并返回堆栈顶部的项。然后,您实现 .__contains__() 方法,该方法将目标项作为参数,并使用 for 循环来确定该项是否在存储堆栈数据的列表中。注意:你可以通过利用 in 运算符本身来使 .__contains__() 的上述实现更加简洁。因为 Python 列表默认支持成员资格测试。

例如,您可以执行如下操作:

class Stack:
    # ...

    def __contains__(self, item):
        return item in self.items

在此版本的 .__contains__() 中,使用 in 运算符而不是 for 循环。因此,您直接检查成员资格,而无需显式迭代数据。
下面介绍如何在成员身份测试中使用 Stack 的实例:

>>> from stack import Stack

>>> stack = Stack([2, 3, 5, 9, 7])

>>> 2 in stack
True
>>> 10 in stack
False

>>> 2 not in stack
False
>>> 10 not in stack
True

通过在自定义类中实现 .__contains__() 方法,您可以使用 in 和 not in 运算符自定义这些类的对象如何响应成员资格检查。

按位运算符(Bitwise Operators)

Python 的按位运算符允许您在最精细的级别上操作单个数据位。使用这些运算符,您可以执行按位 AND、OR、XOR、NOT 和移位运算。这些运算符也是通过特殊方法实现的。
以下是 Python 的按位运算符及其支持方法的摘要:

Operator 算子支持方法
&.__and__(self, other)
1 .__or__(self, other)
^__xor__(self, other)
<< .__lshift__(self, other)
>>.__rshift__(self, other)
~.__invert__() .__invert__()

使用这些方法,您可以使您的自定义类支持按位运算符。请考虑以下示例:

class BitwiseNumber:
    def __init__(self, value):
        self.value = value

    def __and__(self, other):
        return type(self)(self.value & other.value)

    def __or__(self, other):
        return type(self)(self.value | other.value)

    def __xor__(self, other):
        return type(self)(self.value ^ other.value)

    def __invert__(self):
        return type(self)(~self.value)

    def __lshift__(self, places):
        return type(self)(self.value << places)

    def __rshift__(self, places):
        return type(self)(self.value >> places)

    def __repr__(self):
        return bin(self.value)

要在自定义类 BitwiseNumber 中实现所需的方法,请使用与手头方法对应的按位运算符。.__and__()、.__or__() 和 .__xor__() 方法适用于当前 BitwiseNumber 实例的值和其他实例。第二个操作数应该是 BitwiseNumber 的另一个实例。

.__invert__() 方法支持按位 NOT 运算符,该运算符是一元运算符,因为它作用于单个操作数。因此,此方法不需要第二个参数。

最后,支持按位移位运算符的两种方法采用名为 places 的参数。此参数表示您希望将位向任一方向移动的位数。
以下是您的类在实践中的工作原理:

>>> from bitwise_number import BitwiseNumber

>>> five = BitwiseNumber(5)
>>> ten = BitwiseNumber(10)

>>> # Bitwise AND
>>> #    0b101
>>> # & 0b1010
>>> # --------
>>> #      0b0
>>> five & ten
0b0

>>> # Bitwise OR
>>> #    0b101
>>> # | 0b1010
>>> # --------
>>> #   0b1111
>>> five | ten
0b1111

>>> five ^ ten
0b1111
>>> ~five
-0b110
>>> five << 2
0b10100
>>> ten >> 1
0b101

自增

当涉及到算术运算时,你会发现一个常见的表达式,它使用变量的当前值来更新变量本身。此操作的一个典型示例是当您需要更新 counter 或 accumulator 时:

>>> counter = 0
>>> counter = counter + 1
>>> counter = counter + 1
>>> counter
2

此代码片段中的第二行和第三行使用前一个值更新计数器的值。这种类型的操作在编程中非常常见,以至于 Python 有一个快捷方式,称为自增赋值运算符。例如,您可以使用 augmented assignment 运算符缩短上面的代码以进行加法:

>>> counter = 0
>>> counter += 1
>>> counter += 1
>>> counter
2

此代码看起来更简洁,并且更 Pythonic。它也更优雅。这个运算符 (+=) 并不是 Python 支持的唯一运算符。以下是 math 上下文中支持的运算符的摘要:

算子支持方法
+= .__iadd__(self, other)
-= .__isub__(self, other)
*= .__imul__(self, other)
/=.__itruediv__(self, other)
//==.__ifloordiv__(self, other)
%=.__imod__(self, other)
**= .__ipow__(self, other[, modulo])
&=.__iand__(self, other)
!= .__ior__(self, other)
^= .__ixor__(self, other)
<<=.__ilshift__(self, other)
>>=.__irshift__(self, other)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值