Python 之所以强大,其中一个促成因素就是魔术方法。在某些 Pythonista 圈子里,您也会听到它们被称为“dunder 方法”,源于它们由双下划线构成。
它们在 Python 中的作用是执行一种称为“运算符重载”的过程,简单来说,就是为预定义运算符赋予扩展含义。换言之,运算符重载用于为我们的类添加自定义行为,从而使其能够配合 Python 的运算符和内置功能使用。
在本文中,我们将更深入地探讨 Python 魔术方法:
区分方法与函数的差异
解释什么是魔术方法
给出一些 Python 示例。
查看这个DataLab 工作簿 ,跟着本教程中的代码一起练习。
Python 方法与函数
在讨论魔术方法之前,我们需要先理解 Python 中方法和函数的区别。
Python 使用方法来表示对象的行为。因此,方法与某个对象相关联,并会对对象中的数据执行某些操作。另一个关键的区分点是方法的第一个参数必须是 self 参数。
注意 :第一个参数不一定非要叫 self,但 Python 开发者普遍认为这样做是最佳实践。
class Human :
def walk ( self) :
print ( "I am walking" ) 技术支持
方法依赖于它们所属的类。因此,要调用方法,必须先创建该类的一个实例,然后通过点运算符访问该方法。
joe = Human( )
joe. walk( )
"""
I am walking
"""
walk( )
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameError Traceback ( most recent call last)
/ tmp/ ipykernel_153/ 2327344440 . py in < module>
4
5
- - - - > 6 walk( )
NameError: name 'walk' is not defined技术支持
另一方面,函数不同:我们不必创建类的实例。
Python 函数 是在被调用时才作为一个代码块执行的步骤。它们独立于任何类对象进行定义,这意味着可以直接通过其名称调用。由于它们不属于任何类,我们无需传递 self 作为参数——参数是可选的。
本质上,函数就是一组定义明确的指令,解释器用它来执行某项任务。
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 类显式传给自己的类来明确继承关系。
class Car :
pass
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 语句会调用它并将返回值传回主程序。
熟悉继承的人都知道,子类可以通过定义同名方法来覆盖其父类的方法。
class Vehicle :
def __repr__ ( self) :
return f" { self. __class__. __qualname__} "
vehicle = Vehicle( )
print ( vehicle)
"""
Vehicle
""" 技术支持
您也可以使用 __str__ 魔术方法来创建一个在调用 print 语句时使用的字符串表示。
事实上,__repr__ 魔术方法是 __str__ 魔术方法的后备行为。因此,当我们调用 print 语句时,Python 会先在您的类中查找是否定义了 __str__ 方法,如果没有则回退到 __repr__ 方法。
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( )
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__ 魔术方法,如下所示:
class Numbers :
def __init__ ( self, a, b) :
self. a = a
self. b = b
def __add__ ( self, other) :
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)
"""
RandomNumbers(5, 9)
""" 技术支持
上面的代码示例只允许我们将两个 Numbers 对象相加。让我们再实现另一个数学魔术方法,使我们可以将 Numbers 对象与整数相乘。
class Numbers :
def __init__ ( self, a, b) :
self. a = a
self. b = b
def __add__ ( self, other) :
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 )
"""
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__。
class Numbers :
def __init__ ( self, a, b) :
self. a = a
self. b = b
def __add__ ( self, other) :
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)
"""
Numbers(10, 20)
""" 技术支持
现在,我们在书写乘法表达式时,无需再考虑 Numbers 对象位于运算符的哪一侧。
总结
利用 Python 魔术方法的基本原则保持不变:创建一个类,并重写 object 类中的魔术方法。Python 中的魔术方法非常多,如果逐一讲解,教程会异常冗长。我们强烈建议您查阅Python 文档 ,了解可以用来增强Python 编程技能 的魔术方法全貌。
Python 之所以强大,其中一个促成因素就是魔术方法。在某些 Pythonista 圈子里,您也会听到它们被称为“dunder 方法”,源于它们由双下划线构成。
它们在 Python 中的作用是执行一种称为“运算符重载”的过程,简单来说,就是为预定义运算符赋予扩展含义。换言之,运算符重载用于为我们的类添加自定义行为,从而使其能够配合 Python 的运算符和内置功能使用。
在本文中,我们将更深入地探讨 Python 魔术方法:
查看这个DataLab 工作簿,跟着本教程中的代码一起练习。
Python 方法与函数
在讨论魔术方法之前,我们需要先理解 Python 中方法和函数的区别。
Python 使用方法来表示对象的行为。因此,方法与某个对象相关联,并会对对象中的数据执行某些操作。另一个关键的区分点是方法的第一个参数必须是 self 参数。
注意:第一个参数不一定非要叫 self,但 Python 开发者普遍认为这样做是最佳实践。
方法依赖于它们所属的类。因此,要调用方法,必须先创建该类的一个实例,然后通过点运算符访问该方法。
另一方面,函数不同:我们不必创建类的实例。
Python 函数是在被调用时才作为一个代码块执行的步骤。它们独立于任何类对象进行定义,这意味着可以直接通过其名称调用。由于它们不属于任何类,我们无需传递 self 作为参数——参数是可选的。
本质上,函数就是一组定义明确的指令,解释器用它来执行某项任务。
正如我们在如何调用与编写函数一文中所探讨的,函数也有两种类型:内置函数和用户自定义函数。
在许多方面,方法和函数是相同的——例如,它们都是用 def 语句定义的。根本区别在于,方法与某个对象相关联,用于操作类中包含的数据;而函数是独立定义的,可以在程序的任何位置通过名称调用。
什么是 Python 魔术方法?
在本教程开头,我们提到魔术方法也被称为 dunder 方法,因为它们由双下划线构成。我们的意思是,所有的 Python 魔术方法都以两个下划线开始并以两个下划线结束。
最为人熟知的魔术方法是 __init__ 方法,它使类能够用特定属性进行初始化。
要实例化该类的一个实例,我们必须指定该实例的 name、age 和 role 属性。例如:
否则将引发TypeError。
__init__ 方法属于初始化与构造类的魔术方法范畴,但它只是其中之一。下表列出了其他一些初始化与构造相关的魔术方法。
__new__(cls, other)
用于创建类的新实例
__init__(self, other)
在新实例创建后调用;传递给类构造表达式的所有属性都将用作实例属性。
__del__(self)
也称为析构函数;当实例将被销毁时调用。
理解魔术方法的最简单方式,或许是将其看作您的实现与 Python 解释器之间的一份契约。契约中的一个条款要求 Python 在某些特定情形下在幕后执行某些操作,例如使用特定对象初始化类实例。
换言之,直接调用并显式触发魔术方法并非您的职责——Python 会在幕后为您完成。
字符串表示方法
Python 是一种面向对象编程(OOP)语言——在 Python 中一切皆对象。当创建一个新对象时,我们隐式地创建了一个相关对象,因为所有类都继承自 object。
注意:您也可以通过将 object 类显式传给自己的类来明确继承关系。
在上面的代码中,我们创建了两个空类,但当我们调用 print 语句时,Python 解释器仍然知道打印什么。这之所以可能,是因为所有类都继承自 object 类(Python 中所有类的父类),而它有一个名为 __repr__ 的魔术方法,print 语句会调用它并将返回值传回主程序。
熟悉继承的人都知道,子类可以通过定义同名方法来覆盖其父类的方法。
您也可以使用 __str__ 魔术方法来创建一个在调用 print 语句时使用的字符串表示。
事实上,__repr__ 魔术方法是 __str__ 魔术方法的后备行为。因此,当我们调用 print 语句时,Python 会先在您的类中查找是否定义了 __str__ 方法,如果没有则回退到 __repr__ 方法。
下表列出了其他一些字符串表示相关的魔术方法。
__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__ 是一种封装方案,使我们能够为属性赋值定义行为,无论该属性是否已存在。换言之,可以为属性值的变更定义规则。
在上面的代码中,我们创建了一个名为 Alphabet 的类,它没有任何属性,但我们告诉 Python:无论给该类实例设置什么属性,都要将其值转换为大写。
我们理解 OOP 对某些读者来说可能有些困难。若想快速掌握 Python 中的 OOP,请查看 DataCamp 的Python 程序员学习路径。
数学类魔术方法
另一类 Python 魔术方法是数学类魔术方法(也称为常规算术运算符)。在 Python 中,我们使用称为“运算符”的特殊符号来创建表达式。在表达式中使用这些运算符,涉及到创建带有相应数学魔术方法的对象。若不遵循此规则,将引发 TypeError。
在上述代码中,我们尝试创建 Numbers 类的多个实例并将它们相加,理所当然引发了错误。一个解决方案是在类中创建一个方法来相加,但这会妨碍我们使用表达式。
更好的解决方案是实现一个 __add__ 魔术方法,如下所示:
上面的代码示例只允许我们将两个 Numbers 对象相加。让我们再实现另一个数学魔术方法,使我们可以将 Numbers 对象与整数相乘。
在上述代码中,我们添加了 __mul__ 魔术方法,使我们能够将自定义类中的数字与整数相乘。
请注意,Numbers 对象出现在 * 运算符的左侧——这不是偶然的。让我们把它放到运算符右侧,看看会发生什么。
Python 抛出了 TypeError。
我们实现的 __mul__ 魔术方法只处理 Numbers 对象位于 * 运算符左侧的情形。若要扩展此功能,我们必须添加反向魔术方法 __rmul__。
现在,我们在书写乘法表达式时,无需再考虑 Numbers 对象位于运算符的哪一侧。
总结
利用 Python 魔术方法的基本原则保持不变:创建一个类,并重写 object 类中的魔术方法。Python 中的魔术方法非常多,如果逐一讲解,教程会异常冗长。我们强烈建议您查阅Python 文档,了解可以用来增强Python 编程技能的魔术方法全貌。