Python开发(人工智能/大数据/机器学习)

28.定制一个类

2018-07-04  本文已影响1人  TensorFlow开发者

前言

前面已经学习了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__()的参数也可能是一个可以作keyobject,例如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__()


更多了解,可关注公众号:人人懂编程


微信公众号:人人懂编程
上一篇下一篇

猜你喜欢

热点阅读