28.定制一个类

作者: TensorFlow开发者 | 来源:发表于2018-07-04 21:00 被阅读1次

    前言

    前面已经学习了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__()


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


    微信公众号:人人懂编程

    相关文章

      网友评论

        本文标题:28.定制一个类

        本文链接:https://www.haomeiwen.com/subject/nesyuftx.html