Python 魔术方法探秘

魔术方法(Magic Methods),顾名思义,就是可以为代码中的类赋予某种“魔力”的方法(methods)。有了它,你可以构造出非常巧妙、优雅的代码。

也许魔术方法这个词你还不是很熟悉,但你一定见过这样的用法:

class Complex: def __init__(self, realpart, imagpart): self.r = realpart self.i = imagpart

x = Complex(3.0, -4.5)x.r, x.i # 输出:(3.0, -4.5)

参考:

https://docs.python.org/zh-cn/3.7/tutorial/classes.html#a-word-about-names-and-objects

_init_() 的作用想必你已经很熟悉了,其实,它就是一个魔术方法。在 Python 中,像 _init_() 这种名字前后带双下划线的方法,被统称为魔术方法。它们能够给 Python 的类增加不同的行为,让类具有神奇的“魔力”。

这里举个简单的例子,让你感受一下魔术方法神奇的地方:

from collections import namedtuple

allcards = namedtuple('allcards', ['ranks', 'color'])

class Poker(object): numberRanks = [str(card) for card in range(2, 11)] charRanks = list('JQKA') allRanks = numberRanks + charRanks colors = 'heart spade diamond club'.split() jokes = 'red black'.split()

def __init__(self): self.cards = [allcards(r, s) for r in self.allRranks for s in self.colors] + [allcards('joke', s) for s in self.jokes]

# 开始施展魔法 def __len__(self): return len(self.cards)

def __getitem__(self, p): return self.cards[p]

def __add__(self, other): return self.cards + list(other)

# 开始表演魔术mycard = Poker()

print(f"扑克牌的数量是 {len(mycard)} 张")# 输出:扑克牌的数量是 54 张

print( list( i for i in mycard ))# 输出:[allcards(ranks='2', color='heart'), allcards(ranks='2', color='spade'), … … allcards(ranks='joke', color='black')]

print(f"第5张牌是 {mycard[4]} ")# 输出:第5张牌是 allcards(ranks='3', color='heart')

import randomprint(f"随机抽一张是 {random.choice(mycard)} ")# 输出:随机抽一张是 allcards(ranks='4', color='diamond')

mycard2 = Poker()newcard = mycard + mycard2

random.shuffle(newcard)print(f"洗牌后抽取一张 {newcard.pop()} ")# 输出:洗牌后抽取一张 allcards(ranks='3', color='club')

print(f"现在桌面上扑克牌的数量是 {len(newcard)} 张")# 输出:现在桌面上扑克牌的数量是 107 张

通过这个例子,相信你已经能看到所谓的魔术方法为类赋予的新能力了吧?接下来我们来详细剖析一下。

除了常见的 _init_() 方法,上面的代码中还用到了_len_(), _getitem_() 和_add_() 这三个魔术方法。

先说 _len_() 方法,假如我没有在 Poker 类中使用它,那么代码运行到 len(mycard) 时就会报错:TypeError: object of type 'Poker' has no len()。因为 Python 内置的 len() 方法只适用于字符串、列表、字典、元组等序列类型的数据结构。那怎么让它支持我们自定义的 Poker 类对象呢?

就像上面那样,我们使用 _len_() 这个魔法函数,并定义其具体实现:

def __len__(self): return len(self.cards)

这样,当我们调用 len(mycard) 时,就会返回扑克牌的总数量。

这样写还有一个好处,你不用为求总数起一个新的方法名而头疼,比如说用 mycard.sum() 还是 mycard.length() 好。你只需要沿用我们最常用的 len() 方法就可以,免去理解和记忆上的麻烦,这也是“不要重复造轮子”的另一种体现。

再来看 _getitem_() 方法,我们在这里通过_getitem_() 方法实现了对 Poker 的索引操作,并通过 random 模块来实现从扑克牌中随机抽取一张牌的功能。

和 len() 类似,如果不使用魔术方法,直接对 mycard 实例进行索引操作:mycard[4] ,这种情况下同样也会报错:TypeError: 'Poker' object is not iterable。同样,我们在 Poker 类中定义好 _getitem_() 方法对具体实现:

def __getitem__(self, p): return self.cards[p]

这样,我们就可以通过 mycard[4] 得出第 5 张牌是 allcards(ranks='3', color='heart') ,也就是红桃 3。

第三个魔法方法 _add_() 和前两个类似,具体原理就不详细展开了,在这里,我们通过将两个 Poker 实例相加,把一副牌变成了两副牌,这样就可以玩四人斗地主了。

通过实现上面的这些特殊方法,我们为 Python 中自定义的类(或者说自定义的数据类型)实现了跟字符串、数组、字典等其他内置数据类型一样的功能。

由于过往形成的路径依赖,很多人在学习 Python 的时候,根据过去学习 Java 、C++ 等语言的经验,往往只关注类的封装、继承、多态等等,却忽略掉了的魔法函数这个强大的语言特性。

那么是不是每一种脚本语言都有这样的特性呢?这倒不一定,Python 之所以能够支持魔术方法,是因为它是一门动态类型的语言,这里的动态是指它只有在运行时才能确定对象的类型,而在运行之前,程序并不关注该对象到底是什么类型。

意大利软件工程师、Python 软件基金会研究员 Alex Martelli 将这种程序设计的理念叫做“鸭子类型”(Duck typing)。

这个叫法其实来源于美国诗人 James Whitcomb Riley 的诗句:

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.(一只鸟走起路来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。)

James Whitcomb Riley 对鸭子类型的解释是:

In other words, don't check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with.

换句话说,不需要去关注对象的类型(它到底是不是一个鸭子),而是将注意力放在对象具体的行为表现上面(它的行为看起来像不像鸭子),这也是 Python 这类动态语言的类型推断原理。

好了,话题扯得有点远了,我们再说回魔术方法。

当你更深入地探索魔术的时候,你会发现它的威力远远不止这些,例如我们在学习如何编写 Python 风格(Pythonic)的代码时,网上很多教程的说法是,装饰器就是函数功能的扩展,真的只有函数才能实现装饰器吗?如果是自定义的类呢?

这就需要使用一个叫做 _call_() 的特殊方法,让类也可以像函数一样调用。如以下代码:

class Test(object): def __init__(self, func): self._func = func

def __call__(self): print("before running") self._func() print("after running")

通过使用 _call_() 这个魔术方法,Test 类就可以像函数一样被调用,当然也可以使用语法糖 @Test 方式作为装饰器来使用,如:

@Testdef myFunction(): print("function running")

myFunction()

# 输出:# before running# function running# after running

好,以上就是对 Python 魔术方法对一个简单剖析,你可能会问,哪里能找到这些魔术方法的完整出处呢?我们一般参考的是 Python 的官方文档:

https://docs.python.org/zh-cn/3.7/reference/datamodel.html

但不幸的是,它们没有在一个统一的地方进行汇总,而是根据实际实现的功能被划分到了不同的章节,所以我们自己平时要多注意积累,多观察别人的代码中使用了哪些好用的魔法函数,并在自己的项目里多进行实践。

除了魔术方法,其实还有很多可以有效提高我们 Python 代码质量的语法技巧,比如装饰器、抽象基类、列表推导式、字典推导式等等。

我经常发现很多 Python 工程师工作了好几年,还是在用一些很基础的语言特性来写代码,这样就导致他们的实现总是不够优雅,运行效率也不高,很快就碰到了职业上升的天花板。所以说,平时多花一些时间多学习一些语言底层方面的知识和比较高阶的语言特性,对你的技术提升和职业成长是很有帮助的。

因此,我和极客时间合作,开设了一期 Python 进阶训练营,用 4 个实战项目串联起全部关键知识,学完即可建立起系统的 Python 开发进阶知识体系,并独立完成完成一个复杂的实战项目。

我会从一个简单的单线程爬虫案例开始,逐步扩展为多线程爬虫,紧接着对收集到的数据进行存储、清洗、分词以及情感分析,最后再通过 Web 直观地展示出来。学完后你不仅能掌握一系列高阶开发技能,也为后续转向 Web 开发、数据分析与处理、NLP、人工智能等领域打下了坚实的基础。详细课程大纲如下:

(上下滑动查看)

上下滑动查看课程大纲

训练营还配套了哪些服务?

1. 4 天线下教学 + 5 次线上直播 + 7 周刻意练习 + 助教每日答疑

老师将最关键的知识讲解、项目实战演示都放在了 4 天线下课,手把手、面对面进行教学,在高度集中的时间、空间中让你快速理解和掌握核心知识,再通过 7 周线上实战作业帮你进行巩固。老师还会进行 5 次直播,解答你在实战练习中的疑难点,也会有助教一起,随时对你的问题和进展进行反馈。

2. 高效学习社群 + 班主任带班,跟优秀的人一起学习

为了帮你坚持完成 50 天的学习,你的专属班主任会打造一个互助、互相监督的班级社群,让你和来自不同公司的优秀伙伴共同学习。班主任会每周督促你学习,关注你的学习体验,不定期组织大家进行线上分享活动,让你在 50 天内始终保持学习动力。

3. 开启一线大厂和 TGO 鲲鹏会 600 多家企业面试直通车

优秀毕业生毕业一年内,随时可获得极客大学提供的两次企业内推服务,更有 TGO 鲲鹏会的 600 家企业推荐通道为你敞开,获得更多的职业发展机会。

如何报名?

首期 Python 训练营将在北京线下小范围举办,限定人数,12 月 21 日开学。原价 3600,早鸟特惠 2499,线下大课及直播视频均可回看,早鸟仅限 100 人。欢迎你加入进来,跟我一起进阶 Python 技能。扫码加入

开学之前我还准备了一系列的免费公开课,希望帮助更多人在使用 Python 时,可以更加高效有质。公开课名额有限,仅限 500 人参与,你可以扫码添加学习助理,申请参与。

点击阅读原文,进阶 Python 高手,期待与你见面。