在Python中,看见形如__xxx__
的变量或者函数名就要注意,这些在Python中是有特殊用途的,称之为专有方法或魔术方法。比如__len__()
方法我们也知道是为了能让class作用于len()函数。这种特殊用途的函数在Python中有很多,可以帮助我们用来定制类。
__add__
来看下面这个例子,做一个简单的复数加法运算:
1 2 3 4 5 6 |
class Complex: def __init__(self, real, imag=0): self.real = real self.imag = imag def add(self, other): return Complex(self.real + other.real, self.imag + other.imag) |
然后开始计算:
1 2 3 4 5 6 7 |
>>> a = Complex(2,3) #创建实例a; >>> b = Complex(1,2) #创建实例b; >>> c = a.add(b) #加法运算并创建实例c; >>> c.real #打印实数; 3 >>> c.imag #打印虚数; 5 |
可以看出结果没有问题,但是c = a.add(b)有点复杂,我们能不能直接实现c = a + b呢?直接执行肯定会报错的,我们可以把类改成这样:
1 2 3 4 5 6 |
class Complex: def __init__(self, real, imag=0): self.real = real self.imag = imag def __add__(self, other): return Complex(self.real + other.real, self.imag + other.imag) |
其实什么都没有边,支持把add方法变成了一个特殊的方法,我们再来执行一下:
1 2 3 4 5 6 7 |
>>> a = Complex(2,3) >>> b = Complex(1,2) >>> c = a + b >>> c.real 3 >>> c.imag 5 |
把add变成特殊方法__add__
后,就可以直接进行加减了。而这种特殊方法就是Python特有的,称之为专有方法或魔术方法,我们一直使用的__init__
也属于专有方法。为什么称之为专有方法呢?因为这种方法名称都是固定的,比如__add__
,你不能随意改变它的名称,不然就无法使用了。要知道这类专有方法有哪些?可以通过help(数据类型)来查看。
__str__
我们先定义一个Student类,打印一个实例:
1 2 3 |
class Student(object): def __init__(self, name): self.name = name |
1 2 |
>>> print(Student('dkey')) <__main__.Student object at 0x100ab07b8> |
打印出内存地址,怎么样可以显示的好看一点呢?就可以使用专有方法__str__
了,只需要定义好__str__()
方法,返回一个好看的字符串就可以了:
1 2 3 4 5 |
class Student(object): def __init__(self, name): self.name = name def __str__(self): return 'Student object (name: %s)' % self.name |
1 2 |
>>> print(Student('dkey')) Student object (name: dkey) |
这样打印出来的实例,不但好看,而且容易看出实例内部重要的数据。
但是你会发现如果直接敲变量不用print,打印出来的实例还是不好看:
1 2 3 |
>>> s = Student('dkey') >>> s <__main__.Student object at 0x100ab0c50> |
这是因为直接显示变量调用的不是__str__()
,而是__repr__()
,两者的区别是__str__()
返回用户看到的字符串,而__repr__()
返回程序开发者看到的字符串,也就是说,__repr__()
是为调试服务的。
解决办法是再定义一个__repr__()
。但是通常__str__()
和__repr__()
代码都是一样的,所以,有个偷懒的写法:
1 2 3 4 5 6 |
class Student(object): def __init__(self, name): self.name = name def __str__(self): return 'Student object (name=%s)' % self.name __repr__ = __str__ |
这次就可以直接执行变量了。
1 2 3 |
>>> s = Student('dkey') >>> s Student object (name=dkey) |
__iter__
如果一个类想被用于for … in循环,类似list或tuple那样,就必须实现一个__iter__()
方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()
方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。
我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Fib(object): def __init__(self): self.a, self.b = 0, 1 #初始化两个计数器a,b def __iter__(self): return self #实例本身就是迭代对象,故返回自己 def __next__(self): self.a, self.b = self.b, self.a + self.b #计算下一个值 if self.a > 100000: #退出循环的条件 raise StopIteration() return self.a #返回下一个值 |
现在,试试把Fib实例作用于for循环:
1 2 3 4 5 6 7 8 |
>>> for n in Fib(): ... print(n) ... 1 1 2 3 ..... |
或者使用 iter() 函数返回一个迭代器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Cursor: def __init__(self): self.count = 0 def fetchone(self): while True: self.count += 1 if self.count == 10: break return self.count def __iter__(self): return iter(self.fetchone, None) for i in Cursor(): print(i) |
__getitem__
Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:
1 2 3 4 |
>>> Fib()[5] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Fib' object does not support indexing |
要表现得像list那样按照下标取出元素,需要实现__getitem__()
方法:
1 2 3 4 5 6 |
class Fib(object): def __getitem__(self, n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a |
现在,就可以按下标访问数列的任意一项了:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> f = Fib() >>> f[0] 1 >>> f[1] 1 >>> f[2] 2 >>> f[3] 3 >>> f[10] 89 >>> f[100] 573147844013817084101 |
但是list有个神奇的切片方法:
1 2 |
>>> list(range(100))[5:10] [5, 6, 7, 8, 9] |
对于Fib却报错。原因是__getitem__()
传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Fib(object): def __getitem__(self, n): if isinstance(n, int): # n是索引 a, b = 1, 1 for x in range(n): a, b = b, a + b return a if isinstance(n, slice): # n是切片 start = n.start stop = n.stop if start is None: start = 0 a, b = 1, 1 L = [] for x in range(stop): if x >= start: L.append(a) a, b = b, a + b return L |
现在试试Fib的切片:
1 2 3 4 5 |
>>> f = Fib() >>> f[0:5] [1, 1, 2, 3, 5] >>> f[:10] [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] |
但是没有对step参数作处理:
1 2 |
>>> f[:10:2] [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] |
也没有对负数作处理,所以,要正确实现一个__getitem__()
还是有很多工作要做的。此外,如果把对象看成dict,__getitem__()
的参数也可能是一个可以作key的object,例如str。
与之对应的是__setitem__()
方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()
方法,用于删除某个元素。
总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
__getattr__
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义Student类:
1 2 3 |
class Student(object): def __init__(self): self.name = 'dkey' |
调用name属性,没问题,但是,调用不存在的score属性,就有问题了:
1 2 3 4 5 6 7 |
>>> s = Student() >>> print(s.name) dkey >>> print(s.score) Traceback (most recent call last): ... AttributeError: 'Student' object has no attribute 'score' |
错误信息很清楚地告诉我们,没有找到score这个attribute。
要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()
方法,动态返回一个属性。修改如下:
1 2 3 4 5 6 |
class Student(object): def __init__(self): self.name = 'dkey' def __getattr__(self, attr): if attr == 'score': return 99 |
当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self, 'score')
来尝试获得属性,这样,我们就有机会返回score的值:
1 2 3 4 5 |
>>> s = Student() >>> s.name 'dkey' >>> s.score 99 |
返回函数也是完全可以的:
1 2 3 4 |
class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25 |
只是调用方式要变为:
1 2 |
>>> s.age() 25 |
注意,只有在没有找到属性的情况下,才调用__getattr__
,已有的属性,比如name,不会在__getattr__
中查找。
此外,注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__
默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:
1 2 3 4 5 |
class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25 raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr) |
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。举个例子:现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:http://api.server/user/friends、http://api.server/user/timeline/list。
如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。
利用完全动态的__getattr__
,我们可以写出一个链式调用:
1 2 3 4 5 6 7 8 |
class Chain(object): def __init__(self, path=''): self._path = path def __getattr__(self, path): return Chain('%s/%s' % (self._path, path)) def __str__(self): return self._path __repr__ = __str__ |
试试:
1 2 |
>>> Chain().status.user.timeline.list '/status/user/timeline/list' |
这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变!
__call__
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。任何类,只需要定义一个__call__()
方法,就可以直接对实例进行调用。请看示例:
1 2 3 4 5 |
class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s.' % self.name) |
调用方式如下:
1 2 3 |
>>> s = Student('dkey') >>> s() My name is dkey. |
__call__()
还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()
的类实例:
1 2 3 4 5 6 7 8 9 10 |
>>> callable(Student('dkey')) True >>> callable(max) True >>> callable([1, 2, 3]) False >>> callable(None) False >>> callable('str') False |
通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。
任何类,只需要定义一个__call__()
方法,就可以直接对实例进行调用。所以我们可以使用类来完成一个打印函数执行时间的装饰器,如下:
1 2 3 4 5 6 7 8 9 10 11 |
import time from functools import wraps class Timeit: def __init__(self, fn): self.fn = fn def __call__(self, *args, **kwargs): start = time.time() ret = self.fn(*args, **kwargs) print(time.time() - start) return ret |
然后装饰一个sleep函数:
1 2 3 4 5 6 7 8 9 10 |
# 装饰sleep函数; >>> @Timeit ... def sleep(x): ... time.sleep(x) ... ... # 执行函数并打印出函数执行时间; >>> sleep(2) 2.00403308868 |
这就是使用类并利用__call__
专有方法完成的装饰器,拆解一下就是new_sleep = Timeit(sleep(x)) ,由于Timeit有__call__
,所以new_sleep实例变成可调用对象了,可以当函数一样直接执行。但是这样一来,怎么获取sleep函数的签名呢?直接sleep.__name__
肯定不行,正常的函数装饰器我们是使用解释器内置的functools.wraps装饰器来完成的,这里怎么使用wraps装饰器呢?如下定义两个函数测试一下wraps装饰器。
1 2 3 4 5 |
def a(): pass def b(): pass |
使用wraps封装函数a,传递给函数b。
1 2 3 4 5 6 |
>>> wraps(a)(b) <function a at 0x7f124c290048> >>> a.__name__ 'a' >>> b.__name__ #wrap装饰哪个函数,其签名就是谁; 'a' |
如果你使用dir(b)看一下b有些什么的话,会发现比a多了一个__wrapped__
方法,这是wraps函数给接收方增加的,Python3才有。通过__wrapped__
可以获取到被wraps装饰的函数签名。
1 2 3 |
>>> f = wraps(a)(b) >>> f.__wrapped__ <function a at 0x7f124c2902f0> |
下面就可以改造Timeit函数了,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import time from functools import wraps class Timeit: def __init__(self, fn): self.wrapped = wraps(fn)(self) def __call__(self, *args, **kwargs): start = time.time() ret = self.wrapped.__wrapped__(*args, **kwargs) print(time.time() - start) return ret @Timeit def sleep(x): time.sleep(x) |
获取函数签名:
1 2 |
>>> sleep.__name__ 'sleep' |
下面我们再用装饰器完成一个单例模式编程,所谓单例,是指一个类的实例从始至终只能被创建一次,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# 创建装饰器; from functools import wraps def Singleton(cls): instance = None print(instance) @wraps(cls) def wrap(*args, **kwargs): nonlocal instance #Python3才支持nonlocal语句,用来引入外层函数中的变量,也是这个单例的核心所在; if instance is None: instance = cls(*args, **kwargs) print(instance) return instance return wrap # 装饰类A,在装饰时就会实例化Singleton; @Singleton class A(): pass |
实例化两个类:
1 2 3 4 5 |
>>> a = A() #创建实例a,执行wrap函数; <__main__.A object at 0x7f88082e6b00> >>> b = A() #创建实例b,没有执行wrap函数; >>> a is b #比较a和b的对象是否相同; True |
a is b为True可以看出引用对象是相同的,类A只被创建了一次,后面不管创建多少次都是同一个引用对象,这就实现了单例。首先要知道我们创建了装饰器Singleton,所以在创建被装饰的类A时就会实例化Singleton ,初始化了instance = None并返回wrap()函数,然后在创建实例a时就会执行wrap()函数。在创建实例b时由于instance有值了,所以直接返回。
在Python中实现单例编程有多种方法可以实现,除了上面使用nonlocal语句外,还可以使用__call__
专有方法来实现,因为把__call__
专有方法加入到类中后,这个类就变成可调用的了,可以当做装饰器使用了。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Singleton: def __init__(self, cls): self.cls = cls self.instance = None print self.cls def __call__(self, *args, **kwargs): if self.instance is None: self.instance = self.cls(*args, **kwargs) return self.instance @Singleton class A: pass |
看一下执行结果:
1 2 3 4 |
>>> a = A() >>> b = A() >>> a is b True |
同样实现了单例编程。
<参考>