十八、类
类
面向对象编程是最有效的软件编写方法之一
在面向对象编程中,编写表示现实世界中的事物和情景的类,并基于这些类来创建对象,根据类来创建对象被称为实例化,这让你能够使用类的实例;
理解面向对象编程有助于像程序员一样看世界,还可以帮助自己明白编写的代码,使你与其他程序员合作更轻松。
1.1、创建和使用类
使用类几乎可以模拟任何东西,类名必须大写,一个类由方法和属性组成,类中的函数称为方法,可以通过实例访问的变量称为属性
1.1.1、创建类
下面将创建一个dog 类,这个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() + ' roll over!')
上面列子中,init()方法,是一种特殊方法,包含了三个形参,self、name和age,self必不可少,而且必须在气体形参之前,python调用这个方法时,将自动传入实参self,每个与类关联的方法调用都自动传递实参self,它是一个指向实例本身的引用,让实例能访问类中的属性和方法,调用Dog类的init方法时,通过实参向Dog()传递名字和年龄,self自动传递,不需要传递,因此只需给name和age提供值。
由一个类可以生成无数个对象,当一个对象的方法被调用的时候,对象会将自身的引用作为第一个参数传递给该方法。
以self 为前缀的变量,可以供类中所有方法使用,我们可以通过类的任何实例来访问这些变量,它们是类的属性。
1.1.2、根据类创建实例
根据类可以创建无数个实例对象(instance object),也称为类的实例化:
下面来创建一个表示特定的狗的实例对象:
class Dog():
--snip--
my_dog = Dog('willie', 6) # 类的实例化(实例对象),小写
print("My dog's name is " + my_dog.name.title() + '.') # 访问类的属性name
print('My dog is ' + str(my_name.age) + ' years old.') # 访问类的属性age
------
My dog's name is willie.
My dog is 6 years old.
访问类的属性
要访问类的属性,可以使用句点表示法,如要访问name的值,可以使用(my_dog.name)的方法。
调用类的方法
与访问属性一样,调用方法也可以使用句点表示法,如要调用sit()方法,可以使用(my_dog.sit() )
class Dog():
--snip--
my_dog = Dog('willie', 6)
my_dog.sit()
my_dog.roll_over()
-------------
willie is now sitting
willie rolled over!
创建多个实例
根据类可以创建任意数量的实例,条件是将每个实例都存储到不同变量中,或占用列表或字典的不同位置,下面来创建一个名为your_dog 的实例:
class Dog():
--snip--
my_dog = Dog('willie', 6)
your_dog = Dog('lucy', 3)
your_dog.sit()
---------------
lucy is now sitting.
1.2、使用类和实例
类创建后,大部分时间都是根据类创建实例,需要执行的一个重要的任务是修改实例的属性,修改属性的值可以直接修改或者以特定方式修改:
1.2.1、给属性指定默认值
类中属性都必须有初始值,哪怕是0或空字符串,在有些情况下,如设置默认值时,在方法init () 内指定的这种初始值是可行的,如果你对某个属性这样做了,就无需包含为它提供初始值的形参。
class Car():
def __init__(self, make, model, year):
"""初始化描述汽车的属性"""
self.make = make
self.model = model
self.year = year
def get_descriptive_name(self):
"""返回整车的描述信息"""
long_name = str(self.year) + ' ' + self.make + ' ' + self.modle
return long_name.title()
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
------
2016 Audi A4
接下来添加一个名为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 # 添加一个能存储汽车里程信息的属性,其属性默认值为0
def get_descriptive_name(self):
"""返回整车的描述信息"""
long_name = str(self.year) + ' ' + self.make + ' ' + self.modle
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() # 调用read_odometer() 方法,调用self.odometer_reading 属性的值
------
2016 Audi A4
This car has 0 miles on it.
1.2.2、修改属性的值
修改属性的值,有如下三种方法:
- 直接通过实例进行修改
- 通过方法进行设置
- 通过方法进行递增(增加特定值)
直接修改
将里程数(odometer_reading)修改为23,直接通过实例调用属性,修改其值:
class Car():
--snip--
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23 # 通过实例调用属性,访问并修改其值
my_new_car.read_odometer() # 调用方法打印里程信息
------
2016 Audi A4
This car has 23 miles on it.
通过方法修改属性的值
如果有替你更新属性的方法,就无需直接访问属性,而是将值传递给一个方法,由它在内部进行更新。
下面添加一个名为update_odometer() 的方法,其中有一个形参,用于接收里程数:
class Car():
--snip--
def update_odometer(self, mileage): # 定义一个方法,用于内部更新属性odometer_reading 的值,其中一个形参用于接收里程数,并将其指定为属性odometer_reading
"""将里程数读数设置为指定值"""
"""禁止里程往回拨"""
if mileage >= odometer_reading: # 修改属性前,检查指定的数是否合理
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.update_odometer(32) # 调用方法,并传递值32给形参mileage
my_new_car.read_odometer()
------------
2016 Audi A4
This car has 32 miles on it.
通过方法对属性的值进行递增
有时需要将属性值递增特定的值,而不是设置为全新的值,比如买了一辆二手车,从购买到登记期间增加了100英里的里程:
class Car():
--snip--
def update_odometer(self, mileage):
--snip--
def increment_odometer(self, miles): # 新增方法,接受一个单位为英里的数字,并将其存储到属性self.odometer_reading中
"""将里程表读数增加特定的量"""
self.odometer_reading += miles
my_used_car = Car('subaru', 'outback', 2013)
print(my_used_car.get_rescriptive_name())
my_used_car.update_odometer(23500)
my_used_car.read_odometer()
my_used_car.increment_odometer(100)
my_used_car.read_odometer()
------
2013 Subaru Outback
This car has 23500 miles on it.
This car has 23600 miles on it.
练习
添加一个属性number_served,默认值为0,创建一个名为restaurant 的实例,打印这家餐馆有多少人在这家餐馆就餐,然后修改这个值并打印它,添加一个名为set_number_served() 的方法,它能设置就餐人数,调用它传递一个值,再打印这个值,添加一个名为increment_number_served() 的方法,它让你能够将就餐人数递增,调用它并传递一个这样的值:你认为这家餐馆每天可能接待的就餐人数:
class Restaurant():
"""创建一个Restaurant的类,里面包含餐厅名字,菜品类型"""
def __init__(self, restaurant_name, cuisine_type):
self.restaurant_name = restaurant_name.title()
self.cuisine_type = cuisine_type
self.number_served = 0
def describe(self):
"""显示餐馆的基本信息"""
msg = self.restaurant_name + ' serves wonderful ' + self.cuisine_type + '.'
print(msg)
def open_restaurant(self):
"""显示餐馆正在营业"""
msg = self.restaurant_name + ' is open. Come on in!'
print(msg + '\n')
def set_number_served(self, number_served):
"""显示就餐人数"""
self.number_served = number_served
def increment_nums(self, additional_served):
"""人数递增"""
self.number_served += additional_served
restaurant = Restaurant("alice's home", 'pizza')
restaurant.describe()
restaurant.open_restaurant()
restaurant.set_number_served(16)
print('The current number of repast is ' + str(restaurant.number_served))
restaurant.increment_nums(10)
print('The current number of repast is ' + str(restaurant.number_served))
------------------
Alice'S Home serves wonderful pizza.
Alice'S Home is open. Come on in!
The current number of repast is 16
The current number of repast is 26
1.3、继承
编写类时,不是都是从空白开始,有时要编写的类是另一个现成的类的特殊版本,可使用继承。一个类继承另一个类时,它将自动获得另一个类的所有属性和方法,被继承的类称为基类或父类,继承的类称为子类,子类也可以定义自己的属性和方法。
下面我们来创建一个名为 ElectricCar()的子类,继承于父类Car(),用来描述电动汽车的相关信息,它具备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_time = str(self.year) + ' ' + self.make + ' ' + self.model
return long_time.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
def increment_odometer(self, miles):
self.odometer_reading += miles
class ElectricCar(Car): # 子类,定义子类时,括号内必须指定父类名称
"""电动车的独特之处"""
def __init__(self, make, model, year): # 方法__init__()接受创建父类实例所需信息
"""初始化父类的属性"""
super().__init__(make, model, year) # super()是一个特殊函数,可以将父类与子类关联起来,它可以让python调用父类的方法__init__(),让子类实例包含父类的所有属性,父类也称为超类(superclass)
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
------------------------
2016 Tesla Model S
上面例子,我们首先定一个名为ElectricCar的子类,它继承父类Car,创建子类时,父类必须在子类之前,定义子类时括号内必须指定父类名称,方法init() 接受Car实例所需的信息(初始化父类的所有属性和方法)。
super()函数是一个特殊函数,帮助python将父类与子类关联起来,自动找到父类的方法,不需要给出父类的名字,这行代码让python调用父类的方法init(),让子类实例包含父类的所有属性。
为子类创建一个实例对象my_tesla,传入实参,它将调用子类的init() 方法,子类的init() 方法将调用父类的init() 方法。
1.3.1、给子类定义属性和方法
继承后,可以添加区分子类和父类所需的新属性和方法,需要注意的是,子类可以添加任意数量的属性和方法,但只适用子类,如果要适用子类和父类,那么应该添加到父类中。
下面为子类ElectricCar 添加一个电动汽车特有的属性(电瓶),以及一个描述该属性的方法,存储电瓶的容量,并编写一个打印电瓶描述的方法:
class Car():
--snip--
class ElectricCar(Car):
""""""
def __init__(self, make, model, year):
""""""
super().__init__(make, model, year)
self.battery_size = 70 # 添加新属性存储电瓶容量,初始值为70
def describe_battery(self): # 定一个方法,用于描述打印这辆车的电瓶容量信息
print('This car has a ' + str(self.battery_size) + '-kwh battery.')
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery() # 调用方法describe_battery()
------------------------
Tesla Model S 2016
This car has a 70-kwh battery.
1.3.2、重写父类的方法
如果父类的方法不能满足子类的需求,可以对其进行重写,为此可以在子类中定一个方法,与要重写的父类方法同名,这样python 就不会考虑这个父类的方法,而只关注这个子类中定义的相应方法。
假设Car类有一个名为fill_gas_tank()的方法,它对电动汽车来说没有意义,因此你可能需要重写它:
class Car():
--snip--
def fill_gas_tank(self):
print('This car has a gas tank.')
class ElectricCar(Car):
--snip--
def fill_gas_tank(self): # 重写父类方法fill_gas_tank(),因为父类的方法不能满足子类
"""电动车没有油箱"""
print("This car doesn't need a gas tank!")
my_tesla = ElectricCar('tesla', 'model s' 2016)
my_new_car = Car('audi', 'a4', 2016)
my_tesla.fill_gas_tank() # 父类的方法一旦被重写,父类的实例调用被重写的方法也会被忽略
my_new_car.fill_gas_tank()
------------------
This car doesn't need a gas tank!
This car doesn't need a gas tank!
1.3.3、将实例用作属性
使用代码模拟实物时,你可能会给类添加越来越多的细节,属性和方法以及文件越来越长,这种情况下可能需要将类的一部分作为一个独立的类提取出来,拆分成多个协同工作的小类。
给ElectricCar 类添加细节,可能会包含很多专门对汽车电瓶的属性和方法,可以将这些属性和方法提取出来,放到另外一个名为Battery的类中,并将Battery实例用作ElectricCar类的一个属性:
class Car():
--snip--
class Battery(): # 定义一个新类,用于存储ElectricCar的属性和方法
"""一次模拟电动汽车电瓶的简单尝试"""
def __init__(self, battery_size=70): # 设置电瓶容量初始值
"""初始化电瓶的属性"""
self.battery_size = battery_size
def describe_battery(self):
"""打印一条描述电瓶容量的消息"""
print('This car has a ' + str(self.battery_size) + '-kwh battery.')
class ElectricCar(Car):
"""电动车的独特之处"""
def __init__(self, make, model, year):
"""初始化父类的属性,再初始化电动车的特有属性"""
super().__init__(make, model, year)
self.battery = Battery() # 这行代码让python创建了一个新的Battery实例(由于没有指定参数,默认值为70),并将实例存储在属性self.battert中,每当__init__()被调用时,都将执行该操作
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery() # 调用时采用ElectricCar的实例加Battery实例的方式调用Battery中的方法
-------------------
2016 Tesla Model S
This car has a 70-kwh battery.
把子类拆分成很多小类去协同处理,看似多了很多步骤,但是可以避免子类混乱不堪,现在我们可以去拓展电动汽车的其他信息,比如描述电瓶容量的续航里程,在Battery中添加一个名为get_range ()的方法,用于描述续航里程:
class Car():
--snip--
class Battery():
--snip--
def get_range(self): # 定义一个方法,用于描述打印电瓶续航里程信息
if self.battery_size == 70:
range = 240
elif self.battery_size == 85:
range = 270
msg = 'This car can go approximately ' + str(range)
msg += ' miles on a full charge.'
print(msg)
class ElectricCar(Car):
--snip--
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range() # 调用get_battery() 方法
----------------
2016 Tesla Model S
This car has a 70-kwh battery.
This car can go approximately 240 miles on a full charge.
1.4、导入类
随着我们给类不断添加功能,代码、文件越来越长,为遵循python的总体理念,应让文件尽可能简洁,为此我们可以将类存储到模块中,然后在主程序中导入所需的模块即可。
1.4.1、导入单个类
上面我们举例写了一个Car类,现在我们把它放到一个名为car.py的文件中(模块),再新建一个my_car.py 的文件,我们在my_car.py 中导入Car 类试试:
# 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 = 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 not roll back an odometer!')
def increment_odometer(self, miles):
"""将里程表读数增加指定量"""
self.odometer_reading += miles
class Battery():
"""一次模拟电动汽车电瓶的简单尝试"""
def __init__(self, battery_size=70):
"""初始化电瓶的属性"""
self.battery_size = battery_size
def describe_battery(self):
"""打印一条描述电瓶容量的信息"""
print('This car has a ' + str(self.battery_size) + '-kwh battery.')
def get_range(self):
"""打印一条消息,指出电瓶的续航里程"""
if self.battery_size == 70:
range_0 = 240
elif self.battery_size == 85:
range_0 = 270
msg = 'This car can go approximately ' + str(range_0)
msg += ' miles on a full charge.'
print(msg)
class ElectricCar(Car):
"""电动车的独特之处"""
def __init__(self, make, model, year):
"""初始化父类的属性,再初始化子类的属性"""
super().__init__(make, model, year)
self.battery = Battery()
# my_car.py
from car import Car # 从模块car.py中导入Car 类
my_new_car = Car('audi', 'a4', 2016) # Car 类实例化,传入参数
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
--------------------
2016 Audi A4
This car has 23 miles on it.
1.4.2、从一个模块中导入多个类
一个模块可以存储多个类,我们可以一次导入一个或者多个类到主程序中,
导入多个类时,类之间用逗号分隔即可,导入后即可根据类创建任意数量的实例。
如果我们要在同一个程序中创建普通汽车和电动汽车,就需要将Car 和 ElectricCar 类都导入:
# my_car.py
from car import Car, ElectricCar # 从模块car.py中导入Car和ElectricCar 类
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
my_tesla = ElectricCar('tesla', 'model s', 2016)
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()
1.4.3、导入整个模块
也可以导入整个模块,再使用句点表示法访问所需要的类,这种方法,代码易读,也不会与当前文件使用的任何名称发生冲突:
# my_car.py
import car
my_new_car = car.Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
------------------
2016 Audi A4
1.4.4、导入模块中所有类
# 语法
from module_name import *
不推荐使用这种方法导入模块中所有类,这样容易导致名称方面的困惑,引发难以诊断的错误,应使用导入模块名,再采用句点表示法的方法访问。
1.4.5、在一个模块中导入另一个模块
有时需要将类分散到多个模块中,以免模块太大,或在同一模块中存储不相关的类,将类存储到多个模块中时,你可能会发现一个模块中的类依赖与另一个模块中的类,这种情况下,可在前一个模块中导入必要的类。
下面,我们将Car 类存储到模块car.py 中,将Battery和 ElectricCar 类存储到模块 electric_car.py 中,在调用ElectricCar 类时需要依赖Car 类,因此我们可以在electric_car.py 中导入car.py。
# electric_car.py
"""一组可用于表示电动汽车的类"""
from car import Car # ElectricCar需要访问其父类,因此我们直接将Car类导入到该模块中
class Battery():
--snip--
class ElectricCar():
--snip--
# my_car.py # 在my_car模块中导入car和electric_car 模块
from car import Car
from electric_car import ElectricCar
1.5、Python标准库
Python标准库是一组模块,安装的python都包含它,现在对类有一定的了解,可以使用其他程序员编写好的模块,以及标准库中的任何函数和类,为此只需一句在程序中包含一条简单的imp 语句,下面来看模块collections中的一个类-----OrdereDict
OrdereDict 实例行为与字典几乎一致,区别在于它记录了添加键-值对的添加顺序,而字典不能。
# favorite_language.py
from collections import OrderedDict
favorite_languages = OrderedDict()
favorite_languages['jen'] = 'Python'
favorite_languages['sarah'] = 'c'
favorite_languages['edward'] = 'ruby'
favorite_languages['phil'] = 'java'
for name, languages in favorite_languages.items():
print(name.title() + "'s favorite language is " + languages.title() + '.')
----------------------------
Jen's favorite language is Python.
Sarah's favorite language is C.
Edward's favorite language is Ruby.
Phil's favorite language is Java.
1.6、类编码风格
与类有关的编码风格问题,在编写复杂程序更应遵循:
- 类名:驼峰式命名法(如ElectricCar),即首字母大写,而不是用下划线
- 实例名:小写格式,单词间使用下划线
- 每个类都应包含一个文档字符串,用于描述类的功能,每个模块也应有。
- 空行:可用来组织代码,但不应滥用,类中,一个空行分隔方法,模块中,两个空行分隔类。
- 需要同时导入标准库中的模块和你编写的模块时,先编写导入标准库的import 语句,再添加一个空行,然后在编写导入你自己编写的模块的import 语句,在包含多条import 语句的程序中,这种做法让人更容易明白程序使用的各个模块来自何方。
练习
模块random 包含以各种方式生成随机数的函数,其中randint()返回一个位于指定范围内的整数,创建一个Die 类,包含一个名为sides 的属性,默认值为6,编写一个名为 roll_die()的方法,它打印位于1和骰子面数之间的随机数,创建一个6 面、10面、20面的骰子,再分别掷 10 次:
from random import randint # 导入random 模块以及randint 函数
class Die():
def __init__(self, sides=6):
self.sides = sides
def roll_die(self):
return randit(1, self.sides) # 返回生成1 到self.sides 间的随机数,不传入参数就使用默认值 6
d_6 = Die() # Die 类实例化,6 面骰子抛掷10次
results = [] # 将结果存储到 results 列表中
for roll_num in range(10):
result = d_6.roll_die()
results.append(result)
print('10 rolls of a 6-sided die:')
print(results)
d_10 = Die(sides=10) # 10 面骰子
results = []
for roll_num in range(10):
result = d_10.roll_die()
results.append(result)
print('10 rolls of a 6-sided die:')
print(results)
d_20 = Die(sides=20) # 20 面骰子
results = []
for roll_num in range(10):
result = d_10.roll_die()
results.append(result)
print('10 rolls of a 6-sided die:')
print(results)
--------------------
10 rolls of a 6-sided die:
[4, 1, 1, 1, 5, 5, 2, 2, 3, 4]
10 rolls of a 6-sided die:
[5, 2, 2, 3, 9, 4, 2, 7, 10, 8]
10 rolls of a 6-sided die:
[1, 19, 5, 18, 19, 19, 8, 10, 8, 10]