本系列是对入门书籍《Python编程:从入门到实践》的笔记整理,属于初级内容。标题顺序采用书中标题。
本章主要是对上一章Python类的补充。
从一个类派生出所有类
上一篇文章说道Python类的定义与继承一般是如下形式:
class A: # 或者写成class A(): pass class B(A): pass
其实,对于类
A
,它并不是算是一个真正意义上的基类,而是和Java类似,Python中所有的类最终都继承自object
类(首字母小写,比较特殊),所以对于A
的定义可以写成如下形式:class A(object): pass
只是通常把
object
给省略了。访问限制
从上一篇中我们知道,类的属性可以被直接访问,如果需要对访问做一些限制,我们可以通过定义相应的方法。在Python中,对于一般的属性,用C++或Java的话来说,它们都是公有属性,外部可以直接访问,比如像下面的这个
name
属性:class A: def __init__(self, name): self.name = name
但如果我们在这个属性前面加两个下划线,将其变成如下形式:
class A: def __init__(self, name): self.__name = name
那么
name
就变成了一个私有属性,它只能在对象的内部被访问,如果想以如下形式访问则会报错:# 代码: class A: -- snip -- a = A("test") print(a.__name) # 结果: AttributeError: 'A' object has no attribute '__name'
那是不是真的就访问不到这个属性了呢?说不清是有幸还是不幸,Python没有所谓的真正的私有属性,Python中类的所有属性都能被访问。Python解释器只是将
__name
换了个名称,变成了:self._A__name
即在前面加了一个单下划线和类名。
# 代码: class A: -- snip -- a = A("test") print(a._A__name) # 结果: test
强烈不建议这样访问属性!而且,不同版本的Python解释器会将这样的属性改成不同的名字。
使用装饰器(decorator)
在上一点中说到了通过方法来访问类的属性,这种方式一般叫做
get/set方法
,最后在调用时调用的是类的方法,现在我们使用Python内置的@property
装饰器来访问类的属性,最后在调用时是调用的属性,实际上它是将类的方法通过装饰器变为属性。以下是通过装饰器和通过get/set方法
来访问属性的代码比较:class Teacher: def __init__(self, name): self.name = name def get_name(self): return self.name def set_name(self, name): # 可以加上一些限制 self.name = name class Student: def __init__(self, name): self._name = name @property def name(self): return self._name @name.setter def name(self, name): # 可以加上一些限制 self._name = name t = Teacher("Miss") s = Student("Boy") print(t.get_name()) print(s.name) t.set_name("Miss Lee") s.name = "Kevin"
从上述代码也可以看出,定义的时候,两者的代码量区别其实不大,但是在调用的时候,明显使用装饰器更方便些。
类中其它类型的属性
类中除了普通的属性,以及上述的私有属性,还有前后都有双下划线的属性,例如
__xxx__
,它们是特殊变量,可以被直接访问,不是私有属性,所以一般不要起__name__
,__score__
这样的属性名,对于方法也是如此,不光有想__init__()
这样的方法,还有很多前后都有双下划线的方法,比如__del__()
,它是类的析构函数。在以后的文章中还会介绍许多这种方法。不光有双下划线的属性,还有单下划线的比如
_name
,前单下划线,它表示的意思是:虽然能被访问,但请将其看做私有属性,不要随便访问。对于多态的补充
子类可以被看成是父类的类型,但父类不能被看成是子类的类型。比如:
# 代码: class Animal: pass class Dog(Animal): pass a = Animal() d = Dog() print(isinstance(d, Animal)) print(isinstance(a, Dog)) # 结果: True False
也就是说,如果我们定义了这样一个函数:
def animal_run(animal): animal.run()
它接收
Animal
及其子类的所有对象,只要该类的run()
方法正确编写,Python都能在解释时正确调用相应类的run()
方法,即调用方只管调用animal_run()
函数,不用管类的run()
方法的细节,不管是现有的类还是新扩展出的子类,只要保证run()
正确实现了,那么animal_run()
就是正确的。这就是著名的“开闭原则”
:对扩展开放,对修改封闭。静态语言与动态语言
仍以上面的
animal_run()
函数为例。对于像Java这样的静态语言,传入的参数必须是Animal
及其子类,否则就无法调用run()
方法。而对于像Python这样的动态语言,传入的不一定要求是Animal
及其子类,只要这个对象有run()
方法就行了。这就是动态语言的“鸭子类型”
,只要“看起来像鸭子,走起道来像鸭子”,那它就能被看做是鸭子。Python的“file-like object”
就是一种“鸭子类型”,对于真正的文件对象,都有一个read()
方法,用于返回文件内容。但对于其他对象,只要正确实现了read()
方法,即使它不是文件对象,它也能被看做是文件。多重继承与MixIn设计
前一篇文章中的继承是单继承,但Python和C++一样,支持多重继承;Java只支持单继承,她通过接口类来实现多重继承的效果。首先需要搞清楚多重继承为什么存在。仍然以
Animal
类为例,动物里有哺乳动物,卵生动物,有能飞的动物和不能飞的动物,这是两种大的分类方式。如果我们要派生出一个能飞的哺乳动物(比如蝙蝠),如果按照单一继承,可以按如下方式:
也可以先从Animal
继承出Runnable
和Flyable
两个类,再继承出哺乳类和卵生类(相当于将上图的二三层换了位置),但从这种单继承可以看出,如果分类增多,类的数量将呈指数级增加。故而一般采用多重继承的方式:class Animal: pass class Mammalia(Animal): pass class Flyable: def fly(self): print("Flying...") class Bat(Mammalia, Flyable): pass
这样
Bat
类将具有Mammalia
和Flyable
两个父类的所有属性与方法。一般在Java中,以able
为结尾类的都作为接口。在设计类的继承的时候,一般主线都是单一继承的,像上述例子中的从
Animal
派生出Manmalia
,但如果后续的类中要混入一些额外的功能,但这功能又不是这个子类所独有的,比如上述的Flyable
,那么就可以通过多重继承,从Manmalia
和Runnable
派生出Bat
类,这就是MinIn设计
,Java中采用接口来实现这种设计。为了更好的看出继承关系,一般将
Runnable
和Flyable
类的名字改为RunnableMixIn
和FlyableMixIn
,同时,还可以定义出肉食动物CarnivorousMixIn
和植食动物HerbivoresMixIn
,让子类同时拥有好几个MinIn
:class Dog(Mammalia, RunnableMixIn, CarnivorousMixIn): pass
所以在设计类时,我们应该优先考虑通过多重继承来组合多个
MinIn
,而不是直接考虑更多层次的继承关系。最后,本篇较多内容是根据廖雪峰老师的博客再理解而来的,感谢廖雪峰老师!