Python——类(Class)速通

8 阅读10分钟

预热

  • 类乍一听好像是新东西,但是我们之前学习的字符串、列表、字典等数据类型,本质上都是类
  • 例如字符 "a" 是字符串类型,也是字符串类的一个实例
  • Python中,对象相互独立,但不同的变量可以绑定同一对象。这在我们学习列表的那一章节已经仔细学过
  • 在学习函数那一章节的时候,我们学习过python寻找变量的值的过程,现在我们需要复习一下python的作用域和命名空间
  • 命名空间指的是名称到对象的映射,说直白点就是我们对变量赋值时,例如 a = 2026,变量 a 和数字 2026 间的映射就是命名空间,包括模块中的函数、对象中的属性,也都属于命名空间的一种
  • 内置变量的命名空间在python解释器启动时创建。模块的全局命名空间在读取模块定义时创建。函数的局部命名空间在函数被调用时被创建,并在函数返回或抛出未在函数内被处理的异常时,被删除
  • 作用域中的查找顺序:内层作用域——>外层作用域——>全局作用域——>内置变量作用域
  • 可以用 nonlocal 语句在内层作用域修改外层作用域的变量,用 global 语句在内层作用域修改全局作用域的变量05 类-1769998296615.png
  • 用图画出所用的作用域如下图05 类-1769999329941.png
  • 调用 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"
  • 我们点击运行,输出结果如下05 类-1769999845388.png
  • 当我们彻底了解作用域的运作方式,我们就可以来正式地学习类

类的属性和方法

  • 我们来定义一个简单的示例类05 类-1770001051072.png
  • class 关键字和 def 关键字一样用来进行定义,名字、文档的书写和函数的规范一样。和函数不同的是,类当中的变量 i 和函数 f 是类的各种属性,包括说明文档
  • 我们可以访问或者说输出类的属性05 类-1770001297487.png
  • 类最重要的是通过定义的类,创建不同的实例。我们可以写一个复数的类,创建复数实例05 类-1770001554698.png
  • __init__() 属性是对实例进行初始化,当我们通过类创建实例时,就会调用该方法
  • 那么很多人会说,实例和类的作用是什么呢?我们要用它做什么?
  • 很简单,我们尝试一下,不用上面的类,只用普通的复数定义05 类-1770001860780.png
  • 虽然我们也可以通过属性查看它的实部和虚部,但是我们不能对单个属性进行修改05 类-1770001922987.png
  • 如果我们要对变量 x 进行修改,只能重新赋值
  • 但是如果使用类的实例,我们就可以通过属性对实例进行部分修改05 类-1770002081385.png
  • 也就是说,我们可以给定义的类赋予普适性的属性,再通过修改来创建以这些属性为基础的不同实例,本质上其实也是代码的复用(无需重复定义类中普适性的属性)
  • 我们在回到最初的那个简单的示例,然后做一些小小的修改,注意,x = MyClass() 这个语句一定要有括号,否则 x 就不是 MyClass 的实例,而是 MyClass 这个类05 类-1770002467338.png
  • 我们将 x.f() 进行输出,得到了返回值。像 f() 这样类中的函数,称之为类的方法
  • 大家是不是想起了什么?
  • append()count() 这样的函数,其实也是列表这个类中的方法
  • 但是,不知道大家注意到没有,我们定义的类中,f() 函数有一个参数 self,但是我们调用实例的时候,却没有给这个参数赋值,结果也没有报错,那么它到底是什么呢?
  • 答案就是这个参数,就像它的字面意思一样,指的是实例本身,也就是我们的实例 x
  • x.f() 其实就等同于 MyClass().f(x),所有代表==方法==的函数,第一个参数都是调用该方法的实例本身
  • 很多人还是不理解,那么我们尝试一下,把类的 f() 函数中的 self 参数去掉05 类-1770009037634.png
  • 这时候我们再运行,程序会报错,提示我们在定义时没有参数,但是调用时给了一个参数05 类-1770009099403.png
  • 但是我们执行的语句 x.f() 中明明没有参数。所以其实和我们前面说的,当实例调用参数时,实例本身就是参数。x.f() 就是 MyClass.f(x),那个多出来的参数,就是实例 x 本身。也就是说,类的方法中使用 self 作为第一个参数的作用,就是让实例能够调用方法,这是 self 参数存在的最根本的意义
  • 到这里,可能还有人会疑惑,那为什么要 self 呢?直接规定好类里面的方法能像普通函数一样进行调用不就好了?
  • 大家忘了,我们为什么要用类吗?
  • 往上翻一翻,我们之所以用类,就是要用类来统一相同的属性,方便创建不同的实例。那么如果类中的方法不用 self 参数,每个实例调用出来的方法就都是一样的了
  • 举个例子,我们把上述的示例修改一下05 类-1770010622184.png
  • 此时,当我们创建两个实例并调用同样的方法后,不同实例会有不同结果05 类-1770010674666.png
  • 假设向我们所说的那样不用 self,我们就无法进行不同实例间的差异化
  • 我们在定义类的时候,要注意类的属性尽量不要用可变类型05 类-1770011192339.png
  • 结果并没有像我们预期的那样,具有差异化05 类-1770011228733.png
  • 要修正这个问题,我们只需要做一点小小的改变,将可变类型的赋值放入 init 中,相当于每次创建实例都进行重置,抵消了可变类型带来的问题05 类-1770011349615.png
  • 结果正常05 类-1770011392269.png
  • 如果实例的属性和类的属性名字一样,值不同,在我们输出实例属性的时候会输出什么?结合作用域的知识想一想05 类-1770012034010.png

类的继承

  • 学完了类的基本知识,我们来学习一下继承
  • 什么是继承呢?简单的理解就是让一个类有选择性地挑选另一个类中的属性和方法,把这些属性和方法拿过来,然后再添加它自己的属性和方法
  • 那怎么样进行继承呢?不知道大家有没有发现,我们在定义类的时候,类名后的括号一直空着?
  • 我们就用这个括号,来进行继承05 类-1770013688850.png
  • 我们使用类 Felidae 猫科动物类继承了 Mammal 哺乳动物类,随后创建了一个名为 gaffey 的实例。这个实例就拥有了两个类的属性。可以理解为它又有父亲的基因,又有也爷爷的基因
  • 相信大家可以猜到 gaffey.reproduction 的值是什么,其实类的属性查找和上面的作用域中的变量查找一样,优先查找实例——>类——>父类
  • 我们可以用 isinstance() 判断实例和类的关系,可以用 issubclass() 判断一个类是否是另一个类的子类05 类-1770014043420.png
  • 我们能继承一个类,那当然也能继承多个类。继承多个类相对来说比较复杂,我们用简单的方式来进行示范05 类-1770015745734.png
  • 大家可以思考下,输出的是什么?应该很简单
  • 我们再修改下,看看结果是什么05 类-1770015854494.png
  • 大家可以随意修改,看看结果是什么?
  • 我们只需要记住,继承查找的顺序是从深到浅,从左至右,同一级先查左边,再往上一级查。按上述的例子就是E——>C——>A——>B——>D——>A——>B
  • 当我们使用继承时,如果我们不希望父类的属性和方法被子类修改,我们可以在变量前加两个下划线05 类-1770017657921.png
  • 加上双下划线的变量就变成了==私有变量==,在其他地方无法进行修改和访问,加单下划线的变量是非公有变量,可以通过子类访问

迭代器

  • 在python中,当我们遍历所有可迭代对象时,其实就是在把该对象变成一个迭代器05 类-1770018181052.png
  • 迭代器的本质是用 __iter__() 方法返回一个带有 __next__() 方法的对象05 类-1770018522847.png
  • 这个类的作用是创建能反向迭代的实例,我们来看各个语句的作用
  • class Reverse 是基本的定义
  • 然后就是说明文本,也就是 __doc__ 的值
  • __init__(self, data) 是对实例进行初始化
  • 接下来的 __iter__() 就是代表该类是迭代器,返回实例本身
  • __next__() 就是进行迭代的方式,一般和 __iter__() 配套使用
  • 我们需要的是,在 __next__() 中,我们需要给定终止迭代,即 raise StopIteration 的条件, 它的返回值,其实就是每次执行迭代的返回值
  • 现在,大家可以尝试一下对上面的代码进行一下简单的修改,把他改成一个简单的返回三倍值的类

生成器

  • 生成器的作用是快速创建一个迭代器,它会自动创建 __iter__()__next__() 方法
  • 刚才我们写的方向迭代也可以用生成器完成05 类-1770020442085.png
  • range() 函数是左闭右开,就是就是从后往前遍历对象,yield 语句代表生成
  • 上面的生成器可以用类似于列表推导式的方法完成05 类-1770021157033.png
  • 要注意,生成器表达式用的是 () 圆括号,返回的对象不是元组,而是生成器对象05 类-1770021217344.png
  • 和列表推导式最大的区别是,生成器表达式记住了你定义的方式,在调用时一个个生成,而不是直接生成一大串。在处理大量数据时,生成器可以节省大量内存