老齐教室

Python中5对必知的魔法方法

引言

在Python中,我们可以使用下划线、字母和数字来命名函数。单词之间的下划线并没有太大的意义——它们只是通过在单词之间创建空格来提高可读性。这就是众所周知的s蛇形命名风格。例如,calculate_mean_scorecalculatemeanscore更容易阅读。你可能知道,除了这种使用下划线的常见方式,我们还在函数名之前加上一个或两个下划线(例如:_func__func) 来表示类或模块内的私有化函数,那些没有以下划线为前缀的名称被认为是公共 API。

下划线在方法命名中的另一用途就是定义“魔法方法”,也称为“特殊方法”。具体地说,我们在方法的名称前和后分别使用了两个下划线——类似于__func__。由于使用了两个下划线,一些人将特殊方法称为“dunder方法”或简单地称为“dunders”。在本文中,将介绍五对密切相关的常用魔法方法,每一对方法表示一个Python概念。

1. 实例化:__new____init__

在学习了Python数据结构的基础知识(例如字典、列表)之后,你应该已经看到了一些自定义类的示例,其中肯定有一个神奇的方法:__init__。此方法用于对实例对象进行初始化。具体地说,在__init__方法中,你需要为创建的实例对象设置初始属性。下面是一个简单的例子:

1
2
3
4
class Product:
def __init__(self, name, price):
self.name = name
self.price = price

我们不会直接调用类中的__init__方法时,它在类中担负着初始化实例的作用,例如,要创建一个新的Product实例,可以使用以下代码:

1
product = Product("Vacuum", 150.0)

__init__方法密切相关的是__new__方法,我们通常不会在自定义类中实现__new__方法。实际上,__new__才是真正的构造方法,它构造了实例对象,该对象被传给__init__方法,以完成初始化过程。

换句话说,构造一个新的实例对象(一个叫做实例化的过程)需要依次调用__new____init__方法。

下面的代码展示了此连锁反应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class Product:
... def __new__(cls, *args):
... new_product = object.__new__(cls)
... print("Product __new__ gets called")
... return new_product
...
... def __init__(self, name, price):
... self.name = name
... self.price = price
... print("Product __init__ gets called")
...
>>> product = Product("Vacuum", 150.0)
Product __new__ gets called
Product __init__ gets called

2.字符串表示法:__repr____str__

这两个方法都很重要,因为它们为能在自定义类中指定的字符串表示方法。在解释它们之前,我们先快速看一看下面的实现:

1
2
3
4
5
6
7
8
9
10
class Product:
def __init__(self, name, price):
self.name = name
self.price = price

def __repr__(self):
return f"Product({self.name!r}, {self.price!r})"

def __str__(self):
return f"Product: {self.name}, ${self.price:.2f}"

__repr__ 方法应该返回一个字符串,该字符串的内容是创建实例对象。具体来说,字符串可以被传给eval()来创建实例对象。下面的代码片段展示了这样的操作:

1
2
3
4
5
6
>>> product = Product("Vacuum", 150.0)
>>> repr(product)
"Product('Vacuum', 150.0)"
>>> evaluated = eval(repr(product))
>>> type(evaluated)
<class '__main__.Product'>

__str__ 方法的返回结果对于实例对象的描述性更强。应该注意的是,print()函数使用__str__ 方法来显示与实例相关的信息,如下所示。

1
2
>>> print(product)
Product: Vacuum, $150.00

虽然两种方法都应该返回字符串,但是__repr__方法通常是面向开发人员的,主要显示实例化信息;而__str__方法是面向普通用户的,通常要显示更多的信息内容。

3. 迭代: __iter____next__

用代码自动重复执行的某个作业,称为迭代,对此可以用for循环实现。我们将可以用于for循环的对象称为可迭代对象。for循环的基本形式如下所示:

1
2
for item in iterable:
# Operations go here

在后台,可迭代对象被转换为迭代器对象,它为每次循环提供可迭代对象中的项。一般来说,迭代器是Python对象,可用于实现项的迭代。转换过程是通过实现 __iter__特殊方法来完成的。此外,检索迭代器的下一项涉及到__next__ 特殊方法的实现。现在我们继续前面的例子,把Product类用作for循环的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> class Product:
... def __init__(self, name, price):
... self.name = name
... self.price = price
...
... def __str__(self):
... return f"Product: {self.name}, ${self.price:.2f}"
...
... def __iter__(self):
... self._free_samples = [Product(self.name, 0) for _ in range(3)]
... print("Iterator of the product is created.")
... return self
...
... def __next__(self):
... if self._free_samples:
... return self._free_samples.pop()
... else:
... raise StopIteration("All free samples have been dispensed.")
...
>>> product = Product("Perfume", 5.0)
>>> for i, sample in enumerate(product, 1):
... print(f"Dispense the next sample #{i}: {sample}")
...
Iterator of the product is created.
Dispense the next sample #1: Product: Perfume, $0.00
Dispense the next sample #2: Product: Perfume, $0.00
Dispense the next sample #3: Product: Perfume, $0.00

如上所示,我们用__iter__方法创建了一些样本数据,并创建了一个迭代器实例。为了实现迭代行为,在循环过程中调用了__next__方法,从而将实例中的样本都显示出来。当样本读取完毕时,迭代结束。

4. 上下文管理器:__enter____exit__

在Python中处理文件对象时,最常见的语法可能是这样的:

1
2
with open('filename.txt') as file:
# Your file operations go here

with语句的使用被称为上下文管理器。具体来说,在上面的文件操作示例中,with语句将为file对象创建一个上下文管理器,在文件操作之后,上下文管理器将帮助我们关闭文件对象,以便共享资源(即文件)可以用于其他进程。

因此,一般来说,上下文管理器是Python对象,用于管理共享资源,例如openclose。没有上下文管理器,我们必须手动管理这些资源,这很容易出错。

为了用一个自定义类实现这样的行为,需要实现__enter____exit__方法。__enter__ 方法设置上下文管理器,为我们准备操作所需的资源,而__exit__ 方法则是清理应释放的任何已用资源,使其可用。我们来思考与前面的Product类有关的一个简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> class Product:
... def __init__(self, name, price):
... self.name = name
... self.price = price
...
... def __str__(self):
... return f"Product: {self.name}, ${self.price:.2f}"
...
... def _move_to_center(self):
... print(f"The product ({self}) occupies the center exhibit spot.")
...
... def _move_to_side(self):
... print(f"Move {self} back.")
...
... def __enter__(self):
... print("__enter__ is called")
... self._move_to_center()
...
... def __exit__(self, exc_type, exc_val, exc_tb):
... print("__exit__ is called")
... self._move_to_side()
...
>>> product = Product("BMW Car", 50000)
>>> with product:
... print("It's a very good car.")
...
__enter__ is called
The product (Product: BMW Car, $50000.00) occupies the center exhibit spot.
It's a very good car.
__exit__ is called
Move Product: BMW Car, $50000.00 back.

如你所见,当实例对象嵌入到with语句中时,将调用__enter__方法。在with语句中完成操作后,将调用__exit__方法。

但是,需要注意的是,我们可以用__enter____exit__方法来创建上下文管理器。使用上下文管理器的装饰器函数可以更容易地完成这项工作。

5. 属性访问控制:__getattr____setattr__

如果你有其他语言的编程经验,可能为实例属性设置过显式的getters(访问属性)和setters(设置属性)。在Python中,我们不需要对每个属性使用这些访问控制技术。但是,我们可以通过实现__getattr____setattr__方法来实现某种控制。具体地说,当访问实例对象的属性时,调用__getattr__ 方法;而在设置实例对象的属性时,调用__setattr__ 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> class Product:
... def __init__(self, name):
... self.name = name
...
... def __getattr__(self, item):
... if item == "formatted_name":
... print(f"__getattr__ is called for {item}")
... formatted = self.name.capitalize()
... setattr(self, "formatted_name", formatted)
... return formatted
... else:
... raise AttributeError(f"no attribute of {item}")
...
... def __setattr__(self, key, value):
... print(f"__setattr__ is called for {key!r}: {value!r}")
... super().__setattr__(key, value)
...
>>> product = Product("taBLe")
__setattr__ is called for 'name': 'taBLe'
>>> product.name
'taBLe'
>>> product.formatted_name
__getattr__ is called for formatted_name
__setattr__ is called for 'formatted_name': 'Table'
'Table'
>>> product.formatted_name
'Table'

每次尝试设置对象的属性时,都会调用 __setattr__方法。要正确使用它,你必须借助super()来使用超类方法。否则,它将陷入无限递归。

设置formatted_name属性后,该属性将成为 __dict__对象的一部分,因此不会调用__getattr__

另外,还有一种与访问控制密切相关的特殊方法叫做__getattribute__。它类似于__getattr__,但每次访问属性时都会调用它。在这一点上,它类似于__setattr__。同样,你应该使用super()来实现__getattribute__方法,以避免无限递归的错误。

结论

在本文中,回顾了五对重要的特殊方法。通过这些方法,我们了解了与之相关的五个Python概念。我希望你能够更好地理解这些概念、更好地理解如何在自己的Python项目中使用特殊方法。

原文链接:https://medium.com/better-programming/5-pairs-of-magic-methods-in-python-you-should-know-f98f0e5356d6

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能