5.1 从面向过程到面向对象
5.1.1 面向过程概述
编程的最终目的都是为了借助计算机解决特定的问题。解决问题的理念不同,促成了面向过程和面向对象这两种截然不同的编程思想。
面向过程的核心思想是“怎么做”:通常把要解决的问题分解为一系列的步骤,再根据开发的需求将某些功能独立的代码封装成一系列的函数。最后完成的代码,就是按顺序调用函数。
对于规模较小的问题,面向过程行之有效,但面对复杂的需求时,面向过程将暴露其弊端:代码中的一系列函数将互相调用,你中有我,我中有你,工程将难以维护。
5.1.2 面向对象概述
和面向过程不同,面向对象的核心思想是“谁来做”:根据职责确定不同的对象,并将一系列的方法与属性封装进不同的对象中。
面向对象注重对象和职责,不同的对象承担不同的职责。最终的代码就是顺序地让不同的对象调用其特定的方法。和面向过程相比,面向对象更适合复杂项目的开发。
不过需要注意的是,面向对象和面向过程并不是完全孤立的。即使一个项目使用了面向对象进行开发,但是一旦涉及到具体的执行步骤时,也将无法避免地使用面向过程。如同要建造一座房屋一样,即使有着再精巧的设计理念与施工策略,但是一砖一瓦的铺设也需要一步步地去完成。
5.1.3 类和对象
面向对象是对现实世界中的事物和情景的抽象,为了提高代码的复用性,面向对象将现实世界中那些具有共同特征的事物和情景抽象成“类”。换句话说,类是一群具有相同特征或者行为的事物的一个统称。在面向对象中,用属性描述类的特征,用方法描述类的行为。
类是抽象的,并不能直接使用。“对象”则不同,它是基于类创建的具体实例,可以直接使用。这些基于类创建的不同对象将拥有通用的特征和行为。当然,根据实际需要可以为对象赋予某些专属的特性。根据类来创建对象被称为“实例化”,对象也被称为是类的实例。
类就像是图纸或模板,根据给定的图纸或模板,可以实例化出一系列的对象。
5.2 类与实例的创建和使用
5.2.1 创建 Dog 类
创建类的过程就是将属性和方法封装到类中的过程——封装,是面向对象的一大特点。把一些具体的细节封装到类中,将帮助我们真正明白自己的代码:不仅是各行代码的作用,还有代码背后更宏大的概念。
下面创建的是一个表示小狗的简单类 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!")
如代码所示,定义类使用的是关键字 class,其后紧跟的是类名。根据约定,在 Python 中,类名的首字母大写,这种命名风格被称为大驼峰命名法。
每个类的定义中都会有一个特殊的方法:__init__(),当使用类创建实例时,Python 都会自动运行它,并通过它来控制如何初始化对象。在这段代码中,__init__() 方法中初始化了对象的 name 和 age 两个属性。注意该方法的名称中,开头和结尾都有两条下划线,这是 Python 中的一种约定,旨在避免 Python 默认方法与普通方法发生命名冲突。
在类的方法中,形参 self 是必不可少的,且必须位于其他形参的前面。Python 在调用每个与实例相关联的方法时都自动传递实参 self,它是一个指向实例本身的引用,让实例能够访问类中的属性与方法。 以 self 为前缀的变量可供类中的所有方法使用,也可以通过类的任何实例来访问。
语句 self.name = name
获取与形参 name 相关联的值,并将其赋值给变量 name,然后该变量被关联到当前创建的实例。self.age = age
的作用与此类似。像这样可以通过实例访问的变量便是属性。
5.2.2 根据类创建实例
定义好了类以后,便可以基于类创建任意多的实例。下面的代码基于 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!")
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.
这段代码创建了两个小狗的实例,并为其指定了姓名与年龄这两个属性的值。在执行创建对象的语句时,Python 将自动调用 __init__() 方法并传入实参值,以完成属性的值设定。随后将创建好的对象与一个变量关联起来,这样,便可以使用关联了对象的变量调用对象的方法以执行相关任务,这里调用了两个小狗对象的 sit() 方法。
5.2.3 修改实例的属性
在编写好类以后,大部分的时间将花在实例的创建上。一个重要的工作就是根据实际情况修改实例的属性。
下面是一个表示汽车的类:
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()
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
------------------------------------------------------------------------
2019 Audi A4
在创建 Car 的实例时,通过向 __init__() 方法定义的形参为对象指定了三个属性。不过,有些属性无需通过形参来定义,可在方法 __init__() 中直接为其指定默认值。
下面的程序中添加了一个名为 odometer_reading 的属性,其初始值总是为 0。此外,程序还添加了名为 read_odometer() 的方法,用于读取 odometer_reading 的数值:
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} {self.model}"
return long_name.title()
def read_odometer(self):
"""打印一条指出汽车里程的消息。"""
print(f"The car has {self.odometer_reading} miles on it.")
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()
------------------------------------------------------------------------
2019 Audi A4
This car has 0 miles on it.
如果想要修改属性的值,可以通过以下三种方法:直接通过实例进行修改,通过方法进行修改,以及通过方法增加特定的值。
-
直接修改属性的值
要想修改属性的值,最简单的方法是通过实例直接访问它:
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} {self.model}" return long_name.title() def read_odometer(self): """打印一条指出汽车里程的消息。""" print(f"The car has {self.odometer_reading} miles on it.") my_new_car = Car('audi', 'a4', 2019) print(my_new_car.get_descriptive_name()) my_new_car.odometer_reading = 23 # 通过实例访问属性并修改其值 my_new_car.read_odometer() ------------------------------------------------------------------ 2019 Audi A4 This car has 23 miles on it.
在这段程序中,在类的外部使用了语句
my_new_car.odometer_reading = 23
修改了属性 odometer_reading 的值。 -
通过方法修改属性的值
有时候,直接修改属性的值并不可取,而应该通过方法来更新属性的值。下面的程序中定义了一个 update_odometer() 的方法:
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} {self.model}" return long_name.title() def read_odometer(self): """打印一条指出汽车里程的消息。""" print(f"The car has {self.odometer_reading} miles on it.") def update_odometer(self, mileage): """ 将里程表读数设置为指定的值。 同时,禁止将里程表读数往回调。 """ if mileage >= self.odometer_reading: self.odometer_reading = mileage else: print("You can't roll back an odometer!") my_new_car = Car('audi', 'a4', 2019) print(my_new_car.get_descriptive_name()) my_new_car.update_odometer(23) my_new_car.read_odometer() ------------------------------------------------------------------ 2019 Audi A4 This car has 23 miles on it.
程序通过调用对象的 update_odometer() 方法来实现对属性 odometer_reading 的修改。同时,该方法中还使用了 if-else 语句来判断传入的参数是否满足实际情况,即禁止将里程表的读数往回调。
-
通过方法对属性的值进行递增
有时候,我们需要的不是直接更改属性的值,而是对原有的值进行递增。例如,我们记录了汽车在某一段时间内增加的里程数,这时,就应该对原有的属性值进行递增操作:
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} {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): """ 将里程表读数设置为指定的值。 同时,禁止将里程表读数往回调。 """ if mileage >= self.odometer_reading: self.odometer_reading = mileage else: print("You can't roll back an odometer!") def increment_odometer(self, miles): """将里程表增加到指定的量。""" self.odometer_reading += miles my_new_car = Car('audi', 'a4', 2019) print(my_new_car.get_descriptive_name()) my_new_car.update_odometer(23) my_new_car.read_odometer() print("-----汽车又跑了一段距离-----") my_new_car.increment_odometer(10) my_new_car.read_odometer() ------------------------------------------------------------------ 2019 Audi A4 This car has 23 miles on it. -----汽车又跑了一段距离----- This car has 33 miles on it.
在这段程序中新增了一个 increment_odometer() 方法,用以接受增加的里程数。同时,也可以在这个方法中增添 if-else 语句,以防止递增的数值为负数。
5.3 继承
5.3.1 子类的 __init__() 方法
编写类时,并非总要从空白开始。如果要编写的类是另一个现成类的特殊版本,可使用继承。当一个类继承另一个类时,将自动获得另一个类的所有属性和方法。原有的类称为父类,而新的类称为子类。
下面定义的是一个表示电动汽车的类,它是汽车类的一个子类:
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} {self.model}"
return long_name.title()
def read_odometer(self):
print(f"The car has {self.odometer_reading} miles on it.")
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
self.odometer_reading += miles
class ElectricCar(Car):
"""电动汽车的独特之处。"""
def __init__(self, make, model, year):
"""初始化父类的属性。"""
super().__init__(make, model, year)
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
------------------------------------------------------------------------
2019 Tesla Model S
如果只在一个文件中创建子类的话,父类也必须包含在当前文件中,且位于子类的前面。在定义子类时,必须在子类名后的圆括号中指定父类的名称。这里子类的 __init__() 方法同样用于接收创建父类实例时所需的信息。
在面向对象编程中,有一个重写的概念。子类会继承父类的方法,但如果在子类中重写了从父类继承的方法后,在实例化子类后,对象所拥有的将是重写后的方法,而不是从父类继承的方法。使用继承时,可让子类保留从父类那里继承而来的精华,并剔除不需要的糟粕。
如果想让子类直接使用父类定义好的属性,就不要在子类中定义 __init__() 方法,因为这将覆盖父类的 __init__() 方法。不过有时候,我们想给子类增添一些其特有的属性,那么我们又不得不在子类中定义 __init__() 方法。为了在重写父类 __init__() 方法的同时还能继承父类的属性,这里使用了 super() 函数调用了父类的 __init__() 方法。super() 是一个特殊的函数,它会自动去父类中寻找子类所需的方法,而且不再需要传递参数 self。
5.3.2 给子类定义属性和方法
下面将为电动汽车类增添一个特有的属性:电瓶,以及描述该属性的方法。
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} {self.model}"
return long_name.title()
def read_odometer(self):
print(f"The car has {self.odometer_reading} miles on it.")
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
self.odometer_reading += miles
class ElectricCar(Car):
"""电动汽车的独特之处。"""
def __init__(self, make, model, year):
"""初始化父类的属性。"""
super().__init__(make, model, year)
self.battery_size = 75
def describe_battery(self):
"""打印一条描述电瓶容量的消息。"""
print(f"The car has a {self.battery_size}-kWh battery.")
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery()
------------------------------------------------------------------------
2019 Tesla Model S
The car has a 75-kWh battery.
5.3.3 将实例用作属性
使用代码模拟实物时,为类添加的细节可能会越来越多:属性和方法清单以及文件都越来越长。在这种情况下,可以将类的一部分提取出来,作为一个独立的类。
下面的代码演示了将电动汽车类中关于汽车电瓶的属性和方法提取出来,放到了一个名为 Battery 的类中,并将一个 Battery 的实例作为 ElectricCar 类的属性。
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} {self.model}"
return long_name.title()
def read_odometer(self):
print(f"The car has {self.odometer_reading} miles on it.")
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
self.odometer_reading += miles
class Battery:
"""一次模拟电动汽车电瓶的简单尝试"""
def __init__(self, battery_size=75):
"""初始化电瓶的属性"""
self.battery_size = battery_size
def describe_battery(self):
"""打印一条描述电瓶容量的消息。"""
print(f"The car has a {self.battery_size}-kWh battery.")
class ElectricCar(Car):
"""电动汽车的独特之处。"""
def __init__(self, make, model, year):
"""
初始化父类的属性。
再初始化电动汽车的特有属性。
"""
super().__init__(make, model, year)
self.battery = Battery()
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
------------------------------------------------------------------------
2019 Tesla Model S
The car has a 75-kWh battery.
Battery 类中的 __init__() 方法除了形参 self 外,还有一个可选形参 battery_size,如果没有为其提供值,电瓶的容量将被设置为 75。
这里看似做了很多额外的工作,但是现在想多详细地描述电瓶都可以,且不会导致 ElectricCar 类混乱不堪。
5.4 使用模块及类的编写规范
5.4.1 类的存储与导入
随着不断给类添加功能,文件将越来越长,即使妥善地使用了继承也是如此。为遵循 Python 的总体理念,应让文件尽可能整洁。Python 允许将类存储在模块中,然后在主程序中导入所需的模块即可。
在存储类模块时,将涉及到微妙的命名问题。尽管类名遵循大驼峰命名规范,但是对于模块名,即存储该类的文件名应该全部使用小写字母,并使用下划线连接单词。这和存储函数的模块命名遵循同一原则。我们还要注意另一个问题,例如我们把 Car 类存储到了 car.py 的文件中,为了避免命名重复,在使用该模块的程序时必须使用更具体的文件名,如 my_car.py。
和导入函数模块一样,类模块的导入也是使用 import 关键字。下面是一个存储了 Car 类的模块:
"""一个用于表示汽车的类。"""
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} {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):
"""
将里程表读数设置为指定的值。
同时,禁止将里程表读数往回调。
"""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
"""将里程表增加到指定的量。"""
self.odometer_reading += miles
在模块的开头,使用了文档字符串以对模块的内容做简要描述。下面创建的是一个 my_car.py 的文件,在其中导入 Car 类并创建其实例:
from car import Car
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
------------------------------------------------------------------------
2019 Audi A4
This car has 23 miles on it.
也可以将多个类存储在一个模块中。例如 Battery 类和 ElectricCar 类都可以用来帮助模拟汽车,所以可以把它们都存储到模块 car.py 中。和导入函数一样,可根据需要在程序文件中导入任意数量的类。例如在程序中导入模块 car 中的 Battery 类和 ElectricCar 类:
from car import Battery, ElectricCar
还可以导入整个模块,再使用句点表示法访问需要的类。这种导入方式简单,代码也易于阅读。因为创建类实例的代码都包含模块名,所以不会与当前文件使用的任何名称发生冲突。例如要导入整个 car 模块,并创建一个实例,代码如下:
import car
my_car = car.Car('audi', 'a4', 2019)
如果想导入模块中的每个类,使用类似下面的语法即可:
from car import *
不过并不推荐这种导入方式,原有有二。第一,如果只看文件开头的 import 语句就能清楚地知道程序使用了哪些类,将大有裨益。然而这种导入方式并没有指出使用了模块中的哪个类。第二,这种导入方式可能会引发名称的重复,将引发难以诊断的错误。之所以介绍它,是因为你可能在别人的代码中看到它。
需要从一个模块导入很多类时,最好导入整个模块,并使用句点表示法来访问类。这样做时,虽然没有列出所用到的类,但能清楚地知道程序在哪些地方使用了导入的模块,同时也避免了名称冲突。
有时候,需要将类分散到多个模块中,以免模块太大或在同一模块中存储不相干的类。将类存储在多个模块中时,可能会出现一个模块中的类依赖于另一个模块中的类。在这种情况下,可以在前一个模块中导入必要的类。例如将 ElectricCar 类存储在模块 electric_car.py 中时,需要先调用父类 Car:
from car import Car
class ElectricCar(Car):
"""电动汽车的独特之处。"""
def __init__(self, make, model, year):
"""
初始化父类的属性。
再初始化电动汽车的特有属性。
"""
super().__init__(make, model, year)
self.battery = Battery()
最后,在导入类时也可以为其指定别名。例如,ElecricCar 这一类名太长了,编写时太麻烦,可以为其指定别名:
from electric_car import ElecricCar as EC
my_tesla = EC('tesla', 'roadster', 2019)
5.4.2 Python 标准库
Python 标准库是一组模块,随 Python 安装包一起发布,用户可以随时使用。受限于安装包的设定大小,标准库数量为 270 个左右。如果想使用标准库中的任何函数和类,只需在程序开头包含一条简单的 import 语句即可。
例如想要产生随机数,可以调用 Python 的 random 模块。这里介绍这个模块中的两个函数。第一个函数是 randint(),它将两个整数作为参数,并返回一个位于这两个整数之间(含)的的整数。下面的程序演示了如何生成一个位于 1 和 6 之间的随机整数:
>>> from random import randint
>>> randint(1, 6)
1
另一个函数是 choice(),它将一个列表或元组作为参数,并随机返回其中的一个元素:
>>> from random import choice
>>> nums = [1, 2, 3, 4, 5]
>>> lucky_num = choice(nums)
>>> lucky_num
4
5.4.3 类的编写规范
类的编写应当遵循以下规范:
- 类名使用大驼峰命名法,即类名中的每个单词的首字母都大写,而不使用下划线。实例名和模块名都采用小写格式,并在单词之间加下划线。
- 对于每个类,都应紧跟在类定义后包含一个文档字符串,用以简要描述类的功能。每个模块也应包含一个文档字符串,对其中的类可用于什么用途进行描述。
- 可使用空行来组织代码,但不要滥用。在类中,可使用一个空行来分隔方法;在模块中,可使用两个空行来分隔类。
- 需要同时导入标准库中的模块和自己编写的模块时,先编写导入标准库模块的 import 语句,再添加一个空行,然后编写导入自己编写的模块的 import 语句。这种做法让人更容易让人明白程序使用的各个模块都来自何处。