yokon's blog

Python的定制类笔记

2018.01.28

前言

最近在阅读flask源码的时候,经常看到一些python类中,类似于__xxx__的变量和 函数名,这些大多是在python中有特殊用途的。Pythonclass中有许多类似于这样 的函数,我们用他们来定制我们的类,来实现一些class本身不具有的方法。由于有些方 法我也不是很明白,为了能更好的理解他们,索性就做一个总结。参考谷歌的博客及官方 文档

先简单定义一个类:

class Me(object):

    def __init__(self, name):
		self.name = name

__call__

Python中,调用函数可以直接使用abs(123)来调用,而调用一个类的实例方法时, 都是使用instance.method()来调用。那如果我们想像调用函数那样调用实例,就需要 __call__()方法了。以上面的类为例:

...

def __call__(self, age):
    print('{name}\'s age is {age}.'.format(name=self.name,age=age))

直接调用它:

>>> me = Me('GuTianle')
>>> me(20)
GuTianle's age is 20.

其实在Python中,对象和函数的界限是很模糊的,函数可以动态创建出来,当然类的实 例对象也是动态创建出来的,如果类中有__call__()方法,那其实对象和函数没有根本 的区别。

如果有一个变量a = 1,我们如何判断实例mea是对象或者函数还是变量呢?答案 是使用callable()函数,一个可被调用的对象就是一个Callable对象:

>>> callable(me) # Me类的实例
True
>>> callable(a) # 变量 a=1
False
>>> callable(abs) # 求绝对值的内置函数
True

__repr__

__repr__()方法是比较常用的方法,通常用于调试,表示对象的主要信息。依然是上面 的例子:

>>> me = Me('GuTianle')
>>> me
<__main__.Me object at 0x0000000002A01E48>

返回的结果显然不是我们想要的,如果能直接打印出实例的信息就好了,解决方法是定义 __repr__()方法,让他返回一个我们需要的信息。

...

def __repr__(self):
    return '<Me object {name}>'.format(name=self.name)

接着打印实例:

>>> me = Me('GuTianle')
>>> me
<Me object GuTianle>

__getattr__

仍然以上面的类为例,当我们调用他的sex属性时,由于他并不存在,就会报错:

>>> me = Me('GuTianle')
>>> print(me.name)
GuTianle
>>> print(me.sex)
Traceback (most recent call last):
...
AttributeError: 'Me' object has no attribute 'sex'

通常我们调用一个类的属性和方法时,如果调用的不存在,就会报错。这种情况,我们只 要给他加一个sex属性就好了。但是,有些情况我们并不需要sex属性,但是当调用 sex属性的属性的时候可以给他返回结果。这就需要定义一个__getattr__()方法来动 态返回一个属性:

...

def __getattr__(self, attr):
    if attr == 'name':
		return 'Zhazhahui'
    if attr == 'sex':
		if self.name == 'GuTianle':
	    	return 'male'
		return 'female'

现在再调用他:

>>> print(me.sex)
'male'
>>> print(me.name)
'GuTianle'
>>> print(me.height)
None

但是如果我们在__getattr__()方法下,在判断if attr == 'name':...,当我们调用 name属性时,程序首先在类中查找是否有name属性,如果有,程序是不会执行 __getattr__()下的判断的。

此外,从上面的调用可以看到调用没有定义的height属性时,是直接返回None,并没 有报错,这是因为__getattr__()方法默认是返回None,我们只需要对不需要的属性调 用,加一个抛出异常的处理就可以了.

...

def __getattr__(self, attr):
	if attr == 'name':
		return 'Zhazhahui'
	if attr == 'sex':
		if self.name == 'GuTianle':
			return 'male'
		return 'female'
	raise AttributeError('\'Me\' object has no attribute \'{attr}\''.format(attr=attr)

当然动态返回一个属性可以,返回一个方法当然也是可以的,比如给类加一个返回年龄的 方法:

...

def __getattr__(self, attr):
	if attr == 'age':
		return lambda: 20

现在,虽然类中没有age方法,但是调用他并不会保错:

>>> me.age()
20

__iter__和__getitem__

如果一个类中定义了__iter__()或者__getitem__()方法,那么这个类就是可迭代类。

如果我们需要写一个迭代器类,即是希望一个类能被for in循环,就必须要定义一个 __iter__()方法,有了__iter__()方法,我们还需要__next__()方法 (__next__()不能还有参数)用来循环调用返回下一个值,知道遇到StopIteration错 误退出循环。我们以一个循环为例:

class Adder(object):

    def __init__(self):
		self.a = 0

    def __iter__(self):
        return self

    def __next__(self):
		self.a += 1
		if self.a > 100:
	    	raise StopIteration()
	    return self.a

循环这个Adder()实例:

>>> for i in Adder():
...    print(i)
...
1
2
3
...
100

虽然__iter__()方法只是返回自身,似乎只要实现__next__()方法就可以了,其实不 然,程序在循环迭代的时候会首先查找实例是否有__iter__()方法,来确定他是否是一 个迭代对象。如果我们去掉__iter__()方法,再次调用就会得到报错:

TypeError: 'Adder' object is not iterable

如果实例中没有__iter__但是有__getitem__的时候,也可以被迭代。循环会从0开 始依次读取相应的下标,直到发生IndexError为止:

class Test(object):

	def __getitem__(self, n):
		a = 0
		for i in range(n):
			a += 1
		return a

循环这个实例:

>>> t = Test()
>>> for i in t:
...     print(i)
...
0
1
2
......

__getitem__()方法最主要的功能是让实例像list一样按住所以取出元素,仍然是上面的 例子:

>>> t = Test()
>>> t[0]
0
>>> t[99]
99
>>> t[1000]
1000

如果要想像 list 那样实现切片功能,就需要判断__getitem__()方法传入的参数是 int还是切片对象slice

...
def __getitem__(self, n):
	a = 0
	if isinstance(n, int):
		...

	if isinstance(n, slice):
		start = n.start
		stop = n.stop
		if start is None:
			start = 0
		if stop is None:
			stop = 100
		List = []
		for i in range(stop):
			if i >= start:
				List.append(a)
			a += 1
		return List

调用实例:

>>> t = Test()
>>> t[:10]
[0,1,2,3,4,5,6,7,8,9]

当然这只是基础的切片处理,还有很多比如对step参数,负数,做处理。但是这也不难 看出__getitem__()的强大。对应的是__setitem__()方法,把对象视作 list 或 dict 来对集合赋值。最后,还有__delitem__()方法,用于删除某个元素。

__len__

如果一个类的某个属性是一个集合,如果想要使用len()函数直接获取类的某个属性有多 少个属性,类必须提供一个__len__()方法,让他定义返回某个属性元素的个数:

class Test(object):

	def __init__(self, n):
		a, test_list = 0, []
		for i in range(n):
			if a % 2 == 0:
				test_list.append(a)
			a += 1
		self.L = test_list

	def __len__(self):
		return len(self.L)

	def __repr__(self):
		return str(self.L)

调用测试:

>>> t = Test(10)
>>> t
[0, 2, 4, 6, 8]
>>> len(t)
5

有了__len__()方法我们可以很方便的去定制我们的类,去返回我们需要返回的属性个 数。

结尾

本篇笔记主要总结了常遇到的python类的定制方法,不难看出这些定制方法可以让我们 很方便的定制一些特殊的类。至于还有一些比如实现加运算的:__add__,实现减运算 的:__sub__,乘和除运算的:__mul__,__div__。大家可以参考文档自己了解。