面向对象有3大基本特性:封装、继承、多态。而这里面的封装就是让我们把现实中客观存在的某一类事物,封装成一个具有抽象逻辑的类。何为抽象,即无具象。就是我们封装的类它没有具体的数据,也不会发生具体的行为,它只是一堆逻辑的集合体。但是我们实例化类后,类的实例就变成了一个具体的对象,既拥有具体的数据又能发生具体的行为。就像老子在道德经中所述:无名天地之始,有名万物之母。类在在实例化之前拥有无限的可能,在实例化之后只有唯一的可能,这就是从抽象到具象。实例:一个实实在在的例子,是具体的事物,而不是抽象的逻辑。实列化:从抽象到具象的过程。

封装一个类

基本结构

class 类名(要继承的类,可以继承多个类):
    定义属性及方法的逻辑语句

        下面我将以人类为例子,给大家展示说明如何封装一个抽象的类。人具有姓名、性别、年龄、身高、体重等属性,这些就属于一个类中的属性;人具有吃饭、喝水、睡觉、行走、跳跃等行为,这些就属于一个类中的方法。现在我们根据这些信息来封装一个类:

class Person:  # 使用class定义一个名为Person的类,默认继承object
    """人类"""
    name = None  # 定义姓名属性
    sex = None  # 定义性别属性
    age = None  # 定义年龄属性
    height = None  # 定义身高属性
    weight = None  # 定义体重属性
    
    def eat(self):  # 定义吃饭行为
        """吃饭"""
        
    def drink(self):  # 定义喝水行为
        """喝水"""
        
    def sleep(self):  # 定义睡觉行为
        """睡觉"""
        
    def walk(self):  # 定义行走行为
        """行走"""
        
    def jump(self):  # 定义跳跃行为
        """跳跃"""

现在我们就得到了一个Person类,这个Person是以客观事物人抽象出来的概念。Person没有具体的数据,就是Person不能具体表示出某一个人,所以Person具有表示出任何人的能力。Person的属性name可以为张三、李四、王五、小明、爆笑蛙等等,Person的属性sex可以为男或女,age、height、weight的值可以在人类的正常范围内去选择。虽然Person不是具体的某个人,但是Person可以成为任何一个人。现在能理解类这种抽象的概念了吧,那我们就使用Person来具体表示某个人(类的实例化)。

类的实例化

        类的实例化跟执行函数差不多,都是在变量名后加一个括号,函数表示执行,类就表示实例化。函数执行后会返回一个值,类实例化后会返回一个类的实例,我们需要用一个变量去接收返回的实例。这个实例就是类具象出来的一个具体的事物,因此实例拥有类中的所有属性和方法。我们可以给这个实例的属性赋值以及使用实例中的方法。

person = Person()  # 实例化Person
person.name = '小明'  # 给name属性赋值
person.sex = '男'  # 给sex属性赋值
person.age = 20  # 给age属性赋值
person.height = 170  # 给height属性赋值
person.weight = 60  # 给weight属性赋值
print(f'姓名: {person.name}\n性别: {person.sex}\n年龄: {person.age}\n身高: {person.height}\n体重: {person.weight}')

执行结果如下:

我们在实例化Person后,再给这个实例的属性赋值,这个实例就变成了小明(一个实实在在的事物,有名有姓的人)。

初始化方法__init__

        虽然我们实例化了Person类,但是你有没有发现这种实例化方式非常的麻烦,需要我们一个一个的去给属性赋值。那有没有快速给属性赋值的方式呢,答案有的。初始化方法__init__就可以做到,__init__是python描述对象的底层方法之一,python中描述对象的底层方法又被称为魔法方法。python实例化一个对象会经历以下过程:

python会首先调用类中的__new__方法创建类的实例对象,实例对象创建好之后再调用__init__方法初始化实例对象,初始化之后我们就得到了一个对象。但是你可能会说Person里面并没有定义__new__和__init__方法,为什么Person可以被实例化。这就涉及到了面向对象的第二个基本特性继承,python3中规定所有使用class自定义的类,如果没有定义继承的类时默认继承object类。所以Person是继承了object类的,object是python提供的基础类。object中定义了__new__、__init__等底层的描述方法,builtins.py文件中可以找到object类。但我们只能在里面看到方法名,实现方法的逻辑全部集成在python解释器中,使用C语言实现的。可以说builtins.py文件中的所有方法没有任何真正的逻辑,只是为了让我们在编辑器中写代码时不会报错而提供的。

        上面的Person类因为继承了object中的__new__和__init__方法,所以Person可以进行初始化。现在呢,我们不再使用object中的__init__方法,而选择在Person中自定义__init__方法。如果Person中定义了__init__方法,Person实例化时就会优先使用自己的__init__方法。这是继承的特性,自己有时就用自己的,自己没有时才用继承的。

class Person:
    """人类"""
    name = None
    sex = None
    age = None
    height = None
    weight = None

    def __init__(self, name, sex, age, height, weight):
        """初始化"""
        self.name = name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight


person = Person('小明', '男', 20, 170, 60)  # 实例化Person
print(f'姓名: {person.name}\n性别: {person.sex}\n年龄: {person.age}\n身高: {person.height}\n体重: {person.weight}')

执行结果如下:

这种实例化方式是不是简单多了,你可能会有一些疑问,为什么__init__的第一个参数是self,这个self是什么,你可能对self的存在感到困惑。其实这个self就是用来接收实例的参数,因为函数分为全局函数和局部函数。全局函数我们可以在任何地方直接调用,全局函数执行时不需要绑定其他对象。局部函数则只能在局部范围内使用,所以局部函数必须绑定一个对象才能执行,局部函数我们通常称其为方法。python在执行局部函数时,会先去检查函数绑定的对象中是否定义了此方法,只有对象内部定义了此方法才能执行,如果未定义则会抛出属性错误。所以self就是用来接收Person的实例对象的,一旦Person的实例被__new__创建出来后就会把实例和我们传入的值给__init__,实例是传过来的第一个参数赋值给self,__init__再把出入的其他值赋给实例(self)的属性。

        这个self不单单只出现在__init__中,它还会出现在所有的实例方法中,并占据第一个参数。因为所有的实例方法都需要绑定实例对象才能被执行。请看下面的例子:

class Person:
    name = None
    sex = None
    age = None
    height = None
    weight = None

    def __init__(self, name, sex, age, height, weight):
        self.name = name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    def eat(self):
        """吃饭"""
        print(f'{self.name}正在吃饭')


person = Person('小明', '男', 23, 170, 60)

person.eat()
Person.eat(person)

执行结果如下:

我们可以看到person.eat()和Person.eat(person)都可以打印出'小明正在吃饭',person.eat()是实例直接调用实例方法,这种方式能直接把实例person传递给eat的参数self;Person.eat(person)是类使用实例方法,需要传递实例person给eat的参数self。这就是self的作用,self就是用来接收实例的局部变量,在类中我们常常把self就当作类的实例来使用。

        所以只要跟self有关的,都跟实例有关;例如第一个参数为self的函数叫实例函数,self.name、self.age、self.sex叫实例属性。那self.name和name有什么区别呢,self.name是实例属性,name是类属性。实例属性只能在实例方法中定义,类属性只能在类或类方法中定义,实例属性和类属性可以同名。实例属性只能被实例方法使用,不能被类方法和静态方法使用;类属性可以被实例方法和类方法使用,不能被静态方法使用。实例方法使用类属性时也是通过self.xxx来使用,所以当实例属性和类属性同名时(就像self.name和name)实例方法使用的是实例属性。

类属性

        只能在类或类方法中定义,不能在其他方法中定义,最好只在类中定义。可以被类方法使用,可以被实例方法使用,不能被静态方法使用。

class Person:
    name = None  # 类属性
    sex = None  # 类属性
    age = None  # 类属性
    height = None  # 类属性
    weight = None  # 类属性
    complexion = None  # 类属性

也可以在类外新增类属性,例如Person.blood_type = 'AB'。但这样做没什么太大的意义,我们一般不这样使用。

实例属性

        只能在实例方法中定义,不能在其他地方定义,最好只在__init__中定义。只能被实例方法使用,不能被类方法和静态方法使用。

class Person:

    def __init__(self, name, sex, age, height, weight):
        self.name = name  # 实例属性
        self.sex = sex  # 实例属性
        self.age = age  # 实例属性
        self.height = height  # 实例属性
        self.weight = weight  # 实例属性

也可以给实例新增属性,person.blood_type = 'AB'。这样做的意义也不大,还不如直接使用公共变量或局部变量。除非你在类中自定义了__setattr__,想搞点什么特殊的东西。

实例方法

        实例方法就是在类中以实例(self)作为第一个参数的方法,实例方法中可以使用实例属性和类属性。实例可以直接调用实例方法,类调用实例方法时第一个参数必须传入实例。

class Person:
    name = None  # 类属性
    sex = None  # 类属性
    age = None  # 类属性
    height = None  # 类属性
    weight = None  # 类属性
    food = '大米饭'  # 类属性

    def __init__(self, name, sex, age, height, weight):
        self.name = name  # 实例属性
        self.sex = sex  # 实例属性
        self.age = age  # 实例属性
        self.height = height  # 实例属性
        self.weight = weight  # 实例属性

    def eat(self, location):  # 实例方法
        """
        吃东西行为

        :param location: 位置
        :return:
        """
        print(f'{self.name}正在{location}吃{self.food}')  # 同时使用了类属性和实例属性


person = Person('小明', '男', 23, 170, 60)  # 实例化Person类,并把实例赋值给person
person.eat('家里')  # 实例调用实例方法
Person.eat(person, '家里')  # 类调用实例方法,第一个参数必须传入实例

执行结果如下:

实例方法eat()既能使用实例属性self.name又能使用类属性food。实例可以直接调用实例方法,类调用实例方法时第一个参数必须传入实例。

类方法(@classmethod)

        类方法就是使用@classmethod装饰器修饰的方法,它使用类(cls)作为第一个参数。类方法只能使用类属性,不能使用实例属性。实例和类都可以直接调用类方法。

class Person:
    name = None
    sex = None
    age = None
    height = None
    weight = None
    food = '大米饭'

    def __init__(self, name, sex, age, height, weight):
        self.name = name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    @classmethod
    def eat(cls, location):  # 使用@classmethod修饰后,就成了类方法,第一个参数cls接收类
        """
        吃东西

        :param location: 位置
        :return:
        """
        print(f'{cls.name}正在{location}吃{cls.food}')  # 只能使用类属性


person = Person('小明', '男', 23, 170, 60)
person.eat('家里')  # 实例调用类方法
Person.eat('家里')  # 类调用类方法

执行结果如下:

实例属性self.name = '小明',类属性name = None。因为类方法eat只能使用类属性name,所以执行结果为None正在家里吃大米饭。实例和类都可以直接调用类方法。

静态方法(@staticmethod)

        静态方法就是使用@staticmethod装饰器修饰的方法,它是一种可以脱离类存在的方法。我们为了方便维护和调用函数,常常会把一些跟类的属性没有关系的方法写在类中,这种方法就是静态方法。静态方法既不能使用类属性又不能使用实例属性,实例和类都可以直接调用静态方法。

class Person:
    name = None
    sex = None
    age = None
    height = None
    weight = None
    food = '大米饭'

    def __init__(self, name, sex, age, height, weight):
        self.name = name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    @staticmethod
    def eat(location):  # 使用@staticmethod修饰后,就变成了静态方法
        """
        吃东西

        :param location: 位置
        :return:
        """
        print(f'正在{location}吃')


person = Person('小明', '男', 23, 170, 60)
person.eat('家里')  # 实例调用静态方法
Person.eat('家里')  # 类调用静态方法

执行结果如下:

类的公有和私有

        类的公有属性和方法就是我们想给别人使用的属性和方法,也被称作对外的接口;类的私有属性和方法就是我们不想给别人使用的属性和方法,不能在类外直接调用。我们在定义一个类时,为了确保类中逻辑的准确性,通常会防止类中的某些方法和属性被恶意调用和篡改。这时我们就会把类中的某些属性和方法设定为私有属性和方法,私有的属性和方法只能在类中使用,不能在类外调用和修改。这样就有效的保障了类中的方法能正确的返回数据,但可惜的是python中并没有真正的私有属性和方法,python中的私有是通过隐藏和改名实现的伪私有。

        在编辑器中写代码时,公有的属性和方法会展示出来方便我们使用。如下图所示:

恶意修改属性值

        下面我给大家举一个因为没有私有属性,而被恶意修改属性值影响函数输出结果的例子。

class Person:
    name = None
    sex = None
    age = None
    height = None
    weight = None
    food = '大米饭'

    def __init__(self, name, sex, age, height, weight):
        self.name = name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    def eat(self, location):
        """
        吃东西

        :param location: 位置
        :return:
        """
        print(f'{self.name}正在{location}吃{self.food}')


person = Person('小明', '男', 23, 170, 60)
person.name = '爆笑蛙'  # 在类外修改属性的值
person.eat('家里')

执行结果如下:

我们明明实例化的时候用的是小明,但是通过在类的外部直接修改self.name的值,使用eat()方法的输出结果就不符合我们预期了。在复杂的程序中,如果出现这种纰漏就会严重影响运算结果的准确性。所以我们需要把一些不能被随意修改的属性设为私有属性。

私有属性

        私有属性有两种,一种是通过隐藏属性名实现的私有,一种是通过修改属性名实现的私有。

隐藏属性名

        我们可以在属性名前面加一个下划线来对外隐藏属性名,类属性和实例属性都是通过加下划线来隐藏自己的。隐藏的属性在类内部可以随便使用,在类外也可以被强行使用。

class Person:
    _name = None  # 加下划线对外隐藏类属性name
    sex = None
    age = None
    height = None
    weight = None
    food = '大米饭'

类属性隐藏后,在类外将不可见(编辑器中写代码时)。

name属性已不可见,也不会出现_name这种属性。

        虽然name属性被隐藏了,但是我们还是可以强行使用。

Person._name = '小明'
print(Person._name)  # 小明

        实例属性也是同样的方法对外隐藏。

class Person:

    def __init__(self, name, sex, age, height, weight):
        self._name = name  # 加下划线对外隐藏实例属性name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

实例属性隐藏后,在实例中将不可见(编辑器中写代码时)。

name属性已不可见,也不会出现_name这种属性。

        虽然name属性被隐藏了,但是我们还是可以强行使用。

person = Person('小明', '男', 23, 170, 60)
person._name = '爆笑蛙'
print(person._name)  # 爆笑蛙
修改属性名

        我们可以在属性名前面加两个下划线来修改属性名,类属性和实例属性都是通过加下划线来修改自己的。修改的属性在类内部可以随便使用,在类外也可以被强行使用。

class Person:
    __name = None  # 加两个下划线把类属性name改为_Person__name
    sex = None
    age = None
    height = None
    weight = None
    food = '大米饭'

类属性被修改后,在类的外部同样是不可见的。并且修改后的类属性不能通过两个下划线加属性名的方式强行使用,因为属性名已被修改为_类名__属性名的形式了。

print(Person.__name)

通过__name的方式访问属性会报错,执行结果如下:

报错显示Person中没有属性__name。

        我们可以通过_Person__name的方式访问类属性name,因为类属性name已被修改为_Person__name。

print(Person._Person__name)  # None

        实例属性也是同样的方式来修改属性名。

class Person:

    def __init__(self, name, sex, age, height, weight):
        self.__name = name  # 加两个下划线把实例属性name改为_Person__name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

实例属性被修改后,在实例中同样是不可见的。并且修改后的实例属性不能通过两个下划线加属性名的方式强行使用,因为属性名已被修改为_类名__属性名的形式了。

person = Person('小明', '男', 23, 170, 60)
print(person.__name)

通过__name的方式访问属性会报错,执行结果如下:

报错显示Person中没有属性__name。python先在实例中查找有没有__name属性,如果没有再去类中查找有没有__name,结果类中也没有所以报错显示Person中没有属性__name。

        我们可以通过_Person__name的方式访问实例属性name,因为实例属性name已被修改为_Person__name。

person = Person('小明', '男', 23, 170, 60)
print(person._Person__name)  # 小明

私有方法

        私有方法跟私有属性有一样也是两种,一种是通过隐藏属性名实现的私有,一种是通过修改属性名实现的私有。

隐藏方法名

        我们可以在方法名前面加一个下划线来对外隐藏方法名,类方法和实例方法都是通过加下划线来隐藏自己的。隐藏的方法在类内部可以随便使用,在类外也可以被强行使用。

class Person:

    __name = None  # 加两个下划线把类属性name改为_Person__name
    sex = None
    age = None
    height = None
    weight = None
    food = '大米饭'

    @classmethod
    def _eat(cls, location):  # 使用下划线对外隐藏类方法
        """
        吃东西

        :param location: 位置
        :return:
        """
        print(f'{cls.__name}正在{location}吃{cls.food}')

类方法隐藏后,在类外将不可见(编辑器中写代码时)。

eat方法已不可见,也不会出现_eat这种方法。

        虽然eat方法被隐藏了,但是我们还是可以强行调用。

Person._eat('家里')

执行结果如下:

        实例方法也是使用同样的方式对外隐藏。

class Person:
    food = '大米饭'

    def __init__(self, name, sex, age, height, weight):
        self.__name = name  # 加两个下划线把实例属性name改为_Person__name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    def _eat(self, location):  # 使用下划线对外隐藏实例方法
        """
        吃东西

        :param location: 位置
        :return:
        """
        print(f'{self.__name}正在{location}吃{self.food}')

实例方法隐藏后,在实例中将不可见(编辑器中写代码时)。

eat方法已不可见,也不会出现_eat这种方法。

        虽然eat方法被隐藏了,但是我们还是可以强行调用。

person = Person('小明', '男', 23, 170, 60)
person._eat('家里')

执行结果如下:

修改方法名

        我们可以在方法名前面加两个下划线来修改方法名,类方法和实例方法都是通过加下划线来修改自己的。修改的方法在类内部可以随便使用,在类外也可以被强行调用。

class Person:

    __name = None  # 加两个下划线把类属性name改为_Person__name
    sex = None
    age = None
    height = None
    weight = None
    food = '大米饭'

    @classmethod
    def __eat(cls, location):  # 使用两个下划线把类方法eat改为_Person__eat
        """
        吃东西

        :param location: 位置
        :return:
        """
        print(f'{cls.__name}正在{location}吃{cls.food}')

类方法被修改后,在类的外部同样是不可见的。并且修改后的类方法不能通过两个下划线加方法名的方式强行使用,因为方法名已被修改为_类名__方法名的形式了。

Person.__eat('家里')

通过__eat的方式调用类方法eat会报错,执行结果如下:

报错显示Person中没有属性__eat。

        我们可以通过_Person__eat的方式访问类方法eat,因为类方法eat已被修改为_Person__eat。

Person._Person__eat('家里')

执行结果如下:

        实例方法也是使用同样的方式修改方法名。

class Person:
    food = '大米饭'

    def __init__(self, name, sex, age, height, weight):
        self.__name = name  # 加两个下划线把实例属性name改为_Person__name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    def __eat(self, location):  # 使用两个下划线把实例方法eat改为_Person__eat
        """
        吃东西

        :param location: 位置
        :return:
        """
        print(f'{self.__name}正在{location}吃{self.food}')

实例方法被修改后,在实例中同样是不可见的。并且修改后的实例方法不能通过两个下划线加方法名的方式强行使用,因为方法名已被修改为_类名__方法名的形式了。

person = Person('小明', '男', 23, 170, 60)
person.__eat('家里')

通过__eat的方式调用实例方法eat会报错,执行结果如下:

报错显示Person中没有属性__eat。python先在实例中查找有没有__eat方法,如果没有再去类中查找有没有__eat,结果类中也没有所以报错显示Person中没有属性__eat。

        我们可以通过_Person__eat的方式访问实例方法eat,因为实例方法eat已被修改为_Person__eat。

person = Person('小明', '男', 23, 170, 60)
person._Person__eat('家里')

执行结果如下:

        因为python中的私有属性可以被强行调用,所以python中的私有属性只是伪私有。所以我们如果在别人写的类中发现了私有属性,不要修改私有属性和调用私有方法,以免在使用别人写的类时出现BUG。

类的继承

继承一个类

        一个类是可以继承于其他类的,可以只继承一个类,也可以继承多个类。在python3中,所有以class定义的类都继承于object类。当一个类B继承于另一个类A时,B就会得到A中的所有属性和方法。

class Person:
    """人类"""
    food = '大米饭'

    def __init__(self, name, sex, age, height, weight):
        self._name = name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    def eat(self, location):
        """
        吃东西

        :param location: 地点
        :return:
        """
        print(f'{self._name}正在{location}吃{self.food}')


class Student(Person):
    """学生类"""


student = Student('小明', '男', 16, 170, 60)
print(student._name)
student.eat('家里')

上面的代码中Student继承于Person,所以Student会得到Person中的所有属性和方法。Student中什么方法和属性都不用定义,也可以通过Person中的__init__方法实例化,以及使用Person中的属性和方法。执行结果如下:

继承多个类

        当一个类继承多个类时,它会得到所有继承类中的属性和方法。

class Singer:
    """歌者"""
    song = '罗刹海市'

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

    def singing(self):
        print(f'{self.name}正在唱{self.song}')


class Dancer:
    """舞者"""
    dance = '街舞'

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

    def dancing(self):
        print(f'{self.name}正在跳{self.dance}')


class Perform(Singer, Dancer):
    """表演者"""


perform = Perform('小明')
perform.singing()
perform.dancing()

执行结果如下:

Perfom继承了Singer和Dancer,所以Perform拥有了Singer和Dancer中的属性和方法。但是Singer和Dancer中都有__init__方法,那么Perfom使用的是谁的__init__方法呢?答案是Singer。

        在继承的多个类中如果存在相同的方法时,会使用继承位置靠前的类中的方法。

class Singer:
    """歌者"""
    song = '罗刹海市'

    def __init__(self, name):
        print('使用Singer实例化')
        self.name = name

    def practice(self):
        print(f'{self.name}正在练习唱{self.song}')


class Dancer:
    """舞者"""
    dance = '街舞'

    def __init__(self, name):
        print('使用Dancer实例化')
        self.name = name

    def practice(self):
        print(f'{self.name}正在练习跳{self.dance}')


class Perform(Singer, Dancer):
    """表演者"""


perform = Perform('小明')
perform.practice()

执行结果如下:

我们可以看到实例化时,使用的是Singer中的方法,调用practice()方法时使用的也是Singer中的方法。因为Singer排在继承顺序的第一位,而Dancer排在第二位。所以当Singer和Dancer中存在相同方法时,会使用Singer中的方法。

多重继承

        上面讲的继承关系都是一个类继承于另一个类,只有一重的继承关系。类还可以多重继承,子类继承于父类、父类又继承于爷爷类、爷爷类又继承于太爷爷类。可以这样一直向上追溯,反过来就是一直向下继承。

class Animal:
    """动物"""

    def __init__(self, name):
        print('使用Animal实例化')
        self.name = name

    def move(self):
        """移动"""
        print(f'{self.name}可以动')


class Mammals(Animal):
    """哺乳类"""

    def __init__(self, name):
        print('使用Mammals实例化')
        self.name = name

    def nurse(self):
        """哺乳"""
        print(f'{self.name}小时候会喝奶')


class Person(Mammals):
    """人类"""

    def study(self):
        """学习"""
        print(f'{self.name}会向他人学习')


person = Person('小明')
person.move()
person.nurse()
person.study()

执行结果如下:

Person继承于Mammals,Mammals又继承于Animal,Person就拥有了Mammals和Animal中的属性和方法。从执行结果中我们可以发现,当一个类属于多重继承时,它能获得的属性和方法可以一直向上追溯,追溯到对应的属性和方法时就停止追溯。例如Mammals和Animal中都定义了__init__方法,但Person使用的是Mammals中的__init__方法。

类中的多态

        前面我们讲过多态函数,是使用overload装饰函数来实现的。这里呢我们在类的继承中实现多态方法,不需要overload装饰函数。当B类继承于A类时,B中可以重写A中的方法,方法名一定相同,接收的参数、方法中的运算逻辑、返回的内容都可以不同。就像我们上面讲的__init__方法一样,虽然在父类中有__init__方法,但是在子类中也可以定义自己的__init__方法。属性也可以是多态的,父类中定义了某个属性,在子类中可以定义同名属性,而且属性的值可以不同。

方法的多态

class Person:
    """人类"""
    food = '大米饭'

    def __init__(self, name, sex, age, height, weight):
        self._name = name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    def eat(self, location):
        """
        吃东西

        :param location: 地点
        :return:
        """
        print(f'{self._name}正在{location}吃{self.food}')


class Student(Person):
    """学生类"""
    book = '理论力学'

    def eat(self):
        """吃饭"""
        print(f'{self._name}边看{self.book}边吃饭')


student = Student('小明', '男', 16, 170, 60)
student.eat()

执行结果如下:

Person中的eat()方法和Student中的eat()方法,方法名是相同的,但是接收的参数个数不同,打印的内容也不同。不管是方法也好还是属性也好,自己拥有的就有自己的,自己没有的才用继承的。所以执行的结果为小明边看理论力学边吃饭,这就是方法的多态。

        我们实例化Person类时,使用的就还是Person中的eat()方法。

person = Person('小明', '男', 16, 170, 60)
person.eat('家里')

执行结果如下:

属性的多态

class Person:
    """人类"""
    food = '大米饭'

    def __init__(self, name, sex, age, height, weight):
        self._name = name
        self.sex = sex
        self.age = age
        self.height = height
        self.weight = weight

    def eat(self, location):
        """
        吃东西

        :param location: 地点
        :return:
        """
        print(f'{self._name}正在{location}吃{self.food}')


class Student(Person):
    """学生类"""
    food = '面包'


student = Student('小明', '男', 16, 170, 60)
student.eat('家里')

执行结果如下:

Person中的food属性值为'大米饭',而Student中的food属性值为'面包'。不管是方法也好还是属性也好,自己拥有的就有自己的,自己没有的才用继承的。所以执行结果为小明正在家里吃面包,这就是属性的多态。

        我们实例化Person类时,使用的就还是Person中的food属性。

person = Person('小明', '男', 16, 170, 60)
person.eat('家里')

执行结果如下:

super(超类)

        顾名思义super是一个类,被称作超类。它可以根据继承的顺序(MRO顺序),找到某个类的父类,并返回这个父类。super还可以把返回的父类绑定给一个实例对象,绑定实例对象后就可以通过实例对象去使用父类中的方法和属性了。

MRO顺序

        在python中一个类可以通过__mro__方法得到它的MRO顺序(继承顺序)。

class A:
    """A"""


class B(A):
    """B"""


class C(A):
    """C"""


class D(B, C):
    """D"""


print(D.__mro__)

执行结果如下:

我们可以看到D的继承顺序是D -> B -> C -> A -> object,D的第一个继承就是D自身。现在我们应该清楚的知道继承的特性为什么是自己有就用自己的,自己没有才用继承的。

super的使用方式

        super可以接收两个参数,第一个参数为类(class),第二个参数为类(class)或实例(self)。super会在第二个参数的MRO顺序中找到第一个参数的下一个类,并返回下一个类。第二个参数为类时super只能直接使用类属性和类方法,要使用实例方法时必须传入一个实例;第二个参数为实例时super可以使用类属性、类方法和实例方法。当我们不给super传参时,super默认第一个参数为当前类,第二个参数为当前类的实例或当前类(在实例方法中使用时为当前类的实例,在类方法中使用时为当前类)。

super()

class A:
    value = 'a'

    def say(self):
        print(f'我是a value = {self.value}')


class B(A):
    value = 'b'

    def say(self):
        print(f'我是b value = {self.value}')


class C(A):
    value = 'c'

    def say(self):
        print(f'我是c value = {self.value}')


class D(B, C):
    value = 'd'

    def say(self):
        print(super().value)  # 打印上一级的属性
        super().say()  # 调用上一级的方法


d = D()
d.say()

在D中使用super()就相当于super(D, self),第一个参数是D本身,第二个参数是D的实例。D的MRO顺序为:D -> B -> C -> A -> object,在MRO顺序中找到D的下一个类是B,所以super()返回B并且B绑定了D的实例。因此一定要在D的实例方法中使用super(),不然会报缺少参数的错误。执行结果如下:

super(type, obj)

class A:
    value = 'a'

    def say(self):
        print(f'我是a value = {self.value}')


class B(A):
    value = 'b'

    def say(self):
        print(f'我是b value = {self.value}')


class C(B):
    value = 'c'

    def say(self):
        print(f'我是c value = {self.value}')


c = C()  # 定义C的实例
a_super = super(B, c)  # 返回B的下一级A,并绑定C的实例c
print(a_super.value)  # 打印A的属性
a_super.say()  # 调用A的方法

C的MRO顺序为:C -> B -> A -> object,在MRO顺序中找到B的下一个类是A。所以a_super.value是A中value的值,a_super.say()是执行A中的say()方法。且say()方法绑定的是C的实例c,相当于A.say(c),因此A.say()中使用的self.value是c.value。执行结果如下:

super(type, type2)

class A:
    value = 'a'

    def say(self):
        print(f'我是a value = {self.value}')


class B(A):
    value = 'b'

    def say(self):
        print(f'我是b value = {self.value}')


class C(B):
    value = 'c'

    def say(self):
        print(f'我是c value = {self.value}')


c = C()  # 定义C的实例
b_super = super(C, C)  # 返回C的下一级B
print(b_super.value)  # 打印B的属性
b_super.say(c)  # 要调用实例方法时必须传入实例

C的MRO顺序为:C -> B -> A -> object,在MRO顺序中找到C的下一个类是B。所以b_super.value是B中value的值,b_super.say()是执行B中的say()方法。相当于B.say(),因此需要给实例方法say()传入实例,且B.say()中使用的self.value是c.value。执行结果如下:

抽象类

        类本来就是一个抽象的概念了,那抽象类有是个怎样的类呢?抽象类就是:在一堆相似的类中提取出相似的属性和方法,再把这些属性和方法组合成一个新的类,并且这个新的类中存在一个或多个抽象方法。简单来说如果一个类中存在抽象方法时,这个类就是一个抽象类。类在实例化时,会把所有的属性和方法都添加到实例中。但是由于抽象方法不能被使用,所以在实例化时抽象方法并不能添加到实例中。因此抽象类的实例化过程会因添加抽象方法报错而失败,所以抽象类最大的特点就是不能实例化。

抽象方法

        python的abc库中有一个以ABCMeta作为元类的工具类ABC,当一个类继承于ABC或以ABCMeta作为元类时,可以使用abc库中的abstractmethod装饰函数来装饰出一个抽象方法。

抽象实例方法

        继承于ABC或以ABCMeta作为元类的类中,使用@abstractmethod装饰的方法。

from abc import ABC, abstractmethod


class Person(ABC):  # 直接继承ABC工具类
    """人类"""
    food = '面包'

    @abstractmethod  # 使用abstractmethod函数装饰eat方法,把eat方法变成抽象实例方法
    def eat(self, location):
        """
        吃东西

        :param location: 地点
        :return:
        """


person = Person()
person.eat('家里')

当我们实例化Person时就会报错,执行结果如下:

报错显示不能实例化Person,因为Person中有抽象方法eat()。

抽象类方法

        继承于ABC或以ABCMeta作为元类的类中,使用@abstractmethod和@classmethod装饰的方法。

from abc import ABCMeta, abstractmethod


class Person(metaclass=ABCMeta):  # 使用metaclass指定元类为ABCMeta
    """人类"""

    @classmethod  # 使用classmethod装饰abstractmethod(eat)方法,把abstractmethod(eat)变成类方法
    @abstractmethod  # 使用abstractmethod装饰eat方法,把eat方法变成抽象方法
    def eat(cls, location):
        """
        吃东西

        :param location: 地点
        :return:
        """


person = Person()
person.eat('家里')

当我们实例化Person时就会报错,执行结果如下:

报错显示不能实例化Person,因为Person中有抽象方法eat()。

抽象静态方法

        继承于ABC或以ABCMeta作为元类的类中,使用@abstractmethod和@staticmethod装饰的方法。

from abc import ABC, abstractmethod


class Person(ABC):  # 直接继承ABC工具类
    """人类"""

    @staticmethod  # 使用staticmethod装饰abstractmethod(eat)方法,把abstractmethod(eat)变成静态方法
    @abstractmethod  # 使用abstractmethod装饰eat方法,把eat方法变成抽象方法
    def eat(location):
        """
        吃东西

        :param location: 地点
        :return:
        """


person = Person()
person.eat('家里')

当我们实例化Person时就会报错,执行结果如下:

报错显示不能实例化Person,因为Person中有抽象方法eat()。

抽象类的应用

        抽象类既然连实例化都做不到,那么抽象类能有什么用呢?存在即合理,在面向对象的范式编程中,抽象类不但有用而且有大用。抽象类可以帮助我们实现依赖倒置的功能,是实现接口类的基石。抽象类虽然不能实例化,但抽象类的子类可以实例化,前提是子类中实现了抽象类中的抽象方法,让抽象方法不再抽象。

依赖倒置

        从类的继承特性来看,子类是依赖父类的,父类中的方法子类可以不用自己实现,通过继承父类就可以使用父类中的方法。依赖倒置就是父类去依赖子类,父类中只有方法名没有具体逻辑,执行父类中的方法时只能使用子类中方法的逻辑。通过依赖倒置可以降低类与类之间的耦合,增强系统的稳定性,提高代码的可维护性。

        我先举一个不依赖倒置的例子:一个宠物培养游戏,里面有猫、狗、仓鼠等多种宠物可以选择,我们可以选择给这些宠物喂食、让它们睡觉等操作。那么我们可以设计出如下代码:

class Cat:

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

    def cat_eat(self):
        print(f'给{self.name}喂食猫粮')

    def cat_sleep(self):
        print(f'让{self.name}进猫窝睡觉')


class Dog:

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

    def dog_eat(self):
        print(f'给{self.name}喂食狗粮')

    def dog_sleep(self):
        print(f'让{self.name}进狗窝睡觉')


class GameSystem:

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

    def eat(self):
        if type(self.typ) == Cat:
            self.typ.cat_eat()
        else:
            self.typ.dog_eat()

    def sleep(self):
        if type(self.typ) == Cat:
            self.typ.cat_sleep()
        else:
            self.typ.dog_sleep()


cat = Cat('小猫')
dog = Dog('小狗')
game = GameSystem(cat)
game.eat()
game.sleep()
game.typ = dog
game.eat()
game.sleep()

执行结果如下:

我们发现GameSystem中的代码非常臃肿,在GameSystem的eat()和sleep()方法中使用判断语句来判断typ属性的类型。如果要增加更多的宠物类型的话,就需要在GameSystem中写很多的判断语句。通过判断属性的类型来执行对应的方法,就属于高耦合的代码。但你可能会说为什么不在所有的宠物类型中规定相同的方法名,这样就不用写这么多判断语句了。说的不错,但这个例子中的方法比较少,我们还可以强行去给他们定义相同的方法名,但是一旦这个系统变复杂了之后,想强行去定义相同的方法名就变得困难了。你写完一个类后得记住里面的所有方法名以及对应的逻辑,在另一个类中以同样的方式去写一遍。如果是多个人同时开发,想要同步方法名就更困难,每当你新写一个方法都需要跟其他人同步。

        所以我们需要一个抽象类来主导所有的宠物类,在抽象类中只需要定义抽象方法名,很快就能定义出一个抽象类。然后其他的宠物类都继承这个抽象类,这样就可以保证方法同名。不管多少个人同时开发,只要他没有在自己写的宠物类中实现抽象类中的抽象方法,他写的类在调试的时候就不能实例化,他就能明确知道自己少写了哪些方法。下面我将以抽象类来实现依赖倒置:

from abc import ABC, abstractmethod


class Pet(ABC):

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

    @abstractmethod
    def eat(self):
        """喂食"""

    @abstractmethod
    def sleep(self):
        """睡觉"""


class Cat(Pet):

    def eat(self):
        print(f'给{self.name}喂食猫粮')

    def sleep(self):
        print(f'让{self.name}进猫窝睡觉')


class Dog(Pet):

    def eat(self):
        print(f'给{self.name}喂食狗粮')

    def sleep(self):
        print(f'让{self.name}进狗窝睡觉')


class GameSystem:

    def __init__(self, typ: Pet):
        self.typ = typ

    def eat(self):
        self.typ.eat()

    def sleep(self):
        self.typ.sleep()


cat = Cat('小猫')
dog = Dog('小狗')
game = GameSystem(cat)
game.eat()
game.sleep()
game.typ = dog
game.eat()
game.sleep()

执行结果如下:

通过依赖倒置的方式,就可以保证所有的宠物类中该有的方法都有,而且要增加新的宠物类型也不会出错。所以依赖倒置可以降低类与类之间的耦合,增强系统的稳定性,提高代码的可维护性。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐