本页面仅用于内容预览,不包含动画交互效果
讲义加载中,请耐心等待……
Python 编程:从入门到实践Python 编程:从入门到实践(第三版)(第三版)Teacher Name / Email1
与《Python 编程:从入门到实践(第三版)》一书配套使用讲义中的文本及绘图采用署名-非商业性使用-相同方式共享协议CC BY-NC-SA 4.0进行许可引用的网络图片附有超链接,可用于访问来源讨论、意见、答疑、勘误、更新:https://github.com/scruel/pcc_3e_slides作者:@Scruel Tao关于本讲义关于本讲义2
99 9.1 创建和使用类9.2 使用类和实例9.3 继承9.4 导入类 9.5 Python 标准库9.6 类的编程风格 9.7 小结3
99 面向对象编程object-oriented programmingOOP)是最有效的软件编写方法之一,在面向对象编程中,我们可以:编写表示现实世界中事物或情景的类(class,并基于这些类来创建对象(object通过类的实例化来根据类创建对象,并在代码中使用类的实例instance)对象通过面向对象编程模拟出的现实世界,逼真程度定会让你惊讶!4

注:建议使用“实例对象”的表述而不是“实例”,是因为 Python 中的一切皆为对象,目前来说你只需知道这个概念即可。

不过为了节省版面,使用“实例”的表述也是完全可以的。

9.19.1 创建和使用类创建和使用类使用类几乎可以模拟任何东西,例如一个表示小狗的简单类。为了创建这样一个简单类,我们需要找找小狗们的共性,比如说大多数宠物狗都有名字和年龄,同时我们假设它们都会坐下和打滚。5
9.19.1 创建和使用类创建和使用类使用类几乎可以模拟任何东西,例如一个表示小狗的简单类。为了创建这样一个简单类,我们需要找找小狗们的共性,比如说大多数宠物狗都有名字和年龄,同时我们假设它们都会坐下和打滚。接下来我们将创建的 Dog 类,会包含上述两项信息和两种行为,通过这个类,Python 能让知道如何创建表示小狗的实例对象。6

注:用一个类来包含抽象事物的属性和方法,这是面向对象的三大特性之一——”封装“

9.1.19.1.1 创建创建 DogDog 下面的代码创建了 Dog 类,包含了提到的两项信息和两种行为:class Dog: """一次模拟小狗的简单尝试""" def __init__(self, name, age): """初始化属性 name age""" self.name = name self.age = age def sit(self): """模拟小狗收到命令时坐下""" print(f"{self.name} is now sitting.") def roll_over(self): """模拟小狗收到命令时打滚""" print(f"{self.name} rolled over!")7

代码保存在 python_work/car.py 文件中

9.1.19.1.1 创建创建 DogDog 让我们来看看代码的具体内容:class Dog: """一次模拟小狗的简单尝试""" def __init__(self, name, age): """初始化属性 name age""" self.name = name self.age = age def sit(self): """模拟小狗收到命令时坐下""" print(f"{self.name} is now sitting.") def roll_over(self): """模拟小狗收到命令时打滚""" print(f"{self.name} rolled over!")class 关键字:用于定义类我们约定类名以大写字母开头紧随其后的是文档字符串:对这个类的功能做了描述由于是新建的全新类,所以定义时不加括号,最后则是一个冒号。8

注:这里说新建的是全新类,是因为还有其他创建类的形式,后面我们会给出。

9.1.19.1.1 创建创建 DogDog 让我们来看看代码的具体内容:__init__ 方法(函数):和定义其他函数一样,我们使用的是 def 关键字。这个函数有一些特殊的是:函数名的开头和末尾各有两个下划线,这是一种约定,旨在避免命名冲突。每次创建 Dog 类的实例, Python 都会自动运行这个函数我们一般称它为初始化方法class Dog: """一次模拟小狗的简单尝试""" def __init__(self, name, age): """初始化属性 name age""" self.name = name self.age = age def sit(self): """模拟小狗收到命令时坐下""" print(f"{self.name} is now sitting.") def roll_over(self): """模拟小狗收到命令时打滚""" print(f"{self.name} rolled over!")9

注1:对于 __init__ 方法的方法名来说,实际上是一种规定,因为它是有特殊作用的,如果不加双下划线,Python 将不会自动调用这个方法。

注1:在类中,名称前后都带有双下划线的函数,是特殊的函数,也被称作魔术方法,一般读作 dunder method,dunder 表示“前后双下划线”。

关于函数和方法关于函数和方法如果你想如之前一样,让解释器来告诉你类方法是什么,一不小心就会得到两个答案,这符合预期的,因为方法是特殊的函数:在第二章中,我们提到:可以把方法视为特殊的函数。在这里,我们更新一下定义:类中的函数称为方法。10

注1:这里“类中的函数称为方法”的表述是没有问题的,因为方法是特殊的函数,你可以称 __init__ 等类方法为函数,但称作方法则更合适和准确,因为方法是与类的实例对象相关联的,而函数不与特定的实例对象相关联。对于一个类方法来说,我们一般会通过其实例来使用它。

注2:类内的方法叫什么,实际上取决于你怎么使用它,这是 Python 中特有的产物,不过目前来说,你不需要太过纠结,只需要大概了解即可,这只是一个称呼而已,不过这下,你应该能理解为什么许多人会混用方法和函数的称呼了吧!

【拓展补充】如果你想要更好地理解函数和方法的异同,则可以参考知名问答网站上对此的一些回答:

https://stackoverflow.com/questions/155609/whats-the-difference-between-a-method-and-a-function

9.1.19.1.1 创建创建 DogDog 让我们来看看代码的具体内容:__init__ 方法的形参:除了有 name age 接收两项信息的形参外,值得关注的是 self 形参它是一个指向实例本身的引用,实例的每次调用都会自动传入自身,这样使得实例能够访问类中的属性和方法self 形参必不可少,而且必须位于其他形参的前面class Dog: """一次模拟小狗的简单尝试""" def __init__(self, name, age): """初始化属性 name age""" self.name = name self.age = age def sit(self): """模拟小狗收到命令时坐下""" print(f"{self.name} is now sitting.") def roll_over(self): """模拟小狗收到命令时打滚""" print(f"{self.name} rolled over!")11

self 的写法只是一个惯例,你完全可以使用其他的写法,比如其他语言的程序员可能会更熟悉 this,但建议大家遵循约定,这样有助于以后的协作。

9.1.19.1.1 创建创建 DogDog 让我们来看看代码的具体内容:__init__ 方法的方法体:第一行是说明性注释第二行和第三行定义的两个变量都有 self. 的前缀这两个变量可在类的所有方法中通过同样的 self. 前缀来使用这些可以通过实例访问的变量我们称之为属性(attributeclass Dog: """一次模拟小狗的简单尝试""" def __init__(self, name, age): """初始化属性 name age""" self.name = name self.age = age def sit(self): """模拟小狗收到命令时坐下""" print(f"{self.name} is now sitting.") def roll_over(self): """模拟小狗收到命令时打滚""" print(f"{self.name} rolled over!")12

注:方法体即是“类中函数的函数体”的别称,毕竟我们现在会将类中的函数称为方法。

9.1.19.1.1 创建创建 DogDog 让我们来看看代码的具体内容:属性(attributeclass Dog: """一次模拟小狗的简单尝试""" def __init__(self, name, age): """初始化属性 name age""" self.name = name self.age = age def sit(self): """模拟小狗收到命令时坐下""" print(f"{self.name} is now sitting.") def roll_over(self): """模拟小狗收到命令时打滚""" print(f"{self.name} rolled over!")13

类的属性可以简单地理解为“有些什么”

9.1.19.1.1 创建创建 DogDog 让我们来看看代码的具体内容:还定义了两种行为对应的方法这两个方法执行时不需要额外信息,但同初始化方法一样,我们需要写上 self 形参。后面创建的小狗实例,都能够调用这些方法,换句话说,它们都会坐下和打滚。class Dog: """一次模拟小狗的简单尝试""" def __init__(self, name, age): """初始化属性 name age""" self.name = name self.age = age def sit(self): """模拟小狗收到命令时坐下""" print(f"{self.name} is now sitting.") def roll_over(self): """模拟小狗收到命令时打滚""" print(f"{self.name} rolled over!")14

注:类的属性可以简单地理解为“有些什么”。

9.1.19.1.1 创建创建 DogDog 让我们来看看代码的具体内容:class Dog: """一次模拟小狗的简单尝试""" def __init__(self, name, age): """初始化属性 name age""" self.name = name self.age = age def sit(self): """模拟小狗收到命令时坐下""" print(f"{self.name} is now sitting.") def roll_over(self): """模拟小狗收到命令时打滚""" print(f"{self.name} rolled over!")方法(method15

类的属性可以简单地理解为“有些什么”

9.1.29.1.2 根据类创建实例根据类创建实例可以将类视为有关如何创建实例的说明。例如,Dog 类就是一系列说明,让 Python 知道如何创建表示特定小狗的实例。下面创建一个表示特定小狗的实例:通过箭头处的代码:我们让 Python 创建了一条名字为 'Willie'、年龄为 6 小狗。不需要传入 self 实参, Python 会在调用初始化方法时自动传入它class Dog: ...my_dog = Dog('Willie', 6)16
9.1.29.1.2 根据类创建实例根据类创建实例可以将类视为有关如何创建实例的说明。例如,Dog 类就是一系列说明,让 Python 知道如何创建表示特定小狗的实例。下面创建一个表示特定小狗的实例:通过箭头处的代码:通过接收实参,初始化方法将会设置 name age 属性,表示小狗的名字和年龄,随后返回表示小狗的实例最后将这个实例赋值给 my_dog 变量class Dog: ...my_dog = Dog('Willie', 6)17
9.1.29.1.2 根据类创建实例根据类创建实例可以将类视为有关如何创建实例的说明。例如,Dog 类就是一系列说明,让 Python 知道如何创建表示特定小狗的实例。下面创建一个表示特定小狗的实例:接着可以通过点号语法,来访问实例的属性执行调用时,Python 首先会找 my_dog 实例,随后通过在类中使用 self.name self.age 来引用与这个实例相关联的属性,以获取名字和年龄class Dog: ...my_dog = Dog('Willie', 6)print(f"My dog's name is {my_dog.name}.")print(f"My dog is {my_dog.age} years old.")运行结果My dog's name is Willie.My dog is 6 years old.18

在这里,命名约定很有用:

通常可以认为首字母大写的名称(如 Dog)指的是类,

而全小写的名称(如 my_dog)指的是根据类创建的实例。

9.1.29.1.2 根据类创建实例根据类创建实例可以将类视为有关如何创建实例的说明。例如,Dog 类就是一系列说明,让 Python 知道如何创建表示特定小狗的实例。下面创建一个表示特定小狗的实例:class Dog: ...my_dog = Dog('Willie', 6)my_dog.sit()my_dog.roll_over()与访问属性类似,同样使用点号语法来调用实例的方法,从而让实例“动起来”Python 会在类中查找调用的方法,然后运行方法体中的语句运行结果Willie is now sitting.Willie rolled over!19
9.1.29.1.2 根据类创建实例根据类创建实例可以根据类创建任意数量的实例:class Dog: ...my_dog = Dog('Willie', 6)your_dog = Dog('Lucy', 3)print(f"My dog's name is {my_dog.name}.")print(f"My dog is {my_dog.age} years old.")my_dog.sit()print(f"\nYour dog's name is {your_dog.name}.")print(f"Your dog is {your_dog.age} years old.")your_dog.sit()运行结果My dog's name is Willie.My dog is 6 years old.Willie is now sitting.Your dog's name is Lucy.Your dog is 3 years old.Lucy is now sitting.20

在这个示例中,创建了两条小狗,分别名为Willie 和 Lucy。每条小狗都是一个独立的实例,有自己的一组属性,能够执行相同的操作。

你可以创建属性完全相同的新实例,比如给第二条小狗指定同样的名字和年龄,这个实例看起来没什么不同,但这个实例是 Python 是根据 Dog 类创建另一个新的实例。

9.29.2 使用类和实例使用类和实例上一节中我们创建了一个简单类,并且调用了其中的方法和属性,21
9.29.2 使用类和实例使用类和实例上一节中我们创建了一个简单类,并且调用了其中的方法和属性,在这一节中,我们的任务是学习修改实例的属性的方式下面将编写一个表示汽车的类,它将存储汽车的相关信息,并提供一个汇总和显示这些信息的方法。22
class Car: """一次模拟汽车的简单尝试""" def __init__(self, make, model, year): """初始化描述汽车的属性""" self.make = make self.model = model self.year = year def get_descriptive_name(self): """返回格式规范的描述性信息""" long_name = f"{self.year} {self.make} {self.model}" return long_name.title()首先给出汽车类的初始代码:9.2.1 Car9.2.1 Car 23
class Car: """一次模拟汽车的简单尝试""" def __init__(self, make, model, year): """初始化描述汽车的属性""" self.make = make self.model = model self.year = year def get_descriptive_name(self): """返回格式规范的描述性信息""" long_name = f"{self.year} {self.make} {self.model}" return long_name.title()Car 类的定义和 Dog 类区别不大,它包含了三个属性:制造商(make型号(model生产年份(year9.2.1 Car9.2.1 Car 24
class Car: """一次模拟汽车的简单尝试""" def __init__(self, make, model, year): """初始化描述汽车的属性""" self.make = make self.model = model self.year = year def get_descriptive_name(self): """返回格式规范的描述性信息""" long_name = f"{self.year} {self.make} {self.model}" return long_name.title()类中还包含了一个名为 get_descriptive_name() 的方法:使用汽车类中的三个属性,创建并返回一个对汽车信息进行描述的字符串。9.2.1 Car9.2.1 Car 25
9.2.1 Car9.2.1 Car class Car: ...my_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())定义好 Car 类后,我们来创建一个它的实例,并将其赋给变量 my_new_car接下来,调用get_descriptive_name() 方法,Python 将指出我们创建了一辆什么样的汽车。运行结果2023 Audi A4接着我们将为这个类添加一个动态变化的属性26
9.2.29.2.2 给属性指定默认值给属性指定默认值class Car: """一次模拟汽车的简单尝试""" def __init__(self, make, model, year): ... self.odometer_reading = 0 def get_descriptive_name(self): ... def read_odometer(self): """打印一条指出汽车行驶里程的消息""" print(f"This car has {self.odometer_reading} miles on it.")有些属性无须通过形参来定义,可以在 __init__() 方法中为其指定默认值。这里添加了一个初始值为 0 名为 odometer_reading 的属性,用于表示汽车里程表的读数同时增加了 read_odometer()方法,让我们能轻松地知道汽车的行驶里程信息27

odometer reading:里程表读数

9.2.29.2.2 给属性指定默认值给属性指定默认值class Car: ...my_new_car = Car('audi', 'a4', 2023)像刚才一样,我们可以创建一个实例,现在这个实例有些许不同,它额外包含了:一个初值为 0 的里程属性一个打印里程读数的方法28
9.2.29.2.2 给属性指定默认值给属性指定默认值class Car: ...my_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())my_new_car.read_odometer()像刚才一样,我们可以创建一个实例,现在这个实例有些许不同,它额外包含了:一个初值为 0 的里程属性一个打印里程读数的方法调用打印方法,我们可以看到这辆新车的里程表读数为 0运行结果2023 Audi A4This car has 0 miles on it.29
9.2.29.2.2 给属性指定默认值给属性指定默认值class Car: ...my_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())my_new_car.read_odometer()像刚才一样,我们可以创建一个实例,现在这个实例有些许不同,它额外包含了:一个初值为 0 的里程属性一个打印里程读数的方法运行结果2023 Audi A4This car has 0 miles on it.30
9.2.39.2.3 修改属性的值修改属性的值接下来将介绍三种修改属性值的方式:直接通过实例修改通过方法设置通过方法递增(增加特定的值)31
9.2.39.2.3 通过实例修改属性的值通过实例修改属性的值class Car: ...my_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())my_new_car.odometer_reading = 23最简单的方式,便是直接通过实例访问属性后重新赋值:32
9.2.39.2.3 通过实例修改属性的值通过实例修改属性的值class Car: ...my_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())my_new_car.odometer_reading = 23my_new_car.read_odometer()最简单的方式,便是直接通过实例访问属性后重新赋值:调用方法后,Python 会按预期修改实例中的属性:运行结果2023 Audi A4This car has 23 miles on it.33
9.2.39.2.3 通过实例修改属性的值通过实例修改属性的值class Car: ...my_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())my_new_car.odometer_reading = 23my_new_car.read_odometer()最简单的方式,便是直接通过实例访问属性后重新赋值:34
9.2.39.2.3 通过实例修改属性的值通过实例修改属性的值class Car: ... def update_odometer(self, mileage): """将里程表读数设置为指定的值""" self.odometer_reading = mileage属性也可以通过在类内部定义的方法来更新35

扩展 update_odometer() 方法部分略,可参考原书以禁止将里程表读数往回调:

9.2.39.2.3 通过实例修改属性的值通过实例修改属性的值class Car: ... def update_odometer(self, mileage): """将里程表读数设置为指定的值""" self.odometer_reading = mileagemy_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())my_new_car.update_odometer(23)my_new_car.read_odometer()属性也可以通过在类内部定义的方法来更新对实例来说,只要调用对应的方法即可方法会按预期修改实例中的属性:运行结果2023 Audi A4This car has 23 miles on it.36

扩展 update_odometer() 方法部分略,可参考原书以禁止将里程表读数往回调:

9.2.39.2.3 通过方法递增属性的值通过方法递增属性的值class Car: ... def increment_odometer(self, miles): """让里程表读数增加指定的量""" self.odometer_reading += milesmy_new_car = Car('audi', 'a4', 2023)my_new_car.update_odometer(23_500)my_new_car.read_odometer()my_new_car.increment_odometer(100)my_new_car.read_odometer()为了使小车能在行驶时让里程表读数增加,我们可以添加一个对应的方法:方法会按预期递增实例中的属性:运行结果2023 Audi A4This car has 23500 miles on it.This car has 23600 miles on it.37
9.39.3 继承继承在编写类时,并非总是要从头开始,如果要编写的类是一个已有类的特殊版本,那么我们就可以使用继承(inheritance被继承的类称为父类parent class),而继承的类称为子类child class子类可以继承父类的所有属性和方法,也可以定义自己的属性和方法。38

注:用一个已存在的类作为基础建立的类,是面向对象的三大特性之一——”继承“

9.39.3 继承继承下面来模拟电动汽车,电动汽车是一种特殊的汽车,和之前的汽车一样,它也有里程表,也需要能够显示信息。因此可在之前 Car 类的基础上,创建它的继承类 ElectricCar使用继承后,只需为电动汽车特有的属性和行为编写代码即可。电动汽车是一种特殊的汽车39
9.3.19.3.1 子类的初始化方法子类的初始化方法class Car: ...在这个例子中,我们将父类代码放在同一个文件中要注意的是,父类代码必须放在在子类代码的前面40

类的第二个单词依然用大写字母

9.3.19.3.1 子类的初始化方法子类的初始化方法class Car: ...class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """初始化父类的属性""" super().__init__(make, model, year)在这个例子中,我们将父类代码放在同一个文件中要注意的是,父类代码必须放在在子类代码的前面要定义子类 ElectricCar我们需要在名称后添加括号,并在其中指定父类的名称,括号中指定了当前类继承的父类名称不要忘了尾部的英文冒号41

类的第二个单词依然用大写字母

9.3.19.3.1 子类的初始化方法子类的初始化方法class Car: ...class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """初始化父类的属性""" super().__init__(make, model, year)在子类的初始化方法中, 我们使用了 super() 函数这是一个特殊的函数,让你能够调用父类的方法。这句代码执行了父类的初始化方法,其中定义了一些属性,子类通过这样的方式,也拥有了父类的属性父类也称为超类(superclass函数名 super 就是由此而来的。42

类的第二个单词依然用大写字母

9.3.29.3.2 给子类定义属性和方法给子类定义属性和方法class Car: ...class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """初始化父类的属性""" super().__init__(make, model, year)my_leaf = ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name())接着我们来试试创建电动汽车,以测试继承能够正确的发挥作用,看看它是否拥有父类中的方法和属性:运行结果2024 Nissan Leaf43
9.3.29.3.2 给子类定义属性和方法给子类定义属性和方法class Car: ...class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """初始化父类的属性""" super().__init__(make, model, year)my_leaf = ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name())接着我们来试试创建电动汽车,以测试继承能够正确的发挥作用,看看它是否拥有父类中的方法和属性:看起来一切符合预期,接下来我们将定义它特有的属性和方法44
9.3.29.3.2 给子类定义属性和方法给子类定义属性和方法class Car: ...class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """先初始化父类的,再初始化子类特有的""" super().__init__(make, model, year) self.battery_size = 40 def describe_battery(self): """打印一条描述电池容量的消息""" print(f"This car has a {self.battery_size}-kWh battery.")首先我们添加一个电动汽车特有的属性 battery_size电池容量45
9.3.29.3.2 给子类定义属性和方法给子类定义属性和方法class Car: ...class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """先初始化父类的,再初始化子类特有的""" super().__init__(make, model, year) self.battery_size = 40 def describe_battery(self): """打印一条描述电池容量的消息""" print(f"This car has a {self.battery_size}-kWh battery.")首先我们添加一个电动汽车特有的属性 battery_size即电池容量接着我们定义一个方法,用于打印电池容量信息46
9.3.29.3.2 给子类定义属性和方法给子类定义属性和方法class Car: ...class ElectricCar(Car): ...my_leaf = ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name())my_leaf.describe_battery()和之前的实例一样,我们能调用类中定义的describe_battery() 方法这将输出对电池容量信息:运行结果2024 Nissan LeafThis car has a 40-kWh battery.47

ElectricCar 类中可以添加任意的特殊属性或方法,但如果要添加的属性或方法是所有汽车都有的,那应该考虑将其添加进 Car 类。

ElectricCar 类只包含处理电动汽车特有属性和行为的代码。

9.3.39.3.3 重写父类中的方法重写父类中的方法class Car: ... def get_descriptive_name(self): ...class ElectricCar(Car): ... def get_descriptive_name(self): """返回格式规范的描述性信息""" return f"{super.get_descriptive_name()} {self.battery_size}".title()当父类中定义的一些方法不能满足子类的要求时,可以在子类中随时重写父类的方法:定义一个同名的方法当调用子类生成的实例时,Python 首先会查找子类中的方法,从而忽略父类中被重写的同名方法48

当重写方法时,不建议改变原有父类的行为逻辑,例如例子中仅仅是对返回描述信息做了补充,让其包含了电动汽车特有的电池容量属性信息。

注:电动汽车可视为汽车的不同形态,当为子类重写方法后,针对实例对象的不同,调用”同名方法“将产生不同的效果,这是面向对象的三大特性之一——”多态“。

9.3.49.3.4 将实例用作属性将实例用作属性随着属性和方法越来越多,类定义的代码行可能会越来越长。为了解决这一问题,我们可以将类中的一部分属性和方法提取出来,作为一个独立的类,这种方法称为组合(composition49

这里我们将针对电池这个部分进行拆分

9.3.49.3.4 将实例用作属性将实例用作属性class Car: ...class Battery: """一次模拟电动汽车电池的简单尝试""" def __init__(self, size=40): """初始化电池的属性""" self.size = size def describe_battery(self): """打印一条描述电池容量的消息""" print(f"This car has a {self.size}-kWh battery.")首先把电池这部分属性和方法提取出来,作为一个独立 Battery 50
9.3.49.3.4 将实例用作属性将实例用作属性class Car: ...class Battery: """一次模拟电动汽车电池的简单尝试""" def __init__(self, size=40): """初始化电池的属性""" self.size = size def describe_battery(self): """打印一条描述电池容量的消息""" print(f"This car has a {self.size}-kWh battery.")首先把电池这部分属性和方法提取出来,作为一个独立 Battery describe_battery() 方法是 ElectricCar 类剪切过来的,ElectricCar 类不再需要它了51

剪切:复制并删除,然后再粘贴(意思就是删除 ElectricCar 类中这个方法的代码,然后粘贴到这里,当然需要把属性名从 battery_size 改为 size)

9.3.49.3.4 将实例用作属性将实例用作属性class Car: ...class Battery: ...class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """初始化父类的属性""" super().__init__(make, model, year) self.battery_size = 40接着在 ElectricCar 类中,我们将之前电池容量属性,改为表示电池的属性 self.battery,然后将电池实例赋值给这个属性52
9.3.49.3.4 将实例用作属性将实例用作属性class Car: ...class Battery: ...class ElectricCar(Car): """电动汽车的独特之处""" def __init__(self, make, model, year): """初始化父类的属性""" super().__init__(make, model, year) self.battery = Battery()接着在 ElectricCar 类中,我们将之前电池容量属性,改为表示电池的属性 self.battery,然后将电池实例赋值给这个属性在创建电池实例时,我们没有提供实参,它的电池容量将会是默认值 4053
9.3.49.3.4 将实例用作属性将实例用作属性class Car: ...class Battery: ...class ElectricCar(Car): ...my_leaf = ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name())my_leaf.battery.describe_battery()要获取电池相关信息,我们可以通过属性来调用其方法Python 将在实例 my_leaf 中查找属性 battery,然后会通过存储在这个属性中的 Battery 类的实例,查找并调用 describe_battery()方法。输出仍与之前相同:运行结果2024 Nissan LeafThis car has a 40-kWh battery.54
继承和组合继承和组合继承:电动汽车是一辆汽车(is-a55
继承和组合继承和组合继承:电动汽车是一辆汽车(is-a56
继承和组合继承和组合继承:电动汽车是一辆汽车(is-a组合:电动汽车有一块电池(has-a57
9.3.49.3.4 将实例用作属性将实例用作属性...class Battery: ... def get_range(self): """打印一条消息,指出电池的续航里程""" if self.size == 40: range = 150 elif self.size == 65: range = 225 print(f"This car can go about {range} miles on a full charge.")下面再给Battery 类添加一个方法,它将根据电池容量报告汽车的续航里程:这个方法将会根据电池容量的大小来报告总的续航里程58
9.3.49.3.4 将实例用作属性将实例用作属性...my_leaf = ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name()) my_leaf.battery.describe_battery()my_leaf.battery.get_range()同样的,也需要通过属性 battery 来调用:输出将会根据电池容量显示对应的续航里程:运行结果2024 Nissan LeafThis car has a 40-kWh battery.This car can go about 150 miles on a full charge.59
9.3.59.3.5 模拟实物模拟实物你或许会考虑,续航里程应该是电池的属性还是汽车的属性呢?实际上,我们应该尝试从较高的逻辑层面思考(而不是语法层面),即先考虑如何用代码表示实际事物,然后再调整代码的实现。建议多多尝试不同的方法来重写类,这是很常见的做法。要想编写出高效、准确的代码,不断地重写是必经之路。60

(原书)注:如果只描述一辆汽车,将 get_range() 方法放在 Battery 类中也许是合适的,但如果要描述一家汽车制造商的整条产品线,也许应该将 get_range() 方法移到 ElectricCar 类中。在这种情况下,get_range() 依然根据电池容量来确定续航里程,但报告的是一款汽车的续航里程。也可以这样做:仍将 get_range() 方法留在Battery 类中,但向它传递一个参数,如 car_model。此时,get_range() 方法可以根据电池容量和汽车型号报告续航里程。

9.49.4 导入类导入类 为了避免在一个文件中包含过多的类,我们将继续学习模块的相关内容。我们首先需要将类存储在模块中,然后在主程序中导入所需要的模块,并使用其中定义的类来创建对象。61
9.49.4 导入类导入类 和第八章一样,我们需要做一些准备工作:新建一个文件夹(比如叫做 module-class-demo在文件夹中新建两个文件:car.py :类模块文件my_car.py:主程序文件62

文件的命名也应遵循蛇形命名法

9.4.19.4.1 导入单个类导入单个类打开 car.py 文件,写入 Car 类的代码:首行是一个模块级文档字符串,对该模块的内容做了简要的描述"""一个用来表示汽车的类"""class Car: ...63

```python

"""一个用来表示汽车的类"""

class Car:

"""一次模拟汽车的简单尝试"""

def __init__(self, make, model, year):

"""初始化描述汽车的属性"""

self.make = make

self.model = model

self.year = year

self.odometer_reading = 0

def get_descriptive_name(self):

"""返回格式规范的描述性信息"""

long_name = f"{self.year} {self.make}" \

f" {self.model}"

return long_name.title()

def read_odometer(self):

"""打印一条指出汽车行驶里程的消息"""

print(f"This car has {self.odometer_reading} miles on it.")

def update_odometer(self, mileage):

"""将里程表读数设置为指定的值"""

self.odometer_reading = mileage

def increment_odometer(self, miles):

"""让里程表读数增加指定的量"""

self.odometer_reading += miles

```

9.4.19.4.1 导入单个类导入单个类接着打开 my_car.py 文件,写入下面的代码作为主程序:第一行语句 from car import Car 便是导入语句,将会让 Python car 模块中导入 Car from car import Carmy_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())my_new_car.odometer_reading = 23my_new_car.read_odometer()64
9.4.19.4.1 导入单个类导入单个类接着打开 my_car.py 文件,写入下面的代码作为主程序:执行主程序得到的输出,和之前将类放在同一个文件时一样:from car import Carmy_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name())my_new_car.odometer_reading = 23my_new_car.read_odometer()运行结果2023 Audi A4This car has 23 miles on it.65

导入类是一种高效的编程方式。通过将这个类移到一个模块中并导入该模块,依然可使用其所有功能,但主程序文件变得整洁易读了。这还让你能够将大部分逻辑存储在独立的文件中。在确定类能像你希望的那样工作后,就可以不管这些文件,专注于主程序的高级逻辑了。

9.4.29.4.2 在一个模块中存储多个类在一个模块中存储多个类在一个模块中,我们能和之前一样,在一个文件中存储多个类,现在将 Battery 类和 ElectricCar 类都加入 car.py 文件:"""一组用于表示燃油汽车和电动汽车的类"""class Car: ...class Battery: ...class ElectricCar(Car): ...66

```python

class Battery:

"""一次模拟电动汽车电池的简单尝试"""

def __init__(self, size=40):

"""初始化电池的属性"""

self.size = size

def describe_battery(self):

"""打印一条描述电池容量的消息"""

print(f"This car has a {self.size}-kWh battery.")

def get_range(self):

"""打印一条消息,指出电池的续航里程"""

if self.size == 40:

range = 150

elif self.size == 65:

range = 225

print(f"This car can go about {range} miles on a full charge.")

class ElectricCar(Car):

"""电动汽车的独特之处"""

def __init__(self, make, model, year):

"""初始化父类的属性"""

super().__init__(make, model, year)

self.battery = Battery()

def describe_battery(self):

"""打印一条描述电池容量的消息"""

print(f"This car has a {self.battery_size}-kWh battery.")

def get_descriptive_name(self):

"""返回格式规范的描述性信息"""

return f"{super.get_descriptive_name()}

```

9.4.29.4.2 在一个模块中存储多个类在一个模块中存储多个类接着新建一个名为 my_electric_car.py 的文件,只要导入 ElectricCar 类,便可以创建一辆电动汽车:输出仍然还是一样的,这里不再重复展示from car import ElectricCarmy_leaf = ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name())my_leaf.battery.describe_battery() my_leaf.battery.get_range()67
9.4.39.4.3 从一个模块中导入多个类从一个模块中导入多个类根据需要也可以在文件中导入任意数量的类,比如同时导入之前的两个汽车类,为此创建 my_cars.py 文件,并写入:和导入多个函数的形式一样,我们使用逗号分隔各个类from car import Car, ElectricCarmy_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name()) my_leaf = ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name())68
9.4.49.4.4 导入整个模块导入整个模块我们同样可以直接导入整个模块,然后通过点号语法来调用其中的类,改写 my_cars.py 文件:import carmy_new_car = car.Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name()) my_leaf = car.ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name())69
9.4.59.4.5 导入模块中的所有类导入模块中的所有类导入模块中的所有类的语法也与导入函数的一样:from module_name import *这种方法仍然不推荐使用,原因和之前提到过的类似:我们无法明确地知道程序使用了模块中的哪些类可能出现同名类的名称覆盖问题70
9.4.69.4.6 在一个模块中导入另一个模块在一个模块中导入另一个模块回到本节最初的要求:我们想要避免一个文件中包含过多的类为此,创建 electric_car.py 模块文件,并将 Battery 类和 ElectricCar 类剪切到这个模块中:electric_car.py"""一组可用于表示电动汽车的类"""from car import Carclass Battery: ...class ElectricCar(Car): ...car.py"""一个用来表示汽车的类"""class Car: ...71

完整代码限于版面略,请参考原书

9.4.69.4.6 在一个模块中导入另一个模块在一个模块中导入另一个模块回到本节最初的要求:我们想要避免一个文件中包含过多的类为此,创建 electric_car.py 模块文件,并将 Battery 类和 ElectricCar 类剪切到这个模块中:electric_car.py"""一组可用于表示电动汽车的类"""from car import Carclass Battery: ...class ElectricCar(Car): ...ElectricCar 类需要使用其父 Car,因此需要将它导入请不要忘记这行代码,否则 Python 将会给出错误72

完整代码限于版面略,请参考原书

9.4.69.4.6 在一个模块中导入另一个模块在一个模块中导入另一个模块现在就可以从每个模块中分别导入类,以根据需要创建任意类型的汽车了,为此改写 my_cars.py 文件:from car import Carfrom electric_car import ElectricCarmy_new_car = Car('audi', 'a4', 2023)print(my_new_car.get_descriptive_name()) my_leaf = ElectricCar('nissan', 'leaf', 2024)print(my_leaf.get_descriptive_name())73
9.4.79.4.7 使用别名使用别名在导入类时也可以指定别名,以简化输入,例如我们可以给 ElectricCar 这个较长的类名取一个简短的别名 ECfrom electric_car import ElectricCar as EC现在要创建电动汽车实例时,都可以使用这个别名:my_leaf = EC('nissan', 'leaf', 2024)74
9.4.79.4.7 使用别名使用别名还可以给模块指定别名import electric_car as ec然后可以结合使用模块别名和完整类名来创建实例:my_leaf = ec.ElectricCar('nissan', 'leaf', 2024)75
9.4.89.4.8 找到合适的工作流程找到合适的工作流程Python 提供了很多选项来实现相同的效果,我们应该熟悉这些选项,从而让我们能确定最佳的项目组织方式,同时易于理解别人开发的项目我们应在一开始让代码结构尽量简单,然后再尝试让代码更加整洁例如和本节的讲解顺序一样,我们完全可以在一个文件中完成所有工作,在确认一切正常后,再将类移到独立的模块中。76
9.59.5 PythonPython 标准库标准库Python 标准库是一组模块,是 Python 在安装时包含的,其中包含了其他程序员编写好的模块,它们经过了大量的实践论证,能减少你的编写重复功能的工作量。在编写代码时,可以直接使用 import 语句导入这些模块来使用。77
9.59.5 PythonPython 标准库标准库例如与随机有关的 random 模块其中就包含一个有趣的 randint 函数,它将接受指定的两个整数,然后随机选择一个位于这两个整数之间(含)的整数,作为它的返回值进行返回。例如,下面演示了如何生成一个位于 1 6 之间的随机整数:>>> from random import randint>>> randint(1, 6)378

由于是随机的,运行的结果不一定相同,记得多运行几次试试哦

注意:random 模块能用来创建众多有趣的项目,不过在创建与安全相关的应用程序时,请不要使用它,建议改用 secrets 等其他模块。

9.59.5 PythonPython 标准库标准库例如与随机打交道的 random 模块还有一个很有用的函数是 choice,它将一个列表或元组作为参数,并随机返回其中的一个元素:>>> from random import choice>>> players = ['charles', 'martina', 'michael', 'florence']>>> choice(players)'florence'之后我们将介绍更多有用的标准库模块,并引入功能强大的外部(第三方)模块79

由于是随机的,运行的结果不一定相同,记得多运行几次试试哦

Python 3 Module of the Week:https://pymotw.com/3/

这是一个不错的持续介绍 Python 中外部模块的网站。

9.69.6 类的编程风格类的编程风格 类名中的每个单词的首字母都应大写,并且不使用下划线这种命名规则叫做大驼峰式命名法(也称为 Pascal 命名法)实例名和模块名都应该使用全小写,并在单词之间加上下划线(_这种命名规则叫做下划线命名法(也称为 🐍 蛇形命名法)每个类定义后的第一行和模块文件的首行,都应该包含一个文档字符串,简要地描述类的功能,以帮助其他人理解我们的代码使用两个空行来分隔不同类,使用一个空行分隔类内的不同方法当使用 import 语句时,使用一个空行分隔标准库和自己的库80

当然,别忘了还有一个常量命名法:字母使用全大写,单词之间加上下划线。

另外对于包含多个单词的文件夹,我们一般使用全小写,并在单词之间加上横杠(-)

9.79.7 小结小结学习了如何编写类,并编写初始化方法,通过定义属性在类中存储信息,并编写了方法让类具备所需的行为。学习了通过几种不同方式来修改实例的属性,以及类的继承和组学习了如何通过将类存储在模块中,使得项目变得整洁,并了解 Python 标准库的使用和编写类与模块时应遵循的约定在下一章中,我们将学习学习如何使用文件以及如何捕获并处理异常81
课后拓展课后拓展完成书中的动手试一试部分可选拓展了解 __init__ 函数执行的顺序和机制了解内置函数 id isinstance了解面向对象中的 SOLID 原则学习 abc 库,并使用它来定义抽象类及方法等82