从零开始学Python-Day46-面向对象高级编程-定制类

Python零基础 木人张 3年前 (2020-04-20) 697次浏览 0个评论 扫描二维码
文章目录[隐藏]

类似__slots__这种形式的变量或者函数名在Python中是有特殊用途的。除此之外,Python的类还有许多这样有特殊用途的函数,可以帮助我们定制类。

__str__

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

>>> class Student():
	def __init__(self, name):
		self.name = name

		
>>> print(Student('Woodman'))
<__main__.Student object at 0x106391130>

打印返回的默认字符串略显复杂,我们其实可以通过__str__来定义返回的字符串:

>>> class Student():
	def __init__(self, name):
		self.name = name
	def __str__(self):
		return 'Student object (name: %s)' % self.name

	
>>> print(Student('Woodman'))
Student object (name: Woodman)

好看其实是次要的,主要我们很清楚的看到实例内部的重要数据。但是我们发现,如果不用print,直接调用变量好像__str__不起作用了:

>>> s = Student('Woodmanzhang')
>>> s
<__main__.Student object at 0x106391130>

这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。
解决办法是再定义一个__repr__()。__str__()和__repr__()代码都是一样的,所以:

>>> class Student():
	def __init__(self, name):
		self.name = name
	def __str__(self):
		return 'Student object (name: %s)' % self.name
	__repr__ = __str__

	
>>> s = Student('WoodmanZhang')
>>> s
Student object (name: WoodmanZhang)

__iter__

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

>>> class Fib():
	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 > 1000:            # 退出循环的条件
			raise StopIteration()
		return self.a                 # 返回下一个值

	
>>> for n in Fib():
	print(n)

	
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987

__getitem__

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

>>> Fib()[5]
Traceback (most recent call last):
  File "<pyshell#41>", line 1, in <module>
    Fib()[5]
TypeError: 'Fib' object is not subscriptable

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

>>> class Fib():
	def __getitem__(self, n):
		a, b = 1, 1
		for x in range(n):
			a, b = b, a + b
		return a

	
>>> f=Fib()
>>> f[0]
1
>>> f[100]
573147844013817084101
>>> f[10]
89

如果我们对它切片呢?

>>> f[0:5]
Traceback (most recent call last):
  File "<pyshell#53>", line 1, in <module>
    f[0:5]
  File "<pyshell#48>", line 4, in __getitem__
    for x in range(n):
TypeError: 'slice' object cannot be interpreted as an integer

这是因为__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以需要做判断:

>>> class Fib():
	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

		
>>> f= Fib()
>>> f[1:5]
[1, 2, 3, 5]
>>> f[:5]
[1, 1, 2, 3, 5]

但其实这里没有对step参数和负数做处理,所以要正确完整使用__getitem__()还需要更多代码工作。此外,如果把对象看成dict,__getitem__()的参数也可能是一个可以作key的object,例如str。与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

__getattr__

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

>>> class  Student():
	def __init__(self):
		self.name = 'Woodman'

		
>>> s = Student()
>>> print(s.name)
Woodman
>>> print(s.score)
Traceback (most recent call last):
  File "<pyshell#83>", line 1, in <module>
    print(s.score)
AttributeError: 'Student' object has no attribute 'score'

name我们在类中定义了,而score没有,直接报错提示我们没有找到score这个attribute。
要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。修改如下:

>>> class Student():
	def __init__(self):
		self.name = 'Woodman'
	def __getattr__(self, attr):
		if attr == 'score':
			return 99

		
>>> s = Student()
>>> s.name
'Woodman'
>>> s.score
99
>>> 

对于并没有直接定义的score属性,Python解释器会试图调用__getattr__(self, ‘score’)来尝试获得属性,这样返回的score值。
返回函数也是完全可以的,只不过调用方式也要变:

>>> class Student():
	def __getattr__(self, attr):
		if attr == 'age':
			return lambda: 35

		
>>> s = Student()
>>> s.age()
35

注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如name,不会在__getattr__中查找。
此外,注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:

>>> class Student():
	def __getattr__(self, attr):
		if attr=='age':
			return lambda: 35
		raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。

__call__

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

>>> class Student():
	def __init__(self, name):
		self.name = name
	def __call__(self):
		print('My name is %s.' % self.name)

		
>>> s = Student('WoodmanZhang')
>>> s()
My name is WoodmanZhang.

__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例,通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。

>>> callable(Student('Woodmanzhang'))
True
>>> callable(max)
True
>>> callable(Student)
True
>>> callable([1,2,3])
False
>>> callable(None)
False
>>> callable('str')
False

木人张,版权所有丨如未注明 , 均为原创,禁止转载。
喜欢 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址