Python3之定制类
看到类似的__slots__这种形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的
Python中还有许多有特殊用途的函数,可以帮助我们定制类
__str__
先定义一个Student类,打印一个实例
>>> class Student(object): ... def __init__(self,name): ... self.name=name ... >>> print(Student('Zhangsan')) <__main__.Student object at 0x7f8a4830a748>
打印出<__main__.Student object at 0x7f8a4830a748>不好看,不直观
怎么才能打印的好看呢?只需要定义好__str__()方法,返回一个好看的字符串就可以了
>>> class Student(object): ... def __init__(self,name): ... self.name=name ... def __str__(self): ... return 'Student object(name:%s)' % self.name ... >>> >>> print(Student('Zhangsan')) Student object(name:Zhangsan)
这样打印出来的实例,不但好看 ,而且容易看出实例内部重要的数据
但是如果直接在终端敲变量而不用print,打印出来还是一样不好看
>>> Student('Zhangsan') <__main__.Student object at 0x7f8a4830a8d0>
这是因为直接显示变量调用的不是__str__()
,而是__repr__()
,两者的区别是__str__()
返回用户看到的字符串,而__repr__()
返回程序开发者看到的字符串,也就是说,__repr__()
是为调试服务的。
解决办法是再定义一个__repr__()
。但是通常__str__()
和__repr__()
代码都是一样的,所以,有个偷懒的写法:
>>> class Student(object): ... def __init__(self,name): ... self.name=name ... def __str__(self): ... return 'Student object(name:%s)' % self.name ... __repr__=__str__ ... >>> >>> Student('Zhangsan') Student object(name:Zhangsan)
__iter__
如果一个类想被用于for..in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法循环的拿到下一个值,直到遇到StopIteration错误退出循环
class Fib(object): def __init__(self): #初始化两个计数器 self.a,self.b=0,1 def __iter__(self): #实例本身就是迭代对象,返回自己 return self def __next__(self): self.a,self.b=self.b,self.a+self.b #设置循环退出条件 if self.a>10000: raise StopIteration() return self.a for n in Fib(): print(n)
输出
>>> for n in Fib(): ... print(n) ... 1 1 2 3 5 ... 46368 75025
__getitem__
Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素
>>> Fib()[5] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Fib' object is not subscriptable
要表现的象list那样按照下标取出元素,需要实现__getitem__()方法
special_getitem.py
class Fib(object): def __getitem__(self,n): a,b=1,1 for x in range(n): a,b=b,a+b return a
现在,就可以按下标访问数列的任意一项了
>>> f[0] 1 >>> f[1] 1 >>> f[100] 573147844013817084101
f[0]相当于把n=0参数传递给方法__getitem__(0),因为n=0,for循环不执行,返回a=1,
f[1]相当于把n=1参数传递给方法__getitem__(1),因为n=1,for循环执行一次a=b=1,b=a+b=2 返回值为a=1
f[2]相当于把n=2参数传递给方法__geritem__(2),第一次循环,a=b=1,b=a+b=2 再次循环 a=b=2,b=a+b=1+2=3 两次循环结束返回值a=2
以此类推
但是list有个切片的方法对应Fib确报错
>>> f[1:5] Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __getitem__ TypeError: 'slice' object cannot be interpreted as an integer
原因是__getitem__()传入的参数可能是一个int,也可以是一个切片对象slice,所以要判断
special_getitem.py
class Fib(object): def __getitem__(self,n): #n是整数 if isinstance(n,int): a,b=1,1 for x in range(n): a,b=b,a+b return a #n是切片类似[0:2] if isinstance(n,slice): 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 f=Fib() print(f[0]) print(f[1]) print(f[2]) print(f[3]) print(f[0:3])
如果传递的参数是整数则和上面的方法不变,一次返回一个整数
如果传递的参数是切片slice则从切片的start开始至stop结束返回一个列表
运行结果如下
1 1 2 3 [1, 1, 2]
如果传递参数是切片,执行过程分析f[0:1]切片取索引为0及第一个元素的列表
if判断[0:1]是slice start=n.start 所以start=None stop=n.stop stop=1 if判断如果start为None则把start置为0 定义初始两位数为1,1 定义空列表L=[] 执行循环 for x in range(1): x=1执行第一次循环 if判断1>=1满足条件 执行append语句后L=[1] 接着执行往下语句a,b=b,a+b a=b,b=a+b是同时执行执行完毕后 a=1 b=2 退出for循环返回列表L=[1]
如果传递的切片参数为[0:2]
if判断[0:2]是slice start=n.start 所以start=None stop=n.stop stop=2 if判断如果start为None则把start置为0 定义初始两位数为1,1 定义空列表L=[] 执行循环 for x in range(1): x=0执行第一次循环 if判断0>=0满足条件 执行append语句后L.append(a) 及L.append(1) L=[1] 接着执行往下语句a,b=b,a+b a=b,b=a+b是同时执行执行完毕后 a=1 b=2 for循环返回列表L=[1] x=1执行第二次循环 if判断1>=1满足条件 执行append语句L.append(a)及L.append(1) L=[1,1]
执行a,b=b,a+b执行完毕后
a=2 b=3 因为stop=2执行两次以后退出循环 返回列表L=[1,1]
如果传递是切片参数是[0:3]
if判断[0:3]是slice start=n.start 所以start=None stop=n.stop stop=3 if判断如果start为None则把start置为0 定义初始两位数为1,1 定义空列表L=[] 执行循环 for x in range(1): x=0执行第一次循环 if判断0>=0满足条件 执行append语句后L.append(a) 及L.append(1) L=[1] 接着执行往下语句a,b=b,a+b a=b,b=a+b是同时执行执行完毕后 a=1 b=2 for循环返回列表L=[1] x=1执行第二次循环 if判断1>=0满足条件 执行append语句L.append(a)及L.append(1) L=[1,1] 执行a,b=b,a+b执行完毕后 a=2 b=3 x=2执行第三次循环 if判断2>=0满足条件 执行append语句L.append(a)及L.append(2) L=[1,1,2] 因为stop=3执行三次次以后退出循环 返回列表L=[1,1,2]
以此类推
如果start不是 0
for循环会继续执行,但是还没有到start处因为不满足x>=start条件所以L.append(a)不会执行,不会往列表内追加元素 但是a,b=b,a+b会继续执行
关于切片及切片函数slice参考:https://www.cnblogs.com/minseo/p/11113638.html
但是以上还是有缺陷没有对step参数做处理
print(f[0:10:2]) [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
虽然加了参数:2步数还是1
也没有对负数进行处理,所以,要正确实现以下__getitem__()还有很多工作要做
此外,如果把对象看成dict,__getitem__()的参数也可能是一个可以做key的object,例如str。
与之对应的是__setitem__()
方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()
方法,用于删除某个元素。
__getattr__
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定了Student类
class Student(object): def __init__(self): self.name="Zhangsan"
调用存在的属性name没有问题,但是调用不存在的score属性就有问题了
>>> s.name 'Zhangsan' >>> s.score Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute 'score'
错误信息很清楚地告诉我们,没有找到score这个attribute
要避免这个错误,除了可以加上一个score属性外,python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性
special_getattr.py
class Student(object): def __init__(self): self.name = 'Michael' def __getattr__(self, attr): if attr=='score': return 99
当调用不存在的属性时,比如score
,Python解释器会试图调用__getattr__(self, 'score')
来尝试获得属性,这样,我们就有机会返回score
的值:
>>> s=Student() >>> s.name 'Michael' >>> s.score 99
返回函数也是完全可以的
class Student(object): def __init__(self): self.name = 'Michael' def __getattr__(self, attr): if attr=='score': return 99 def __getattr__(self,attr): if attr=='age': return lambda:25 s=Student() print(s.age())
调用方式改为s.age
注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性比如name,不会再__getattr__中查找
此外,注意到任意调用如s.abc
都会返回None
,这是因为我们定义的__getattr__
默认返回就是None
。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError
的错误:
>>> s=Student() >>> s.name 'Michael' >>> s.aba >>> print(s.aba) None
改成如下
def __getattr__(self,attr): if attr=='age': return lambda:25 raise AttributeError('\'Student\' object has no attribute \'%s\'' %attr)
__call__一个对象实例可以有自己是属性和方法,当我们调用实例方法时,我们用instance.method()来调用,能不能直接在实例本身上调用呢?
任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用
special_call.py
class Student(object): def __init__(self,name): self.name=name def __call__(self): print('My name is %s.' % self.name)
调用方法如下
>>> s=Student('Zhansan') >>> s() My name is Zhansan.
__call__()
还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable
对象,比如函数和我们上面定义的带有__call__()
的类实例:
>>> callable(Student('Zhangsan')) True >>> callable(max) True >>> callable([1,2,3]) False >>> callable(None) False >>> callable('str') False
通过callable()
函数,我们就可以判断一个对象是否是“可调用”对象。