Python个人学习笔记(9)——文件与异常

文件和异常

学习处理文件——让程序能快速地分析大量数据;
学习错误处理——避免在程序在面对意外情形时崩溃;
学习异常——Python创建的特殊对象,用于管理程序运行时的错误;
学习模块json——能让你能够保存用户数据,以免程序停止运行后丢失。


本章内容可提高程序的适用性、可用性和稳定性


从文件中读取内容

  • 读取整个文件
    pi_digits.txt

3.1415926535
  8979323846
  2643383279

file_reader.py


with open('pi_digits.txt') as file_object:
    contents = file_object.read()
    print(contents)

运行结果:
3.1415926535
8979323846
2643383279
例子如上,现在来详细分析一下吧:

  • 函数open()接受一个参数:要打开的文件名称.Python在当前执行的目录中查找指定的文件,然后open()函数返回一个表示指定文件的对象;Python将这个对象存储到后面的变量中.
  • 关键字with在不再需要访问文件后将其关闭.在这个程序中,我们调用了open(),而没有调用close();你也可以调用open()和close()来打开和关闭文件,但这样做有两个害处.一:未妥善地关闭文件可能会导致数据丢失或者受损;二:过早地调用close(),你会发现需要使用文件时,它已关闭(无法访问),这将导致更多的错误.因此,最好的办法就是,使用上面的结构,可让python去确定:只负责打开文件,并在需要的时使用它,Python自会在合适的时候自动将其关闭.
  • 文件路径
    • 相对路径
      当将如pi_digits.txt这么简单文件名传递给函数open()时,Python将在当前执行的文件所在目录查找文件.
    • 绝对路径
      根据组织文件的方式,有时可能要打开不在文件所属目录中的文件.这时候就要使用绝对路径了.
      Windows系统中,类似于下面这样:

    file_path = r'D:\vscode\PYTHON\demo\pi_digits.txt'
    with open(file_path) as file_object:
    

通过绝对路径,可读取系统任何地方的文件.就目前而言,最简单的做法是,要么将数据文件存储在程序文件所在的目录,要么将其存储在吃呢工序所在目录下的一个文件夹中.

注意:windows系统有时候能够正确地读取文件路径中的斜杆.如果使用的是Windows系统,且结果不符合预期,请确保在文件路径中使用的是反斜杠.另外,由于反斜杠在Python中被视为转义标记,为在Windows中确保万无一失,应以原始字符串的方式指定路径,即在开头的单引号加上r

  • 逐行读取
    读取文件时,常常需要检查其中的每一行.要以每次一行的方式检查文件,可对文件对象使用for循环.
with open('pi_digits.txt') as file_object:
  for line in file_object:
    print(line)

运行结果:

3.1415926535

  8979323846

  2643383279

我们打印每一行时,发现空白行更多了.为何会出现这些空白行?因为在这个文件中,每行的末尾都有一个看不见的换行符,而print语句也会加上一个换行符,因此每行有两个换行符:一个来自文件,一个来自print语句.要消除这些多余的空白,使用rstrip():

file_name = 'pi_digits.txt'
with open(file_name) as file_object:
  for line in file_object:
    print(line.rstrip())

运行结果:

3.1415926535
  8979323846
  2643383279

现在.输出又跟文件内容相同了

  • 创建一个包含文件各行内容的列表
    使用关键字with时,open()返回的文件对象只会在with代码块内可用.如果要在with代码块外访问文件的内容,可在with代码块内将文件各行的内容存储在一个列表中,并在with代码块外使用该列表:你可以立即处理文件的各个部分,也可推迟到程序后面再处理.
file_name = 'pi_digits.txt'
with open(file_name) as file_object:
  lines = file_object.readlines()

for line in lines:
  print(line.rstrip())

运行结果:

3.1415926535
  8979323846
  2643383279

方法readlines()从文件中读取每一行,并将其存储在一个列表中;接下来,该列表被存储到变量中;在with代码块外,我们依然可以使用这个变量.下面的for循环的输出结果则印证了这一点.

  • 使用文件的内容
    当文件读取到内存中后,就可以以任何方式使用这些数据.下面以简单的方式使用圆周率.
    pi_string.py

file_name = 'pi_digits.txt'
with open(file_name) as file_object:
  lines = file_object.readlines()

pi_string = ''
for line in lines:
  pi_string += line.strip()

print(pi_string)
print(len(pi_string))

运行结果:
3.141592653589793238462643383279
32
为什么这个例子使用的是strip()方法而不是rstrip方法呢?这是有原因的,rstrip()方法仅能删除后面的空白,但数字右边也存在空白.因此,要用strip()方法.
这样,我们就获得了一个这样的字符串:它包含精确到30位小数的圆周率值

注意:读取文本文件时,Python将其中的所有文本都解读为字符串.如果你读取的是数字,并要将其作为数值使用,就必须使用函数int()将其转换成整数,或使用函数float()将其转换成浮点数.

  • 包含一百万位的大型文件
    对于你可以处理的数据量,Python没有任何限制;只要系统的内存足够多,你想处理多少数据都可以.

写入文件

保存数据的最简单的方式之一是将其写入到文件中.通过将输出写入文件,即便关闭包含程序输出的终端窗口,这些输出也依然存在:你可以在程序结束运行后查看这些输出,可与别人分享输出文件,还可以编写程序来将这些输出读入到内存中并进行处理.

  • 写入空文件
    要将文本写入文件,在调用open()函数时需要提供另一个实参,告诉Python你要写入打开的文件.
    write_message.py
file_name = 'programming.txt'

with open(file_name,'w') as file_object:
  file_object.read("I love programming.")

运行结果:


programming.txt


I love programming.
  • 在这个示例中,调用open()时提供了两个实参.第一个实参也是要打开的文件的名称;第二个实参(‘w’)告诉Python,我们要以写入方式打开这个文件.打开文件时,可指定读取模式(‘r’),写入模式(‘w’),附加模式(‘a’)或让你能够读取和写入文件的模式(‘r+’).如果你忽略了模式实参,Python将以默认的只读模式打开文件.
  • 如果你要写入的文件不存在,open()将自动创建它.然而,以写入(‘w’)模式打开文件时千万要小心,因为如果指定文件已经存在,Python将在返回文件对象前清空该文件.
  • 想比于计算机中的其他文件,python创建的文件并没有任何的区别.

注意:Python只能将字符串写入文本文件.要将数值数据存储到文本文件,必须先使用str()将其转换为字符串格式.

  • 写入多行
    函数write()不会在你写入的文本末尾添加换行符,因此如果写入多行时没有指定换行符,文件看起来不会像你想象中的那样,你会发现多行内容挤到了一起.因此,要让每个字符串都单独占一行,需要在write()语句中包含换行符:
file_name = 'programming.txt'

with open(file_name,'w') as file_object:
  file_object.write("I love programming.\n")
  file_object.write("I love creating new games.\n")

运行结果:


programming.txt


I love programming.
I love creating new games.

像显示到终端的输出一样,还可以使用空格、制表符和空行来设置输出的格式。

  • 附加到文件
    如果你要给文件添加内容,而不是覆盖原有的内容时,可以附加模式打开文件.以附加模式打开文件时,Python不会在返回文件对象前清空文件,而你写到文件的行都会添加到文件末尾.如果指定的文件不存在,Python会为你创建一个空文件.对上方代码进行修改:

programming.txt


file_name = 'programming.txt'

with open(file_name,'a') as file_object:
  file_object.write("I also love finding meanig in large datasets.\n")
  file_object.write("I love creating apps that can run in a browser.\n")

运行结果:


programming.txt


I love programming.
I love creating new games.
I also love finding meanig in large datasets.
I love creating apps that can run in a browser.

在打开方式处,我们选择了附加模式(即实参’a’),以便将内容附加到文件末尾,而不是覆盖文件原来的内容.最终的结果是:文件原来的内容还在,它们后面是我们刚添加的内容.

异常

Python使用被称为异常的特殊对象来管理程序执行期间发生的错误.每当发生让python不知所措的错误时,它都会创建一个异常对象.如果你编写了处理该异常的代码,程序将继续运行;如果你未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告.
异常是使用try-except代码块处理的.try-except代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办.使用了try-except代码块时,即便出现了异常,程序也将继续运行:显示你编写的友好的错误信息,而不是令用户迷惑的traceback.

  • 处理ZerpDivisionError异常
    下面是一个简单的异常,众所周知,不能将一个数字除以0,这样写会怎么样呢?
print(5/0)

显然,会导致异常:

Traceback (most recent call last):
  File "", line 1, in 
    print(5/0)
ZeroDivisionError: division by zero

在traceback中,错误ZeroDivisionError是一个异常对象.python无法按要求做时,就会创建这种对象.在这种情况下,python将停止运行程序,并指出发生了哪种对象,而我们可根据这些消息对程序进行修改.下面我们将告诉python,发生这种错误时怎么办.

  • 使用try-except代码块
    当你认为可能发生错误时,可编写一个try-except代码块来处理可能引发的异常.你让python尝试运行一些代码,并告诉它如果这些代码引发了指定的代码,该怎么办.

处理ZeroDivisionError异常的try-except代码块类似下面这样:

try:
  print(5/0)
except ZeroDivisionError:
  print("You can't divide by zero.")

运行结果:
You can’t divide by zero.
将引发错误的代码行放在try代码块中.如果try代码块中的代码运行起来没有问题.Python将跳过except代码块;如果try代码块中的代码导致了错误,python将查找这样的代码块,并运行其中的代码,即其中指定的错误与引发的错误相同.

如果try-except代码块后面还有其他代码,程序将继续运行,因为已经告诉python如果处理这种错误.

  • 使用异常避免崩溃
    发生错误时,如果程序还有工作没有完成,妥善地处理错误就尤为重要.这种情况经常会出现在需要用户输入数据的程序中;如果程序能够妥善地处理无效输入,就能提示用户提供有效的输入而不至于崩溃.
print("Give me two numbers,and I will divide them.")
print("Input 'q' to quit.")

while True:
  first_num = input("\nfirst number:")
  if first_num == 'q':
    break
  second_num = input("second number:")
  try:
    answer = int(first_num)/int(second_num)
  except ZeroDivisionError:
    print("You can't divide by 0!")
  else:
    print(answer)

我们让Python尝试执行try代码块里的除法运算,这个代码块只包含可能导致错误的代码.依赖于try代码块成功执行的代码都放在else代码块中,如果除法运算成功,我们就使用else代码块打印结果
try-except-else代码块的工作原理大致如下:python尝试try代码块中的代码;只有可能引发异常得到代码才需要放在try代码块中.有时候,有一些仅在try代码块运行成功时才需要运行的代码;这些代码应放在else代码块中.except代码块告诉python,如果它尝试运行try代码块中的代码时引发了指定的异常,该怎么办.
倘若不使用try-except代码块会发生什么呢?将会发生下面的错误:

Traceback (most recent call last):
  File "d:\vscode\test.py", line 1, in 
    print(5/0)
ZeroDivisionError: division by zero

程序崩溃可不好,但让用户看到traceback也不是好主意.不懂技术的用户会让它们搞糊涂,而且如果用户有恶意,他会通过traceback获悉你不希望他知道的信息.有时候,训练有素的攻击者可根据这些信息判断出可对你的代码发出什么样的攻击.

通过预测可能可能发生错误的代码,可编写健壮的程序,它们即便面临无效数据或缺少资源,也能继续运行,从而能够抵挡无意的用户错误和恶意的攻击.

  • 处理FileNotFoundError异常
    使用文件时,一种常见的问题是找不到文件:你要查找的文件可能在其他地方、文件名不正确或者这个文件根本不存在。对于所有的这些情形,都可使用try-except代码块已直观的方式进行处理。下面我们将利用try-except代码块编写一个函数来计算一个文本中包含了多少单词:
def count_words(filename):
  """计算一个文件大概包含多少单词"""
  try:
    with open(filename) as f_obj:
      contents = f_obj.read()
  except FileNotFound_Error:
    msg = "Sorry,the file " + filename + " does not exist."
    print(msg)
  else:
    #计算文件内大概包含多少个单词
    words = contents.split()
    num_words = len(words)
    print("The file " + filename + " has about " + str(num_words) + " words.")

#分析单个文本
filename = "alice.txt'
count_word(filename)

#分析多个文件
filenames = ['alice.txt','siddhartha.txt','little_women.txt','moby_dick.txt']
for filename in filenames:
  count_word(filename)

运行结果:

The file alice.txt has about 29461 words.
The file alice.txt has about 29461 words.
The file siddhartha.txt has about 42172 words.
Sorry,the file little_women.txt does not exist.
The file moby_dict.txt has about 215136 words.

让我们来分析以上代码!

  • 方法split()以空格作为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中.结果是一个包含字符串中所有单词的列表,虽然这些单词可能包含标点.
  • 利用方法split()得到列表后,利用方法len()来得到单词的个数
  • 分析多个文件时,先将所有文件名放在一个列表中,再利用for循环遍历列表、调用函数。
  • 在这个示例中,我故意没有让little_women.txt在该目录下.事实证明,使用try-except代码提供了两个优点:避免让用户看到traceback;让程序能够继续分析能够找到的其他文件.上述代码中little_women.txt不存在,但这丝毫不影响程序处理其他文件.而倘若不捕获因找不到little_women.txt引发的异常,用户将看到完整的traceback,而程序将在尝试分析little_women.txt后停止运行——根本不分析moby_dict.txt。
  • 当然,我们也可以使文件打开失败时一声不吭
    只需要将except代码块中的代码换成pass语句,像下面一样:
except FileNotFound_Error:
  pass
  • 决定报告那些错误
    在什么时候该想用户报告错误?什么时候在失败时一声不吭?如果用户知道要分析那些文件,他们可能希望在有文件没有分析时出现一条消息将其中的原因告诉他们.如果用户只想看到结果,而并不知道要分析那些文件,可能就无需在有些文件不存在时告知他们.向用户显示他不想看到的信息可能会降低程序的可用性.
    编写得很好且经过详尽测试得代码不容易出现内部错误,如语法或逻辑错误,但只要程序依赖于外部因素,如用户输入、存在指定的文件、有网络链接,就有可能出现异常。凭借经验可判断该在程序的什么地方包含异常处理块,以及出现错误时该向用户提供多少相关的信息。

存储数据

很多程序都要求用户输入某种信息,如让用户存储游戏首选项或提供可视化的数据。不管专注的是什么,程序都要将程序提供的信息存储在列表和字典等数据结构。用户关闭程序时,你几乎总是要保存他们提供的信息;一种简单的方式是使用模块json来存储数据。
模块json让你能够将简单的Python数据结构转储在文件中,并在程序再次运行时加载该文件中的数据.你还可以使用json在python程序之间分享数据.更重要的是,JSON数据格式并非python专用的,这让你能够将以Json格式存储的数据与使用其他编程语言的人分享.这是一种轻便格式,很有用,也易于学习.

注意:JSON(JavaScript Object Notation)格式最初是为JavaScript开发的,但随后成为了一种常见格式,被包括Python在内的众多语言采用.

  • 使用json.dump()和json.load()保存和读取用户生成的数据
import json

filename = 'username.json'
try:
  #如果以前存储了用户名,就加载它
  with open(filename) as f_obj:
    username = json.load(f_obj)
except FileNotFoundError:
  #否则,就提示用户输入用户名并存储它
  username = input("What is your name?")
  with open(filename,'w') as f_obj:
    json.dump(username,f_obj)
    print("We'll remenber you when you come back," + usename + "!")
else:
  print("Welcome back," + username + "!")

让我们分析下这个例子:

  • 我们首先要导入模块json.通常使用文件扩展名.json来指出文件存储的数据为JSON格式.
  • json.jump(filename)使得我们以写入模式打开文件,让json能够将数据写入其中.
  • json.load(filename)使得我们以读取方式打开这个文件
  • #如果以前存储了用户名,就加载它(即至少运行过一次程序),假设username的值为Eric,输出:
Welcome back,Eric!
  • 否则,就提示用户输入用户名并存储它,将数据写入文件得同时输出:
What is your name?Eric
We'll remenber you when you come back,Eric!
  • 重构
    经常与这样的情况:代码能够正确运行,但可做进一步改进——将代码划分为一系列完成具体工作的函数这样的过程被成为重构。重构让代码更清晰、更易于理解、更容易扩展。上面的代码可重构为:
import json

def get_stored_username():
  """如果存储了用户名,就获取它"""
  filename = "username.json"
  try:
    with open(filename) as f_obj:
      username = json.load(f_obj)
  except FileNotFoundError:
    return None
  else:
    return username

def get_new_username():
  """提示用户输入用户名"""
  username = input("What is your name?")
  filename = "username.json"
  with open(filename) as f_obj:
    json.dump(usename,f_obj)
  return username

def greet_user():
  """问候用户,并指出其名字"""
  usename = get_stored_username()
  if username:
    print("Welcome back," + username +"!")
  else:
    username = get_new_username()
    print("We'll remenber you when you come back." + username + "!")

greet_user()

重构之后,每个函数都执行单一而清晰的任务.我们调用greet_user(),它打印一条合适的消息:要么欢迎老用户回来,要么问候新用户.为此,它先调用get_stored_username(),这个函数只负责获取存储的用户名(如果存储了的话),再在必要时调用get_new_username(),这个函数只负责获取并存储新用户.要编写出清晰而易于维护和拓展的代码,这种工作必不可少.

个人从书本学到的知识,作为自己学习笔记的同时,与各位朋友分享,如有不足,请多多指教

参考文献:《Python编程从入门到实践》【美】Eric Matthes 著 袁国忠 译

你可能感兴趣的:(Python个人学习笔记,编程语言,python)