Dunder 方法 | Pydon‘t

这是对 Python 中 dunder 方法的介绍,以帮助您了解它们是什么以及它们的用途。

在这里插入图片描述

(如果您是新来的并且不知道 Pydon’t 是什么,您可能需要阅读 Pydon’t 宣言。)

介绍

Python 是一种具有丰富内置函数和运算符的语言,它们可以很好地与内置类型配合使用。例如,运算符可以+对数字进行加法运算,也可以对字符串、列表和元组进行连接运算:

>>> 1 + 2.3
3.3

> > > [1, 2, 3] + [4, 5, 6] > > > [1, 2, 3, 4, 5, 6]

但是,是什么定义了+数字(整数和浮点数)的加法和列表、元组、字符串的连接?如果我想+处理其他类型怎么办?我可以这样做吗?

简短的回答是“是”,这通过dunder 方法实现,这是本篇 Pydon’t 中的研究对象。在本篇 Pydon’t 中,你将

  • 了解什么是 dunder 方法;
  • 为什么它们被这样称呼;
  • 了解各种有用的 dunder 方法;
  • 了解哪些 dunder 方法与哪些内置方法相对应;
  • 为示例类编写自己的 dunder 方法;以及
  • 意识到 dunder 方法与您之前编写的任何其他方法类似。

您现在可以在 Gumroad 上免费获取电子书“Pydon’ts——编写优雅的 Python 代码”, 以帮助支持“Pydon’t”系列文章💪。

什么是 dunder 方法?

在 Python 中, dunder 方法是允许类实例与语言的内置函数和运算符交互的方法__str__。“dunder”一词来自“双下划线”,因为 dunder 方法的名称以两个下划线开头和结尾,例如或__add__。通常,dunder 方法不由程序员直接调用,看起来像是被魔法调用。这就是为什么 dunder 方法有时也被称为“魔术方法” 。1

不过,Dunder 方法的调用并不是凭空而来的。它们只是由语言在明确定义的特定时间隐式调用,并且取决于所讨论的 Dunder 方法。

众所周知的 Dunder 方法

如果你已经在 Python 中定义了类,那么你一定会遇到一个 dunder 方法:__init__。dunder 方法__init__负责初始化类的实例,这就是为什么你通常会在其中设置一堆与类接收的参数相关的属性。

例如,如果您正在创建一个类的实例Square,那么您将在中创建边长的属性__init__

`class Square:
def init(self, side_length):
“”"init is the dunder method that INITialises the instance.

    To create a square, we need to know the length of its side,
    so that will be passed as an argument later, e.g. with Square(1).
    To make sure the instance knows its own side length,
    we save it with self.side_length = side_length.
    """
    print("Inside init!")
    self.side_length = side_length

sq = Square(1)

Inside init!`

如果你运行上面的代码,你会看到打印出“Inside init!”消息,但你并没有直接调用该方法!当你创建一个正方形实例时,语言会隐式调用__init__dunder 方法。__init__

为什么 dunder 方法以两个下划线开始和结束?

双下划线方法名开头和结尾的两个下划线没有任何特殊含义。换句话说,方法名以两个下划线开头和结尾这一事实本身并没有什么特殊之处。两个下划线只是为了防止与毫无戒心的程序员实现的其他方法发生名称冲突。

可以这样想:Python 有一个名为 的内置函数sum。您可以将其定义sum为其他函数,但这样您就无法访问用于求和的内置函数,对吗?

>>> sum(range(10))
45

> > > sum = 45
> > > sum(range(10))
> > > Traceback (most recent call last):
> > > File "<stdin>", line 1, in <module>
> > > TypeError: 'int' object is not callable

通常,你会看到初学者使用sum作为变量名,因为他们不知道这sum实际上是一个内置函数。如果将内置函数命名__sum__为 而不是sum,那么你就不会那么难以错误地覆盖它,对吧?但这也会使使用起来不那么方便sum……

然而,对于魔法方法,我们不需要它们的名称非常方便输入,因为您几乎从不输入魔法方法的名称。因此,Python 决定魔法方法的名称将以两个下划线开头和结尾,以减少有人意外覆盖其中一个方法的可能性!

总而言之,dunder 方法就像您实现的任何其他方法一样,但有一点不同,即 dunder 方法可以被语言隐式调用。

Python 中的运算符重载和 dunder 方法

所有 Python 运算符(例如+==in)都依赖 dunder 方法来实现其行为。

例如,当 Python 遇到代码时value in container,它实际上将其转换为对适当的 dunder 方法的调用__contains__,这意味着 Python 实际上运行表达式container.__contains__(value)

我来给你展示:

>>> my_list = [2, 4, 6]

> > > 3 in my_list
> > > False
> > > my_list.**contains**(3)
> > > False
> > > 6 in my_list
> > > True
> > > my_list.**contains**(6)
> > > True

因此,当您想要重载某些运算符以使它们以自定义方式与您自己的对象一起工作时,您需要实现相应的 dunder 方法。

因此,如果您要创建自己的容器类型,则可以实现 dunder 方法__contains__以确保您的容器可以位于带有运算符的表达式的右侧in

dunder 方法及其交互列表

正如我们所见,dunder 方法 (通常) 由语言隐式调用… 但是什么时候调用呢?dunder 方法__init__在初始化类的实例时被调用,但是__str__,或者__bool__,或其他 dunder 方法呢?

下表列出了所有 dunder 方法以及一个或多个(简化的)使用示例,这些示例将隐式调用相应的 dunder 方法。这可能包括可能调用相关 dunder 方法的情况的简要描述,或依赖于该 dunder 方法的示例函数调用。这些示例情况可能带有相关注意事项,因此, 每当您想要使用不熟悉的 dunder 方法时,请务必阅读有关 dunder 方法的文档。

表的行顺序与文档的“数据模型”页面中提及这些 dunder 方法的顺序相匹配,这并不意味着各种 dunder 方法之间存在任何依赖关系,也不意味着理解这些方法的难度级别。

魔法方法用途/需要了解更多
__init__初始化对象文档
__new__创建对象文档
__del__销毁对象文档
__repr__计算“官方”字符串表示 /repr(obj)博客文档
__str__漂亮打印对象 / str(obj)/print(obj)博客文档
__bytes__bytes(obj)文档
__format__自定义字符串格式博客文档
__lt__obj < ...文档
__le__obj <= ...文档
__eq__obj == ...文档
__ne__obj != ...文档
__gt__obj > ...文档
__ge__obj >= ...文档
__hash__hash(obj)对象作为字典键文档
__bool__bool(obj)定义对象的 Truthy/Falsy 值博客文档
__getattr__属性访问的后备文档
__getattribute__实现属性访问:obj.name文档
__setattr__设置属性值:obj.name = value文档
__delattr__删除属性:del obj.name文档
__dir__dir(obj)文档
__get__描述符中的属性访问文档
__set__在描述符中设置属性文档
__delete__描述符中的属性删除文档
__init_subclass__初始化子类文档
__set_name__所有者类分配回调文档
__instancecheck__isinstance(obj, ...)文档
__subclasscheck__issubclass(obj, ...)文档
__class_getitem__模拟泛型类型文档
__call__模拟可调用函数 /obj(*args, **kwargs)文档
__len__len(obj)文档
__length_hint__估算长度以进行优化文档
__getitem__使用权obj[key]博客文档
__setitem__obj[key] = ...或者obj[]博客文档
__delitem__del obj[key]博客文档
__missing__处理dict子类中丢失的键文档
__iter__iter(obj)/ for ... in obj(迭代)文档
__reversed__reverse(obj)文档
__contains__... in obj(会员测试)文档
__add__obj + ...博客文档
__radd__... + obj博客文档
__iadd__obj += ...博客文档
__sub__obj - ...博客文档
__mul__obj * ...博客文档
__matmul__obj @ ...博客文档
__truediv__obj / ...博客文档
__floordiv__obj // ...博客文档
__mod__obj % ...博客文档
__divmod__divmod(obj, ...)博客文档
__pow__obj ** ...博客文档
__lshift__obj << ...博客文档
__rshift__obj >> ...博客文档
__and__obj & ...博客文档
__xor__obj ^ ...博客文档
__or__obj | ...博客文档
__neg__-obj(一元)博客文档
__pos__+obj(一元)博客文档
__abs__abs(obj)博客文档
__invert__~obj(一元)博客文档
__complex__complex(obj)文档
__int__int(obj)文档
__float__float(obj)文档
__index__无损转换为整数文档
__round__round(obj)文档
__trunc__math.trunc(obj)文档
__floor__math.floor(obj)文档
__ceil__math.ceil(obj)文档
__enter__with obj(进入上下文管理器)文档
__exit__with obj(退出上下文管理器)文档
__await__实现可等待对象文档
__aiter__aiter(obj)文档
__anext__anext(obj)文档
__aenter__async with obj(进入异步上下文管理器)文档
__aexit__async with obj(退出异步上下文管理器)文档

探索 dunder 方法

每当我了解到一种新的 dunder 方法时,我做的第一件事就是尝试一下。

下面,我与大家分享我在探索新的 dunder 方法时遵循的三个步骤:

  1. 尝试了解何时调用 dunder 方法;
  2. 为该方法实现一个存根并用代码触发它;并且
  3. 在有用的场合使用 dunder 方法。

我将通过一个实际示例(dunder 方法)向您展示如何遵循这些步骤__missing__

dunder 方法是用来做什么的?

dunder 方法有什么__missing__用?dunder 方法的文档__missing__如下

“当不在字典中时,由 调用来dict.__getitem__()实现self[key]子类。”dict``key

换句话说,dunder 方法__missing__仅与的子类相关dict,并且只要我们在字典中找不到给定的键,就会调用该方法。

如何触发 dunder 方法?

在什么情况下我可以重新创建 dunder 方法__missing__被调用?

从文档文本来看,我们可能需要一个字典子类,然后我们需要访问该字典中不存在的键。因此,这足以触发 dunder 方法__missing__

class DictSubclass(dict):
    def __missing__(self, key):
        print("Hello, world!")

my_dict = DictSubclass()
my_dict["this key isn't available"]
# Hello, world!

请注意上面的代码是多么的简单:我只是定义了一个调用的方法__missing__并进行了打印,这样我就可以检查该方法__missing__是否被调用。

现在我要再做几个测试,只是为了确保__missing__只有在尝试获取不存在的键的值时才会调用它:

class DictSubclass(dict):
    def __missing__(self, key):
        print(f"Missing {key = }")

my_dict = DictSubclass()
my_dict[0] = True
if my_dict[0]:
    print("Key 0 was `True`.")
# Prints: Key 0 was `True`
my_dict[1]  # Prints: Missing key = 1

在有用的情况下使用 dunder 方法

__missing__现在我们对何时发挥作用有了更清晰的认识,我们可以将其用于一些有用的事情。例如,我们可以尝试defaultdict基于实现__missing__

defaultdict是来自模块的容器collections,它就像一本字典,只不过当缺少键时它使用工厂来生成默认值。

例如,这是一个默认defaultdict返回值的实例:0

from collections import defaultdict

olympic_medals = defaultdict(lambda: 0)  # Produce 0 by default
olympic_medals["Phelps"] = 28

print(olympic_medals["Phelps"])  # 28
print(olympic_medals["me"])  # 0

因此,要重新实现defaultdict,我们需要接受一个工厂函数,我们需要保存该工厂,并且我们需要在里面使用它__missing__

顺便提一下,请注意,它defaultdict不仅返回默认值,而且还将其分配给之前不存在的键:

>>> from collections import defaultdict
>>> olympic_medals = defaultdict(lambda: 0)  # Produce 0 by default
>>> olympic_medals
defaultdict(<function <lambda> at 0x000001F15404F1F0>, {})
>>> # Notice the underlying dictionary is empty -------^^
>>> olympic_medals["me"]
0
>>> olympic_medals
defaultdict(<function <lambda> at 0x000001F15404F1F0>, {'me': 0})
>>> # It's not empty anymore --------------------------^^^^^^^^^

考虑到所有这些,以下是可能的重新实现defaultdict

class my_defaultdict(dict):
    def __init__(self, default_factory, **kwargs):
        super().__init__(**kwargs)
        self.default_factory = default_factory

    def __missing__(self, key):
        """Populate the missing key and return its value."""
        self[key] = self.default_factory()
        return self[key]

olympic_medals = my_defaultdict(lambda: 0)  # Produce 0 by default
olympic_medals["Phelps"] = 28

print(olympic_medals["Phelps"])  # 28
print(olympic_medals["me"])  # 0

结论

以下是本次 Pydon’t 的主要内容,对你来说,这已经是一份丰厚的奖励了:

Dunder 方法是特定的方法,允许您指定对象如何与 Python 语法、关键字、运算符和内置函数交互。

这个 Pydon’t 向您展示了:

  • dunder 方法是 Python 语言在特定情况下隐式调用的方法;
  • “dunder” 源自“双下划线”,指所有 dunder 方法的前缀和后缀均为两个下划线;
  • dunder 方法有时被称为魔术方法,因为它们通常不需要显式调用;
  • 学习新的 dunder 方法可以通过一系列简单的小步骤来完成;
  • dunder 方法是常规 Python 类的常规 Python 方法。

  1. 我更喜欢“dunder 方法”这个名字,而不是“魔法方法”,因为“魔法方法”让它看起来很难理解,因为其中有巫术在起作用!剧透:没有 。↩
  2. 这个 dunder 方法还有一个“右”版本,名字相同但前缀为"r",当对象位于操作的右侧而左侧的对象未实现该行为时,将调用该方法。见上文__radd__。↩
  3. 此 dunder 方法还有一个“就地”版本,名称相同但前缀为"i",并使用给定运算符进行增强赋值。见上文__iadd__。↩

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值