目录
练习9-16:Python Module of the Week
9.1 创建和使用类
使用类几乎可以模拟任何东西。下面来编写一个表示小狗的简单类Dog ,它表示的不是特定的小狗,而是任何小狗。对于大多数宠物狗,我们都知道些什么呢?它们都有名字和年龄。我们还知道,大多数小狗还会蹲下和打滚。由于大多数小狗都具备上述两项信息(名字和年龄)和两种行为(蹲下和打滚),我们的Dog 类将包含它们。这个类让Python知道如何创建表示小狗的对象。编写这个类后,我们将使用它来创建表示特定小狗的实例。
9.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 必不可少,
而且
必须位于其他形参的前面
。
为何必须在方法定义中包含形参
self
呢?因为Python调用这个方法来创建
Dog 实例时,将自动传入实参
self
。每个与实例相关联的方法调用都自动传递实参
self ,它是
一个指向实例本身的引用
,让实例能够访问类中的属性和方法。创建
Dog 实例时,Python将调用
Dog
类的方法
__init__()
。我们将通过实参向
Dog() 传递名字和年龄,self
会自动传递,因此不需要传递它。每当根据
Dog 类创建实例时,都只需给最后两个形参(
name
和
age
)提供值。
❹
处定义的两个变量都有前缀
self
。以
self 为前缀的变量可供类中的所有方法使用,可以通过类的任何实例来访问。
self.name = name
获取与形参
name 相关联的值,并将其赋给变量
name
,然后该变量被关联到当前创建的实例。self.age= age
的作用与此类似。像这样可通过实例访问的变量称为
属性
。
Dog
类还定义了另外两个方法:
sit()
和
roll_over()
。这些方法执行时不需要额外的信息,因此它们只有一个形参self 。我们随后将创建的实例能够访问这些方法,换句话说,它们都会蹲下和打滚。当前,sit() 和roll_over()所做的有限,只是打印一条消息,指出小狗正在蹲下或打滚。但可以扩展这些方法以模拟实际情况:如果这个类包含在一个计算机游戏中,这些方法将包含创建小狗蹲下和打滚动画效果的代码;如果这个类是用于控制机器狗的,这些方法将让机器狗做出蹲下和打滚的动作。
9.1.2 根据类创建实例
可将类视为有关如何创建实例的说明。
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!")
#❶处
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
的小狗。遇到这行代码时,Python使用实参
'Willie' 和6
调用
Dog
类的方法
__init__()
。方法
__init__() 创建一个表示特定小狗的实例,并使用提供的值来设置属性
name
和
age 。接下来,Python返回一个表示这条小狗的实例,而我们将这个实例赋给了
变量my_dog
。在这里,命名约定很有用:通常可认为首字母大写的名称(如
Dog )指的是类,而小写的名称(如my_dog
)指的是根据类创建的实例。
a. 访问属性
要访问实例的属性,可使用句点表示法。
❷
处编写了如下代码来访问my_dog的属性
name
的值:
my_dog.name
句点表示法在Python中很常用,这种语法演示了Python如何获悉属性的值。在这里, Python先找到实例
my_dog
,再查找与该实例相关联的属性
name 。在Dog
类中引用这个属性时,使用的是
self.name
。在
❸处,使用同样的方法来获取属性
age
的值。
输出是有关
my_dog
的摘要:
My dog's name is Willie.
My dog is 6 years old.
b. 调用方法
根据
Dog
类创建实例后,就能使用句点表示法来调用
Dog 类中定义的任何方法了。下面来让小狗蹲下和打滚:
class Dog:
--snip--
my_dog = Dog('Willie', 6)
my_dog.sit()
my_dog.roll_over()
要调用方法,可指定实例的名称(这里是
my_dog )和要调用的方法,并用句点分隔。遇到代码
my_dog.sit()
时,Python在类
Dog
中查找方法
sit() 并运行其代码。Python以同样的方式解读代码
my_dog.roll_over()
。
Willie按我们的命令做了:
Willie is Now sitting.
Willie rolled over!
c. 创建多个实例
可按需求根据类创建任意数量的实例。下面再创建一个名为
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()
在本例中创建了两条小狗,分别名为Willie和Lucy。每条小狗都是一个独立的实例,有自己的一组属性,能够执行相同的操作:
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.
9.1.3 练习
练习9-1:餐馆
创建一个名为
Restaurant
的类,为其方法__init__()设置属性
restaurant_name
和
cuisine_type 。创建一个名为describe_restaurant()
的方法和一个名为
open_restaurant() 的方法,前者打印前述两项信息,而后者打印一条消息,指出餐馆正在营业。
根据这个类创建一个名为
restaurant 的实例,分别打印其两个属性,再调用前述两个方法。
class Restaurant:
"""餐馆信息"""
def __init__(self, restaurant_name, cuisine_type):
"""初始化属性"""
self.restaurant_name = restaurant_name
self.cuisine_type = cuisine_type
def describe_restaurant(self):
"""餐馆信息"""
print(f"This {self.restaurant_name}'s style is {self.cuisine_type}")
def open_restaurant(self):
"""营业情况"""
print("opening")
my_restaurant = Restaurant('xiao mi fan dian', 'chuan cai')
my_restaurant.describe_restaurant()
my_restaurant.open_restaurant()
练习9-2:三家餐馆
本题代码和9-1一样添加了如下测试用例:
my_restaurant = Restaurant('xiao mi fan dian', 'chuan cai')
your_restaurant = Restaurant('huazhong fandian', 'xiang cai')
his_restaurant = Restaurant('zhongnan fandian', 'yue cai')
my_restaurant.describe_restaurant()
your_restaurant.describe_restaurant()
his_restaurant.describe_restaurant()
练习9-3:用户
创建一个名为
User
的类,其中包含属性
first_name 和last_name
,以及用户简介通常会存储的其他几个属性。在类
User
中定义一
个名为
describe_user() 的方法,用于打印用户信息摘要。再定义一个名为greet_user()
的方法,用于向用户发出个性化的问候。
创建多个表示不同用户的实例,并对每个实例调用上述两个方法。
class User:
"""用户信息"""
def __init__(self, first_name, last_name, **info):
"""信息初始化"""
self.first_name = first_name
self.last_name = last_name
self.info = info
def describe_user(self):
"""描述用户信息"""
print(f"{self.first_name.title()} {self.last_name.title()}")
print(self.info)
def greet_user(self):
"""打招呼"""
print(f"Hello,{self.first_name.title()} {self.last_name.title()}!")
first_person = User('bob', 'cruise', age='14', habit='eat')
first_person.describe_user()
first_person.greet_user()
9.2 使用类和实例
9.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', 2019)
print(my_new_car.get_descriptive_name())
在
❶
处,定义了方法
__init__()
。与前面的
Dog 类中一样,这个方法的第一个形参为
self
。该方法还包含另外三个形参:
make
、
model
和
year 。方法__init__() 接受这些形参的值,并将它们赋给根据这个类创建的实例的属性。创建新的
Car
实例时,需要指定其制造商、型号和生产年份。
在
❷
处,定义了一个名为
get_descriptive_name()
的方法。它使用属性year、
make
和
model 创建一个对汽车进行描述的字符串,让我们无须分别打印每个属性的值。为在这个方法中访问属性的值,使用了
self.make
、
self.model 和self.year
。在
❸
处,根据
Car 类创建了一个实例,并将其赋给变量my_new_car
。接下来,调用方法
get_descriptive_name() ,指出我们拥有一辆什么样的汽车:
2019 Audi A4
9.2.2 给属性指定默认值
创建实例时,有些属性无须通过形参来定义,可在方法
__init__() 中为其指定默认值。
下面来添加一个名为
odometer_reading 的属性,其初始值总是为0。我们还添加了一个名为
read_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"This 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()
现在,当Python调用方法__init__() 来创建新实例时,将像前一个示例一样以属性的方式存储制造商、型号和生产年份。接下来,Python将创建一个名为odometer_reading
的属性,并将其初始值设置为0
。
定义一个名为
read_odometer()
的方法,让你能够轻松地获悉汽车的里程
。
一开始汽车的里程为0:
2019 Audi A4
This car has 0 miles on it.
9.2.3 修改属性的值
a. 直接修改属性的值
要修改属性的值,最简单的方式是通过实例直接访问它。下面的代码直接将里程表读数设置为23:
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.")
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()
在
❶
处,使用句点表示法直接访问并设置汽车的属性
odometer_reading 。这行代码让Python在实例
my_new_car
中找到属性
odometer_reading ,并将其值设置为23:
2019 Audi A4
This car has 23 miles on it.
b. 通过方法修改属性的值
下面的示例演示了一个名为
update_odometer()
的方法:
class Car:
--snip--
# ❶
def update_odometer(self, mileage):
"""将里程表读数设置为指定的值。"""
self.odometer_reading = mileage
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()
对
Car
类所做的唯一修改是在
❶
处添加了方法
update_odometer() 。这个方法接受一个里程值,并将其赋给
self.odometer_reading
。在
❷处,调用update_odometer()
,并向它提供了实参23(该实参对应于方法定义中的形
参
mileage
)。它将里程表读数设置为23,而方法
read_odometer()
打印
该读数:
2019 Audi A4
This car has 23 miles on it.
可对方法
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!")
现在,
update_odometer() 在修改属性前检查指定的读数是否合理。如果新指定的里程(
mileage )大于或等于原来的里程(
self.odometer_reading ),就将里程表读数改为新指定的里程;否则发出警告,指出不能将里程表往回调
。
c. 通过方法对属性的值进行递增
有时候需要将属性值递增特定的量,而不是将其设置为全新的值。假设我们购买了一辆二手车,且从购买到登记期间增加了100英里的里程。下面的方法让我们能够传递这个增量,并相应地增大里程表读数:
class Car:
--snip--
def update_odometer(self, mileage):
--snip--
def increment_odometer(self, miles):
"""将里程表读数增加指定的量。"""
self.odometer_reading += miles
my_used_car = Car('subaru', 'outback', 2015)
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()
你可以轻松地修改这个方法,以禁止增量为负值,从而防止有人利用它来回调里程表。
注意 你可以使用类似于上面的方法来控制用户修改属性值(如里程表读数)的方式,但能够访问程序的人都可以通过直接访问属性来将里程表修改为任何值。要确保安全,除了进行类似于前面的基本检查外,还需特别注意细节。
9.2.4 练习
练习9-4:就餐人数
在为完成练习9-1而编写的程序中,添加一个名为number_served 的属性,并将其默认值设置为0。根据这个类创建一个名为restaurant 的实例。打印有多少人在这家餐馆就餐过,然后修改这个值并再次打印它。
添加一个名为increment_number_served() 的方法,让你能够将就餐人数递增。调用这个方法并向它传递一个这样的值:你认为这家餐馆每天可能接待的就餐人数。
class Restaurant:
"""餐馆信息"""
def __init__(self, restaurant_name, cuisine_type):
"""初始化属性"""
self.restaurant_name = restaurant_name
self.cuisine_type = cuisine_type
self.number_served = 0
def describe_restaurant(self):
"""餐馆信息"""
print(f"This {self.restaurant_name}'s style is {self.cuisine_type}")
def open_restaurant(self):
"""营业情况"""
print("opening")
def read_number_served(self):
"""打印就餐人数"""
print(f"{self.number_served} people have been here.")
def set_number_served(self, numbers):
"""修改就餐人数"""
if numbers >= self.number_served:
self.number_served = numbers
else:
print("You can't roll back!")
def increment_number_served(self, numbers):
"""增添大于零数量的就餐人数"""
if numbers >= 0:
self.number_served += numbers
else:
print("You can't roll back!")
his_restaurant = Restaurant('FoShan fandian', 'yue cai')
his_restaurant.describe_restaurant()
his_restaurant.read_number_served()
his_restaurant.number_served = 23
his_restaurant.read_number_served()
his_restaurant.set_number_served(66)
his_restaurant.increment_number_served(66)
his_restaurant.read_number_served()
练习9-5:尝试登录次数
increment_login_attempts()
的方法,将属性
login_attempts 的值加1。再编写一个名为
reset_login_attempts() 的方法,将属性login_attempts
的值重置为0。
根据
User
类创建一个实例,再调用方法increment_login_attempts()多次。打印属性
login_attempts
的值,确认它被正确地递增。然后,调用
方法
reset_login_attempts()
,并再次打印属性l
ogin_attempts 的值,确认它被重置为0。
class User:
"""用户信息"""
def __init__(self, first_name, last_name, **info):
"""信息初始化"""
self.first_name = first_name
self.last_name = last_name
self.info = info
self.login_attempts = 0
def describe_user(self):
"""描述用户信息"""
print(f"{self.first_name.title()} {self.last_name.title()}")
print(self.info)
def greet_user(self):
"""打招呼"""
print(f"Hello,{self.first_name.title()} {self.last_name.title()}!")
def increment_login_attempts(self):
self.login_attempts += 1
def reset_login_attempts(self):
self.login_attempts = 0
first_person = User('bob', 'cruise', age='14', habit='eat')
first_person.describe_user()
first_person.increment_login_attempts()
print(first_person.login_attempts)
first_person.reset_login_attempts()
print(first_person.login_attempts)
9.3 继承
编写类时,并非总是要从空白开始。如果要编写的类是另一个现成类的特殊版本,可使用
继承
。一个类
继承 另一个类时,将自动获得另一个类的所有属性和方法。原有的类称为
父类
,而新类称为
子类 。子类继承了父类的所有属性和方法,同时还可以定义自己的属性和方法。
9.3.1 子类的方法__init__()
下面来创建
ElectricCar
类的一个简单版本,它具备
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
# ❷
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())
首先是
Car
类的代码。创建子类时,父类必须包含在
当前文件
中,且位于子类前面。在
❷
处,定义了子类
ElectricCar 。定义子类时,必须在圆括号内指定父类的名称。方法
__init__()
接受创建
Car
实例所需的信息(见
❸
)。
❹
处的
super() 是一个特殊函数,让你能够调用父类的方法。这行代码让Python调用
Car
类的方法
__init__()
,让
ElectricCar 实例包含这个方法中定义的所有属性。父类也称为
超类 (superclass)
,名称
super 由此而来。
为测试继承能够正确地发挥作用,我们尝试创建一辆电动汽车,但提供的信息与创建普通汽车时相同。在
❺
处,创建
ElectricCar 类的一个实例,并将其赋给变量my_tesla
。这行代码调用
ElectricCar
类中定义的方法
__init__() ,后者让Python调用父类
Car
中定义的方法
__init__()
。我们提供了实参'tesla'、
'model s'
和
2019
。
除方法
__init___() 外,电动汽车没有其他特有的属性和方法。当前,我们只想确认电动汽车具备普通汽车的行为:
2019 Tesla Model S
9.3.2 给子类定义属性和方法
让一个类继承另一个类后,就可以添加区分子类和父类所需的新属性和新方法了。下面来添加一个电动汽车特有的属性(电瓶),以及一个描述该属性的方法。我们将存储电瓶容量,并编写一个打印电瓶描述的方法:
class Car:
--snip--
class ElectricCar(Car):
"""电动汽车的独特之处。"""
def __init__(self, make, model, year):
"""
初始化父类的属性。
再初始化电动汽车特有的属性。
"""
super().__init__(make, model, year)
self.battery_size = 75
def describe_battery(self):
"""打印一条描述电瓶容量的消息。"""
print(f"This 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()
还添加了一个名为
describe_battery() 的方法,打印有关电瓶的信息。调用这个方法时,将看到一条电动汽车特有的描述:
2019 Tesla Model S
This car has a 75-kWh battery.
对于ElectricCar 类的特殊程度没有任何限制。模拟电动汽车时,可根据所需的准确程度添加任意数量的属性和方法。如果一个属性或方法是任何汽车都有的,而不是电动汽车特有的,就应将其加入到
Car
类而非
ElectricCar 类中。这样,使用
Car
类的人将获得相应的功能,而
ElectricCar 类只包含处理电动汽车特有属性和行为的代码。
9.3.3 重写父类的方法
假设
Car
类有一个名为
fill_gas_tank() 的方法,它对全电动汽车来说毫无意义,因此你可能想重写它。下面演示了一种重写方式:
class ElectricCar(Car):
--snip--
def fill_gas_tank(self):
"""电动汽车没有油箱。"""
print("This car doesn't need a gas tank!")
现在,如果有人对电动汽车调用方法
fill_gas_tank()
,Python将忽略
Car 类中的方法
fill_gas_tank() ,转而运行上述代码。使用继承时,可让子类保留从父类那里继承而来的精华,并剔除不需要的糟粕。
9.3.4 将实例用作属性
例如,不断给ElectricCar 类添加细节时,我们可能发现其中包含很多专门针对汽车电瓶的属性和方法。在这种情况下,可将这些属性和方法提取出来,放到一个名为
Battery
的类中,并将一个
Battery
实例作为
ElectricCar
类的属性:
class Car:
--snip--
class Battery:
"""一次模拟电动汽车电瓶的简单尝试。"""
def __init__(self, battery_size=75):
"""初始化电瓶的属性。"""
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_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
定义一个名为
Battery
的新类,它没有继承任何类。
方法__init__()除
self
外,还有另一个形参
battery_size 。这个形参是可选的:如果没有给它提供值,电瓶容量将被设置为75。方法
describe_battery() 也移到了这个类中
。
在
ElectricCar
类中,添加了一个名为
self.battery
的属性。这行代码让Python创建一个新的
Battery 实例(因为没有指定容量,所以为默认值75),并将该实例赋给属性
self.battery
。每当方法
__init__() 被调用时,都将执行该操作,因此现在每个
ElectricCar
实例都包含一个自动创建的
Battery 实例。
我们创建一辆电动汽车,并将其赋给变量
my_tesla 。描述电瓶时,需要使用电动汽车的属性
battery
:
my_tesla.battery.describe_battery()
输出与你在前面看到的相同:
2019 Tesla Model S
This car has a 75-kWh battery.
这看似做了很多额外的工作,但是现在想多详细地描述电瓶都可以,且不会导致ElectricCar
类混乱不堪。下面再给
Battery 类添加一个方法,它根据电瓶容量报告汽车的续航里程:
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 Battery:
"""一次模拟电动汽车电瓶的简单尝试。"""
def __init__(self, battery_size=75):
"""初始化电瓶的属性。"""
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 == 75:
range = 260
elif self.battery_size == 100:
range = 315
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_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()
新增的方法get_range() 做了一些简单的分析:如果电瓶的容量为75 kWh,就将续航里程设置为260英里;如果容量为100 kWh,就将续航里程设置为315英里,然后报告这个值。为使用这个方法,也需要通过汽车的属性
battery 来调用
输出指出了汽车的续航里程(这取决于电瓶的容量):
2019 Tesla Model S
This car has a 75-kWh battery.
This car can go about 260 miles on a full charge.
9.3.5 模拟实物
模拟较复杂的物件(如电动汽车)时,需要解决一些有趣的问题。续航里程是电瓶的属性还是汽车的属性呢?如果只描述一辆汽车,将方法
get_range() 放在Battery 类中也许是合适的,但如果要描述一家汽车制造商的整个产品线,也许应该将方法
get_range()
移到
ElectricCar 类中。在这种情况下,get_range()依然根据电瓶容量来确定续航里程,但报告的是一款汽车的续航里程。也可以这样做:仍将方法
get_range()
留在
Battery 类中,但向它传递一个参数,如car_model
。在这种情况下,方法
get_range() 将根据电瓶容量和汽车型号报告续航里程。
这让你进入了程序员的另一个境界:解决上述问题时,从较高的逻辑层面(而不是语法层面)考虑;考虑的不是Python,而是如何使用代码来表示实物。达到这种境界后,你会经常发现,对现实世界的建模方法没有对错之分。有些方法的效率更高,但要找出效率最高的表示法,需要经过一定的实践。只要代码像你希望的那样运行,就说明你做得很好!即便发现自己不得不多次尝试使用不同的方法来重写类,也不必气馁。要编写出高效、准确的代码,都得经过这样的过程。
9.3.6 练习
练习9-6:冰激凌小店
冰激凌小店是一种特殊的餐馆。编写一个名为IceCreamStand 的类,让它继承为完成练习9-1或练习9-4而编写的Restaurant
类。这两个版本的
Restaurant 类都可以,挑选你更喜欢的那个即可。添加一个名为
flavors 的属性,用于存储一个由各种口味的冰激凌组成的列表。编写一个显示这些冰激凌的方法。创建一个
IceCreamStand 实例,并调用这个方法。
class Restaurant:
-- snip --
class IceCreamStand(Restaurant):
"""初始化父类"""
def __init__(self, restaurant_name, cuisine_type):
"""初始化子类"""
super().__init__(restaurant_name, cuisine_type)
self.flavors = ['xiangcao', 'mo cha', 'choco']
def ice_show(self):
for self.flavor in self.flavors:
print(f"{self.flavor}")
his_restaurant = IceCreamStand('huazhong fandian', 'xiangcai')
his_restaurant.ice_show()
练习9-7:管理员
管理员是一种特殊的用户。编写一个名为
Admin 的类,让它继承为完成练习9-3或练习9-5而编写的
User 类。添加一个名为privileges
的属性,用于存储一个由字符串(如
"can add post"
、
"can
delete post"
、
"can ban user" 等)组成的列表。编写一个名为show_privileges()
的方法,显示管理员的权限。创建一个
Admin 实例,并调用这个方法。
--snip--
class Admin:
def __init__(self, *privileges):
self.privileges = privileges
def show_privileges(self):
for self.privilege in self.privileges:
print(f"{self.privilege}")
first_admin = Admin('can add post', 'can delete post', 'can ban user')
first_admin.show_privileges()
练习9-8:权限
编写一个名为
Privileges 的类,它只有一个属性privileges ,其中存储了练习9-7所述的字符串列表。将方法show_privileges()
移到这个类中。在
Admin
类中,将一个Privileges实例用作其属性。创建一个
Admin
实例,并使用方法show_privileges()来显示其权限。
class Admin:
def __init__(self):
self.privileges = Privileges()
class Privileges:
def __init__(self, privileges=['can add post', 'can delete post', 'can ban user']):
self.privileges = privileges
def show_privileges(self):
for self.privilege in self.privileges:
print(f"{self.privilege}")
first_admin = Admin()
first_admin.privileges.show_privileges()
练习9-9:电瓶升级
在本节最后一个electric_car.py版本中,给Battery类添加一个名为upgrade_battery() 的方法。该方法检查电瓶容量,如果不是100,就将其设置为100。创建一辆电瓶容量为默认值的电动汽车,调用方法get_range()
,然后对电瓶进行升级,并再次调用
get_range() 。你将看到这辆汽车的续航里程增加了。
--snip--
class Battery:
--snip--
def upgrade_battery(self):
if self.battery_size != 100:
self.battery_size = 100
my_tesla = ElectricCar('tesla', 'model s', 2019)
my_tesla.battery.get_range()
my_tesla.battery.upgrade_battery()
my_tesla.battery.get_range()
9.4 导入类
随着不断给类添加功能,文件可能变得很长,即便妥善地使用了继承亦如此。为遵循Python的总体理念,应让文件尽可能整洁。Python在这方面提供了帮助,允许将类存储在模块中,然后在主程序中导入所需的模块。
9.4.1 导入单个类
下面来创建一个只包含Car 类的模块。这让我们面临一个微妙的命名问题:在本章中已经有一个名为car.py的文件,但这个模块也应命名为car.py,因为它包含表示汽车的代码。我们将这样解决这个命名问题:将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', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
import
语句让Python打开模块
car
并导入其中的
Car 类。这样,我们就可以使用
Car 类,就像它是在这个文件中定义的一样。输出与我们在前面看到的一样:
2019 Audi A4
This car has 23 miles on it.
导入类是一种有效的编程方式。如果这个程序包含整个Class 类,它该有多长啊!通过将这个类移到一个模块中并导入该模块,依然可以使用其所有功能,但主程序文件变得整洁而易于阅读了。这还让你能够将大部分逻辑存储在独立的文件中。确定类像你希望的那样工作后,就可以不管这些文件,而专注于主程序的高级逻辑了。
9.4.2 在一个模块中存储多个类
虽然同一个模块中的类之间应存在某种相关性,但可根据需要在一个模块中存储任意数量的类。
Battery
类和
ElectricCar 类都可帮助模拟汽车,下面将它们都加入模块car.py中:
# ❶
"""一个可用于表示汽车的类。"""
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 Battery:
"""一次模拟电动汽车电瓶的简单尝试。"""
def __init__(self, battery_size=75):
"""初始化电瓶的属性。"""
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 == 75:
range = 260
elif self.battery_size == 100:
range = 315
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_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()
输出与我们在前面看到的相同,但大部分逻辑隐藏在一个模块中:
2019 Tesla Model S
This car has a 75-kWh battery.
This car can go about 260 miles on a full charge.
9.4.3 从一个模块中导入多个类
可根据需要在程序文件中导入任意数量的类。如果要在同一个程序中创建普通汽车和电动汽车,就需要将
Car
类和
ElectricCar
类都导入:
from car import Car, ElectricCar
my_beetle = Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())
my_tesla = ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())
在本例中,
创建了一辆大众甲壳虫普通汽车,并
创建了一辆特斯拉Roadster电动汽车:
2019 Volkswagen Beetle
2019 Tesla Roadster
9.4.4 导入整个模块
下面的代码导入整个
car
模块,并创建一辆普通汽车和一辆电动汽车:
import car
my_beetle = car.Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())
my_tesla = car.ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())
导入了整个
car
模块。接下来,使用语法
module_name.ClassName
访问需要的类。像前面一样,
创建一辆大众甲壳虫汽车,并创建一辆特斯拉Roadster汽车。
9.4.5 导入模块中的所有类
要导入模块中的每个类,可使用下面的语法:
from module_name import *
不推荐使用这种导入方式,原因有二。第一,如果只看文件开头的import 语句,就能清楚地知道程序使用了哪些类,将大有裨益。然而这种导入方式没有明确地指出使用了模块中的哪些类。第二,这种方式还可能引发名称方面的迷惑。如果不小心导入了一个与程序文件中其他东西同名的类,将引发难以诊断的错误。这里之所以介绍这种导入方式,是因为虽然不推荐使用,但你可能在别人编写的代码中见到它。
需要从一个模块中导入很多类时,最好导入整个模块,并使用module_name.ClassName 语法来访问类。这样做时,虽然文件开头并没有列出用到的所有类,但你清楚地知道在程序的哪些地方使用了导入的模块。这也避免了导入模块中的每个类可能引发的名称冲突。
9.4.6 在一个模块中导入另一个模块
下面将
Car
类存储在一个模块中,并将
ElectricCar
类和Battery 类存储在另一个模块中。将第二个模块命名为electric_car.py(这将覆盖前面创建的文件electric_car.py),并将
Battery
类和
ElectricCar
类复制到这个模块中:
"""一组可用于表示电动汽车的类。"""
from car import Car
class Battery:
--snip--
class ElectricCar(Car):
--snip--
ElectricCar
类需要访问其父类
Car
,因此在
直接将
Car 类导入该模块中。如果忘记了这行代码,Python将在我们试图创建
ElectricCar 实例时引发错误。还需要更新模块
car
,使其只包含
Car
类:
"""一个可用于表示汽车的类。"""
class Car:
--snip--
现在可以分别从每个模块中导入类,以根据需要创建任何类型的汽车了:
from car import Car
from electric_car import ElectricCar
my_beetle = Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())
my_tesla = ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())
从模块
car
中导入了
Car
类,并从模块
electric_car 中导入ElectricCar 类。接下来,创建了一辆普通汽车和一辆电动汽车。这两种汽车都被正确地创建出来了:
2019 Volkswagen Beetle
2019 Tesla Roadster
9.4.7 使用别名
第8章说过,使用模块来组织项目代码时,别名大有裨益。导入类时,也可为其指定别名。
例如,要在程序中创建大量电动汽车实例,需要反复输入
ElectricCar ,非常烦琐。为避免这种烦恼,可在
import
语句中给
ElectricCar
指定一个别名:
from electric_car import ElectricCar as EC
现在每当需要创建电动汽车实例时,都可使用这个别名:
my_tesla = EC('tesla', 'roadster', 2019)
9.4.8 自定义工作流程
如你所见,在组织大型项目的代码方面,Python提供了很多选项。熟悉所有这些选项很重要,这样你才能确定哪种项目组织方式是最佳的,并能理解别人开发的项目。
一开始应让代码结构尽可能简单。先尽可能在一个文件中完成所有的工作,确定一切都能正确运行后,再将类移到独立的模块中。如果你喜欢模块和文件的交互方式,可在项目开始时就尝试将类存储到模块中。先找出让你能够编写出可行代码的方式,再尝试改进代码。
9.4.9 练习
练习9-10:导入Restaurant 类
将最新的
Restaurant 类存储在一个模块中。在另一个文件中,导入
Restaurant
类,创建一个
Restaurant 实例并调用
Restaurant
的一个方法,以确认
import
语句正确无误。
from practice9_1 import Restaurant as Re
my_res = Re('nanjing fandian', 'sucai')
my_res.describe_restaurant()
练习9-11:导入Admin 类
以为完成练习9-8而做的工作为基础。将User类、
Privileges
类和
Admin 类存储在一个模块中,再创建一个文件,在其中创建一个
Admin
实例并对其调用方法
show_privileges() ,以确认一切都能正确运行。
import practice9_3 as pc
Admin = pc.Admin()
Admin.privileges.show_privileges()
练习9-12:多个模块
将
User
类存储在一个模块中,并将
Privileges 类和
Admin
类存储在另一个模块中。再创建一个文件,在其中创建一个Admin实例并对其调用方法
show_privileges()
,以确认一切依然能够正确运行。
import practice9_3_1 as pr
Admin = pr.Admin()
Admin.privileges.show_privileges()
9.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 ,但该模块可以很好地用于创建众多有趣的项目。
注意 还可以从其他地方下载外部模块。第二部分的每个项目都需要使用外部模块,届时你将看到很多此类示例。
9.5.1 练习
练习9-13:骰子
创建一个10面的骰子和一个20面的骰子,再分别掷10次。
from random import randint
class Die:
def __init__(self, sides=6):
self.sides = sides
def roll_die(self):
self.sides = randint(1, 10)
print(f"{self.sides}")
f_die = Die(10)
x = 1
while True:
if x < 11:
f_die.roll_die()
x += 1
else:
break
练习9-14:彩票
创建一个列表或元组,其中包含10个数和5个字母。从这个列表或元组中随机选择4个数或字母,并打印一条消息,指出只要彩票上是这4个数或字母,就中大奖了。
from secrets import choice
numbers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 'a', 'b', 'c', 'e', 'd')
a = choice(numbers)
b = choice(numbers)
c = choice(numbers)
d = choice(numbers)
goal = f"{a}{b}{c}{d}"
print(f"if you have {a} {b} {c} {d} you got prize")
练习9-15:彩票分析
可以使用一个循环来明白前述彩票大奖有多难中奖。为此,创建一个名为my_ticket 的列表或元组,再编写一个循环,不断地随机选择数或字母,直到中大奖为止。请打印一条消息,报告执行循环多少次才中了大奖。
from secrets import choice
# 创建列表包含数字和字母
numbers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 'a', 'b', 'c', 'e', 'd')
a = choice(numbers)
b = choice(numbers)
c = choice(numbers)
d = choice(numbers)
# 将目标数字组合起来并赋值给goal
goal = f"{a}{b}{c}{d}"
print(f"if you have {a} {b} {c} {d} you got prize")
x = 0
# 编写循环当goal_two与goal相等时退出循环
while True:
e = choice(numbers)
f = choice(numbers)
g = choice(numbers)
h = choice(numbers)
goal_two = f"{e}{f}{g}{h}"
if goal_two == goal:
print("Congratulations!")
print(x)
break
else:
x += 1
练习9-16:Python Module of the Week
要了解Python标准库,一个很不错的资源是网站Python Module of the Week。请访问该网站并查看其中的目录,找一个你感兴趣的模块进行探索。从模块
random
开始可能是个不错的选择。
可以访问python标准库:
9.6 类编码风格
你必须熟悉有些与类相关的编码风格问题,在编写的程序较复杂时尤其如此。
类名应采用
驼峰命名法 ,即:
1.
类
名中的
每个单词的首字母都大写
,而
不使用下划线
。
2.对于每个类,都应紧跟在
类定义后面
包含一个
文档字符串
。这种文档字符串简要地
描述类的功能
,并遵循编写函数的文档字符串时采用的格式约定。每个模块也都应包含一个文档字符串,对其中的类可用于做什么进行描述。
3.可使用空行来组织代码,但不要滥用。
而在模块中,可使用
两个空行来分隔类
。
在包含多条
import 语句的程序中,这种做法让人更容易明白程序使用的各个模块都来自何处。
原文地址:https://www.jb51.cc/wenti/3280213.html
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。