目录
关于这个系列
《最值得收藏的python3语法汇总》,是我为了准备公众号“跟哥一起学python”上面视频教程而写的课件。整个课件将近200页,10w字,几乎囊括了python3所有的语法知识点。
你可以关注这个公众号“跟哥一起学python”,获取对应的视频和实例源码。
这是我和几位老程序员一起维护的个人公众号,全是原创性的干货编程类技术文章,欢迎关注。
1、POP和OOP
有两种主流的编程思想:面向过程编程(Procedure-Oriented Programming,简称POP)和面向对象编程(Object-Oriented Programming,简称OOP)。
面向过程编程:就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
面向对象编程:是把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
面向过程编程,关注的是“做一件事”;而面向对象编程,关注的是“造一堆东西”。前者强调的是一系列行为,而后者强调的是一系列事物(对象)。
这比较抽象,我们举个实际的例子,比如我们要实现“把大象放进冰箱”这样一段程序。
面向过程编程: 打开冰箱门 – 把大象放进去 – 关上冰箱门。
面向对象编程: 创建一个对象(冰箱),给它定义三个动作,开门(open)、放置物品(put)、关门(close)。然后分别让冰箱执行这三个动作。
大家看出区别了吗?面向过程编程,强调的是动作,程序员关注的是如何执行一系列动作的问题。而面向对象编程,强调的是对象,程序员关注的是如何合理地定义一个对象的属性和行为的问题。虽然它们都能实现相同的功能,但是编程的思想是截然不同的。
不同的高级编程语言,对OOP的支持是不一样的。比如C语言,我们通常认为它是一种面向过程的语言,因为它没有提供对面向对象编程的直接支持,但是C语言也可以封装对象。C++和JAVA就是很明确的支持面向对象编程的语言。Python支持面向对象编程,同样我们也可以不面向对象,而面向过程编程。POP和OOP仅仅是一种编程思想。
本节我们要讲的类,就是Python支持OOP的语法机制。
2、类的定义语法
什么是类(Class)?我们还是以上面“冰箱” 的例子来说明。大家可以想想我们该如何去描述“冰箱”呢?
首先,“冰箱”有很多品牌,海尔、美菱、三星、西门子等等,“冰箱”也有很多类型,单门、双开门、三开门等等,“冰箱”也有不同的节能级别,一级能效、二级能效、三级能效等等,“冰箱”还有很多其他的特征。这些我们可以统一称之为“冰箱”的属性。
然后,我们对“冰箱”有很多操作方式。比如:打开冰箱门、关上冰箱门、调节保鲜区域温度、调节冷冻区域温度等等。这些不是“冰箱”的特征,而是“冰箱”的操作,我们统一称之为“冰箱”的方法、行为。
我们可以将这些“属性”和“行为”组合在一起,就可以描述“冰箱”了,我们将这个组合称之为“冰箱类”。如下图所示:
在Python中,我们使用类(Class)的语法来描述这样一个对象。它的表述如下图所示:
面向对象编程中,我们通常使用UML类图来设计类。以上就是冰箱类的UML类图设计。我们定义了一个类叫Refrigerator,它里面包含了一系列的属性和成员函数。注意,前面的+号反应了这个属性和成员函数的可见范围是public,关于可见范围我们后面会讲。
我们可以将这个类图转换为python的代码(某些UML工具可以自动根据上面的UML类图生成代码,这里我们手动敲代码)。
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./12/12_1.py
# 定义一个冰箱类
class Refrigerator:
brand = '海尔'
color = 'white'
price = 0
power_level = 1
door_type = '双开门'
def open_door(self):
pass
def close_door(self):
pass
def temp_mod(self):
pass
def temp_show(self):
pass
def put(self):
pass
这个代码里面,我们所有的成员函数都没有具体实现,用了pass空语句占位。
类以关键词class开头,后面跟类名(自定义名字,规则参考变量名。按照编程规范,我们建议采用驼峰命名法,就是每个单词首字母大写),再后面跟一个冒号:。
类里面的属性,其实就是在类里面定义的一系列变量。而类的成员函数,其实就是在类里面定义的一系列函数。
这样,我们就定义了一个基础的冰箱类。
下面我们将里面的一些方法补充一些实现代码,看看类该如何使用。我们在open_door()和close_door()方法中分别打印一些输出。
def open_door(self):
print("refrigerator's door is opened!")
def close_door(self):
print("refrigerator's door is closed!")
然后使用这个类来模拟冰箱开门和关门的动作:
if __name__ == '__main__':
inst1 = Refrigerator()
inst1.open_door()
inst1.close_door()
输出为:
refrigerator's door is opened!
refrigerator's door is closed!
上面的第二行代码 inst1 = Refrigerator() ,它定义了一个变量inst1,并且指向了一个新创建的Refrigerator实例对象。我们把这个过程叫做类的实例化过程,实例化的结果是新生成一个实例对象。
如何理解实例化呢?Refrigerator类表示是冰箱这个类型,而inst1则表示的是具体的一款冰箱甚至是一台冰箱。所谓实例化,就是通过冰箱这种类型,创建一个具体的冰箱对象的过程。实例化最终会生成一个具体的实例对象。
这里我们提到了两个比较容易混淆的概念:类对象和实例对象。
Python是万物皆对象的,所以类本身也是一个对象,我们叫做类对象。类对象经过实例化之后,也会生成一个对象,这个叫实例对象。
3、类对象
类对象支持两种操作,一个是属性和成员函数的引用,一个是实例化。
属性引用
类对象的属于引用,包括对类里面变量和成员函数的引用。我们采用obj.name的方式来引用。如果name是一个变量,那么返回一个变量对象;而如果name是一个成员函数,那么返回的就是一个函数对象。
通过属性引用,我们可以查看、调用甚至修改变量或者成员函数。
if __name__ == '__main__':
# 类对象属性引用
Refrigerator.brand = '西门子'
func_open = Refrigerator.open_door
func_open(Refrigerator('三星'))
上面代码中,我们修改了类对象的变量brand。同时将函数对象open_door赋值给了一个临时变量func_open,通过func_open去调用这个函数。
对类对象属性的改变,会影响其例化的所有实例对象。
实例一、在实例化之前改变类对象属性,直接影响实例对象。
Refrigerator.color = 'red'
inst4 = Refrigerator('三星')
print(inst4.color)
输出为:
red
实例二、在实例化之后改变类对象属性,同样会影响所有实例。
Refrigerator.color = 'green' # 不可变类型
print(inst4.color)
print(inst4.goods_list)
Refrigerator.goods_list.append("pear") # 可变类型,受影响
print(inst4.goods_list)
输出为:
Green
[]
[‘pear’]
我们把在类里面直接定义的这些变量,叫做类变量。类变量是在类对象和实例对象之间共享的,任何对类变量的修改,都会影响其他实例对象。同理,在类里面定义的成员函数也是这样的。
类的实例化
上一节的例子中,我们已经对冰箱类做了一个简单的实例化,它是不带任何参数的。而实际代码中,我们通常需要携带一些参数。比如,我们希望实例化一个海尔牌的冰箱,那么需要携带一个品牌名参数。
类的实例化通过在类里面重写__init__()成员函数来实现,这个成员函数是python类自带的。如果我们不重写,那么会采用自带函数默认处理。自带的__init__()是不携带参数的,比如我们前面的例子。
如果我们要支持带品牌名参数的实例化方法,那么我们需要在Refrigerator类里面增加一个__init__的定义:
class Refrigerator:
brand = '海尔'
def __init__(self, brand):
self.brand = brand
if __name__ == '__main__':
inst1 = Refrigerator("三星")
这样在实例化之后,inst1对象的brand属性就是“三星”。我们创建了一个三星冰箱的实例对象!
__init__()这个成员函数我们通常称之为“构造方法”。在类的实例化过程中,这个函数会被系统自动调用。我们可以通过重写该函数,实现我们自己的实例化逻辑,通常是给类里面的属性赋一些初始值。
在我们重写的构造方法里面,我们用到了self参数,实际上我们类里面定义的所有成员函数的第一个参数都是self。
当我们调用实例对象的方法(obj.method)时,系统会自动给这个方法插入第一个实参,这个实参就是该对象本身的引用。所以self就是对象本身的引用,成员函数里面可以用self来操作该对象的属性和方法。
Self参数是系统给自动填充的,我们在调用实例对象方法的时候看不到这个参数。
这个参数不一定要命名为self,你可以命名为任意合法的参数名都行。只是习惯上我们将其命名为self,这样别人一看就明白它是实例对象的引用。这是一个约定俗成的习惯,就像其它编程语言中可能习惯将其命名为this一样。
实例对象
类对象经过实例化,产生的这个对象就是实例对象。实例对象只有一种操作类型:属性引用。注意这里的属性包含了对象里面的变量和方法。
属性引用的语法和类对象是一样的:obj.name。
当name是一个变量时,返回的就是一个变量对象;当name是一个函数时,返回的就是一个方法对象。
大家要注意,我们前面提到类对象的成员函数时,说的是函数对象。而这里说的是方法对象。通常我们在说类的时候,喜欢把它们里面定义的函数称之为成员函数;而在说实例对象时,喜欢把它们叫做方法。比如类的成员函数、实例的方法。
类对象引用的函数对象,和实例对象引用的方法对象,两者是有差别的。它们在解释器中是完全不同的两种对象类型。
inst4 = Refrigerator(5000)
print(f'refrence by class obj: {type(Refrigerator.door_open)}')
print(f'refrence by instance obj: {type(inst4.door_open)}')
输出为:
refrence by class obj: <class 'function'>
refrence by instance obj: <class 'method'>
在引用函数对象时,系统不会自动给你补齐self参数;而在引用方法对象时,系统会把当前对象自动填充为self实参。
比如我们前面在引用函数对象时,需要自己实例化一个对象作为self参数传入:
func_open(Refrigerator('三星'))
而如果是方法对象,则不需要:
# 实例对象属性引用
inst1 = Refrigerator("三星")
inst1.open_door()
4、类变量和实例变量
类变量,是我们在类里面定义的变量;而实例变量,则是某个实例定义的变量。看下面的例子:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./12/12_2.py
# 类变量和实例变量
class Dog:
age = 2 # 类变量
def __init__(self, name):
self.name = name # 实例变量
self.color = 'white' # 实例变量
def set_color(self, color):
self.color = color
def get_name(self):
print(self.name)
if __name__ == '__main__':
Dog.height = 30 # 类变量
mydog = Dog('apple')
mydog.weight = 10 # 实例变量
pass
我们可以断点查看Dog类对象和mydog实例对象的变量:
可以看到,Dog类对象只有两个变量age和height,它们都是类变量。而mydog除了从Dog类继承过来的这两个类变量以外,还增加了color、name、weight,这三个变量就是实例变量。
在成员函数中,采用self.name定义的变量,或者在外面通过实例名.name定义的变量,都是实例变量。
在类里面直接定义的变量,或者在外面通过类对象.name定义的变量,都是类变量。
类变量在类对象和各个实例对象之间是共享的,而实例变量则只在本实例有效,实例之间是独立的。我们看下面的例子:
# 类变量是共享的
mydog2 = Dog('coco')
Dog.age = 4
print(f"apple'age is {mydog.age}, coco'age is {mydog2.age}")
输出为:
apple'age is 4, coco'age is 4
当我们通过类引用的方式改变类变量age的值为4之后,我们发现两个实例对象mydog和mydog2对应的age都变成了4。可以看出,类变量age是共享的。
下面的代码,你可能会觉得和上面的原则是矛盾的:
mydog2.age = 6 # 注意,这里改变的不是类变量age,
# 解释器认为这行代码表示新定义了一个实例变量age
print(f"apple'age is {mydog.age}, coco'age is {mydog2.age}")
输出为:
apple'age is 4, coco'age is 6
表象上看来,类变量age并没有被共享。实际上,mydog2.age=6,这行代码会被python解释器理解为新定义了一个实例变量age,只是它和类变量重名了。前面我们学过命名空间,所以我们知道类对象和实例对象是两个独立的命名空间,是可以重名的。对于这种重名的情况,系统会认为实例变量优先。
定义了实例变量age之后,无论类变量age怎么改变,都不会对mydog2产生影响了。如下代码:
# 之后修改类变量age,将不会再影响mydog2了,因为实例变量优先
Dog.age = 8
print(f"apple'age is {mydog.age}, coco'age is {mydog2.age}")
输出为:
apple'age is 8, coco'age is 6
可以看到,修改类变量age=8,mydog的age变了,而mydog2则还是6,不受影响。
如果类变量是一个可变数据类型呢?它依然遵循这一规则。
# 增加一个可变数据类型的类变量
Dog.foods = []
print(f"apple'foods is {mydog.foods}, coco'foods is {mydog2.foods}")
Dog.foods.append('meat')
print(f"apple'foods is {mydog.foods}, coco'foods is {mydog2.foods}")
mydog.foods.append('rice') # 这里是对类变量的引用,不是定义
print(f"apple'foods is {mydog.foods}, coco'foods is {mydog2.foods}")
mydog2.foods = ['fish'] # 这里是定义了一个新的实例变量
print(f"apple'foods is {mydog.foods}, coco'foods is {mydog2.foods}")
输出为:
apple'foods is [], coco'foods is []
apple'foods is ['meat'], coco'foods is ['meat']
apple'foods is ['meat', 'rice'], coco'foods is ['meat', 'rice']
apple'foods is ['meat', 'rice'], coco'foods is ['fish']
对于实例变量来说,它的作用域仅限于本实例对象,不同实例对象之间的实例变量是完全独立的,互不影响。实例变量对类对象是不可见的,也就是说类对象不能引用实例变量。看下面的例子:
# 不同实例对象的实例变量之间互不影响
mydog.color = 'black'
mydog2.color = 'yellow'
print(f"apple'color is {mydog.color}, coco'color is {mydog2.color}")
输出为:
apple'color is black, coco'color is yellow
另外补充一点,我们前面学过作用域,知道函数嵌套时,内层函数是可以访问外层函数定义的变量的。我们刚接触类的时候,会以为类的成员函数可以直接访问类变量,这其实是错误的。实际上,很多面向对象编程语言中,确实也是这样设计的。但是python却不是这样的。比如下面的代码:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./12/12_3.py
# 成员函数不能访问类变量
class Dog:
foods = [] # 类变量
def __init__(self, name):
self.name = name # 实例变量
self.color = 'white' # 实例变量
def set_age(self):
foods.append('rice') # 变量未定义
Set_age中引用变量foods,会报变量未定义错误。事实上,类对象和它里面的成员函数,都有自己独立的命名空间,它们之间不存在嵌套关系。所以在名字搜索时,并不会搜索到类对象的命名空间。
成员函数里面只能对self实例对象里面的变量进行操作。它可以通过这种self.name的间接引用方式来访问类变量。
5、继承
概念
“继承”是面向对象编程的重要机制,python当然也支持类的“继承”。注意,‘继承’特指的是类对象之间的继承关系,实例对象没有继承的概念。
什么是“继承”?我们来看看下面的例子:
上图中,我们定义了四个类,分别是Person(人)、Student(学生)、Speaker(演讲者)、StudentSpeaker(演讲的学生)。
这些类之间,存在这样的语义逻辑:
Student和Speaker,一定是Person;StudentSpeaker则一定同时是Student和Speaker。
换一种更加专业一点的说法,就是:
Student和Speaker,一定是拥有Person的属性和行为;StudentSpeaker则一定同时拥有Student和Speaker的属性和行为。
既然是这样,那么我们为什么要重复定义这些属性和行为呢?我们是否可以将Person的属性和行为直接“继承”下来为我所用呢?这就是面向对象编程中“继承”的概念。
我们把继承者称为“子类(Child),或者叫派生类”,被继承者称为“父类(Parent),或者叫基类”。这个概念是相对的,比如Student是Person的子类,同时它又是StudentSpeaker的父类。
我们把这种继承关系转换为python代码来表述,如下:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./12/12_4.py
# 类继承
class Person:
def __init__(self, name, age=20, sex='male'):
self.name = name
self.age = age
self.sex = sex
def introduce(self):
print(f'I\'m {self.name}, {self.age} years old.')
class Student(Person):
def school_set(self, school_name, grade=1):
self.school = school_name
self.grade = grade
子类在定义时,可以在类名后面括号里面列出其继承的父类名。这就完成了“继承”过程。子类对象继承了父类对象的全部属性和成员函数。同时,子类对象可以增加自己的属性和成员函数,这些对父类对象不可见。
if __name__ == '__main__':
s1 = Student(name='Jack')
s1.introduce()
输出为:
I'm Jack, 20 years old.
大家可以看到,我们在子类Student中并没有定义构造方法__init__(),它继承了父类Person的构造方法。
同理,Speaker类也类似地继承了Person类,我们不赘述。
多重继承
我们再来看看StudentSpeaker类,它同时继承了两个父类(Student、Speaker),这种拥有多个父类的继承方式,称之为“多重继承”。
多重继承的实现方式很简单,你只需要在定义子类的时候,括号里面列出所有父类的名称即可。
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./12/12_4.py
# 类继承
class Person:
def __init__(self, name, age=20, sex='male'):
self.name = name
self.age = age
self.sex = sex
def introduce(self):
print(f'I\'m {self.name}, {self.age} years old.')
class Student(Person):
def school_set(self, school_name):
self.school = school_name
def grade_set(self, grade):
self.grade = grade
class Speaker(Person):
topic = ''
def speak(self):
print(f'I\'m a speaker')
class StudentSpeaker(Student, Speaker):
def student_speak(self):
print(f'I\'m a student and a speaker.')
if __name__ == '__main__':
ss1 = StudentSpeaker(name='Jack')
ss1.introduce() # 继承自父类的父类 Person
ss1.speak() # 继承自父类Speaker
ss1.school_set('Beijing 101 Middle School') # 继承自父类Student
ss1.student_speak() # 自己的方法
StudentSpeaker类对象同时继承了两个父类Student和Speaker,可以看到,它的实例对象可以直接调用继承自父类的所有方法。
类的继承可以让我们很方便的实现代码功能的复用,你不需要重复实现这些功能,只需要简单的继承即可。
对于多重继承,会出现以下问题,就是如果不同父类之间出现了相同命名的属性或者成员函数,该怎么办呢?比如Student类和Speaker类同时定义了一个成员函数用于打印年龄age_get(),通过下面例子我们看看StudentSpeaker调用的是哪个父类的这个函数。
class Student(Person):
def age_get(self):
print(f'I\'m a student, {self.age} years old')
class Speaker(Person):
def age_get(self):
print(f'I\'m a speaker, {self.age} years old')
class StudentSpeaker(Student, Speaker):
def student_speak(self):
print(f'I\'m a student and a speaker.')
if __name__ == '__main__':
ss1 = StudentSpeaker(name='Jack')
ss1.age_get()
输出为:
I'm a student, 20 years old
很显然,它最终调用的是Student的成员函数。
实际上,python解释器在多重继承的情况下,我们可以简单认为它采用了深度优先、从左至右的原则去搜索。也就是说,它会按照StudentSpeaker的父类列表顺序,先查找Student类,找不到就查找Student类的父类,依次往上查找,这就是所谓的深度优先。如果最终也找不到,则查找父类列表中的下一个,也就是Speaker,这就是所谓的从左至右。简单讲,就是父类列表从左至右搜索,每个父类要深度优先搜索。
但是我们这个例子中,Student和Speaker继承自同一个父类Person,如果按照上面的规则,则Person会被搜索两遍。
事实上python解释器真实的搜索过程要复杂很多。它采用了一种线性化算法(C3算法),可以将这种复杂的搜索关系线性化为一个列表,我们称之为方法解析顺序列表(Method Resolution Order, MRO)。这个列表存储在类对象的__mro__变量中。
print(StudentSpeaker.__mro__)
输出为:
(<class '__main__.StudentSpeaker'>, <class '__main__.Student'>, <class '__main__.Speaker'>, <class '__main__.Person'>, <class 'object'>)
这就是真正的搜索顺序。StudentSpeaker->Student->Speaker->Person->object。
它把共同父类Person放在了所以子类的后面。最后的object是python所有类的最终父类(祖先类),如果一个类定义时没有指定父类,那么它实质上是继承至object类。
看下面这个复杂的例子,它的搜索路径是什么呢?
它的MRO是: ABECDFobject,算法如下;
首先找入度为0的点,只有A,把A取出,把A相关的边去掉,再找下一个入度为0的点,B和C满足条件,从左侧开始取,取出B,这时顺序是AB,然后去掉B相关的边,这时候入度为0的点有E和C,依然取左边的E,这时候顺序为ABE,接着去掉E相关的边,这时只有一个点入度为0,那就是C,取C,顺序为ABEC。去掉C的边得到两个入度为0的点D和F,取出D,顺序为ABECD,然后去掉D相关的边,那么下一个入度为0的就是F,然后是object。
方法重写
相同的行为,子类和父类的处理方式很可能不一样。
比如,对于“自我介绍”这一行为,父类的处理是“我叫XXX,我xxx岁”。而子类学生的处理是“我叫XXX,我XXX岁,我来自xxxxx学校”。
这种相同行为,处理却不一致的情况,我们就需要在子类中对父类的这个函数进行重写,这就是方法重写。按照前面讲的搜索原则,子类肯定是调用自己重写后的这个方法。
class Person:
def introduce(self):
print(f'I\'m {self.name}, {self.age} years old.')
class Student(Person):
def introduce(self):
print(f'I\'m {self.name}, {self.age} years old. I study in {self.school}')
if __name__ == '__main__':
s1 = Student(name='Tom')
s1.school_set('Beijing 101 Middle School')
s1.introduce()
输出为:
I'm Tom, 20 years old. I study in Beijing 101 Middle School
方法重写是面向对象编程中非常实用的机制,因为它让子类很容易具备自己的“个性”,而不仅仅是盲目地从父类那里继承。
方法重写,在类的实例化中用得更多。通常,子类和父类的实例化过程是不太一样的,我们需要重写__init__()方法。
class Person:
def __init__(self, name, age=20, sex='male'):
self.name = name
self.age = age
self.sex = sex
class Speaker(Person):
def __init__(self, name, topic, age=20, sex='male'):
super().__init__(name, age, sex)
self.topic = topic
if __name__ == '__main__':
sp1 = Speaker('Jeffery', 'Python在大数据分析领域的应用实践')
sp1.speak()
输出为:
I'm a speaker, the topic of my talk is "Python在大数据分析领域的应用实践"
我们在子类Speaker中重写了构造方法__init__(),在实例化Speaker对象时,系统是调用了Speaker类的构造方法,而不是Person类。
有意思的是,我们在Speaker类构造方法中,我们使用了下面的语句:
super().__init__(name, age, sex)
相信大家应该能看明白,它的功能是调用了父类的构造方法。通过super()去调用父类方法,是非常常用的,尤其是重写构造方法时。这样做能保证父类的属性被正确初始化,同时也能减少子类和父类之间代码的重复量。
这个简单例子中,我们可以认为super是调用了父类,但是这个说法并不完全正确。下面我们讲讲super()的本质。
理解super
Super()在多重继承的情况下并不一定是调用了父类,看下面的例子:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./12/12_5.py
# super
class Base:
def __init__(self):
print("enter Base")
print("leave Base")
class A(Base):
def __init__(self):
print("enter A")
super(A, self).__init__()
print("leave A")
class B(Base):
def __init__(self):
print("enter B")
super(B, self).__init__()
print("leave B")
class C(A, B):
def __init__(self):
print("enter C")
super(C, self).__init__()
print("leave C")
if __name__ == '__main__':
inst_c = C()
输出为:
enter C
enter A
enter B
enter Base
leave Base
leave B
leave A
leave C
可以看到,当A里面调用super()时,它并没有调用其父类Base,而是调用了B。
这个例子中,类的继承关系如下:
C是多重继承了A、B。
实际上,当我们调用super(cls, inst) 时,python会获取inst的MRO。前面我们学过,这里inst_C的MRO应该是: C->A->B->Base。每次调用super(cls, inst).method(),它实际执行的,是cls在MRO中的下一个类对应的方法。所以,当我们在A的__init__中调用super时,它实际执行的是B的构造方法。
这就是super的本质,它是按照入参inst对应的MRO顺序执行的,不一定是其父类。
Super不只是在构造方法中使用,在所有成员函数中都可以使用。
Isinstance和issubcass
我们介绍两个有用的内置函数,他们在继承机制中比较常用。
isinstance()函数:用来判断一个对象是否是一个已知的类型,类似type()。
语法
isinstance(object, classinfo)
参数
object——实例对象
classinfo——可以是直接或间接类名、基本类型或者由他们组成的元组。
返回值
如果对象类型与参数二的类型(classinfo)相同则返回True,否则返回False。
比如:
sp1 = Speaker('Jeffery', 'Python在大数据分析领域的应用实践')
print(isinstance(sp1, Speaker))
输出为:
True
它和type()有一些区别,type()不考虑继承关系,而isinstance要考虑继承关系。比如:
print(isinstance(sp1, Speaker))
print(isinstance(sp1, Person))
print(type(sp1) == Speaker)
print(type(sp1) == Person)
输出为:
True
True
True
False
issubclass() :来检查类的继承关系。
语法
以下是 issubclass() 方法的语法:
issubclass(class, classinfo)
参数
class -- 类。
classinfo -- 类。
返回值
如果 class 是 classinfo 的子类返回 True,否则返回 False。
比如:
print(issubclass(StudentSpeaker, Person)) # True
print(issubclass(Student, Person)) # True
print(issubclass(Student, Speaker)) # False
输出为:
True
True
False
6、多态
多态性是面向对象编程的一个重要特性,所谓多态,就是一个事物的多种形态。
我们看看JAVA中多态的体现:
- 方法支持重载(overload)和重写(overwrite)
- 对象多态性(将子类的对象赋给父类类型引用)
Python支持方法的重写,前面已经讲过。但是不支持重载,所谓重载,就是允许函数名相同而参数列表不同的函数同时存在。显然,Python中,如果一个类(或者父子类)存在重载的函数,则直接认为是重写覆盖了。
再看对象多态性,Python的变量是不需要声明类型的,它指向什么类型的对象,它就是什么类型,它是一种动态类型语言。所以,它不存在对象多态性的说法。
所以,Python的多态性,其实就是体现在方法重写。多态性在Python中体现很弱,所以大家可以看到很多教程根本就不会提这个特性的。但我们需要明确,python也是支持多态的。
7、成员可见范围
在前面我们所有的例子中,类里面定义的变量或者函数,外面都是可以访问的。在JAVA或者C++中,我们可以通过关键字public、private、protect等,来指明成员变量或者成员函数的可见范围。比如,private就表示是私有的,只能自己内部的成员函数访问;public则是外部可见的;protect则是派生类可见。
在python中,我们规定,如果类里面的成员名字以两个下划线__开头(至少带有两个前缀下划线,至多一个后缀下划线),这样的成员是私有成员(变量或者函数),比如下面例子中的__name。
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./12/12_6.py
# 私有成员
class Fruits:
__name = ''
def __init__(self, name):
self.__name = name
if __name__ == '__main__':
f1 = Fruits('apple')
print(f1.__name)
会报错:
AttributeError: 'Fruits' object has no attribute '__name'
因为__name是私有变量,所以它只能在成员函数中被引用,外层是看不见它的,
但是,Python并没有真正像JAVA的private那样限制访问私有变量。它实际是使用了名字替代的方式将其藏起来了而已。我们可以通过__dict__查看这个实例对象的变量:
print(f1.__dict__)
输出为:
{'_Fruits__name': 'apple'}
这里面出现了一个奇怪的变量_Fruits__name。这其实就是Python隐藏私有变量的方法,它会把满足命名规则的私有变量,替换为_classname+name的形式,这样外部再想用变量名去访问肯定就找不到符号了。
这种私有变量,其实是可以强制访问的,我们只需要使用替换后的名字即可。
print(f1._Fruits__name)
输出为:
apple
所以,严格来说,python并没有提供限制访问私有变量的机制,它仅仅提供了名字替代的方式将其隐藏了。你完全可以强制访问它,全靠自觉。
8、迭代器
迭代器是python中用于遍历容器对象的一种机制,这里的容器包括字符串、列表、集合、字典、文件等等。迭代器提供了两个基本方法iter()和next(),iter()用于返回一个迭代对象,next()用于返回容器中的下一个元素。这两个方法配合使用,可以让我们很方便的遍历一个对象。
看下面的例子:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./12/12_7.py
# 迭代器
list1 = [1, 2, 3, 4]
it = iter(list1)
while True:
try:
print(next(it), end=’’)
except StopIteration:
break
输出为:
1 2 3 4
我们通过iter(list1)获取到了list1的迭代对象it,然后通过next(it)遍历list1的所有元素。每次调用next时,迭代器会记录下当前遍历位置,所以再次调用next时会依次返回元素。当遍历完所有元素时,会抛出异常StopIteration。
同样,字符串、集合、元组、字典等等,都是支持迭代器的。我们知道,python里面万物皆对象,这些数据类型其实也是对象。这些对象里面实现了成员函数__iter__(),它返回一个迭代器对象,没错,iter()方法其实就是调用的这个成员函数。
真正实现遍历的,是这个迭代器对象,list的迭代器对象是list_iterator。这个对象里面需要实现__next__()成员函数,它记录当前遍历位置,并且返回一个值。
所以,迭代器是一个类对象,它需要包含两个成员函数__iter__()和__next__()。类对象本身就可以是迭代器,当然也可以分开写。
我们看下面的例子,它把让类对象本身就成了一个迭代器:
class ExampleAndIter:
def __init__(self, max):
self.max = max
self.position = 0
def __iter__(self):
self.position = 0
return self
def __next__(self):
if self.position > self.max:
raise StopIteration
else:
ret = self.position ** 3
self.position += 1
return ret
inst1 = ExampleAndIter(10)
it1 = iter(inst1)
for item in it1:
print(item, end='')
输出为:
0 1 8 27 64 125 216 343 512 729 1000
__iter__()里面返回的是self,也就是它自己本身就是一个迭代器。这里我们用了for循环的方式来遍历,迭代器也支持这种方式,它实质上也是隐含调用了__next__()方法。
下面我们把迭代器分开写:
class MyIter:
def __init__(self, obj):
self.obj = obj
def __iter__(self):
return self
def __next__(self):
if self.obj.position > self.obj.max:
raise StopIteration
else:
ret = self.obj.position ** 3
self.obj.position += 1
return ret
class Example:
def __init__(self, max):
self.max = max
self.position = 0
def __iter__(self):
self.position = 0
return MyIter(self)
inst2 = Example(10)
it2 = iter(inst2)
for item in it2:
print(item, end=' ')
得到的效果和上面例子一致。
9、生成器
生成器的作用是为了更加简便的生成一个迭代器,而不用向上节那样需要我们在类对象里面实现__iter__和__next__成员函数。
比如上节迭代器的例子,我们要遍历一个范围内的整数的3次方,利用生成器我们可以这样实现:
# 生成器
def pow3(max):
for item in range(max + 1):
yield item ** 3
it3 = pow3(10)
while True:
try:
t = next(it3)
except StopIteration:
break
输出为:
0 1 8 27 64 125 216 343 512 729 1000
它能实现完全相同的功能,但是我们不需要定义一个完整的迭代器类对象,只需要一个简单的函数即可搞定。生成器会帮我们实现一个迭代器。
我们来仔细看看上面的代码,它与众不同的地方在于它使用了一个关键词yield。
Yield用于返回一个值,类似于return,但是它不会像return那样真正返回并结束这个函数。Yield返回值后,函数的上下文会被临时保存,程序切换到外层调用者运行。当再次被调用时,会获取保存的函数上下文继续执行。
如果一个函数包含了yield,那么它就是一个生成器。它被调用时返回的是一个迭代器。我们可以像使用迭代器一样使用它。生成器可以大大简化迭代器的编写。
生成器还有另外一种形态,就是生成器表达式,它和我们前面讲的列表推导式几乎一样,只是它不用方括号[],而是用小括号()。
上面的例子我们也可以用生成器表达式来实现:
# 生成器表达式
it4 = (x ** 3 for x in range(11))
while True:
try:
print(next(it4), end=' ')
except StopIteration:
break
对于一些简单的迭代逻辑,我们完全可以使用生成器表达式来替代生成器。
也许你会说,这个例子中我们为什么不用列表推导式呢?它一样可以实现这个功能啊。没错,它们功能是一样的,但是使用生成器表达式会比列表推导式要更省内存。因为列表推导式会一次性把所有元素全部生成出来,存储在内存中。而生成器表达式则在每次调用__next__时才产生一个元素。当元素的数量巨大时,它们消耗的内存差异会很大。
同样,前面例子中,我们也可以不使用yeild返回,而定义一个列表,将每次获取的值作为元素存在列表里面再返回列表。如果列表太大,这也会面临内存消耗过大的问题。使用yeild可以达到获取一个元素就处理一个元素的效果,可以极大的节省内存。
生成器可以表示一个无限数据流,如下例:
# 无限数据流
def get_all_even_number(): # 获取无限偶数
n = 1
while True:
if n % 2 == 0:
yield n
n += 1
# 获取20次偶数
it5 = get_all_even_number()
for item in range(20):
print(next(it5), end=' ')
输出为:
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40