面向对象编程是最有效的软件编写方法之一。
在面向对象编程中,你编写表示现实世界中的事物和情景的类,并基于这些类来创建对象。
编写类时,你定义一大类对象都有的通用行为。基于类创建对象时,每个对象都自动具备这种通用行为,然后可根据需要赋予每个对象独特的个性。使用面向对象编程可模拟现实情景,其逼真程度达到了令你惊讶的地步。
根据类来创建对象被称为实例化,这让你能够使用类的实例。在本章中,你将编写一些类并创建其实例。你将指定可在实例中存储什么信息,定义可对这些实例执行哪些操作。你还将编写一些类来扩展既有类的功能,让相似的类能够高效地共享代码。你将把自己编写的类存储在模块中,并在自己的程序文件中导入其他程序员编写的类。
理解面向对象编程有助于你像程序员那样看世界,还可以帮助你真正明白自己编写的代码:不仅是各行代码的作用,还有代码背后更宏大的概念。了解类背后的概念可培养逻辑思维,让你能够通过编写程序来解决遇到的几乎任何问题。
随着面临的挑战日益严峻,类还能让你以及与你合作的其他程序员的生活更轻松。如果你与其他程序员基于同样的逻辑来编写代码,你们就能明白对方所做的工作;你编写的程序将能被众多合作者所理解,每个人都能事半功倍。
本章内容安排如下思维导图,大家可以先简单看图了解框架,再进入每一部分的学习。
创建和使用类
01
可将类视为有关如何创建实例的说明。Dog 类是一系列说明,让Python知道如何创建表示特定小狗的实例。
下面来创建一个表示特定小狗的实例:
class Dog(): """一次模拟小狗的简单尝试""" def __init__(self,name,age): """初始化属性name和age""" self.name = name self.age = age def sit(self): """模拟小狗被命令时蹲下""" print(self.name.title() + " is now sitting!") def roll_over(self): """模拟小狗被命令时打滚""" print(self.name.title() + " rolled over!") my_dog = Dog('willie',6)
print("My dog's name is " + my_dog.name.title() + '.')print("My dog is " + str(my_dog.age) + " years old.")
这里使用的是前一个示例中编写的Dog 类。我们让Python创建一条名字为'willie' 、年龄为6 的小狗。遇到这行代码时,Python使用实参'willie' 和6 调用Dog 类中的方法__init__() 。方法__init__() 创建一个表示特定小狗的示例,并使用我们提供的值来设置属性name 和age 。方法__init__() 并未显式地包含return 语句, 但Python自动返回一个表示这条小狗的实例。我们将这个实例存储在变量my_dog 中。在这里,命名约定很有用:通常认为首字母大写的名称(如Dog )指的是类,而小写的名称(如my_dog )指的是根据类创建的实例。
访问属性
要访问实例的属性,可使用句点表示法。在❷处,我们编写了如下代码来访问my_dog 的属性name 的值:
my_dog.name
句点表示法在Python中很常用,这种语法演示了Python如何获悉属性的值。在这里,Python先找到实例my_dog ,再查找与这个实例相关联的属性name 。在Dog 类中引用这个属性时,使用的是self.name 。我们使用同样的方法来获取属性age 的值。在前面的第1条print 语句中,my_dog.name.title() 将my_dog 的属性name 的值'willie' 改为首字母大写的;在第2条print 语句中,str(my_dog.age) 将my_dog 的属性age 的值6 转换为字符串。
输出是有关my_dog 的摘要:
My dog's name is Willie.My dog is 6 years old.
调用方法
根据Dog 类创建实例后,就可以使用句点表示法来调用Dog 类中定义的任何方法。下面来让小狗蹲下和打滚:
class Dog(): """一次模拟小狗的简单尝试""" def __init__(self,name,age): """初始化属性name和age""" self.name = name self.age = age def sit(self): """模拟小狗被命令时蹲下""" print(self.name.title() + " is now sitting!") def roll_over(self): """模拟小狗被命令时打滚""" print(self.name.title() + " rolled over!") 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 is now sitting.Willie rolled over!
如果给属性和方法指定了合适的描述性名称,如name 、age 、sit() 和roll_over() ,即便是从未见过的代码块,我们也能够轻松地推断出它是做什么的。
创建多个实例
可按需求根据类创建任意数量的实例。下面再创建一个名为your_dog 的实例:
class Dog(): """一次模拟小狗的简单尝试""" def __init__(self,name,age): """初始化属性name和age""" self.name = name self.age = age def sit(self): """模拟小狗被命令时蹲下""" print(self.name.title() + " is now sitting!") def roll_over(self): """模拟小狗被命令时打滚""" print(self.name.title() + " rolled over!")
my_dog = Dog('willie',6)your_dog = Dog('Lucy',3)
print("My dog's name is " + my_dog.name.title() + '.')print("My dog is " + str(my_dog.age) + " years old.")my_dog.sit()
print("\nYour dog's name is " + your_dog.name.title() + '.')print("Your dog is " + str(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!
就算我们给第二条小狗指定同样的名字和年龄,Python依然会根据Dog 类创建另一个实例。你可按需求根据一个类创建任意数量的实例,条件是将每个实例都存储在不同的变量中,或占用列表或字典的不同位置。
使用类和实例
02
类中的每个属性都必须有初始值,哪怕这个值是0或空字符串。在有些情况下,如设置默认值时,在方法__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 = str(self.year) + ' ' + self.make + ' ' +self.model return long_name.title() def read_odometer(self): """打印一条指出汽车里程的消息""" print("This car has " + str(self.odometer_reading) + " miles on it")my_new_car = Car('audi','a4',2016)print(my_new_car.get_descriptive_name())my_new_car.read_odometer()
现在,当Python调用方法__init__() 来创建新实例时,将像前一个示例一样以属性的方式存储制造商、型号和生产年份。接下来,Python将创建一个名为odometer_reading 的属性,并将其初始值设置为0。我们还定义了一个名为read_odometer() 的方法,它让你能够轻松地获悉汽车的里程。
一开始汽车的里程为0:
2016 Audi A4This car has 0 miles on it
出售时里程表读数为0的汽车并不多,因此我们需要一个修改该属性的值的途径。
修改属性的值
3
可以以三种不同的方式修改属性的值:直接通过实例进行修改;通过方法进行设置;通过方法进行递增(增加特定的值)。下面依次介绍这些方法。
直接修改属性的值
要修改属性的值,最简单的方式是通过实例直接访问它。下面的代码直接将里程表读数设置为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 = str(self.year) + ' ' + self.make + ' ' +self.model return long_name.title() def read_odometer(self): """打印一条指出汽车里程的消息""" print("This car has " + str(self.odometer_reading) + " miles on it")
my_new_car = Car('audi','a4',2016)print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23my_new_car.read_odometer()
我们使用句点表示法来直接访问并设置汽车的属性odometer_reading 。这行代码让Python在实例my_new_car 中找到属性odometer_reading ,并将该属性的值设置为23:
2016 Audi A4This car has 23 miles on it
有时候需要像这样直接访问属性,但其他时候需要编写对属性进行更新的方法。
通过方法修改属性的值
如果有替你更新属性的方法,将大有裨益。这样,你就无需直接访问属性,而可将值传递给一个方法,由它在内部进行更新。
下面的示例演示了一个名为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 = str(self.year) + ' ' + self.make + ' ' +self.model return long_name.title() def read_odometer(self): """打印一条指出汽车里程的消息""" print("This car has " + str(self.odometer_reading) + " miles on it") def update_odometer(self,mileage): """将里程表读书设置为指定的值""" self.odometer_reading = mileage my_new_car = Car('audi','a4',2016)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() 打印该读数:
2016 Audi A4This car has 23 miles on it
可对方法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 = str(self.year) + ' ' + self.make + ' ' +self.model return long_name.title() def read_odometer(self): """打印一条指出汽车里程的消息""" print("This car has " + str(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!")
现在,update_odometer() 在修改属性前检查指定的读数是否合理。如果新指定的里程(mileage )大于或等于原来的里程(self.odometer_reading ),就将里程表读数改为新指定的里程;否则就发出警告,指出不能将里程表往回拨。
通过方法对属性的值进行递增
有时候需要将属性值递增特定的量,而不是将其设置为全新的值。假设我们购买了一辆二手车,且从购买到登记期间增加了100英里的里程,下面的方法让我们能够传递这个增量,并相应地增加里程表读数:
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 = str(self.year) + ' ' + self.make + ' ' +self.model return long_name.title() def read_odometer(self): """打印一条指出汽车里程的消息""" print("This car has " + str(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_used_car = Car('subaru', 'outback', 2013)print(my_used_car.get_descriptive_name())
my_used_car.update_odometer(23500)my_used_car.read_odometer()
my_used_car.increment_odometer(100)my_used_car.read_odometer()
新增的方法increment_odometer() 接受一个单位为英里的数字,并将其加入到self.odometer_reading 中。
创建了一辆二手车——my_used_car 。
调用方法update_odometer() 并传入23500 ,将这辆二手车的里程表读数设置为23 500。
调用increment_odometer() 并传入100 ,以增加从购买到登记期间行驶的100英里:
2013 Subaru OutbackThis car has 23500 miles on itThis car has 23600 miles on it
你可以轻松地修改这个方法,以禁止增量为负值,从而防止有人利用它来回拨里程表。
注意 :可以使用类似于上面的方法来控制用户修改属性值(如里程表读数)的方式,但能够访问程序的人都可以通过直接访问属性来将里程表修改为任何值。要确保安全,除了进行类似于前面的基本检查外,还需特别注意细节。