注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

Python面向对象:定制类(魔术方法)

Python编程 彭东稳 1185次浏览 已收录 0个评论

在Python中,看见形如 __xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的,称之为专有方法或魔术方法。比如 __len__()方法我们也知道是为了能让class作用于len()函数。这种特殊用途的函数在Python中有很多,可以帮助我们用来定制类。

__add__

来看下面这个例子,做一个简单的复数加法运算:

然后开始计算:

可以看出结果没有问题,但是 c = a.add(b)有点复杂,我们能不能直接实现 c = a + b呢?直接执行肯定会报错的,我们可以把类改成这样:

其实什么都没有边,支持把add方法变成了一个特殊的方法,我们再来执行一下:

把add变成特殊方法 __add__后,就可以直接进行加减了。而这种特殊方法就是Python特有的,称之为专有方法或魔术方法,我们一直使用的 __init__也属于专有方法。为什么称之为专有方法呢?因为这种方法名称都是固定的,比如 __add__,你不能随意改变它的名称,不然就无法使用了。要知道这类专有方法有哪些?可以通过help(数据类型)来查看。

__str__

我们先定义一个Student类,打印一个实例:

打印出内存地址,怎么样可以显示的好看一点呢?就可以使用专有方法 __str__了,只需要定义好 __str__()方法,返回一个好看的字符串就可以了:

这样打印出来的实例,不但好看,而且容易看出实例内部重要的数据。

但是你会发现如果直接敲变量不用print,打印出来的实例还是不好看:

这是因为直接显示变量调用的不是 __str__(),而是 __repr__() ,两者的区别是 __str__()返回用户看到的字符串,而 __repr__()返回程序开发者看到的字符串,也就是说, __repr__()是为调试服务的。

解决办法是再定义一个 __repr__()。但是通常 __str__()__repr__()代码都是一样的,所以,有个偷懒的写法:

这次就可以直接执行变量了。

__iter__

如果一个类想被用于for … in循环,类似list或tuple那样,就必须实现一个 __iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的 __next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:

现在,试试把Fib实例作用于for循环:

__getitem__

Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:

要表现得像list那样按照下标取出元素,需要实现 __getitem__()方法:

现在,就可以按下标访问数列的任意一项了:

但是list有个神奇的切片方法:

对于Fib却报错。原因是__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:

现在试试Fib的切片:

但是没有对step参数作处理:

也没有对负数作处理,所以,要正确实现一个 __getitem__()还是有很多工作要做的。此外,如果把对象看成dict, __getitem__() 的参数也可能是一个可以作key的object,例如str。

与之对应的是 __setitem__()方法,把对象视作list或dict来对集合赋值。最后,还有一个 __delitem__()方法,用于删除某个元素。

总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

__getattr__

正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义Student类:

调用name属性,没问题,但是,调用不存在的score属性,就有问题了:

错误信息很清楚地告诉我们,没有找到score这个attribute。

要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个 __getattr__()方法,动态返回一个属性。修改如下:

当调用不存在的属性时,比如score,Python解释器会试图调用 __getattr__(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值:

返回函数也是完全可以的:

只是调用方式要变为:

注意,只有在没有找到属性的情况下,才调用 __getattr__,已有的属性,比如name,不会在 __getattr__中查找。

此外,注意到任意调用如 s.abc都会返回None,这是因为我们定义的 __getattr__默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:

这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。

这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。举个例子:现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:http://api.server/user/friends、http://api.server/user/timeline/list。

如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。

利用完全动态的 __getattr__,我们可以写出一个链式调用:

试试:

这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变!

__call__

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。任何类,只需要定义一个 __call__()方法,就可以直接对实例进行调用。请看示例:

调用方式如下:

__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。

如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。

那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有 __call__()的类实例:

通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。

任何类,只需要定义一个 __call__()方法,就可以直接对实例进行调用。所以我们可以使用类来完成一个打印函数执行时间的装饰器,如下:

然后装饰一个sleep函数:

这就是使用类并利用 __call__专有方法完成的装饰器,拆解一下就是 new_sleep = Timeit(sleep(x)) ,由于Timeit有 __call__ ,所以 new_sleep实例变成可调用对象了,可以当函数一样直接执行。但是这样一来,怎么获取sleep函数的签名呢?直接 sleep.__name__肯定不行,正常的函数装饰器我们是使用解释器内置的functools.wraps装饰器来完成的,这里怎么使用wraps装饰器呢?如下定义两个函数测试一下wraps装饰器。

使用wraps封装函数a,传递给函数b。

如果你使用dir(b)看一下b有些什么的话,会发现比a多了一个 __wrapped__方法,这是wraps函数给接收方增加的,Python3才有。通过 __wrapped__可以获取到被wraps装饰的函数签名。

下面就可以改造Timeit函数了,如下:

获取函数签名:

下面我们再用装饰器完成一个单例模式编程,所谓单例,是指一个类的实例从始至终只能被创建一次,如下:

实例化两个类:

a is b为True可以看出引用对象是相同的,类A只被创建了一次,后面不管创建多少次都是同一个引用对象,这就实现了单例。首先要知道我们创建了装饰器 Singleton,所以在创建被装饰的类A时就会实例化 Singleton ,初始化了 instance = None并返回 wrap()函数,然后在创建实例 a时就会执行 wrap()函数。在创建实例 b时由于 instance有值了,所以直接返回。

在Python中实现单例编程有多种方法可以实现,除了上面使用nonlocal语句外,还可以使用 __call__专有方法来实现,因为把 __call__专有方法加入到类中后,这个类就变成可调用的了,可以当做装饰器使用了。如下:

看一下执行结果:

同样实现了单例编程。

<参考>

定制类

(译)Python魔法方法指南


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (1)or分享 (0)
关于作者:

您必须 登录 才能发表评论!