Python自学笔记(3)

整理一下Python相关内容,方便查阅。

参考用书:《Python编程:从入门到实践》,作者:Eric Matthes

第8章 文件和异常

8.1 读取文件

这一小节中用到两个文件,一个文件是pi_digits.txt,一个文件是pi_million_digits.txt。它们均保存在与本小节编程文件相同的目录下。可点击下面两个链接下载这两个文件。

pi_digits.txt

pi_million_digits.txt

8.1.1 读取全部文件内容

下面的代码会打开并读取pi_digits.txt文件,并将其内容显示到屏幕上。

# file_reader.py

with open('pi_digits.txt') as file_object:  # 打开文件
    contents = file_object.read()           # 读取文件
    print(contents.rstrip())                # 打印文件内容

运行结果:

3.1415926535
  8979323846
  2643383279

分析程序代码:

  • open()函数:参数为要打开的文件的相对路径绝对路径,返回一个表示文件的对象。使用as为这个对象指定别名file_object。

  • with关键字:它可以在不再需要访问文件后将其关闭。关闭时机由Python自动确定。

也可以不用with关键字,使用open()和close()函数配合来打开和关闭文件,但这样如果程序出现bug可能会导致close()函数得不到调用,从而造成数据丢失,因此不建议这样做。

  • read()函数:文件对象的一个方法,读取这个文件的内容,并将内容作为字符串返回。

  • 空行处理:read()函数到达文件末尾时自动返回一个空字符串,打印时作为空行打印,这样输出的结果就比原文件多了一个空行。因此使用rstrip()方法删除字符串末尾的空白符。

当使用其它目录下的文件时,注意在windows系统中文件路径使用反斜线(\),在Linux和OS X中使用斜线(/)。

8.1.2 逐行读取文件内容

可对文件对象使用for循环来遍历文件的每一行:

# file_reader.py

filename = 'pi_digits.txt'

with open(filename) as file_object:
    for line in file_object:
        print(line.rstrip())

运行结果:

3.1415926535
  8979323846
  2643383279

这里要注意,逐行读取时依然有空行处理,处理的原理和方式与上面相同。

8.1.3 创建包含文件各行内容的列表

使用with关键字时,open()返回的文件对象只能在with代码块内使用,因此需要有适当的变量来存储文件内容。可使用函数readlines()将文件各行的内容存储在一个列表中:

filename = 'pi_million_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[:52] + "...")
    print(len(pi_string))

运行结果:

3.14159265358979323846264338327950288419716939937510...
100002

8.2 写入文件

下面的程序将创建名为programming.txt的文件,并在其中写入一句话I love programming.

filename = 'programming.txt'

with open(filename, 'w') as file_object:
    file_object.write("I love programming.")

我们来看这段代码中的两个函数:

  • open()函数:在open()函数中,第一个参数表示要打开的文件,如果文件不存在,则新建该文件。

第二个参数表示文件的打开模式,'r'表示以读取模式打开,'r'表示以写入模式打开,'a'表示以附加模式打开。

值得注意的是,以写入模式打开文件后,文件对象中的内容会被清空,因此后面写入的内容会完全覆盖原文件的内容。如果想要保留原来的内容,承接在原内容之后写入内容,则应当使用附加模式打开。

  • write()函数文件对象.write(str)会在指定的文件对象中写入字符串str。需要注意两点:① str必须为字符串,如果是数值,请用str()转换数据类型;② write()函数写入字符串后不会换行,想换行请手动添加'\n'

8.3 异常

8.3.1 使用try-except-else代码块处理异常

一个常见的错误是,除数为零。这时程序会抛出ZeroDivisionError,并终止运行。如果我们能在代码中提前告诉Python发生这种异常时应该怎么办,就有备无患了:

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

运行结果:

You can't divide by zero!

这时try-except代码块,使用它处理异常的好处:首先,错误提示很友好,不是traceback;其次,如果后面还有代码,程序将继续运行。即,我们可以使用异常避免崩溃

避免崩溃而出现traceback有两个好处:首先,不懂得技术的用户看到traceback会感到迷惑,而看到友好的错误提示信息;其次,懂得技术的程序员看到traceback中包含了许多与源代码相关的信息,这可以被用来展开攻击,隐藏traceback在避免攻击中显得尤为重要。

print("Give me two numbers, and I'll divide 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 zero!")
    else:
        print(answer)

try-except-else大致的原理是,运行try中的代码块,如果出现了except后面提到的异常,执行except代码块中的内容,如果没有抛出异常,执行else代码块中的内容。这段完成后继续执行后面的程序。

如果try中的代码块抛出异常,但不是except后面跟的异常,就会出现traceback并崩溃。

8.3.2 处理FileNotFoundError异常

上面提到了ZeroDivisionError,还有一种异常是FileNotFoundError,它发生在尝试打开一个文件而该文件不存在时。例如,假设当前目录中不存在alice.txt文件,那么执行下面的代码:

filename = 'alice.txt'

with open(filename) as f_obj:
    contents = f_obj.read()

会抛出一个含FileNotFoundError异常的trackback。为避免这一点,我们可以将with代码块放入try-except-else语句中。我们下面试图写一个程序,它能计算一个文本中含有多少个单词,为此先介绍split()函数:

split()函数可以将指定的字符串按空白符分隔成一个个单词,将这些单词保存到一个列表中并返回。例如,

title = "Alice's adventures in wonderland"
print(title.split())

运行结果:

["Alice's", 'adventures', 'in', 'wonderland']

我们要计算这几本书的单词数(点击链接可下载):

alice.txt (Alice’s Adventures in Wonderland)

moby_dick.txt (Moby Dick)

little_women.txt (Little Women)

还有一本并没有下载的书siddhartha.txt。

代码如下:

def count_words(filename):
    """计算一个文件大致包含多少个单词"""
    try:
        with open(filename) as f_obj:
            contents = f_obj.read()
    except FileNotFoundError:
        print("Sorry, the file " + filename + " does not exist.")
    else:
        # 计算文件大致包含多少个单词
        words = contents.split()
        num_words = len(words)
        print("The file " + filename + " has about " + str(num_words) + " words.")

filenames  = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
    count_words(filename)

运行结果:

The file alice.txt has about 26436 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 214422 words.
The file little_women.txt has about 187000 words.

下面介绍pass语句。我们将except代码块修改为

...
except FileNotFoundError:
    pass
...

这时遇到FileNotFoundError异常时程序便会“一声不吭”,继续执行,运行结果变为:

The file alice.txt has about 26436 words.
The file moby_dick.txt has about 214422 words.
The file little_women.txt has about 187000 words.

8.4 存储数据

我们使用标准库中的json模块来实现数据的存储。用到其中的两个方法,dump()和load(),前者将内容存储到文件中,后者加载文件内容。

例如,我们想存一串数字列表到文件numbers.json中,便于另外的进程调用,可以用dump()方法实现:

import json

numbers = [2, 3, 5, 7, 11, 13]

filename = 'numbers.json'
with open(filename, 'w') as f_obj:
    json.dump(numbers, f_obj)

该段代码新建了一个名为numbers.json的文件,并将[2, 3, 5, 7, 11, 13]存入其中。

现在我们想调用储存的数字列表,可以用load()方法实现:

import json

filename = 'numbers.json'
with open(filename) as f_obj:
    numbers = json.load(f_obj)

print(numbers)

运行结果:

[2, 3, 5, 7, 11, 13]

第9章 测试代码

9.1 测试函数

假设我们有模块name_function.py:

def get_formatted_name(first, last, middle=''):
    """Generate a neatly formatted full name."""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()

用以下代码测试该模块中给出的函数的正确性:

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """测试name_function.py"""

    def test_first_last_name(self):
        """能够正确地处理像Janis Joplin这样的姓名吗?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

    def test_first_middle_last_name(self):
        """能够正确地处理像Wolfgang Amadeus Mozart这样的姓名吗?"""
        formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')
  • 导入unittest模块,它能提供代码测试工具。

  • 创建一个类NamesTestCase,继承unittest.TestCase类。

  • 所有以test_打头的方法都将自动运行。上例中每个test_方法都能用来测试函数get_formatted_name()的一个方面。

  • unittest类最有用的功能:断言方法。这里用到的是assertEqual()。它能比较传入的两个字符串是否相同。

9.2 测试类

有6个常用的断言方法:

方法 用途
assertEqual(a, b) 核实 a == b
assertNotEqual(a, b) 核实 a != b
assertTrue(x) 核实x为True
assertFalse(x) 核实x为False
assertIn(item, list) 核实item在list中
assertNotIn(item, list) 核实item不在list中

测试类与测试函数基本一致,用到上述断言方法测试类的功能。

unittest.TestCase类还提供了一个setUp()方法,可以用它来提高效率。假设模块survey.py中有类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 results:")
        for response in self.responses:
            print('- ' + response)

我们可以用setUp()方法先创建一个问题和一份回答,供后面的测试方法使用。

# test_survey.py

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """针对AnonymousSurvey类的测试"""

    def setUp(self):
        """
        创建一个调查对象和一组答案,供使用的测试方法使用
        """
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English', 'Spanish', 'Mandarin']

    def test_store_single_response(self):
        """测试单个答案会被妥善地存储"""
        self.my_survey.store_response(self.responses[0])
        self.assertIn('English', 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)