《最值得收藏的python3语法汇总》之面向对象编程 - 完整版

目录

关于这个系列

1、POP和OOP

2、类的定义语法

3、类对象

属性引用

类的实例化

实例对象

4、类变量和实例变量

5、继承

概念

多重继承

方法重写

理解super

Isinstance和issubcass

6、多态

7、成员可见范围

8、迭代器

9、生成器


关于这个系列

《最值得收藏的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的点,BC满足条件,从左侧开始取,取出B,这时顺序是AB,然后去掉B相关的边,这时候入度为0的点有EC,依然取左边的E,这时候顺序为ABE,接着去掉E相关的边,这时只有一个点入度为0,那就是C,取C,顺序为ABEC。去掉C的边得到两个入度为0的点DF,取出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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值