Python面向对象编程(下)

作者: 夏海峰 | 来源:发表于2020-04-14 17:26 被阅读0次

    数据封装、继承和多态是面向对象程序设计中最基础的3个概念,今天学习下Python中的高级特性——多重继承、定制类、元类等。

    1、使用 __slots__

    • 类的实例对象被创建后,我们可以给该实例绑定新的任何属性或方法。
    • 但新绑定的属性和方法仅对当前实例有效,对该类的其它实例无效,要想也生效,我们需要将其绑定在该类自身上。
    class Student(object):
        pass
    
    # 给实例绑定新的属性和方法
    s1 = Student()
    s1.name = 'hello'
    from types import MethodType
    def set_age(self, age):
        self.age = age
    s1.set_age = MethodType(set_age, s1)
    
    # 测试一下
    print(s1.name)  # 'hello'
    s1.set_age(30)
    print(s1.age)  # 30
    s2 = Student()
    print(s2.name)  # 报错'Student' object has no attribute 'name'
    
    
    # 给类绑定新的属性和方法
    Student.grade = '一年级'
    def set_score(self, score):
        self.score = score
    Student.set_score = set_score
    
    
    # 测试一下
    print(s2.grade)  # '一年级'
    s2.set_score(100)
    print(s2.score)  # 100
    
    • 在定义class类的时候,使用__slots__这个特殊的变量,可以限制该class实例能添加哪些属性,如下例子中的Student实例只能追加 name 和 age 属性,追加其它属性会报错。
    class Student(object):
        # 用tuple定义允许绑定的属性名称
        __slots__ = ('name', 'age')
    
    s = Student()
    s.name = 'geekxia'
    print(s.name) # 'geekxia'
    s.grade = '一年级'  # 会报错
    
    • __slots__只对当前类的实例起作用,对其继承的子类不起作用。如果希望子类也能有这样的限制,需给子类自身添加__slots__限制。

    2、Getters 与 Setters

    class Student(object):
        
        # getter
        @property
        def score(self):
            return self._score
        
        # setter
        @score.setter
        def score(self, score):
            if not isinstance(score, int):
                raise ValueError('分数必须是int类型')
            if score<0 or score>100:
                raise ValueError('分数必须在0~100之间')
            self._score = score
        
        @property
        def grade(self):
            return '一年级'
    
    s = Student()
    s.score = 90
    print(s.score)  # 90
    print(s.grade)  # '一年级'
    
    • @property可以把一个getter方法变成属性,在getter方法前加上该装饰器即可。
    • @xxx.setter可以把一个setter方法变成属性,在setter方法前加上该 装饰器即可。
    • 未设置@xxx.setter装饰器的属性,是只读属性,上述代码中的grade就是只读属性。

    3、多重继承

    Python支持多重继承,即一个类允许有多个父类,这是一种Mixin的设计手法。研究下面这个例子:

    # 动物基类
    class Animal(object):
        pass
    
    # 会跑的,基类
    class RunnableMixin(object):
        pass
    
    # 会飞的,基类
    class FlyableMixin(object):
        pass
    
    # 哺乳动物,继承自Animal
    class Mammal(Animal):
        pass
    
    # 鸟类,继承自Animal
    class Bird(Animal):
        pass
    
    # 狗:哺乳动物、会跑
    class Dog(Mammal, RunnableMixin):
        pass
    # 蝙蝠:哺乳动物、会飞
    class Bat(Mammal, FlyableMixin):
        pass
    # 鹦鹉:鸟类、会飞
    class Parrot(Bird, FlyableMixin):
        pass
    # 鸵鸟:鸟类、会跑
    class Ostrich(Bird, RunnableMixin):
        pass
    

    在设计类的继承关系时,通常主线都是单一继承下来的,例如Ostrich->Bird->Animal。当需要“混入(Mixin)”额外的功能时,通过多重继承来实现,额外功能的类通常在命名时习惯加上Mixin,如RunnableMixin,FlyableMixin

    4、特殊函数与定制类

    形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的,可以帮助我们定制类。

    • __slots__用于限制实例对象可以扩展哪些属性。
    • __len__方法能让class作用于len()函数。
    • 更多特殊函数请参见 Python官方文档

    (1)__str____repr__

    class Student(object):
        def __init__(self, name):
            self.name = name
        def __str__(self):
            return 'Student实例对象 (name: %s)' % self.name
        __repr__ = __str__
    
    s = Student('geekxia')
    print(s)  # Student实例对象 (name: geekxia)
    

    当打印实例对象时,__str__指定了打印的内容。
    在交互式控制台,直接输出实例对象时,__repr__指定了显示的内容,常用于调试。

    (2)__iter____next__

    如果一个类想被用于for...in循环,类似list或tuple那样,就必须实现一个__iter__方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

    # 斐波那契数列 类
    class Fib(object):
        def __init__(self):
            # 初始化两个计数器
            self.a, self.b = 0, 1
        def __iter__(self):
            # 实例本身就是迭代对象,所以返回自己
            return self
        def __next__(self):
            # 计算下一个值
            self.a, self.b = self.b, self.a+self.b
            # 退出循环的条件
            if self.a > 100000:
                raise StopIteration()
            # 返回下一个值
            return self.a
    
    f = Fib() 
    for n in f:
        print(n)
        
    # 依次打印:
    # 1
    # 。。。。。
    # 46368
    # 75025
    

    (3)__getitem__

    上述Fib实例虽然能作用于for循环,看起来和list有点像,但使用Fib()[5]Fib()[5:10]的方式取值时会报错,这就需要定义__getitem__了。

    # 斐波那契数列 类
    class Fib(object):
        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() 
    print(f[5])  # 8
    print(f[:10])  # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    

    如果把实例对象当成 dict 看,那么还可以定义两个特殊函数,分别是__setitem____delitem__。我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

    (4)__getattr__

    正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。使用__getattr__可以增强类的健壮性,示例如下:

    class Student(object):
        def __init__(self, name):
            self.name = name
        def __getattr__(self, attr):
            # 属性
            if attr == 'score':
                return 100
            # 方法
            if attr == 'age':
                return lambda x:x+1
            raise AttributeError('Student类,没有 %s 属性' % attr)
    
    s = Student('geekxia')
    print(s.name)  # 'geekxia'
    print(s.score)  # 100
    print(s.age(20))  # 21
    print(s.grade)  # AttributeError: Student类,没有 grade 属性
    

    (5)__call__

    在Python中,能否直接调用一个对象本身呢?答案是肯定的,使用__call__函数可以实现。callable()函数可以判断一个对象是否是“自身可调用”的对象。示例代码如下:

    class Apple(object):
        def __init__(self,color):
            self.color = color
        def __call__(self, arg):
            print('我是苹果:'+arg)
        
    a = Apple('red')
    
    # 调用实例对象本身
    print(a('哈哈'))  # 我是苹果: 哈哈
    
    
    # callable() 检测一个对象是否可被调用
    
    print(callable(a))  # True
    print(callable(Apple('green')))  # True
    print(callable(max))  # True
    print(callable([1,2,3]))  # False
    print(callable(None))  # False
    print(callable('hello'))  # False
    

    5、使用 Enum 枚举类

    当需要定义一组常量时,更好办法是使用枚举类(Enum)。比如一年12个月,示例如下:

    from enum import Enum
    
    Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
    
    print(Month.Jan)  # 'Month.Jan'
    
    for name, member in Month.__members__.items():
        print(name, '=>', member, ',', member.value)
        
    # Jan => Month.Jan , 1
    # Feb => Month.Feb , 2
    # Mar => Month.Mar , 3
    # Apr => Month.Apr , 4
    # May => Month.May , 5
    # Jun => Month.Jun , 6
    # Jul => Month.Jul , 7
    # Aug => Month.Aug , 8
    # Sep => Month.Sep , 9
    # Oct => Month.Oct , 10
    # Nov => Month.Nov , 11
    # Dec => Month.Dec , 12
    
    # value属性则是自动赋给成员的int常量,默认从1开始计数。
    

    如果需要更精确地控制枚举类型(自定义这些常量的值),可以从Enum派生出自定义类。比如一周7天,示例如下:

    from enum import Enum, unique
    
    # 定义Week类,继承自Enum
    # @unique装饰器可以帮助我们检查保证没有重复值。
    @unique
    class Week(Enum):
        Sun = 0 # Sun的value被设定为0
        Mon = 1
        Tue = 2
        Wed = 3
        Thu = 4
        Fri = 5
        Sat = 6
    
    print(Week.Sun)  # 'Week.Sun'
    print(Week['Mon'])  # 'Week.Mon'
    print(Week.Tue.value)  # 2
    print(Week(1))  # 'Week.Mon'
    
    for name, member in Week.__members__.items():
        print(name, '=>', member, ',', member.value)
    
    # Sun => Week.Sun , 0
    # Mon => Week.Mon , 1
    # Tue => Week.Tue , 2
    # Wed => Week.Wed , 3
    # Thu => Week.Thu , 4
    # Fri => Week.Fri , 5
    # Sat => Week.Sat , 6
    

    6、使用元类来创建类

    动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

    (1)使用type()函数来创建类

    type()函数除了可以检测对象和变量的类型,还可以动态地创建类,示例代码如下:

    def fn(self, name='World'):
        print('Hello, %s.' % name)
    
    # 使用 type() 创建 Hello类
    Hello = type('Hello', (object,), dict(hello=fn))
    
    h = Hello()
    h.hello()  # 'Hello, World.'
    h.hello('GeekXia')  # 'Hello, GeekXia.'
    print(type(Hello))  # <class 'type'>
    print(type(h))  # <class '__main__.Hello'>
    

    type()函数创建Hello类,依次传入3个参数:

    • class的名称;
    • 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
    • class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

    (2)使用 metaclass 元类来创建类

    # 定义一个元类
    class ListMetaclass(type):
        def __new__(cls, name, bases, attrs):
            attrs['add'] = lambda self, value: self.append(value)
            return type.__new__(cls, name, bases, attrs)
    
    # 根据上面的元类,创建一个新的类
    class MyList(list, metaclass=ListMetaclass):
        pass
    
    # 测试一下
    ml = MyList()
    ml.add(1)
    ml.add(2)
    ml.add(3)
    print(ml)  # [1, 2, 3]
    
    • 除了使用type()动态创建类以外,如果还想控制类的创建行为,还可以使用metaclass来创建类。
    • metaclass元类允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。
    • 按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass。
    • __new__方法接收到的参数依次是:当前准备创建的类的对象、类的名字、类继承的父类集合、类的方法集合。
    • 元类是类的模板,类是实例的模板。

    参考资源:
    1、廖雪峰Python教程
    2、Python官方文档


    END!!!

    相关文章

      网友评论

        本文标题:Python面向对象编程(下)

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