预热
- 类乍一听好像是新东西,但是我们之前学习的字符串、列表、字典等数据类型,本质上都是类
- 例如字符
"a"是字符串类型,也是字符串类的一个实例 - Python中,对象相互独立,但不同的变量可以绑定同一对象。这在我们学习列表的那一章节已经仔细学过
- 在学习函数那一章节的时候,我们学习过python寻找变量的值的过程,现在我们需要复习一下python的作用域和命名空间
- 命名空间指的是名称到对象的映射,说直白点就是我们对变量赋值时,例如
a = 2026,变量a和数字2026间的映射就是命名空间,包括模块中的函数、对象中的属性,也都属于命名空间的一种 - 内置变量的命名空间在python解释器启动时创建。模块的全局命名空间在读取模块定义时创建。函数的局部命名空间在函数被调用时被创建,并在函数返回或抛出未在函数内被处理的异常时,被删除
- 作用域中的查找顺序:内层作用域——>外层作用域——>全局作用域——>内置变量作用域
- 可以用
nonlocal语句在内层作用域修改外层作用域的变量,用global语句在内层作用域修改全局作用域的变量 - 用图画出所用的作用域如下图
- 调用
scope_test()函数后,scope_test()函数的局部作用域中定义了变量spam = "test_spam",接下来在局部作用域中进行调用do_local()函数,在do_local()函数中定义了spam = "local_spam",此时do_local()函数作用域的内外部有两个不同值的spam,此时在scope_test()的函数的作用域内也就是do_local()函数作用域外进行输出,python在输出变量时,优先在当前作用域进行寻找,找不到才会去更外层的作用域寻找,所以此时输出的变量spam = "test_spam" - 语句从上至下继续执行,在
do_nonlocal()函数中对变量spam进行赋值,用于使用了nonlocal语句,所以对外层也就是scope_test()函数作用域下的spam进行赋值,spam与之前的"test_spam"解除绑定,与"nonlocal_spam"进行绑定,所以在进行输出时,该作用域内的spam = "nonlocal_spam" - 语句继续执行,此时进入
do_global()函数的作用域中,global语句在全局作用域也就是在scope_test()的作用域外,tutorial2.py文件的作用域中声明了变量spam = "global_spam",此时进行输出,函数scope_test()作用域内的变量spam在被do_nonlocal()修改过,值为 `"nonlocal_spam" - 此时函数
scope_test()调用结束,局部作用域消失,在全局作用域进行输出,那么值就是之前do_global()函数定义的spam = "global_spam" - 我们点击运行,输出结果如下
- 当我们彻底了解作用域的运作方式,我们就可以来正式地学习类
类的属性和方法
- 我们来定义一个简单的示例类
class关键字和def关键字一样用来进行定义,名字、文档的书写和函数的规范一样。和函数不同的是,类当中的变量i和函数f是类的各种属性,包括说明文档- 我们可以访问或者说输出类的属性
- 类最重要的是通过定义的类,创建不同的实例。我们可以写一个复数的类,创建复数实例
__init__()属性是对实例进行初始化,当我们通过类创建实例时,就会调用该方法- 那么很多人会说,实例和类的作用是什么呢?我们要用它做什么?
- 很简单,我们尝试一下,不用上面的类,只用普通的复数定义
- 虽然我们也可以通过属性查看它的实部和虚部,但是我们不能对单个属性进行修改
- 如果我们要对变量
x进行修改,只能重新赋值 - 但是如果使用类的实例,我们就可以通过属性对实例进行部分修改
- 也就是说,我们可以给定义的类赋予普适性的属性,再通过修改来创建以这些属性为基础的不同实例,本质上其实也是代码的复用(无需重复定义类中普适性的属性)
- 我们在回到最初的那个简单的示例,然后做一些小小的修改,注意,
x = MyClass()这个语句一定要有括号,否则x就不是MyClass的实例,而是MyClass这个类 - 我们将
x.f()进行输出,得到了返回值。像f()这样类中的函数,称之为类的方法 - 大家是不是想起了什么?
- 像
append()、count()这样的函数,其实也是列表这个类中的方法 - 但是,不知道大家注意到没有,我们定义的类中,
f()函数有一个参数self,但是我们调用实例的时候,却没有给这个参数赋值,结果也没有报错,那么它到底是什么呢? - 答案就是这个参数,就像它的字面意思一样,指的是实例本身,也就是我们的实例
x x.f()其实就等同于MyClass().f(x),所有代表==方法==的函数,第一个参数都是调用该方法的实例本身- 很多人还是不理解,那么我们尝试一下,把类的
f()函数中的self参数去掉 - 这时候我们再运行,程序会报错,提示我们在定义时没有参数,但是调用时给了一个参数
- 但是我们执行的语句
x.f()中明明没有参数。所以其实和我们前面说的,当实例调用参数时,实例本身就是参数。x.f()就是MyClass.f(x),那个多出来的参数,就是实例x本身。也就是说,类的方法中使用self作为第一个参数的作用,就是让实例能够调用方法,这是self参数存在的最根本的意义 - 到这里,可能还有人会疑惑,那为什么要
self呢?直接规定好类里面的方法能像普通函数一样进行调用不就好了? - 大家忘了,我们为什么要用类吗?
- 往上翻一翻,我们之所以用类,就是要用类来统一相同的属性,方便创建不同的实例。那么如果类中的方法不用
self参数,每个实例调用出来的方法就都是一样的了 - 举个例子,我们把上述的示例修改一下
- 此时,当我们创建两个实例并调用同样的方法后,不同实例会有不同结果
- 假设向我们所说的那样不用
self,我们就无法进行不同实例间的差异化 - 我们在定义类的时候,要注意类的属性尽量不要用可变类型
- 结果并没有像我们预期的那样,具有差异化
- 要修正这个问题,我们只需要做一点小小的改变,将可变类型的赋值放入
init中,相当于每次创建实例都进行重置,抵消了可变类型带来的问题 - 结果正常
- 如果实例的属性和类的属性名字一样,值不同,在我们输出实例属性的时候会输出什么?结合作用域的知识想一想
类的继承
- 学完了类的基本知识,我们来学习一下继承
- 什么是继承呢?简单的理解就是让一个类有选择性地挑选另一个类中的属性和方法,把这些属性和方法拿过来,然后再添加它自己的属性和方法
- 那怎么样进行继承呢?不知道大家有没有发现,我们在定义类的时候,类名后的括号一直空着?
- 我们就用这个括号,来进行继承
- 我们使用类
Felidae猫科动物类继承了Mammal哺乳动物类,随后创建了一个名为gaffey的实例。这个实例就拥有了两个类的属性。可以理解为它又有父亲的基因,又有也爷爷的基因 - 相信大家可以猜到
gaffey.reproduction的值是什么,其实类的属性查找和上面的作用域中的变量查找一样,优先查找实例——>类——>父类 - 我们可以用
isinstance()判断实例和类的关系,可以用issubclass()判断一个类是否是另一个类的子类 - 我们能继承一个类,那当然也能继承多个类。继承多个类相对来说比较复杂,我们用简单的方式来进行示范
- 大家可以思考下,输出的是什么?应该很简单
- 我们再修改下,看看结果是什么
- 大家可以随意修改,看看结果是什么?
- 我们只需要记住,继承查找的顺序是从深到浅,从左至右,同一级先查左边,再往上一级查。按上述的例子就是E——>C——>A——>B——>D——>A——>B
- 当我们使用继承时,如果我们不希望父类的属性和方法被子类修改,我们可以在变量前加两个下划线
- 加上双下划线的变量就变成了==私有变量==,在其他地方无法进行修改和访问,加单下划线的变量是非公有变量,可以通过子类访问
迭代器
- 在python中,当我们遍历所有可迭代对象时,其实就是在把该对象变成一个迭代器
- 迭代器的本质是用
__iter__()方法返回一个带有__next__()方法的对象 - 这个类的作用是创建能反向迭代的实例,我们来看各个语句的作用
class Reverse是基本的定义- 然后就是说明文本,也就是
__doc__的值 __init__(self, data)是对实例进行初始化- 接下来的
__iter__()就是代表该类是迭代器,返回实例本身 - 而
__next__()就是进行迭代的方式,一般和__iter__()配套使用 - 我们需要的是,在
__next__()中,我们需要给定终止迭代,即raise StopIteration的条件, 它的返回值,其实就是每次执行迭代的返回值 - 现在,大家可以尝试一下对上面的代码进行一下简单的修改,把他改成一个简单的返回三倍值的类
生成器
- 生成器的作用是快速创建一个迭代器,它会自动创建
__iter__()和__next__()方法 - 刚才我们写的方向迭代也可以用生成器完成
range()函数是左闭右开,就是就是从后往前遍历对象,yield语句代表生成- 上面的生成器可以用类似于列表推导式的方法完成
- 要注意,生成器表达式用的是
()圆括号,返回的对象不是元组,而是生成器对象 - 和列表推导式最大的区别是,生成器表达式记住了你定义的方式,在调用时一个个生成,而不是直接生成一大串。在处理大量数据时,生成器可以节省大量内存