Python 中类的继承与类装饰器
在上一节的课程中,我们已经初步探索了 Python 中的类,大概了解了类的使用方法。但还有一些需要进一步讲解的内容,那就是类的继承和类装饰器,尤其是类的继承,俗话说的好——“Python不知类继承,便称盖茨也枉然”!
一、什么是类的继承?
“继承”这两个字应该很容易理解,听得最多的就是子女从父母那边继承了财产(当然,也可能是债务),没有财产继承的也会一定程度上继承父母的颜值、身高和性格等基因所决定的先天性因素。
Python 中的类也有这样的特点,可以继承另一个类的所有非私有属性!
说到这里,需要补充一点上节课遗漏的知识点——私有属性。私有属性就是带双下划线(__)的属性,比如:self.__name__ = 'CMOS 三好先生' 中的 __name__ 属性就是私有属性。
言归正传!接着讲类的继承。
比如,我们有一个动物类,有 name 和 age 实例属性和 eat 实例方法(下文若无特殊说明,属性、方法均指实例的属性和方法):
class Animal: def __init__(self, name, age): self.name = name self.age = age def eat(self): print(f"{self.name} is eating.")
我在前一节课说过人、猫、狗和兔子都是动物类,因为它们有一定的共性,比如有名字、年龄,会吃东西;也有一定的差异,比如人会说话会模仿动物叫,猫叫是“喵喵喵”,狗吠是“汪汪汪”,兔子则不会叫。
那么,我们要如何定义人、猫、狗和兔子的类呢?按照 Animal 类重新写一遍?这时候“继承”就需要派上用场了!
下面我就以猫、狗作为例子(至于人嘛,就别来凑热闹了)来看看如何继承 Animal 类:class Cat(Animal): def __init__(self, name, age, breed): super().__init__(name, age) self.breed = breed def meow(self): print(f"{self.name} is meowing.")class Dog(Animal): def __init__(self, name, age, breed): super().__init__(name, age) self.breed = breed def bark(self): print(f"{self.name} is barking.")# 子类用法if __name__ == "__main__": persian = Cat("Tom", 5, "Persian") persian.eat() persian.meow() labrador = Dog("Rufus", 3, "Labrador") labrador.eat() labrador.bark()
从上述的代码可以看出,猫和狗的 class 定义时,都加上了“(Animal)”,这个括号内的类名就表示继承的父类,也就是说 Animal 是 Cat 和 Dog 类的父类,Cat 和 Dog 类是 Animal 类的子类。因为都是继承于 Animal 类,所以我们也把 Cat 和 Dog 类叫做兄弟子类。
因为子类拥有父类的所有非私有属性,所以 Cat 和 Dog 都有 name、age 和 eat 属性,我们就不用再重复写这些代码。
但是,你们会发现在 __init__ 方法中,我们新增了一行 super().__init__(name, age),还增加了一个 breed 参数,然后赋值给了子类的 breed 属性。
先说 breed 属性,这是 Cat 和 Dog 类新增的属性,用来表示猫和狗的品种,这个就不用多解释了。
再来说 super() ,这是类中的内置函数。在子类中,super() 按照继承顺序调用“下一个父类”的方法。通常用来调用被重写的父类方法(比如 __init__ 方法)。
super() 返回的是一个“代理对象”,通过这个对象可以访问父类(或多重继承链上的其他类)中的方法和属性,而不需要写父类的类名。说到这里,就引出了一个新的概念——多重继承,我们后面会详细介绍。
再来看看两个子类里新增的方法:Cat 类里的 meow() 和 Dog 类里的 bark()。这就是定义了该子类特有的方法,其父类和其他兄弟子类的实例都不能调用。
讲到这里,单继承的逻辑就讲完了,接下来我们来学习多重继承。
二、什么是多重继承?
顾名思义,“多重继承”就表示有多个继承的父对象。可以这样理解,一个子类可以继承多个类的属性。比如猫和狗,除了是动物之外,也可以是人类的宠物。我们就可以写下面这样的代码(以 Cat 类为例):
class Pet: def __init__(self, breed, owner, nickname): self.breed = breed self.owner = owner self.nickname = nickname def play(self): print(f"{self.nickname} is playing with {self.owner}.")class PetCat(Cat, Pet): def __init__(self, name, age, breed, owner, nickname): super().__init__(name, age, breed) Pet.__init__(self, breed, owner, nickname) def info(self): print(f"Name: {self.name}, Age: {self.age}, Breed: " + f"{self.breed}, Owner: {self.owner}, Nickname: {self.nickname}")if __name__ == '__main__': pet_persian = PetCat("Tom", 5, "Persian", "John", "Tommy") pet_persian.eat() pet_persian.meow() pet_persian.play() pet_persian.info()
Pet 类是新增的一个表示宠物的类,有breed、owenr、nickname 和 play 属性,PetCat 类是表示宠物猫的类,继承了 Cat 和 Pet 类。通过最后的初始化和调用代码可以看出,PetCat 继承了 Animal、Cat 和 Pet 类的属性。
需要多说几句,PetCat 类的继承顺序很重要,如果写成“PetCat(Pet, Cat)”,运行后续的代码就会报错:
Traceback (most recent call last): File "lesson3_2.py", line 56, in <module> pet_persian.eat() File "lesson3_2.py", line 7, in eat print(f"{self.name} is eating.")AttributeError: 'PetCat' object has no attribute 'name'
因为后面 __init__ 方法里的 super(PetCat, self).__init__ 里的 super 方法会返回多重继承的第一个父类 Pet 的代理对象,然后下面一行 Pet.__init__ 又进行了一次初始化,于是相当于用 Pet.__init__ 初始化了两次,没有调用到 Cat 类的 __init__ 方法,最后在调用 pet_persian.eat() 时就会报错了。
所以,如果在多重继承时,使用 super() 继承初始化方法必须传入第一个继承父类的 __init__ 参数,其他父类的则需要显示调用 __init__ 方法(比如:XXX.__init__(...))。
当然,也可以都用显示调用的方式对子类进行初始化,这样就不需要考虑继承的类的顺序了。比如:
class PetCat(Pet, Cat): def __init__(self, name, age, breed, owner, nickname): Cat.__init__(self, name, age, breed) Pet.__init__(self, breed, owner, nickname) # 等效于 super().__init__(...)
需要注意 super().__init__ 和 <ParentClass>.__init__ 的参数区别,前者不需要传入 self 参数。
至于多重继承的父类顺序的问题,这个是一个叫做 MRO(Method Resolution Order,方法解析顺序)的机制决定的。
MRO 是确定多重继承中方法和属性查找顺序的规则,它通过C3线性化算法(C3 Linearization)生成一个有序列表,保证了查找的一致性、局部优先和单调性,防止了“菱形继承(钻石继承)”等复杂情况下的歧义,该算法在Python 3及新式类中是标准实现,可通过__mro__或mro()方法查看。
[<class'__main__.PetCat'>, <class'__main__.Cat'>, <class'__main__.Animal'>, <class'__main__.Pet'>, <class'object'>]
关于 MRO 的详细讲解,这里就不继续展开了,可以简单理解为:多重继承时,多个父类,如果有相同的属性,那么先继承的父类里的属性优先继承。另外,在多重继承中,super() 保证了公共父类只被初始化一次。比如:
class Base: def __init__(s): print('Base init.')class A(Base): def __init__(s): print('A init.') super().__init__() print('A init end.')class B(Base): def __init__(s): print('B init.') super().__init__() print('B init end.')class C(A, B): def __init__(s): print('C init.') super().__init__() print('C init end.')C()
运行结果如下:
可以看到“Base init.”只显示了一次,说明 Base 类的 __init__ 方法只调用了一次。如果把 C 类中的 super().__init__() 修改为父类显示调用__init__,如下:
class C(A, B): def __init__(s): print('C init.') A.__init__(s) B.__init__(s) print('C init end.')
运行结果如下:
你会发现,不仅 Base.__init__ 调用了两次,A.__init__(s) 调用时,竟然还调用了 B.__init__(s) !!!
可见多重继承时,显示调用 __init__ 有多么浪费资源了吧?所以,多重继承的父类参数相同时,使用 super() 可以大大降低资源消耗。
如果父类的参数不同呢?那就只能像上面的 PetCat 那样对第一个父类使用 super() 调用,其余的就只能显示调用了。所以,要记得把初始化最耗资源的父类作为第一个继承的父类。
最后,还需要对 super() 再进行一个隐藏知识点的说明,仍然以上面的 ABC 类为例。
此时的 MRO 为:
C->A->B->Base->object(Python 中的顶级对象)
- C 类 __init__ 方法中的 super().__init__() 等价于 super(C, s).__init__()。
- super(C, s).__init__()会依次调用 A.__init__(s) 、B.__init__(s) 和 Base.__init__(s)。
- super(A, s).__init__() 会依次调用 B.__init__(s) 和 Base.__init__(s)。
- super(B, s).__init__() 则只会调用 Base.__init__(s)。
也就是说,super() 的第一个参数,表示 MRO 列表中的起点,起点的类不会调用初始化方法(可以课后自己尝试一下)。
讲到这里,关于类的继承这档子事儿基本上就已经讲完了。接下来,我们再来看看类装饰器。
三、装饰的艺术 —— 类装饰器
1、类属性装饰器
类属性装饰器是一种特殊的装饰器,用于定义类的属性访问器 getter、修改器 setter 和删除器 deleter。Python内置的property函数是实现类属性装饰器的常用方法。下面是一个示例:
class Student: def __init__(self): self._score = 0 @property def score(self): return self._scorestudent = Student()print(student.score)student.score = 60 # 报错:AttributeError: can't set attribute
类属性装饰器可以限制类属性的访问权限以及合法性校验。比如上面的例子就是给 Student 类设置了一个只读属性 score,但没有设置修改器 setter,修改属性时就会报错。
下面,我们给 score 属性设置一个修改器 @score.setter:
class Student: def __init__(self): self._score = 0 @property def score(self): return self._score @score.setter def score(self, value): self._score = valuestudent = Student()print(student.score)student.score = 60print(student.score) # 结果:60
这样就不会报错了!如果你想对 score 属性进行合法值校验,就可以在 setter 代码中增加相应的判断,比如:
@score.setter def score(self, value): if not (0 <= value <= 100): raise ValueError("分数必须在 0-100 之间") self._score = value
此时赋值 student.score = 101就会抛出异常,从而保证 score 属性的值不会超过100。
除了类属性装饰器 property 之外,还有哪些类装饰器呢?还有两个分别是:classmethod 和 staticmethod。这两个名字一看就很好理解,一个是类方法,一个是静态方法。那么,它们有什么用处呢?
别急,我们先来看看两者的使用方法:
class X(object): x = 1 def func1(self): print ('func1') @classmethod def func2(c): print ('func2') print (c.x) c().func1() # 调用 foo 方法 @staticmethod def func3(): print ('func3')X.func2() # classmethod 不需要实例化X.func3() # staticmethod 也不需要实例化
可以看出,classmethod 装饰的方法 func2,可以接受一个参数 c(建议写作 cls 更符合 Python 编程约定),这个参数就是类 X 本身,可以用 print(id(c) == id(X)) 来比较两者的 id(内存地址)是否相同。类方法可以访问和修改类的属性状态,通常用于定义那些影响整个类而非某个实例的功能行为。
而 staticmethod 装饰的方法 func3,我们可以认为是类的静态方法,可以直接通过类来调用。一般来说,静态方法用于实现与类的具体实例无关的功能,其功能行为类似于普通函数,仅仅在逻辑上属于类的一种方法属性。
总之,类方法、静态方法再加上实例方法,正确使用这三种类型的方法,可以使代码逻辑更加清晰、结构组织更加良好,让开发者易于理解,从而有效地支持软件开发的面向对象编程范式(这个词经常应该会听说,可以理解为是一种公认的模式、理论或框架体系)。
所以,想要学好 Python 中的类,不仅仅是掌握语法就够了,更多的是要对类的设计理念和运行机制有深刻的理解。希望学完类的这2节课的内容,大家能从“写普通代码”进阶到“用类设计代码”。
好了,今天的课就讲到这里,内容和代码有点多,希望大家能够看懂并掌握相关知识点(一定要实战操作)。我们下一节课的内容是Python 中的迭代器和生成器。同学们,下课!
对了,对本节课的内容有任何疑问,欢迎在留言区给我留言哦~