目录
在⾯向对象编程中,你编写表⽰现实世界中的事物和情 景的类(class),并基于这些类来创建对象(object)。
在编写类时,
你要定义⼀批对象都具备的通⽤⾏为。在基于类创建对象时,每个对 象都⾃动具备这种通⽤⾏为。然后,你可根据需要赋予每个对象独特 的个性。
根据类来创建对象称为实例化,这让你能够使⽤类的实例(instance)。
1、创建和使用类
使⽤类⼏乎可以模拟任何东⻄。
1.1 创建Dog类
根据 Dog 类创建的每个实例都将存储名字和年龄,⽽且我们会赋予每条⼩ 狗坐下(sit())和打滚(roll_over())的能⼒:
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!")
⾸先,定义⼀个名为 Dog 的类。根 据约定,在 Python 中,⾸字⺟⼤写的名称指的是类。因为这是我们创建的全新的类,所以定义时不加括号。然后是⼀个⽂档字串,对这个类的功 能做了描述。
__init__() ⽅法
类中的函数称为⽅法。
_init__()是 ⼀个特殊⽅法,每当你根据 Dog 类创建新实例时,Python 都会⾃动运⾏ 它。在这个⽅法的名称中,开头和末尾各有两个下划线,这是⼀种约定,
旨在避免 Python 默认⽅法与普通⽅法发⽣名称冲突。务必确保 __init__() 的两边都有两个下划线,否则当你使⽤类来创建实例时,将 不会⾃动调⽤这个⽅法,进⽽引发难以发现的错误。
我们将 __init__() ⽅法定义成包含三个形参:self、name 和 age。在 这个⽅法的定义中,形参 self 必不可少,⽽且必须位于其他形参的前 ⾯。
当 Python 调⽤这个⽅ 法来创建 Dog 实例时,将⾃动传⼊实参 self。每个与实例相关联的⽅法 调⽤都会⾃动传递实参 self,该实参是⼀个指向实例本⾝的引⽤,让实例 能够访问类中的属性和⽅法。
在 __init__() ⽅法内定义的两个变量都有前缀 self。以self 为前缀的变量可供类中的所有⽅法使⽤,可以通过类的任意实例来访问。self.name = name 获取与形参name 相关联的值,并将其赋给变量 name,然后该变量被关联到当前创建的实例。像这样可通过实例访问的变量称为属性(attribute)。
Dog 类还定义了另外两个⽅法:sit() 和 roll_over()。由于 这些⽅法执⾏时不需要额外的信息,因此只有⼀个形参 self
1.2 根据类创建实例
可以将类视为有关如何创建实例的说明。
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.")
这⾥使⽤的是上⼀个⽰例中编写的 Dog 类。我们让 Python 创建⼀条名字为 'Willie'、年龄为 6 的⼩狗。
1.2.1 访问属性
要访问实例的属性,可使⽤点号。在❷处,编写如下代码来访问 my_dog 的属性 name 的值:
my_dog.name
1.2.2 调用方法
根据 Dog 类创建实例后,就能使⽤点号来调⽤ Dog 类中定义的任何⽅
法了。下⾯让⼩狗坐下和打滚:
class Dog:
--snip--
my_dog = Dog('Willie', 6)
my_dog.sit()
my_dog.roll_over()
1.2.3 创建多个实例
可按需求根据类创建任意数量的实例。下⾯再创建⼀个名为 your_dog 的⼩狗实例:
class Dog:
--snip--
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()
2、使用类和实例
可以使⽤类来模拟现实世界中的很多情景。类编写好后,你的⼤部分时间 将花在使⽤根据类创建的实例上。
2.1 Car 类
下⾯编写⼀个表⽰汽⻋的类,它存储了有关汽⻋的信息,并提供了⼀个汇 总这些信息的⽅法:
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', 2024)
print(my_new_car.get_descriptive_name())
2.2 属性指定默认值
有些属性⽆须通过形参来定义,可以在 __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):
--snip--
❷ def read_odometer(self):
"""打印⼀条指出汽⻋⾏驶⾥程的消息"""
print(f"This car has {self.odometer_reading} miles on it.")
my_new_car = Car('audi', 'a4', 2024)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()
2.3 修改属性的值
2.3.1 直接修改属性的值
要修改属性的值,最简单的⽅式是通过实例直接访问它。下⾯的代码 直接将⾥程表读数设置为 23:
class Car:
--snip--
my_new_car = Car('audi', 'a4', 2024)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
2.3.2 通过方法修改属性的值
这样⽆须直接访问属性, ⽽是可将值传递给⽅法,由它在内部进⾏更新。
class Car:
--snip--
def update_odometer(self, mileage):
"""将⾥程表读数设置为指定的值"""
self.odometer_reading = mileage
my_new_car = Car('audi', 'a4', 2024)
print(my_new_car.get_descriptive_name())
❶ my_new_car.update_odometer(23)
my_new_car.read_odometer()
还可以对 update_odometer() ⽅法进⾏扩展,使其在修改⾥程表读 数时做些额外的⼯作。下⾯来添加⼀些逻辑,禁⽌将⾥程表读数往回 调:
class Car:
--snip--
def update_odometer(self, mileage):
"""
将⾥程表读数设置为指定的值
禁⽌将⾥程表读数往回调
"""
❶ if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
❷ print("You can't roll back an odometer!")
2.3.3 通过方法让属性的值递增
有时候需要将属性值递增特定的量,⽽不是将其设置为全新的值。
class Car:
--snip--
def update_odometer(self, mileage):
--snip--
def increment_odometer(self, miles):
"""让⾥程表读数增加指定的量"""
self.odometer_reading += miles
❶ my_used_car = Car('subaru', 'outback', 2019)
print(my_used_car.get_descriptive_name())
❷ my_used_car.update_odometer(23_500)
my_used_car.read_odometer()
❸ my_used_car.increment_odometer(100)
my_used_car.read_odometer()
注意:虽然可以使⽤类似于上⾯的⽅法来控制⽤户修改属性值 (如⾥程表读数)的⽅式,但能够访问程序的⼈都能直接访问属 性将⾥程表修改为任意的值。要确保安全,除了进⾏类似于前⾯ 的基本检查以外,还需要极度关注细节。
3、继承
当⼀个类继承另⼀个类时,将⾃动获 得后者的所有属性和⽅法。原有的类称为⽗类(parent class),⽽新类称为 ⼦类(child class)。⼦类不仅继承了⽗类的所有属性和⽅法,还可定义⾃ ⼰的属性和⽅法。
3.1 子类的 __init__() 方法
在既有的类的基础上编写新类,通常要调⽤⽗类的 __init__() ⽅法。这 将初始化在⽗类的 __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"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
❷ 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())
super() 是⼀个特殊的函数,让你能够调⽤⽗类的⽅法(⻅❹)。。⽗类也称为超类(superclass),函数名 super 由此得名。
3.2 子类定义属性和方法
让⼀个类继承另⼀个类后,就可以添加区分⼦类和⽗类所需的新属性和新 ⽅法了。
class Car:
--snip--
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.")
my_leaf = ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
my_leaf.describe_battery()
3.3 重写父类中的方法
在使⽤⼦类模拟的实物的⾏为时,如果⽗类中的⼀些⽅法不能满⾜⼦类的 需求,就可以⽤下⾯的办法重写:在⼦类中定义⼀个与要重写的⽗类⽅法 同名的⽅法。
class ElectricCar(Car):
--snip--
def fill_gas_tank(self):
"""电动汽⻋没有油箱"""
print("This car doesn't have a gas tank!")
现在,如果有⼈对电动汽⻋调⽤ fill_gas_tank() ⽅法,Python 将忽略 Car 类中的 fill_gas_tank() ⽅法,转⽽运⾏上述代码。
3.4 将实例作为属性
属性和 ⽅法越来越多,⽂件越来越⻓。在这种情况下,可能需要将类的⼀部分提 取出来,作为⼀个独⽴的类。将⼤型类拆分成多个协同⼯作的⼩类,这种 ⽅法称为组合(composition)。
class Car:
--snip--
class Battery:
"""⼀次模拟电动汽⻋电池的简单尝试"""
❶ def __init__(self, battery_size=40):
"""初始化电池的属性"""
self.battery_size = battery_size
❷ def describe_battery(self):
"""打印⼀条描述电池容量的消息"""
print(f"This 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_leaf = ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
my_leaf.battery.describe_battery()
。下⾯再给 Battery 类添加⼀个⽅法,
它根据电池容量报告汽⻋的续航⾥程:
class Car:
--snip--
class Battery:
--snip--
def get_range(self):
"""打印⼀条消息,指出电池的续航⾥程"""
if self.battery_size == 40:
range = 150
elif self.battery_size == 65:
range = 225print(f"This car can go about {range} miles on a full
charge.")
class ElectricCar(Car):
--snip--
my_leaf = ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
my_leaf.battery.describe_battery()
❶ my_leaf.battery.get_range() 新增的⽅法 get_range() 做了⼀些简单的分析:如果电池的容量为 40 千 ⽡时,就将续航⾥程设置为 150 英⾥;如果容量为 65 千⽡时,就将续航⾥ 程设置为 225 英⾥。然后,它会报告这个值。为了使⽤这个⽅法,也需要 通过汽⻋的属性 battery 来调⽤(⻅❶)。 输出已经可以根据电池的容量显⽰对应的续航⾥程了:
2024 Nissan Leaf
This car has a 40-kWh battery.
This car can go about 150 miles on a full charge.
3.5 模拟实物
在模拟较复杂的事物(如电动汽⻋)时,需要思考⼀些有趣的问题。
续航 ⾥程是电池的属性还是汽⻋的属性呢?
如果只描述⼀辆汽⻋,将 get_range() ⽅法放在 Battery 类中也许是合适的,
但如果要描述⼀家 汽⻋制造商的整条产品线,也许应该将 get_range() ⽅法移到ElectricCar 类中。
在这种情况下,get_range() 依然根据电池容量来 确定续航⾥程,但报告的是⼀款汽⻋的续航⾥程。
也可以这样做:仍将 get_range() ⽅法留在 Battery 类中,但向它传递⼀个参数,如 car_model。此时get_range() ⽅法将根据电池容量和汽⻋型号报告 续航⾥程。
这让你进⼊了程序员的另⼀个境界:在解决上述问题时,从较⾼的逻辑层 ⾯(⽽不是语法层⾯)思考。你考虑的不是 Python,⽽是如何使⽤代码来 表⽰实际事物。达到这种境界后,你会经常发现,对现实世界的建模⽅法 没有对错之分。有些⽅法的效率更⾼,但要找出效率最⾼的表⽰法,需要⼀定的实践。
4、导入类
随着不断地给类添加功能,⽂件可能变得很⻓,遵循 Python 的整体理念,应该让⽂件尽量整洁。
4.1 导入单个类
将 Car 类存储在⼀个名 为 car.py 的模块中,该模块将覆盖前⾯的⽂件 car.py。从现在开始,使⽤该 模块的程序都必须使⽤更具体的⽂件名,如 my_car.py。下⾯是模块 car.py,其中只包含 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', 2024)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
import 语句让 Python 打开模块car 并导⼊其中的 Car 类。
4.2 在一个模块中存储多个类
尽管同⼀个模块中的类之间应该存在某种相关性,但其实可根据需要在 ⼀个模块中存储任意数量的类。
"""⼀组⽤于表⽰燃油汽⻋和电动汽⻋的类"""
class Car:
--snip--
class Battery:
"""⼀次模拟电动汽⻋电瓶的简单尝试"""
def __init__(self, battery_size=40):
"""初始化电池的属性"""
self.battery_size = battery_size
def describe_battery(self):
"""打印⼀条描述电池容量的消息"""
print(f"This car has a {self.battery_size}-kWh battery.")
def get_range(self):
"""打印⼀条描述电池续航⾥程的消息"""
if self.battery_size == 40:
range = 150
elif self.battery_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()
现在,可以新建⼀个名为 my_electric_car.py 的⽂件,导⼊ ElectricCar 类,并创建⼀辆电动汽⻋了:
from car import ElectricCar
my_leaf = ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
my_leaf.battery.describe_battery()
my_leaf.battery.get_range()
4.3 一个模块导入多个类
可以根据需要在程序⽂件中导⼊任意数量的类。
from car import Car, ElectricCar
my_mustang = Car('ford', 'mustang', 2024)
print(my_mustang.get_descriptive_name())
my_leaf = ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
4.4 导入整个模块
还可以先导⼊整个模块,再使⽤点号访问需要的类。
下⾯的代码导⼊整个 car 模块,并创建⼀辆燃油汽⻋和⼀辆电动汽⻋:
❶ import car
❷ my_mustang = car.Car('ford', 'mustang', 2024)
print(my_mustang.get_descriptive_name())
❸ my_leaf = car.ElectricCar('nissan', 'leaf', 2024)
print(my_leaf.get_descriptive_name())
4.5 导入模块中的所有类
要导⼊模块中的每个类,可使⽤下⾯的语法:
from module_name import *
不推荐这种导⼊⽅式,原因有⼆。
- 第⼀,最好只需要看⼀下⽂件开头的 import 语句,就能清楚地知道程序使⽤了哪些类。但这种导⼊⽅式没有明 确地指出使⽤了模块中的哪些类。
- 第⼆,这种导⼊⽅式还可能引发名称⽅ ⾯的迷惑。如果不⼩⼼导⼊了⼀个与程序⽂件中的其他东⻄同名的类,将 引发难以诊断的错误。
4.6 在一个模块中导入另一个模块
有时候,需要将类分散到多个模块中,以免模块太⼤或者在同⼀个模块中 存储不相关的类。
下⾯将 Car 类存储在⼀个模块中,并将 ElectricCar 和 Battery 类存 储在另⼀个模块中。
"""⼀组可⽤于表⽰电动汽⻋的类"""
from car import Car
class Battery:
--snip--
class ElectricCar(Car):
--snip--
ElectricCar 类需要访问其⽗类 Car,因此直接将 Car 类导⼊该模块
4.7 使用别名
在 导⼊类时,也可以给它指定别名。假设要在程序中创建⼤量电动汽⻋实例,需要反复输⼊ ElectricCar,⾮ 常烦琐。为了避免这种烦恼,可在 import 语句中给 ElectricCar 指定
⼀个别名:
from electric_car import ElectricCar as EC
现在每当需要创建电动汽⻋实例时,都可使⽤这个别名:
my_leaf = EC('nissan', 'leaf', 2024)
还可以给模块指定别名。下⾯导⼊模块 electric_car 并给它指定了别 名:
import electric_car as ec
现在可以结合使⽤模块别名和完整的类名了:
my_leaf = ec.ElectricCar('nissan', 'leaf', 2024)
4.8 找到合适的工作流程
⼀开始应让代码结构尽量简单。
⾸先尝试在⼀个⽂件中完成所有的⼯作, 确定⼀切都能正确运⾏后,再将类移到独⽴的模块中。
如果你喜欢模块和 ⽂件的交互⽅式,可在项⽬开始时就尝试将类存储到模块中。先找出 能够编写出可⾏代码的⽅式,再尝试让代码更加整洁。
5、Python标准库
Python 标准库是⼀组模块,在安装 Python 时已经包含内。你可以使⽤标准库中的任何函数和类,只需在程序开头添加⼀条 简单的 import 语句即可。
下⾯来看看模块 random,在这个模块中,⼀个有趣的函数是 randint()。它将两个整数作为参数, 并随机返回⼀个位于这两个整数之间(含)的整数。下⾯演⽰了如何⽣成 ⼀个位于 1 和 6 之间的随机整数:
>>> from random import randint
>>> randint(1, 6)
3
在模块 random 中,另⼀个很有⽤的函数是 choice()。它将⼀个列表或 元组作为参数,并随机返回其中的⼀个元素:
>>> from random import choice
>>> players = ['charles', 'martina', 'michael', 'florence', 'eli']
>>> first_up = choice(players)
>>> first_up
'florence'
在创建与安全相关的应⽤程序时,不要使⽤模块 random,但它能⽤来创建 众多有趣的项⽬。
6、类的编写风格
类名应采⽤驼峰命名法,即将类名中的每个单词的⾸字⺟都⼤写,并且不 使⽤下划线。实例名和模块名都采⽤全⼩写格式,并在单词之间加上下划 线。
对于每个类,都应在类定义后⾯紧跟⼀个⽂档字符串。这种⽂档字符串简 要地描述类的功能,你应该遵循编写函数的⽂档字符串时采⽤的格式约 定。每个模块也都应包含⼀个⽂档字符串,对其中的类可⽤来做什么进⾏ 描述。
可以使⽤空⾏来组织代码,但不宜过多。在类中,可以使⽤⼀个空⾏来分 隔⽅法;⽽在模块中,可以使⽤两个空⾏来分隔类。
当需要同时导⼊标准库中的模块和你编写的模块时,先编写导⼊标准库模 块的 import 语句,再添加⼀个空⾏,然后编写导⼊你⾃⼰编写的模块的 import 语句。在包含多条 import 语句的程序中,这种做法让⼈更容易 明⽩程序使⽤的各个模块来⾃哪⾥。