28.定制一个类
前言
前面已经学习了Python中自带的__slot__
等,形如:__xxxx__
的都是在Python内部有特殊用途的变量或方法。但开发过程中,很多时候需要自己设计的类,需要类似于Python自带的类的功能。比如,想让自己的类的对象,可以for...in循环等。今天就来学习如何利用Python提供的已有的方法,完全定制一个类。
本文涉及到的方法:__str__()
, __iter__()
, __next__()
, __getitem__()
, __getattr__()
__str__()
方法
# 定义一个Car类
class Car(object):
color = "白色" # 车身颜色,默认:白色
seatCount = 4 # 可乘坐人数,默认4
# 创建一个汽车对象
c = Car()
# 打印
print(c)
运行结果:<__main__.Car object at 0x0000018747A34F28>
我们前面已经知道:打印结果中的__main__
表示直接运行的当前模块,而不是被其他模块引用运行的。Car表示类名;object at 0x0000018747A34F28
表示对象在内存中分配的地址是:0x0000018747A34F28
。该内存地址你我一般是不同,即使我自己运行多次也是分配不同的。
现在,我不想打印出如上的这么一串内存地址,而是希望打印更有意义的信息,改如何办呢?答案是:重新类中的__str__()
方法。这是因为print()
函数在打印是找的就是对象的__str__
方法。
# 修改类的定义为(此类定义中重写了__str__()方法)
class Car(object):
color = "白色" # 车身颜色,默认:白色
seatCount = 4 # 可乘坐人数,默认4
def __str__(self):
return "Car车身颜色:" + self.color + ";可乘坐人数:%s" % self.seatCount
# 创建一个汽车对象
c2 = Car()
# 打印
print(c2)
运行结果:Car车身颜色:白色;可乘坐人数:4
如此,就可以自定义灵活地定义一个对象的打印内容了。
__iter__()
, __next__()
方法
如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()
方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()
方法拿到循环的下一个值,直到遇到StopIteration
错误时退出循环。
我们以斐波那契数列为例,写一个Fibonacci类,可以作用于for循环:
# 斐波那契数列Fibonacci
class Fibonacci(object):
def __init__(self):
self.a, self.b = 1, 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
# 创建一个Fibonacci对象f:
f = Fibonacci()
# for...in 遍历对象f:
for n in f:
print(n)
运行结果:
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
__getitem__()
方法
对Fibonacci
创建的对象f实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第3个元素:
# __getitem__
# 上接Fibonacci类的定义
print(f[3])
运行结果:
File "F:/python_projects/clazz/diy_class.py", line 48, in <module>
print(f[3])
TypeError: 'Fibonacci' object does not support indexing
解释说明:f[3]
在取元素时,报TypeError: 'Fibonacci' object does not support indexing
,即:Fibonacci
的对象不支持通过索引获取元素。
此时,要表现得像list那样按照下标取出元素,需要实现__getitem__()
方法:
# 斐波那契数列Fibonacci
class Fibonacci(object):
def __getitem__(self, index):
a, b = 1, 1
for i in range(index):
a, b = b, a + b
return a
# 创建一个Fibonacci对象f:
f = Fibonacci()
# __getitem__
print(f[3])
print(f[100])
运行结果:
3
573147844013817084101
高阶扩展
但是list有个非常强大好用的切片
功能,
l = list(range(50))[5:10]
print(l)
运行结果:[5, 6, 7, 8, 9]
。我们试着对自定义的类Fibonacci的对象f试试看:
# 上接Fibonacci类的定义:
f[3, 5]
运行结果:
Traceback (most recent call last):
File "F:/python_projects/clazz/diy_class.py", line 62, in <module>
print(f[5:10])
File "F:/python_projects/clazz/diy_class.py", line 28, in __getitem__
for i in range(index):
TypeError: 'slice' object cannot be interpreted as an integer
原因是getitem()传入的参数可能是一个int,也可能是一个切片对象slice,所以我们需要重新定义上面Fibonacci中的__getitem__()
方法,对于参数要做判断是整数还是切片对象:
# 斐波那契数列Fibonacci
class Fibonacci(object):
def __getitem__(self, index):
if isinstance(index, int): # 索引值
a, b = 1, 1
for i in range(index):
a, b = b, a + b
return a
elif isinstance(index, slice): # 切片对象
start = index.start
stop = index.stop
if start is None:
start = 0
a, b = 1, 1
my_list = []
for x in range(stop):
if x > start:
my_list.append(a)
a, b = b, a+b
return my_list
pass
# 创建一个Fibonacci对象f:
f = Fibonacci()
print(f[5:10])
运行结果:[13, 21, 34, 55]
上面对自定义类的对象简单做了切片处理。但距离list还有很多细节处理,如:没有对负数作处理,所以,要正确实现一个__getitem__()
还是有很多工作要做的。
此外,如果把对象看成dict
,__getitem__()
的参数也可能是一个可以作key
的object
,例如str
。
与之对应的是__setitem__()
方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()
方法,用于删除某个元素。
总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
__getattr__()
方法
通过前面学习,我们已经知道正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错:AttributeError
。
比如本文最前面定义的Car
类:我们为Car类设计了两个属性:车身颜色color
、车可乘坐人数seatCount
。假如我要打印车的品牌logo,如下:
# 定义一个Car类
class Car(object):
color = "白色" # 车身颜色,默认:白色
seatCount = 4 # 可乘坐人数,默认4
# 创建一个汽车对象
c = Car()
# 设置车身颜色:银色,并打印出来。
c.color = "银色"
print(c.color)
# 打印车的logo
print(c.logo)
运行结果:
银色
Traceback (most recent call last):
File "F:/python_projects/clazz/diy_class.py", line 21, in <module>
print(c.logo)
AttributeError: 'Car' object has no attribute 'logo'
车身颜色设置成功,并可成功打印出来:银色。而并没有logo
属性,报错AttributeError
。错误信息很清楚地告诉我们,Car类的对象没有找到logo
这个属性。
要避免这个错误,除了可以加上一个logo
属性外,Python还有另一个机制,那就是写一个__getattr__()
方法,动态返回一个属性。修改如下:
# 定义一个Car类
class Car(object):
color = "白色" # 车身颜色,默认:白色
seatCount = 4 # 可乘坐人数,默认4
def __getattr__(self, attr_name):
if "logo" == attr_name:
return "宝马"
# 创建一个汽车对象
c = Car()
# 设置车身颜色:银色,并打印出来。
c.color = "银色"
print(c.color)
# 打印车的logo
print(c.logo)
运行结果:
银色
宝马
解析:当调用不存在的属性时,比如上面例子中的logo
时,Python解释器会试图调用__getattr__(self, 'logo')
来尝试获得属性,这样,我们就有机会返回logo
的值。
特别注意,只有在没有找到属性的情况下,才调用__getattr__()
方法,已有的属性,比如上面例子中的color
,不会在__getattr__
中查找。
此外,注意到任意调用不存在的属性,如c.abc
都会返回None
,这是因为我们定义的__getattr__()
默认返回就是None
。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError
的错误:
更新Car类的定义如下:
# 定义一个Car类
class Car(object):
color = "白色" # 车身颜色,默认:白色
seatCount = 4 # 可乘坐人数,默认4
def __getattr__(self, attr_name):
if "logo" == attr_name:
return "宝马"
raise AttributeError('\'Car\' object has no attribute \'%s\'' % attr_name)
# 创建一个汽车对象
c = Car()
# 随意调用属性abc,仍会报错
c.abc
运行结果:
File "F:/python_projects/clazz/diy_class.py", line 30, in <module>
print(c.abc)
File "F:/python_projects/clazz/diy_class.py", line 9, in __getattr__
raise AttributeError('\'Car\' object has no attribute \'%s\'' % attr_name)
AttributeError: 'Car' object has no attribute 'abc'
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
小结
本文主要学习如何利用Python提供的已有的方法,完全定制一个类。涉及到的方法:__str__()
, __iter__()
, __next__()
, __getitem__()
, __getattr__()
。
更多了解,可关注公众号:人人懂编程
微信公众号:人人懂编程