跳至主要内容

走近 Python 魔术方法

了解什么是魔术方法,并区分方法与函数之间的差异。
· 11分钟 阅读

使用 AI 探索

Python 之所以强大,其中一个促成因素就是魔术方法。在某些 Pythonista 圈子里,您也会听到它们被称为“dunder 方法”,源于它们由双下划线构成。 

它们在 Python 中的作用是执行一种称为“运算符重载”的过程,简单来说,就是为预定义运算符赋予扩展含义。换言之,运算符重载用于为我们的类添加自定义行为,从而使其能够配合 Python 的运算符和内置功能使用。  

在本文中,我们将更深入地探讨 Python 魔术方法: 

  • 区分方法与函数的差异 
  • 解释什么是魔术方法
  • 给出一些 Python 示例。 

查看这个DataLab 工作簿,跟着本教程中的代码一起练习。 

Python 方法与函数

在讨论魔术方法之前,我们需要先理解 Python 中方法和函数的区别。 

Python 使用方法来表示对象的行为。因此,方法与某个对象相关联,并会对对象中的数据执行某些操作。另一个关键的区分点是方法的第一个参数必须是 self 参数。 

注意:第一个参数不一定非要叫 self,但 Python 开发者普遍认为这样做是最佳实践。 

# Example of a method
class Human:
    # This is a method
    def walk(self):
        print("I am walking") 

方法依赖于它们所属的类。因此,要调用方法,必须先创建该类的一个实例,然后通过点运算符访问该方法。 

# This will work
joe = Human()
joe.walk()

"""
I am walking
"""




# This will throw a NameError
walk()

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_153/2327344440.py in <module>
      4
      5 # This will throw a NameError
----> 6 walk()

NameError: name 'walk' is not defined

另一方面,函数不同:我们不必创建类的实例。 

Python 函数是在被调用时才作为一个代码块执行的步骤。它们独立于任何类对象进行定义,这意味着可以直接通过其名称调用。由于它们不属于任何类,我们无需传递 self 作为参数——参数是可选的。 

本质上,函数就是一组定义明确的指令,解释器用它来执行某项任务。 

# Example of a function
def run():
  print("I am running.")
​
run()

"""
I am running.
"""

正如我们在如何调用与编写函数一文中所探讨的,函数也有两种类型:内置函数和用户自定义函数。 

  • 内置函数:包含在 Python 标准库中的函数。 
  • 用户自定义函数:由用户定义的函数——Python 允许用户定义并在程序中使用自己的函数。  

在许多方面,方法和函数是相同的——例如,它们都是用 def 语句定义的。根本区别在于,方法与某个对象相关联,用于操作类中包含的数据;而函数是独立定义的,可以在程序的任何位置通过名称调用。 

什么是 Python 魔术方法?

在本教程开头,我们提到魔术方法也被称为 dunder 方法,因为它们由双下划线构成。我们的意思是,所有的 Python 魔术方法都以两个下划线开始并以两个下划线结束。 

最为人熟知的魔术方法是 __init__ 方法,它使类能够用特定属性进行初始化。 

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

要实例化该类的一个实例,我们必须指定该实例的 name、age 和 role 属性。例如:

user_1 = Employee(name="John", age=24, role="Data Scientist")

否则将引发TypeError。 

__init__ 方法属于初始化与构造类的魔术方法范畴,但它只是其中之一。下表列出了其他一些初始化与构造相关的魔术方法。 

__new__(cls, other)

用于创建类的新实例

__init__(self, other)

在新实例创建后调用;传递给类构造表达式的所有属性都将用作实例属性。 

__del__(self)

也称为析构函数;当实例将被销毁时调用。 

理解魔术方法的最简单方式,或许是将其看作您的实现与 Python 解释器之间的一份契约。契约中的一个条款要求 Python 在某些特定情形下在幕后执行某些操作,例如使用特定对象初始化类实例。 

换言之,直接调用并显式触发魔术方法并非您的职责——Python 会在幕后为您完成。 

字符串表示方法

Python 是一种面向对象编程(OOP)语言——在 Python 中一切皆对象。当创建一个新对象时,我们隐式地创建了一个相关对象,因为所有类都继承自 object。  

注意:您也可以通过将 object 类显式传给自己的类来明确继承关系。 

# This works
class Car:
  pass
​
# This also works
class Vehicle(object):
  pass
​
car = Car()
vehicle = Vehicle()
​
print(car, vehicle, sep="\n")

"""
<__main__.Car object at 0x7f80449dc0a0>
<__main__.Vehicle object at 0x7f80449dc1f0>
"""

在上面的代码中,我们创建了两个空类,但当我们调用 print 语句时,Python 解释器仍然知道打印什么。这之所以可能,是因为所有类都继承自 object 类(Python 中所有类的父类),而它有一个名为 __repr__ 的魔术方法,print 语句会调用它并将返回值传回主程序。  

熟悉继承的人都知道,子类可以通过定义同名方法来覆盖其父类的方法。 

# Overriding the object __repr__
class Vehicle:
  def __repr__(self):
      return f"{self.__class__.__qualname__}"
​
vehicle = Vehicle()
print(vehicle)

"""
Vehicle
"""

您也可以使用 __str__ 魔术方法来创建一个在调用 print 语句时使用的字符串表示。 

事实上,__repr__ 魔术方法是 __str__ 魔术方法的后备行为。因此,当我们调用 print 语句时,Python 会先在您的类中查找是否定义了 __str__ 方法,如果没有则回退到 __repr__ 方法。 

# Using __str__ for string representation
class Vehicle:
  def __str__(self):
      return f"{self.__class__.__qualname__}"
     
vehicle = Vehicle()
print(vehicle)

"""
Vehicle
"""

下表列出了其他一些字符串表示相关的魔术方法。 

__str__(self)

被 Python 内置的 str() 函数和 print 语句调用,用于显示对象的非正式字符串表示。注意,它不要求是有效的 Python 表达式。 

__repr__(self)

被 Python 内置的 repr() 函数和字符串转换调用,用于显示对象的官方字符串表示。注意,它应当看起来像一个有效的 Python 表达式;如果无法做到,返回带有有用描述的字符串更为可取。 

__unicode__(self)

用于实现 Python 内置的 unicode() 函数,并返回一个 Unicode 对象。 

__hash__(self)

被 Python 内置的 hash() 函数以及对哈希集合成员的操作调用。该方法应返回一个整数。 

__nonzero__(self)

被 Python 内置的 bool() 运算调用以实现真值测试。该方法应返回 True 或 False(或其整数等价 1 或 0)。 

封装相关的“魔术”方法 

封装是面向对象编程(OOP)的基本概念之一。它指的是将数据与对该数据进行操作的方法捆绑在一起,或限制对对象中某些组件的直接访问。

来自其他 OOP 语言的开发者经常质疑 Python 未能在类上真正拥抱封装。他们习惯于使用显式的修饰符来标注方法或字段,例如通过公有的 getter 和 setter 定义私有属性。而在 Python 中,不少现有的封装是通过魔术方法来实现的。 

例如,__setattr__ 是一种封装方案,使我们能够为属性赋值定义行为,无论该属性是否已存在。换言之,可以为属性值的变更定义规则。 

import string

class Alphabet():
    def __setattr__(self, name, value):
        self.__dict__[name] = value.upper()

a = Alphabet()

# Assinging an attribute that does not exist
# The setattr should convert it to caps despite being set as lowercase
a.alphabet = string.ascii_lowercase
print(a.alphabet)

在上面的代码中,我们创建了一个名为 Alphabet 的类,它没有任何属性,但我们告诉 Python:无论给该类实例设置什么属性,都要将其值转换为大写。

我们理解 OOP 对某些读者来说可能有些困难。若想快速掌握 Python 中的 OOP,请查看 DataCamp 的Python 程序员学习路径。   

数学类魔术方法

另一类 Python 魔术方法是数学类魔术方法(也称为常规算术运算符)。在 Python 中,我们使用称为“运算符”的特殊符号来创建表达式。在表达式中使用这些运算符,涉及到创建带有相应数学魔术方法的对象。若不遵循此规则,将引发 TypeError。 

class Numbers:
  def __init__(self, a, b):
      self.a = a
      self.b = b
   
set_a = Numbers(2, 4)
set_b = Numbers(3, 5)
​
print(set_a + set_b)
"""
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_152/3639634723.py in <module>
      7 set_b = Numbers(3, 5)
      8
----> 9 print(set_a + set_b)

TypeError: unsupported operand type(s) for +: 'Numbers' and 'Numbers'
"""

在上述代码中,我们尝试创建 Numbers 类的多个实例并将它们相加,理所当然引发了错误。一个解决方案是在类中创建一个方法来相加,但这会妨碍我们使用表达式。 

更好的解决方案是实现一个 __add__ 魔术方法,如下所示:  

# Using add operator
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
b = Numbers(3, 5)
print(a + b) # Should result in Numbers(5, 9)


"""
RandomNumbers(5, 9)
"""

上面的代码示例只允许我们将两个 Numbers 对象相加。让我们再实现另一个数学魔术方法,使我们可以将 Numbers 对象与整数相乘。 

# Multiplying Numbers with an integer
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __mul__(self, other):
      if not isinstance(other, int):
          return NotImplemented
         
      return Numbers(self.a * other, self.b * other)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
print(a * 5) # Should return 10, 20




"""
RandomNumbers(10, 20)
"""

在上述代码中,我们添加了 __mul__ 魔术方法,使我们能够将自定义类中的数字与整数相乘。 

请注意,Numbers 对象出现在 * 运算符的左侧——这不是偶然的。让我们把它放到运算符右侧,看看会发生什么。 

- snip --

a = Numbers(2, 4)
print(3 * a)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_152/2857655049.py in <module>
      1 a = Numbers(2, 4)
----> 2 print(3 * a)

TypeError: unsupported operand type(s) for *: 'int' and 'Numbers'

Python 抛出了 TypeError。 

我们实现的 __mul__ 魔术方法只处理 Numbers 对象位于 * 运算符左侧的情形。若要扩展此功能,我们必须添加反向魔术方法 __rmul__。 

# Enabling reverse multiplication
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __mul__(self, other):
      if not isinstance(other, int):
          return NotImplemented
      return Numbers(self.a * other, self.b * other)

    def __rmul__(self, other):
        return self.__mul__(other)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
print(5 * a) # Should return 10, 20

"""
Numbers(10, 20)
"""

现在,我们在书写乘法表达式时,无需再考虑 Numbers 对象位于运算符的哪一侧。 

总结

利用 Python 魔术方法的基本原则保持不变:创建一个类,并重写 object 类中的魔术方法。Python 中的魔术方法非常多,如果逐一讲解,教程会异常冗长。我们强烈建议您查阅Python 文档,了解可以用来增强Python 编程技能的魔术方法全貌。  



主题