一、类
class dog:
def __init__(self, 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} roll over!")
类中的函数称为方法,init()就是一个特殊的方法,每当创建新实例的时候,Python都会自动运行它,在这个方法的名称中,为了避免与普通方法名称发生冲突,约定开头和末尾各有两个下划线。否则当使用类来创建实例的时候,将不会自动调用这个方法,进而引发难以发现的错误。
init()包含三个形参:self、name、age。其中self必不可少,而且必须位于其他形参的前面。Python在调用这个方法来创建Dog实例的时候,将自动传入实参self,每个与实例相关联的方法都自动传递实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法,类似于C++中的this指针。调用__init__()的时候,我们只需要通过实参向Dog()传递名字和年龄。
- 根据类创建实例。
my_dog=Dog("Willie",6)
prprint(f"My dog's name is {my_dog.name}.")
print(f"My dog is {my_dog.age} years old.")
my_dog.sit()
my_dog.roll_over()
1、使用类和实例
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())
- 给属性指定默认值。添加属性odometer_reading,并指定默认值为0。
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()
- 修改属性的值。
1)直接通过实例进行修改。
my_new_car.odometer_reading=23
2)通过方法修改属性的值。在类中添加方法修改属性。
def update_odometer(self, mileage):
"""
将里程表读数设置为指定的值
禁止将里程表读书往回倒
"""
self.odometer_reading=mileage
if mileage >= self.odometer_reading:
self.odometer_reading=mileage
else:
print("You can't roll back an odometer!")
3)通过方法对属性的值进行递增。
def increment_odometer(self, mile):
"""
将里程表读书增加指定的量
"""
self.odometer_reading+=miles
Notice:可以使用类似于上面的方法来控制用户修改属性值(如里程表读数)的方式,但能过够访问程序的人都可以通过直接访问属性来将里程表修改为任何值。要确保安全,除了进行类似于前面的基本检查外,还需特别注意细节。
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):
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):
"""
将里程表读数设置为指定的值
禁止将里程表读书往回倒
"""
self.odometer_reading=mileage
if mileage >= self.odometer_reading:
self.odometer_reading=mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, mile):
"""
将里程表读书增加指定的量
"""
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())
super()是个特殊函数,让你能够调用父类的方法。父类也称为超类,此名称就由super而来。此时创建的一辆电动汽车可以调用父类中定义的各种方法。
- 给子类定义属性和方法。
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()
- 重写父类的方法。假设Car类有一个fill_gas_tank()的方法,但这对电动汽车来说毫无意义,因此要改写。此时有人对电动汽车调用方法fill_gas_tank(),子类重写的方法将会覆盖父类原有的方法。
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.")
def fill_gas_tank(self):
print("This car doesn't need a gas tank!")
my_tesla=ElectricCar('tesla','model s',2019)
my_tesla.fill_gas_tank()
- 将实例用作属性。
使用代码模拟实物时,可能会发现自己给类添加的细节越来越多:属性和方法清单以及文件都越来越长。在这种情况下,可能需要将类的一部分提取出来,作为一个独立的类。可以将大型类拆分成多个协同工作的小类。例如,此处将电池的属性和方法提取出来,放到一个名为Battery的类中,将Battery的实例作为ElectricCar的属性。
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_size=75
self.battery=Battery()
my_tesla=ElectricCar('tesla','model s',2019)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
再添加一个类,根据电瓶容量报告汽车的续航理程:
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 charges.")
my_tesla=ElectricCar('tesla','model s',2019)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()
- 导入类。
from car import Car
from car import ElectricCar
from car import Car, ElectricCar
import car
my_beetle=car.Car('volkswagen','beetle',2019)
my_tesla=ca.ElectricCar('tesla','roadster',2019)
此处的import语句让Python打开模块car并导入其中的Car类。可根据需要再一个模块中存储任意数量的类。也可以从一个模块中同时导入多个类。也可导入整个模块,再使用句点表示法访问需要的类。还可从一个模板中导入另一个模块。
- 使用别名。
from electric_car import ElectricCar as EC
一开始应让代码结构尽可能简单,先尽可能完成一个文件中所有的工作,确定一切都能正确运行后,再将类移到独立的模块中。
- 类编码风格。类名应采用驼峰命名法,即将类名中的每个单词的首字母都大写,而不适用下划线。实例名和模块名都采用小写格式,并在单词之间加上下划线。
对于每个类,都应紧跟类定义后面包含一个文档字符串,这种文档字符串简要地描述类的功能,并遵循编写函数地文档字符串时采用地格式约定。每个模块也都应包含一个文档字符串,对其中的类可用于做什么进行描述。
可使用空行来组织代码,但不要滥用。在类中,可使用一个空行来分隔方法;而在模块中可以使用两个空行来分隔类。
需要同时导入标准库中的模块和你自己编写的模块时,先编写导入标准库模块的import语句,再添加一个空行,然后编写导入你自己的模块的import语句。
二、文件和异常
1、从文件中读取数据
- 读取整个文件。
with open('pi_digits.txt') as file_object:
contents=file_object.read()
print(contents.rstrip())
首先要打开文件,然后才能访问它。关键字with在不需要访问文件后将其关闭。也可以调用close()来关闭文件,但是如果程序存在bug导致close()未执行,文件将不会关闭,未妥善关闭文件可能导致数据丢失或受损,如果在程序中过早调用close(),可能发现在需要使用文件时它已经关闭。
此时出现的空行是因为read()在到达文件末尾时返回一个空字符串,而这个空字符串显示出来时就是空行,要删除空行,可在函数调用print()中使用rstrip()。
- 文件路径。如果需要打开的文件与当前程序不在同一个文件夹,可以使用相对路径(相对于当前文件的路径)或绝对路径(文件在硬盘上真正存在的路径)去打开。
with open('text_files/filename.txt') as file_object
Notice:显示文件路径时,Windows系统使用反斜杠()而不是斜杠(/),但在代码中依然可以使用斜杠。如果在文件路径中直接使用反斜杠将会出错,因为反斜杠用于对字符串中的字符进行转义。如果一定要使用反斜杠,可对路径中的每个反斜杠都转义,如:“C:\path\to\file.txt”。
- 逐行读取
filename='pi_digits.txt'
with open(filename) as file_object:
for line in file_object:
print(line)
- 创建一个包含文件各行内容的列表
filename='pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
for line in lines:
print(line.rstrip())
- 使用文件的内容
filename='pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string=''
for line in lines:
pi_string+=line.strip()
print(pi_string)
print(len(pi_string))
Notice:读取文本文件时,Python将其中的所有文本都解读为字符串。如果读取的是数,并要将其作为数值使用,就必须要使用函数int()或float()将其转换为浮点数。
2、写入文件
- 写入空文件。要将文本写入文件,在调用Python()时需要提供另一个实参,告诉Python你要写入打开的文件。
filename='programming.txt'
with open(filename,'w') as file_object:
file_object.write("I love programming!")
打开文件时,可指定读取模式(‘r’)、写入模式(‘w’)、附加模式(a)或读写模式(r+),如果省略了模式实参,Python将以默认的只读模式打开文件。如果要写入的文件不存在,函数open()将自动创建它。然而,以写入模式打开文件时千万要小心,因为如果指定的文件已经存在,Python将在返回文件对象前清空该文件的内容。
Notice:Python只能将字符串写入文本文件,要将数值数据存储到文本文件中,必须现使用函数str()将其转换为字符串格式。
- 写入多行。函数wirte()不会再写入的文本末尾添加换行符,要时需要换行则必须再写入的字符串中添加’\n’
- 附加到文件。如果要给文件添加内容,而不是覆盖原有的内容,可以以**附加模式(a)**打开文件。以附加模式打开文件时,Python不会再返回文件对象前清空文件的内容,而是将写入的文件行添加到文件末尾,如果指定的文件不存在,Python将为你创建一个新文件。
filename='programming.txt'
with open(filename,'a') as file_object:
file_object.write("I also love finding meaning in large datasets.\n")
file_object.write("I love creating apps that can run in a browser.\n")
3、异常
Python使用称为异常的特殊对象来管理程序执行期间发生的错误。每当发生让Python不知所措的错误时,它都会创建一个异常对象。如果你编写了处理该异常的代码,程序将继续运行;如果未对异常进行处理,程序将停止并显示traceback,其中包含有关异常处理的报告。
异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用try-except代码块时,即便出现异常,程序也将继续运行,显示你编写的友好的错误消息,而不是令用户迷惑的traceback。
- 处理ZeroDivisionError异常。不能用数除以0。
此时Python停止运行,下面来告诉Python,发生这种错误时怎么办。 - 使用try-except代码块。当你认为可能会发生错误时,可编写一个try-except代码块来处理可能引发的异常。此时异常捕获之后,如果后面还有代码,程序将继续运行。
try :
print(5/0)
except ZeroDivisionError:
print("You Can't divide by zero!")
- 使用异常避免崩溃。发生错误时,如果程序还有工作尚未完成,妥善地处理错误就尤为重要。这种情况经常会出现再要求用户提供输入的程序中;如果程序能够妥善地处理无效输入,就能再提示用户提供有效的输入,而不至于崩溃。
print("Give me two numbers, and I'll dicide them.")
print("Enter 'q' to quit.")
while True:
first_number=input("\nfirst number:")
if first_number=='q':
break
second_number=input("Second number:")
if second_number=='q':
break
try:
answer=int(first_number)/int(second_number)
except ZeroDivisionError:
print("You can't divide by 0!")
else:
print(answer)
try-except-else代码块的工作原理大致如下。Python尝试执行try代码块中的代码,只有可能引发异常的代码才放到try语句中。一些仅在try代码块成功执行时才需要运行的代码,这些代码应放在else代码块中。except代码块告诉Python,如果尝试运行try代码块中的代码时引发了指定的异常该怎么办。
通过预测可能发生错误的代码,可编写健壮的程序。它们即便面临无效数据或缺少资源,也能继续运行,从而抵御无意的用户错误或恶意的攻击。
- 处理FileNotFoundError异常。
filename='alice.txt'
try:
with open(filename,encoding='utf-8') as f:
contents=f.read()
except FileNotFoundError:
print("Sorry, the file {filename} does not exit.")
其中,open()的第二个参数encoding指定了值,在系统的默认编码与要读取文件使用的编码不一致时,必须要这样做。
- 静默失败。并非每次捕获到异常都需要告诉用户,有时候你希望程序在发生异常时保持静默,就像什么都没有发生一样继续运行。要让程序静默失败,在except代码块中明确地告诉Python什么都不要做。即使用pass语句。
filename='alice.txt'
try:
with open(filename,encoding='utf-8') as f:
contents=f.read()
except FileNotFoundError:
pass
pass语句还充当了占位符,提醒你在程序的某个地方什么都没有做,并且以后也许要在这里做些什么。
- 决定报告哪些错误。Python的错误处理结构让你能够细致地控制与用户分享错误信息地程度,要分析多少信息由你决定。编写得很好且经过详尽测试得代码不容易出现内部错误,如语法或逻辑错误,但只要程序依赖于外部因素,如用户输入、存在指定的文件、有网络链接,就有可能出现异常。凭借经验可判断在程序的什么地方包含异常处理块,以及出现错误时该向用户提供多少相关信息。
4、存储数据
模块json让你能够将简单的Python数据结构转储到文件中,并在程序再次运行时加载该文件中的数据。你还可以使用json在Python程序之间分享数据。更重要的是,JSON数据格式并非Python专用的,这让你能够以JSON格式存储的数据与使用其他编程语言的人分享。
Notice:JSON(JavaScript Object Notation)格式最初是为JavaScript开发的,但随后成了一种常见格式,被包括Python在内的众多语言采用。
- 使用json.dump()和json.load().
import json
numbers=[2,3,5,7,11,13]
filename='numbers.json'
with open(filename,'w') as f:
json.dump(numbers,f)
使用json.dump()将数字列表存储到文件number.json中。
with open(filename) as f:
rnumbers=json.load(f)
print(rnumbers)
使用json.load()将numbers.json中的信息读取到rnumbers中。
这是一种在程序之间共享数据的简单方式。
- 保存和读取用户生成的数据。
import json
username=input("What is your name?")
filename='username.json'
with open(filename,'w') as f:
json.dump(username,f)
print("We'll remember you when you come back, {uaername}!")
with open(filename) as f:
username=json.load(f)
print(f"Welcome back, {username}!")
将上述两个过程利用try-except合并到一个程序中:
import json
filename='username.json'
try:
with open(filename) as f:
username=json.load(f)
except FileNotFoundError:
username=input("What's your name? ")
with open(filename,'w'):
json.dump(username,f)
print(f"We'll remember you when you come back, {username}!")
else:
print(f"Welcome back, {username}!")
- 重构。代码能够正确地运行,但通过将其划分为一系列完成具体工作的函数,还可以改进,这样的过程称为重构。
import json
def get_stored_username():
filename='username.json'
try:
with open(filename) as f:
username=json.load(f)
except FileNotFoundError:
return None
else:
return username
def get_new_username():
username=input("What's your name? ")
filename='username.json'
with open(filename,'w') as f:
json.dump(username,f)
return username
def greet_user():
username=get_stored_username()
if username:
print(f"Welcome back, {username}!")
else:
username=get_new_username()
print(f"We'll remember you when you come back, {username}!")
greet_user()
三、测试代码
下面是一个简单的函数,它接受名和姓并返回整洁的姓名:
def get_formatted_name(first, last):
full_name=f"{first} {last}"
return full_name.title()
from name_function import get_formatted_name
print("Enter 'q' at any time to quit.")
while True:
first=input("\nPlease give me a first name: ")
if first=='q':
break
last=input("Please give me a last name: ")
if last=='q':
break
formatted_name=get_formatted_name(first,last)
print(f"\tNeatly formatted name: {formatted_name}.")
如果要修改get_formatted_name()使其还能处理中间名。这样做时,要确保不破坏这个函数处理只含有名和性的姓名的方式。为此,可以在每次修改get_formatted_name()之后进行测试,但这太繁琐了。Python提供了一种自动测试函数输出的高效方式。倘若对get_formatted_name()进行自动测试,就能始终确信当提供测试过的姓名时,该函数都能正确工作。
1、单元测试和测试用例
Python标准库中的模块unittest提供了测试代码的工具。单元测试用于核实函数的某个方面没有问题。测试用例是一组单元测试,它们一道核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了可能收到的各种输入,包含针对所有这些情形的测试。
全覆盖的测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要进行全覆盖的测试可能很难,通常,最初只要针对代码的各种行为编写测试即可,等项目被广泛使用时再考虑全覆盖。
- 可通过的测试。可能需要一段时间习惯创建测试用例的语法,但创建测试用例之后,再添加针对函数的单元测试就很简单了。要为函数编写测试用例,可先导入模块unittest和要测试的函数,再创建一个继承unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase):
"""测试name_function.py"""
def test_first_last_name(self):
"""能够正确处理只有first和last的姓名嘛"""
formatted_name=get_formatted_name('janis','joplin')
self.assertEqual(formatted_name,'Janis Joplin')
if __name__=='__main__':
unittest.main()
首先导入模块unittest和要测试的函数。创建了一个NamesTestCase类,用于包含一系列针对get_formatted_name()的单元测试。这个类可以随意命名,但最好让它看起来与要测试的函数相关。同时这个类必须继承unittest.TestCase类,这样Python才知道如何运行你编写的程序。
NamesTestCase只包含一个方法,用于测试get_formatted_name()的一个方面。运行该文件时,所有以test_打头的方法都将自动运行。
self.assertEqual()使用了unittest类最有用的功能之一:断言方法。断言方法核实得到的结构是否与期望的一致。
最后的if代码块检查特殊变量__name__,这个变量是再程序执行时设置的。如果这个文件作为主程序执行,变量__name__将被设置为’main’,这里调用unittest.main()来运行测试用例。如果这个文件被测试框架导入,变量__name__的值将不是’main’,因此不会调用unittest.main()。
第一行的句点表示有一个测试通过了。接下来的一行指出Python运行了一个测试,消耗的时间不到0.001秒。最后的OK表明该测试用例钟的所有单元都通过了。
- 未通过的测试。修改get_formatted_name()让其能够处理中间名但不能处理只有first和last的姓名。
修改函数代码如下,就可以处理仅包含名和姓以及包含名、中间名和姓的这两种情况。
def get_formatted_name(first, last, middle=''):
if middle:
full_name=f"{first} {middle} {last}"
else:
full_name=f"{first} {last}"
return full_name.title()
- 添加新的测试用例。
def test_first_last_middle_name(self):
formatted_name=get_formatted_name('wolfgang','mozart','amadeus')
self.assertEqual(formatted_name,'Wolfgang Amadeus Mozart')
2、测试类
- 各种断言方法。Python在unittest.TestCase类中提供了很多断言方法。断言方法检查你认为应该满足的条件是否确实满足。以下的6个常用的断言方法可合适返回的值等于或不等于预期的值,返回的值为True或False,以及返回的值在列表中或不在列表中。只能在继承unittest.TestCase的类中使用这些方法。
- assertEqual() 核实a==b
- assertNotEqual() 核实a!=b
- assertTrue(x) 核实x为True
- assertFalse(x) 核实x为False
- assertIn(item, self) 核实item在list中
- assertNotIn(item, list) 核实item不在list中
- 一个要测试的类。帮助管理匿名调查的类。
class AnonymousSurvey:
"""搜集匿名调查问卷的答案"""
def __init__(self, question):
"""存储一个问题,并未存储答案做准备。"""
self.question=question
self.responses=[]
def show_question(self):
"""显示调查问卷。"""
print(self.question)
def store_response(self, new_response):
"""存储单份调查问卷"""
self.response.append(new_response)
def show_result(self):
print("Survey result:")
for response in self.responses:
print(f"- {response}")
为了验证AnonymousSurvey能够正确工作,编写一个使用它的程序:
class AnonymousSurvey:
"""搜集匿名调查问卷的答案"""
def __init__(self, question):
"""存储一个问题,并未存储答案做准备。"""
self.question=question
self.responses=[]
def show_question(self):
"""显示调查问卷。"""
print(self.question)
def store_response(self, new_response):
"""存储单份调查问卷"""
self.responses.append(new_response)
def show_results(self):
print("Survey result:")
for response in self.responses:
print(f"- {response}")
- 测试AnonymousSurvey类。对AnonymousSurvey的行为的一个方面进行验证:如果用户面对调查问题只提供一个答案,这个答案也能被妥善地存储。
import unittest
from survey import AnonymousSurvey
class TestAnonynousSurvey(unittest.TestCase):
def test_store_single_response(self):
question="What language did you learn to speak?"
my_survey=AnonymousSurvey(question)
my_survey.store_response('English')
self.assertIn('English',my_survey.responses)
if __name__=='__main__':
unittest.main()
接下来再核实当用户提供三个答案的时候也能被妥善存储:
def test_store_three_responses(self):
question="What language did you learn to speak?"
my_survey=AnonymousSurvey(question)
responses=['English','Spnish','Manadarin']
for response in responses:
my_survey.store_response(response)
for response in responses:
self.assertIn(response,my_survey.responses)
上面的两个测试用例效果很好,但是有些重复的地方。下面使用unittest的另一项功能。
- 方法setUp()。在前面的测试中,我们在每个测试方法中都创建了AnonymousSurvey实例,并在每个方法中都创建了答案。unittest.testCase类中包含的方法setUp()让我们只需创建这些对象一次就可以在每个测试方法中使用。
如果在TestCase类中包含了方法setUp()。Python将先运行它,再运行各个以test_打头的方法。
import unittest
from survey import AnonymousSurvey
class TestAnonynousSurvey(unittest.TestCase):
def setUp(self):
"""
创建一个调查对象和一组答案,供使用的测试方法使用。
"""
question="What language did you learn to speak?"
self.my_survey=AnonymousSurvey(question)
self.responses=['English','Spnish','Manadarin']
def test_store_single_response(self):
self.my_survey.store_response(self.responses[0])
self.assertIn(self.responses[0],self.my_survey.responses)
def test_store_three_responses(self):
for response in self.responses:
self.my_survey.store_response(response)
for response in self.responses:
self.assertIn(response, self.my_survey.responses)
if __name__=='__main__':
unittest.main()
Notice:运行测试用例时,每完成一个单元测试,Python就会打印一个字符:测试通过时打印一个句点,测试引发错误时打印一个E,而测试导致断言失败时打印一个F。如果测试用例包含很多单元测试,需要运行很长时间,就可以通过观察这些结果来获悉有多少个测试通过了。