Python基础拾遗--看的不多只看一篇(下篇)

Python基础

第4章 函数

4.1 函数定义

下面是一个打印问候语的简单函数,名为greet_user()
greeter.py

def greet_user(): //"""显示简单的问候语""" //print("Hello!") // ③
greet_user()

    ①使用关键字def 来定义一个函数。这是函数定义 ,向Python指出了函数名,还可能在括号内指出函数为完成其任务需要什么样的信息。在这里,函数名为greet_user() ,它不需要任何信息就能完成其工作,因此括号是空的。最后,定义以冒号结尾。

    紧跟在def greet_user(): 后面的所有缩进行构成了函数体。②处的文本是注释,文档字符串用三引号括起,Python使用它们来生成有关程序中函数的文档。

代码行print(“Hello!”) (见③)是函数体内的唯一一行代码,greet_user() 只做一项工作:打印Hello! 。

    要使用这个函数,可调用它。函数调用 让Python执行函数的代码。要调用 函数,可依次指定函数名以及用括号括起的必要信息,如④处所示。由于这个函数不需要任何信息,因此调用它时只需输入greet_user() 即可。和预期的一样,它打印Hello! :

Hello!

4.1.1 向函数传递信息

    只需稍作修改,就可以让函数greet_user() 不仅向用户显示Hello! ,还将用户的名字用作抬头。为此,可在函数定义def greet_user() 的括号内添加username 。通过在这里添加username ,就可让函数接受你给username 指定的任何值。现在,这个函数要求你调用它时给username 指定一个值。调用greet_user() 时,可将一个名字传递给它,如下所示:

def greet_user(username):
    """显示简单的问候语"""
    print("Hello, " + username.title() + "!")
greet_user('jesse')

    代码greet_user(‘jesse’) 调用函数greet_user() ,并向它提供执行print 语句所需的信息。这个函数接受你传递给它的名字,并向这个人发出问候:

Hello, Jesse!

4.2 传递实参

    鉴于函数定义中可能包含多个形参,因此函数调用中也可能包含多个实参。向函数传递实参的方式很多,可使用位置实参 ,这要求实参的顺序与形参的顺序相同;也可使用关键关字实参,其中每个实参都由变量名和值组成;还可使用列表和字典。

下面来依次介绍这些方式。

4.2.1 位置实参

    你调用函数时,Python必须将函数调用中的每个实参都关联到函数定义中的一个形参。为此,最简单的关联方式是基于实参的顺序。这种关联方式被称为位置实参。

    为明白其中的工作原理,来看一个显示宠物信息的函数。这个函数指出一个宠物属于哪种动物以及它叫什么名字,如下所示:
pets.py

def describe_pet(animal_type, pet_name): //"""显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")
describe_pet('hamster', 'harry') //

    这个函数的定义表明,它需要一种动物类型和一个名字(见❶)。调用describe_pet() 时,需要按顺序提供一种动物类型和一个名字。例如,在前面的函数调用中,实参’hamster’ 存储在形参animal_type 中,而实参’harry’ 存储在形参pet_name 中(见❷)。在函数体内,使用了这两个形参来显示宠物的信息。

输出描述了一只名为Harry的仓鼠:

I have a hamster.
My hamster's name is Harry

1. 调用函数多次
你可以根据需要调用函数任意次。要再描述一个宠物,只需再次调用describe_pet() 即可:

def describe_pet(animal_type, pet_name):
    """显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")
describe_pet('hamster', 'harry')
describe_pet('dog', 'willie')

    第二次调用describe_pet() 函数时,我们向它传递了实参’dog’ 和’willie’ 。与第一次调用时一样,Python将实参’dog’ 关联到形参animal_type ,并将实参’willie’ 关联到形参pet_name 。与前面一样,这个函数完成其任务,但打印的是一条名为Willie的小狗的信息。至此,我们有一只名为Harry的仓鼠,还有一条名为Willie的小狗:

I have a hamster.
My hamster's name is Harry.
I have a dog.
My dog's name is Willie.

    调用函数多次是一种效率极高的工作方式。我们只需在函数中编写描述宠物的代码一次,然后每当需要描述新宠物时,都可调用这个函数,并向它提供新宠物的信息。即便描述宠物的代码增加到了10行,你依然只需使用一行调用函数的代码,就可描述一个新宠物。

    在函数中,可根据需要使用任意数量的位置实参,Python将按顺序将函数调用中的实参关联到函数定义中相应的形参。

2. 位置实参的顺序很重要

    使用位置实参来调用函数时,如果实参的顺序不正确,结果可能出乎意料:

def describe_pet(animal_type, pet_name):
    """显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")
describe_pet('harry', 'hamster')

    在这个函数调用中,我们先指定名字,再指定动物类型。由于实参’harry’ 在前,这个值将存储到形参animal_type 中;同理,‘hamster’ 将存储到形参pet_name 中。

结果是我们得到了一个名为Hamster 的harry :

I have a harry.
My harry's name is Hamster.

如果结果像上面一样搞笑,请确认函数调用中实参的顺序与函数定义中形参的顺序一致。

4.2.2 关键字实参

    关键字实参是传递给函数的名称—值对。你直接在实参中将名称和值关联起来了,因此向函数传递实参时不会混淆(不会得到名为Hamster的harry这样的结果)。关键字实参让你无需考虑函数调用中的实参顺序,还清楚地指出了函数调用中各个值的用途。

下面来重新编写pets.py,在其中使用关键字实参来调用describe_pet() :

def describe_pet(animal_type, pet_name):
    """显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")
describe_pet(animal_type='hamster', pet_name='harry')

    函数describe_pet() 还是原来那样,但调用这个函数时,我们向Python明确地指出了各个实参对应的形参。看到这个函数调用时,Python知道应该将实参’hamster’ 和’harry’ 分别存储在形参animal_type 和pet_name 中。输出正确无误,它指出我们有一只名为Harry的仓鼠。

    关键字实参的顺序无关紧要,因为Python知道各个值该存储到哪个形参中。下面两个函数调用是等效的:

describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')

注意 使用关键字实参时,务必准确地指定函数定义中的形参名。

4.2.3 默认值

    编写函数时,可给每个形参指定默认值 默 。在调用函数中给形参提供了实参时,Python将使用指定的实参值;否则,将使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。

    例如,如果你发现调用describe_pet() 时,描述的大都是小狗,就可将形参animal_type 的默认值设置为’dog’ 。这样,调用describe_pet() 来描述小狗时,就可不提供这种信息:

def describe_pet(pet_name, animal_type='dog'):
    """显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")
describe_pet(pet_name='willie')

    这里修改了函数describe_pet() 的定义,在其中给形参animal_type 指定了默认值’dog’ 。这样,调用这个函数时,如果没有给animal_type 指定值,Python将把这个形参设置为’dog’ :

I have a dog.
My dog's name is Willie.

    请注意,在这个函数的定义中,修改了形参的排列顺序。由于给animal_type 指定了默认值,无需通过实参来指定动物类型,因此在函数调用中只包含一个实参——宠物的名字。然而,Python依然将这个实参视为位置实参,因此如果函数调用中只包含宠物的名字,这个实参将关联到函数定义中的第一个形参。这就是需要将pet_name 放在形参列表开头的原因所在。

    现在,使用这个函数的最简单的方式是,在函数调用中只提供小狗的名字:

describe_pet('willie')

    这个函数调用的输出与前一个示例相同。只提供了一个实参——‘willie’ ,这个实参将关联到函数定义中的第一个形参——pet_name 。由于没有给animal_type 提供实参,因此Python使用其默认值’dog’ 。

如果要描述的动物不是小狗,可使用类似于下面的函数调用:

describe_pet(pet_name='harry', animal_type='hamster')

由于显式地给animal_type 提供了实参,因此Python将忽略这个形参的默认值。

注意 使用默认值时,在形参列表中必须先列出没有默认值的形参,再列出有默认值的实参。这让Python依然能够正确地解读位置实参。

4.2.4 等效的函数调用

    鉴于可混合使用位置实参、关键字实参和默认值,通常有多种等效的函数调用方式。请看下面的函数describe_pets() 的定义,其中给一个形参提供了默认值:

def describe_pet(pet_name, animal_type='dog'):

    基于这种定义,在任何情况下都必须给pet_name 提供实参;指定该实参时可以使用位置方式,也可以使用关键字方式。如果要描述的动物不是小狗,还必须在函数调用中给animal_type 提供实参;同样,指定该实参时可以使用位置方式,也可以使用关键字方式。

下面对这个函数的所有调用都可行:

# 一条名为Willie的小狗
describe_pet('willie')
describe_pet(pet_name='willie')
# 一只名为Harry的仓鼠
describe_pet('harry', 'hamster')
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='hamster', pet_name='harry')

这些函数调用的输出与前面的示例相同。

注意 使用哪种调用方式无关紧要,只要函数调用能生成你希望的输出就行。使用对你来说最容易理解的调用方式即可。

4.2.5 避免实参错误

    等你开始使用函数后,如果遇到实参不匹配错误,不要大惊小怪。你提供的实参多于或少于函数完成其工作所需的信息时,将出现实参不匹配错误。例如,如果调用函数describe_pet() 时没有指定任何实参,结果将如何呢?

def describe_pet(animal_type, pet_name):
    """显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")
describe_pet()

Python发现该函数调用缺少必要的信息,而traceback指出了这一点:

Traceback (most recent call last): 
    File "pets.py", line 6, in <module> //❶ 
    describe_pet() //❷ 
    TypeError: describe_pet() missing 2 required positional arguments: 'animal_type' and 'pet_name' //

    在❶处,traceback指出了问题出在什么地方,让我们能够回过头去找出函数调用中的错误。在❷处,指出了导致问题的函数调用。在❸处,traceback指出该函数调用少两个实参,并指出了相应形参的名称。如果这个函数存储在一个独立的文件中,我们也许无需打开这个文件并查看函数的代码,就能重新正确地编写函数调用。

    Python读取函数的代码,并指出我们需要为哪些形参提供实参,这提供了极大的帮助。这也是应该给变量和函数指定描述性名称的另一个原因;如果你这样做了,那么无论对于你,还是可能使用你编写的代码的其他任何人来说,Python提供的错误消息都将更有帮助。

如果提供的实参太多,将出现类似的traceback,帮助你确保函数调用和函数定义匹配。

4.3 返回值

    函数并非总是直接显示输出,相反,它可以处理一些数据,并返回一个或一组值。函数返回的值被称为返回值 返 。在函数中,可使用return 语句将值返回到调用函数的代码行。

返回值让你能够将程序的大部分繁重工作移到函数中去完成,从而简化主程序。

4.3.1 返回简单值

下面来看一个函数,它接受名和姓并返回整洁的姓名:
formatted_name.py

def get_formatted_name(first_name, last_name): //"""返回整洁的姓名""" 
    full_name = first_name + ' ' + last_name  //return full_name.title() //❸ 
musician = get_formatted_name('jimi', 'hendrix') //print(musician)

    函数get_formatted_name() 的定义通过形参接受名和姓(见❶)。它将姓和名合而为一,在它们之间加上一个空格,并将结果存储在变量full_name 中(见❷)。然后,将full_name 的值转换为首字母大写格式,并将结果返回到函数调用行(见❸)。

    调用返回值的函数时,需要提供一个变量,用于存储返回的值。在这里,将返回值存储在了变量musician 中(见❹)。输出为整洁的姓名:

Jimi Hendrix

我们原本只需编写下面的代码就可输出整洁的姓名,相比于此,前面做的工作好像太多了:

print("Jimi Hendrix")

    但在需要分别存储大量名和姓的大型程序中,像get_formatted_name() 这样的函数非常有用。你分别存储名和姓,每当需要显示姓名时都调用这个函数。

4.3.2 让实参变成可选的

    有时候,需要让实参变成可选的,这样使用函数的人就只需在必要时才提供额外的信息。可使用默认值来让实参变成可选的。

    假设我们要扩展函数get_formatted_name() ,使其还处理中间名。为此,可将其修改成类似于下面这样:

def get_formatted_name(first_name, middle_name, last_name):
    """返回整洁的姓名"""
    full_name = first_name + ' ' + middle_name + ' ' + last_name
    return full_name.title()
musician = get_formatted_name('john', 'lee', 'hooker')
print(musician)

    只要同时提供名、中间名和姓,这个函数就能正确地运行。它根据这三部分创建一个字符串,在适当的地方加上空格,并将结果转换为首字母大写格式:

John Lee Hooker

    然而,并非所有的人都有中间名,但如果你调用这个函数时只提供了名和姓,它将不能正确地运行。为让中间名变成可选的,可给实参middle_name 指定一个默认值——空字符串,并在用户没有提供中间名时不使用这个实参。为让get_formatted_name() 在没有提供中间名时依然可行,可给实参middle_name 指定一个默认值——空字符串,并将其移到形参列表的末尾:

def get_formatted_name(first_name, last_name, middle_name=''): //"""返回整洁的姓名"""
    if middle_name: // ❷ 
        full_name = first_name + ' ' + middle_name + ' ' + last_name //else:
        full_name = first_name + ' ' + last_name
    return full_name.title()
musician = get_formatted_name('jimi', 'hendrix')
print(musician)

musician = get_formatted_name('john', 'hooker', 'lee') //print(musician)

    在这个示例中,姓名是根据三个可能提供的部分创建的。由于人都有名和姓,因此在函数定义中首先列出了这两个形参。中间名是可选的,因此在函数定义中最后列出该形参,并将其默认值设置为空字符串(见❶)。

    在函数体中,我们检查是否提供了中间名。Python将非空字符串解读为True ,因此如果函数调用中提供了中间名,if middle_name 将为True (见❷)。如果提供了中间名,就将名、中间名和姓合并为姓名,然后将其修改为首字母大写格式,并返回到函数调用行。在函数调用行,将返回的值存储在变量musician 中;然后将这个变量的值打印出来。如果没有提供中间名,middle_name 将为空字符串,导致if 测试未通过,进而执行else 代码块(见❸):只使用名和姓来生成姓名,并将设置好格式的姓名返回给函数调用行。在函数调用行,将返回的值存储在变量musician 中;然后将这个变量的值打印出来。

    调用这个函数时,如果只想指定名和姓,调用起来将非常简单。如果还要指定中间名,就必须确保它是最后一个实参,这样Python才能正确地将位置实参关联到形参(见❹)。

这个修改后的版本适用于只有名和姓的人,也适用于还有中间名的人:

Jimi Hendrix
John Lee Hooker

可选值让函数能够处理各种不同情形的同时,确保函数调用尽可能简单。

4.3.3 返回字典

    函数可返回任何类型的值,包括列表和字典等较复杂的数据结构。例如,下面的函数接受姓名的组成部分,并返回一个表示人的字典:
person.py

def build_person(first_name, last_name):
"""返回一个字典,其中包含有关一个人的信息""" 
person = {'first': first_name, 'last': last_name}  //return person //❷ 
musician = build_person('jimi', 'hendrix') //
print(musician) //

    函数build_person() 接受名和姓,并将这些值封装到字典中(见❶)。存储first_name 的值时,使用的键为’first’ ,而存储last_name 的值时,使用的键为’last’ 。最后,返回表示人的整个字典(见❷)。在❸处,打印这个返回的值,此时原来的两项文本信息存储在一个字典中:

{'first': 'jimi', 'last': 'hendrix'}

    这个函数接受简单的文本信息,将其放在一个更合适的数据结构中,让你不仅能打印这些信息,还能以其他方式处理它们。当前,字符串’jimi’ 和’hendrix’ 被标记为名和姓。你可以轻松地扩展这个函数,使其接受可选值,如中间名、年龄、职业或你要存储的其他任何信息。例如,下面的修改让你还能存储年龄:

def build_person(first_name, last_name, age=''):
    """返回一个字典,其中包含有关一个人的信息"""
    person = {'first': first_name, 'last': last_name}
    if age:
        person['age'] = age
    return person
    
musician = build_person('jimi', 'hendrix', age=27)
print(musician)

    在函数定义中,我们新增了一个可选形参age ,并将其默认值设置为空字符串。如果函数调用中包含这个形参的值,这个值将存储到字典中。在任何情况下,这个函数都会存储人的姓名,但可对其进行修改,使其也存储有关人的其他信息。

4.3.4 结合使用函数和 结 while 循环循

    可将函数同本书前面介绍的任何Python结构结合起来使用。例如,下面将结合使用函数get_formatted_name() 和while 循环,以更正规的方式问候用户。下面尝试使用名和姓跟用户打招呼:
greeter.py

def get_formatted_name(first_name, last_name):
    """返回整洁的姓名"""
    full_name = first_name + ' ' + last_name
    return full_name.title()
    
# 这是一个无限循环!
    while True: 
    print("\nPlease tell me your name:") //❶ 
    f_name = input("First name: ")
    l_name = input("Last name: ")
    formatted_name = get_formatted_name(f_name, l_name)
    print("\nHello, " + formatted_name + "!")

    在这个示例中,我们使用的是get_formatted_name() 的简单版本,不涉及中间名。其中的while 循环让用户输入姓名:依次提示用户输入名和姓(见❶)。

    但这个while 循环存在一个问题:没有定义退出条件。请用户提供一系列输入时,该在什么地方提供退出条件呢?我们要让用户能够尽可能容易地退出,因此每次提示用户输入时,都应提供退出途径。每次提示用户输入时,都使用break 语句提供了退出循环的简单途径:

def get_formatted_name(first_name, last_name):
    """返回整洁的姓名"""
    full_name = first_name + ' ' + last_name
    return full_name.title()
while True:
    print("\nPlease tell me your name:")
    print("(enter 'q' at any time to quit)")
    f_name = input("First name: ")
    if f_name == 'q':
        break
    l_name = input("Last name: ")
    if l_name == 'q':
        break
        
formatted_name = get_formatted_name(f_name, l_name)
print("\nHello, " + formatted_name + "!")

    我们添加了一条消息来告诉用户如何退出,然后在每次提示用户输入时,都检查他输入的是否是退出值,如果是,就退出循环。现在,这个程序将不断地问候,直到用户输入的姓或名为’q’ 为止:

Please tell me your name:
(enter 'q' at any time to quit)
First name: eric
Last name: matthes
Hello, Eric Matthes!
Please tell me your name:
(enter 'q' at any time to quit)
First name: q

4.4 传递列表

    你经常会发现,向函数传递列表很有用,这种列表包含的可能是名字、数字或更复杂的对象(如字典)。将列表传递给函数后,函数就能直接访问其内容。下面使用函数来提高处理列表的效率。

    假设有一个用户列表,我们要问候其中的每位用户。下面的示例将一个名字列表传递给一个名为greet_users() 的函数,这个函数问候列表中的每个人:
greet_users.py

def greet_users(names):
    """向列表中的每位用户都发出简单的问候"""
    for name in names:
        msg = "Hello, " + name.title() + "!"
        print(msg)
usernames = ['hannah', 'ty', 'margot'] //❶ 
greet_users(usernames)

    我们将greet_users() 定义成接受一个名字列表,并将其存储在形参names 中。这个函数遍历收到的列表,并对其中的每位用户都打印一条问候语。在❶处,我们定义了一个用户列表——usernames ,然后调用greet_users() ,并将这个列表传递给它:

Hello, Hannah!
Hello, Ty!
Hello, Margot!

    输出完全符合预期,每位用户都看到了一条个性化的问候语。每当你要问候一组用户时,都可调用这个函数。

4.4.1 在函数中修改列表

    将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。

    来看一家为用户提交的设计制作3D打印模型的公司。需要打印的设计存储在一个列表中,打印后移到另一个列表中。下面是在不使用函数的情况下模拟这个过程的代码:
printing_models.py

# 首先创建一个列表,其中包含一些要打印的设计
unprinted_designs = ['iphone case', 'robot pendant', 'dodecahedron']
completed_models = []
# 模拟打印每个设计,直到没有未打印的设计为止
# 打印每个设计后,都将其移到列表completed_models中
while unprinted_designs:
    current_design = unprinted_designs.pop()
    #模拟根据设计制作3D打印模型的过程
    print("Printing model: " + current_design)
    completed_models.append(current_design)
# 显示打印好的所有模型
print("\nThe following models have been printed:")
for completed_model in completed_models:
print(completed_model)

    这个程序首先创建一个需要打印的设计列表,还创建一个名为completed_models 的空列表,每个设计打印都将移到这个列表中。只要列表unprinted_designs 中还有设计,while 循环就模拟打印设计的过程:从该列表末尾删除一个设计,将其存储到变量current_design 中,并显示一条消息,指出正在打印当前的设计,再将该设计加入到列表completed_models 中。循环结束后,显示已打印的所有设计:

Printing model: dodecahedron
Printing model: robot pendant
Printing model: iphone case
The following models have been printed:
dodecahedron
robot pendant
iphone case

    为重新组织这些代码,我们可编写两个函数,每个都做一件具体的工作。大部分代码都与原来相同,只是效率更高。第一个函数将负责处理打印设计的工作,而第二个将概述打印了哪些设计:

def print_models(unprinted_designs, completed_models): //""" 模拟打印每个设计,直到没有未打印的设计为止
    打印每个设计后,都将其移到列表completed_models中
    """
    while unprinted_designs:
        current_design = unprinted_designs.pop()
        # 模拟根据设计制作3D打印模型的过程
        print("Printing model: " + current_design)
        completed_models.append(current_design)
def show_completed_models(completed_models): //"""显示打印好的所有模型"""
    print("\nThe following models have been printed:")
    for completed_model in completed_models:
        print(completed_model)
unprinted_designs = ['iphone case', 'robot pendant', 'dodecahedron']
completed_models = []

print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)

    在❶处,我们定义了函数print_models() ,它包含两个形参:一个需要打印的设计列表和一个打印好的模型列表。给定这两个列表,这个函数模拟打印每个设计的过程:将设计逐个地从未打印的设计列表中取出,并加入到打印好的模型列表中。在❷处,我们定义了函数show_completed_models() ,它包含一个形参:打印好的模型列表。给定这个列表,函数show_completed_models() 显示打印出来的每个模型的名称。

    这个程序的输出与未使用函数的版本相同,但组织更为有序。完成大部分工作的代码都移到了两个函数中,让主程序更容易理解。只要看看主程序,你就知道这个程序的功能容易看清得多:

unprinted_designs = ['iphone case', 'robot pendant', 'dodecahedron']
completed_models = []
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)

    我们创建了一个未打印的设计列表,还创建了一个空列表,用于存储打印好的模型。接下来,由于我们已经定义了两个函数,因此只需调用它们并传入正确的实参即可。我们调用print_models() 并向它传递两个列表;像预期的一样,print_models() 模拟打印设计的过程。接下来,我们调用show_completed_models() ,并将打印好的模型列表传递给它,让其能够指出打印了哪些模型。描述性的函数名让别人阅读这些代码时也能明白,虽然其中没有任何注释。

    相比于没有使用函数的版本,这个程序更容易扩展和维护。如果以后需要打印其他设计,只需再次调用print_models() 即可。如果我们发现需要对打印代码进行修改,只需修改这些代码一次,就能影响所有调用该函数的地方;与必须分别修改程序的多个地方相比,这种修改的效率更高。这个程序还演示了这样一种理念,即每个函数都应只负责一项具体的工作。第一个函数打印每个设计,而第二个显示打印好的模型;这优于使用一个函数来完成两项工作。编写函数时,如果你发现它执行的任务太多,请尝试将这些代码划分到两个函数中。别忘了,总是可以在一个函数中调用另一个函数,这有助于将复杂的任务划分成一系列的步骤。

4.4.2 禁止函数修改列表

    有时候,需要禁止函数修改列表。例如,假设像前一个示例那样,你有一个未打印的设计列表,并编写了一个将这些设计移到打印好的模型列表中的函数。你可能会做出这样的决定:即便打印所有设计后,也要保留原来的未打印的设计列表,以供备案。但由于你将所有的设计都移出了unprinted_designs ,这个列表变成了空的,原来的列表没有了。为解决这个问题,可向函数传递列表的副本而不是原件;这样函数所做的任何修改都只影响副本,而丝毫不影响原件。

要将列表的副本传递给函数,可以像下面这样做:

function_name(list_name[:])

    切片表示法[:] 创建列表的副本。在print_models.py中,如果不想清空未打印的设计列表,可像下面这样调用print_models() :

print_models(unprinted_designs[:], completed_models)

    这样函数print_models() 依然能够完成其工作,因为它获得了所有未打印的设计的名称,但它使用的是列表unprinted_designs 的副本,而不是列表unprinted_designs 本身。像以前一样,列表completed_models 也将包含打印好的模型的名称,但函数所做的修改不会影响到列表unprinted_designs 。

    虽然向函数传递列表的副本可保留原始列表的内容,但除非有充分的理由需要传递副本,否则还是应该将原始列表传递给函数,因为让函数使用现成列表可避免花时间和内存创建副本,从而提高效率,在处理大型列表时尤其如此。

4.5 传递任意数量的实参

    有时候,你预先不知道函数需要接受多少个实参,好在Python允许函数从调用语句中收集任意数量的实参。

    例如,来看一个制作比萨的函数,它需要接受很多配料,但你无法预先确定顾客要多少种配料。下面的函数只有一个形参*toppings ,但不管调用语句提供了多少实参,这个形参都将它们统统收入囊中:
pizza.py

def make_pizza(*toppings):
    """打印顾客点的所有配料"""
    print(toppings)
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

    形参名*toppings 中的星号让Python创建一个名为toppings 的空元组,并将收到的所有值都封装到这个元组中。函数体内的print 语句通过生成输出来证明Python能够处理使用一个值调用函数的情形,也能处理使用三个值来调用函数的情形。它以类似的方式处理不同的调用,注意,Python将实参封装到一个元组中,即便函数只收到一个值也如此:

('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')

    现在,我们可以将这条print 语句替换为一个循环,对配料列表进行遍历,并对顾客点的比萨进行描述:

def make_pizza(*toppings):
    """概述要制作的比萨"""
    print("\nMaking a pizza with the following toppings:")
    for topping in toppings:
        print("- " + topping)
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

不管收到的是一个值还是三个值,这个函数都能妥善地处理:

Making a pizza with the following toppings:
- pepperoni
Making a pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese

不管函数收到的实参是多少个,这种语法都管用。

4.5.1 结合使用位置实参和任意数量实参

    如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参和关键字实参,再将余下的实参都收集到最后一个形参中。

例如,如果前面的函数还需要一个表示比萨尺寸的实参,必须将该形参放在形参*toppings 的前面:

def make_pizza(size, *toppings):
    """概述要制作的比萨"""
    print("\nMaking a " + str(size) +
        "-inch pizza with the following toppings:")
    for topping in toppings:
        print("- " + topping)
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

    基于上述函数定义,Python将收到的第一个值存储在形参size 中,并将其他的所有值都存储在元组toppings 中。在函数调用中,首先指定表示比萨尺寸的实参,然后根据需要指定任意数量的配料。

    现在,每个比萨都有了尺寸和一系列配料,这些信息按正确的顺序打印出来了——首先是尺寸,然后是配料:

Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese

4.5.2 使用任意数量的关键字实参

    有时候,需要接受任意数量的实参,但预先不知道传递给函数的会是什么样的信息。在这种情况下,可将函数编写成能够接受任意数量的键—值对——调用语句提供了多少就接受多少。一个这样的示例是创建用户简介:你知道你将收到有关用户的信息,但不确定会是什么样的信息。

在下面的示例中,函数build_profile() 接受名和姓,同时还接受任意数量的关键字实参:
user_profile.py

def build_profile(first, last, **user_info):
    """创建一个字典,其中包含我们知道的有关用户的一切"""
    profile = {}
    profile['first_name'] = first # ❶ 
    profile['last_name'] = last # ❷ 
    for key, value in user_info.items():
        profile[key] = value
    return profile
user_profile = build_profile('albert', 'einstein',
                             location='princeton',
                             field='physics')
print(user_profile)

    函数build_profile() 的定义要求提供名和姓,同时允许用户根据需要提供任意数量的名称—值对。形参**user_info 中的两个星号让Python创建一个名为user_info 的空字典,并将收到的所有名称—值对都封装到这个字典中。在这个函数中,可以像访问其他字典那样访问user_info 中的名称—值对。

    在build_profile() 的函数体内,我们创建了一个名为profile 的空字典,用于存储用户简介。在❶处,我们将名和姓加入到这个字典中,因为我们总是会从用户那里收到这两项信息。在❷处,我们遍历字典user_info 中的键—值对,并将每个键—值对都加入到字典profile 中。最后,我们将字典profile 返回给函数调用行。

    我们调用build_profile() ,向它传递名(‘albert’ )、姓(‘einstein’ )和两个键—值对(location=‘princeton’ 和field=‘physics’ ),并将返回的profile 存储在变量user_profile 中,再打印这个变量:

{'first_name': 'albert', 'last_name': 'einstein',
'location': 'princeton', 'field': 'physics'}

    在这里,返回的字典包含用户的名和姓,还有求学的地方和所学专业。调用这个函数时,不管额外提供了多少个键—值对,它都能正确地处理。

    编写函数时,你可以以各种方式混合使用位置实参、关键字实参和任意数量的实参。知道这些实参类型大有裨益,因为阅读别人编写的代码时经常会见到它们。要正确地使用这些类型的实参并知道它们的使用时机,需要经过一定的练习。就目前而言,牢记使用最简单的方法来完成任务就好了。你继续往下阅读,就会知道在各种情况下哪种方法的效率是最高的。

4.6 将函数存储在模块中

    函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在被称为模块模 的独立文件中,再将模块导入导 到主程序中。import 语句允许在当前运行的程序文件中使用模块中的代码。

    通过将函数存储在独立的文件中,可隐藏程序代码的细节,将重点放在程序的高层逻辑上。这还能让你在众多不同的程序中重用函数。将函数存储在独立文件中后,可与其他程序员共享这些文件而不是整个程序。知道如何导入函数还能让你使用其他程序员编写的函数库。

导入模块的方法有多种,下面对每种都作简要的介绍。

4.6.1 导入整个模块

    要让函数是可导入的,得先创建模块。模块模 是扩展名为.py的文件,包含要导入到程序中的代码。下面来创建一个包含函数make_pizza() 的模块。为此,我们将文件pizza.py中除函数make_pizza() 之外的其他代码都删除:

pizza.py

def make_pizza(size, *toppings):
    """概述要制作的比萨"""
    print("\nMaking a " + str(size) +
        "-inch pizza with the following toppings:")
    for topping in toppings:
        print("- " + topping)

接下来,我们在pizza.py所在的目录中创建另一个名为making_pizzas.py的文件,这个文件导入刚创建的模块,再调用make_pizza() 两次:
making_pizzas.py

import pizza
pizza.make_pizza(16, 'pepperoni') //❶ 
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

    Python读取这个文件时,代码行import pizza 让Python打开文件pizza.py,并将其中的所有函数都复制到这个程序中。你看不到复制的代码,因为这个程序运行时,Python在幕
后复制这些代码。你只需知道,在making_pizzas.py中,可以使用pizza.py中定义的所有函数。
要调用被导入的模块中的函数,可指定导入的模块的名称pizza 和函数名make_pizza() ,并用句点分隔它们(见❶)。这些代码的输出与没有导入模块的原始程序相同:

Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese

    这就是一种导入方法:只需编写一条import 语句并在其中指定模块名,就可在程序中使用该模块中的所有函数。如果你使用这种import 语句导入了名为module_name.py 的整个模块,就可使用下面的语法来使用其中任何一个函数:

module_name.function_name()

4.6.2 导入特定的函数

你还可以导入模块中的特定函数,这种导入方法的语法如下:

from module_name import function_name

通过用逗号分隔函数名,可根据需要从模块中导入任意数量的函数:

from module_name import function_0, function_1, function_2

对于前面的making_pizzas.py示例,如果只想导入要使用的函数,代码将类似于下面这样:

from pizza import make_pizza
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

若使用这种语法,调用函数时就无需使用句点。由于我们在import 语句中显式地导入了函数make_pizza() ,因此调用它时只需指定其名称。

4.6.3 使用使 as 给函数指定别名

    如果要导入的函数的名称可能与程序中现有的名称冲突,或者函数的名称太长,可指定简短而独一无二的别名别 ——函数的另一个名称,类似于外号。要给函数指定这种特殊外号,需要在导入它时这样做。

    下面给函数make_pizza() 指定了别名mp() 。这是在import 语句中使用make_pizza as mp 实现的,关键字as 将函数重命名为你提供的别名:

from pizza import make_pizza as mp
mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')

    上面的import 语句将函数make_pizza() 重命名为mp() ;在这个程序中,每当需要调用make_pizza() 时,都可简写成mp() ,而Python将运行make_pizza() 中的代
码,这可避免与这个程序可能包含的函数make_pizza() 混淆。
指定别名的通用语法如下:

from module_name import function_name as fn

4.6.4 使用使 as 给模块指定别名

    你还可以给模块指定别名。通过给模块指定简短的别名(如给模块pizza 指定别名p ),让你能够更轻松地调用模块中的函数。相比于pizza.make_pizza() ,p.make_pizza() 更为简洁:

import pizza as p
p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

    上述import 语句给模块pizza 指定了别名p ,但该模块中所有函数的名称都没变。调用函数make_pizza() 时,可编写代码p.make_pizza() 而不是pizza.make_pizza() ,这样不仅能使代码更简洁,还可以让你不再关注模块名,而专注于描述性的函数名。这些函数名明确地指出了函数的功能,对理解代码而言,它们比模块名更重要。

给模块指定别名的通用语法如下:

import module_name as mn

4.6.5 导入模块中的所有函数

使用星号(* )运算符可让Python导入模块中的所有函数:

from pizza import *
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

    import 语句中的星号让Python将模块pizza 中的每个函数都复制到这个程序文件中。由于导入了每个函数,可通过名称来调用每个函数,而无需使用句点表示法。然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法:如果模块中有函数的名称与你的项目中使用的名称相同,可能导致意想不到的结果:Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有的函数。

    最佳的做法是,要么只导入你需要使用的函数,要么导入整个模块并使用句点表示法。这能让代码更清晰,更容易阅读和理解。这里之所以介绍这种导入方法,只是想让你在阅读别人编写的代码时,如果遇到类似于下面的import 语句,能够理解它们:

from module_name import *

4.7 函数编写指南

    编写函数时,需要牢记几个细节。应给函数指定描述性名称,且只在其中使用小写字母和下划线。描述性名称可帮助你和别人明白代码想要做什么。给模块命名时也应遵循上述约定。

    每个函数都应包含简要地阐述其功能的注释,该注释应紧跟在函数定义后面,并采用文档字符串格式。文档良好的函数让其他程序员只需阅读文档字符串中的描述就能够使用它:他们完全可以相信代码如描述的那样运行;只要知道函数的名称、需要的实参以及返回值的类型,就能在自己的程序中使用它。

给形参指定默认值时,等号两边不要有空格:

def function_name(parameter_0, parameter_1='default value')

对于函数调用中的关键字实参,也应遵循这种约定:

function_name(value_0, parameter_1='value')

PEP 8(https://www.python.org/dev/peps/pep-0008/ )建议代码行的长度不要超过79字符,这样只要编辑器窗口适中,就能看到整行代码。如果形参很多,导致函数定义的长度超过了79字符,可在函数定义中输入左括号后按回车键,并在下一行按两次Tab键,从而将形参列表和只缩进一层的函数体区分开来。
大多数编辑器都会自动对齐后续参数列表行,使其缩进程度与你给第一个参数列表行指定的缩进程度相同:

def function_name(
parameter_0, parameter_1, parameter_2,
parameter_3, parameter_4, parameter_5):
function body...

   如果程序或模块包含多个函数,可使用两个空行将相邻的函数分开,这样将更容易知道前一个函数在什么地方结束,下一个函数从什么地方开始。

所有的import 语句都应放在文件开头,唯一例外的情形是,在文件开头使用了注释来描述整个程序。

第5章 类

5.1 创建和使用类

    使用类几乎可以模拟任何东西。下面来编写一个表示小狗的简单类Dog ——它表示的不是特定的小狗,而是任何小狗。对于大多数宠物狗,我们都知道些什么呢?它们都有名字和年龄;我们还知道,大多数小狗还会蹲下和打滚。由于大多数小狗都具备上述两项信息(名字和年龄)和两种行为(蹲下和打滚),我们的Dog 类将包含它们。这个类让Python知道如何创建表示小狗的对象。编写这个类后,我们将使用它来创建表示特定小狗的实例。

5.1.1创建创 Dog 类

根据Dog 类创建的每个实例都将存储名字和年龄。我们赋予了每条小狗蹲下(sit() )和打滚(roll_over() )的能力:

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!")
  1. 方法 init()
        init() 是一个特殊的方法,每当你根据Dog 类创建新实例时,Python都会自动运行它。在这个方法的名称中,开头和末尾各有两个下划线,这是一种约定,旨在避免Python默认方法与普通方法发生名称冲突。

    我们将方法__init__() 定义成了包含三个形参:self 、name 和age 。在这个方法的定义中,形参self 必不可少,还必须位于其他形参的前面。为何必须在方法定义中包含形参self 呢?

    因为Python调用这个__init__() 方法来创建Dog 实例时,将自动传入实参self 。每个与类相关联的方法调用都自动传递实参self ,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。我们创建Dog 实例时,Python将调用Dog 类的方法__init__() 。我们将通过实参向Dog() 传递名字和年龄;self 会自动传递,因此我们不需要传递它。每当我们根据Dog 类创建实例时,都只需给最后两个形参(name 和age )提供值。

    处义的两个变量都有前缀self 。以self 为前缀的变量都可供类中的所有方法使用,我们还可以通过类的任何实例来访问这些变量。self.name = name 获取存储在形参name 中的值,并将其存储到变量name 中,然后该变量被关联到当前创建的实例。self.age = age 的作用与此类似。像这样可通过实例访问的变量称为属性属 。

    Dog 类还定义了另外两个方法:sit() 和roll_over() 。由于这些方法不需要额外的信息,如名字或年龄,因此它们只有一个形参self 。我们后面将创建的实例能够访问这些方法,换句话说,它们都会蹲下和打滚。当前,sit() 和roll_over() 所做的有限,它们只是打印一条消息,指出小狗正蹲下或打滚。但可以扩展这些方法以模拟实际情况:如果这个类包含在一个计算机游戏中,这些方法将包含创建小狗蹲下和打滚动画效果的代码。如果这个类是用于控制机器狗的,这些方法将引导机器狗做出蹲下和打滚的动作。

5.1.2 根据类创建实例

   可将类视为有关如何创建实例的说明。Dog 类是一系列说明,让Python知道如何创建表示特定小狗的实例。

下面来创建一个表示特定小狗的实例:

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.") //

    我们让Python创建一条名字为’willie’ 、年龄为6 的小狗。遇到这行代码时,Python使用实参’willie’ 和6 调用Dog 类中的方法__init__() 。方法__init__() 创建一个表示特定小狗的示例,并使用我们提供的值来设置属性name 和age 。方法__init__() 并未显式地包含return 语句,但Python自动返回一个表示这条小狗的实例。我们将这个实例存储在变量my_dog 中。在这里,命名约定很有用:我们通常可以认为首字母大写的名称(如Dog )指的是类,而小写的名称(如my_dog )指的是根据类创建的实例。

  1. 访问属性
       要访问实例的属性,可使用句点表示法。在❷处,我们编写了如下代码来访问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.
  1. 调用方法
       根据Dog 类创建实例后,就可以使用句点表示法来调用Dog 类中定义的任何方法。下面来让小狗蹲下和打滚:
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() 。

  1. 创建多个实例
       可按需求根据类创建任意数量的实例。下面再创建一个名为your_dog 的实例:
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 类创建另一个实例。你可按需求根据一个类创建任意数量的实例,条件是将每个实例都存储在不同的变量中,或占用列表或字典的不同位置。

5.2 使用类和实例

   你可以使用类来模拟现实世界中的很多情景。类编写好后,你的大部分时间都将花在使用根据类创建的实例上。你需要执行的一个重要任务是修改实例的属性。你可以直接修改实例的属性,也可以编写方法以特定的方式进行修改。

5.2.1 Car 类

   下面来编写一个表示汽车的类,它存储了有关汽车的信息,还有一个汇总这些信息的方法:
Car.py

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.model
        return long_name.title()
my_new_car = Car('audi', 'a4', 2016) //print(my_new_car.get_descriptive_name())

   在❶处,我们定义了方法__init__() 。与前面的Dog 类中一样,这个方法的第一个形参为self ;我们还在这个方法中包含了另外三个形参:make 、model 和year 。方法__init__() 接受这些形参的值,并将它们存储在根据这个类创建的实例的属性中。创建新的Car 实例时,我们需要指定其制造商、型号和生产年份。

   在❷处,我们定义了一个名为get_descriptive_name() 的方法,它使用属性year 、make 和model 创建一个对汽车进行描述的字符串,让我们无需分别打印每个属性的值。为在这个方法中访问属性的值,我们使用了self.make 、self.model 和self.year 。在❸处,我们根据Car 类创建了一个实例,并将其存储到变量my_new_car 中。接下来,我们调用方法get_descriptive_name() ,指出我们拥有的是一辆什么样的汽车:

2016 Audi A4

5.2.2 给属性指定默认值

   类中的每个属性都必须有初始值,哪怕这个值是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):
        --snip--
    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 A4
This car has 0 miles on it.

出售时里程表读数为0的汽车并不多,因此我们需要一个修改该属性的值的途径。

9.2.3 修改属性的值

   可以以三种不同的方式修改属性的值:直接通过实例进行修改;通过方法进行设置;通过方法进行递增(增加特定的值)。

下面依次介绍这些方法。

  1. 直接修改属性的值 直
       要修改属性的值,最简单的方式是通过实例直接访问它。下面的代码直接将里程表读数设置为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()

   在❶处,我们使用句点表示法来直接访问并设置汽车的属性odometer_reading 。这行代码让Python在实例my_new_car 中找到属性odometer_reading ,并将该属性的值设置为23:

2016 Audi A4
This car has 23 miles on it.

有时候需要像这样直接访问属性,但其他时候需要编写对属性进行更新的方法。

  1. 通过方法修改属性的值
       如果有替你更新属性的方法,将大有裨益。这样,你就无需直接访问属性,而可将值传递给一个方法,由它在内部进行更新。

下面的示例演示了一个名为update_odometer() 的方法:

class Car():
    --snip--
    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 A4
This car has 23 miles on it.

   可对方法update_odometer() 进行扩展,使其在修改里程表读数时做些额外的工作。下面来添加一些逻辑,禁止任何人将里程表读数往回调:

class Car():
--snip--
    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 ),就将里程表读数改为新指定的里程(见❶);否则就发出警告,指出不能将里程表往回拨(见❷)。

  1. 通过方法对属性的值进行递增
       有时候需要将属性值递增特定的量,而不是将其设置为全新的值。假设我们购买了一辆二手车,且从购买到登记期间增加了100英里的里程,下面的方法让我们能够传递这个增量,并相应地增加里程表读数:
class Car():
--snip--
def update_odometer(self, mileage):
--snip--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 Outback
This car has 23500 miles on it.
This car has 23600 miles on it.

   你可以轻松地修改这个方法,以禁止增量为负值,从而防止有人利用它来回拨里程表。
注意 你可以使用类似于上面的方法来控制用户修改属性值(如里程表读数)的方式,但能够访问程序的人都可以通过直接访问属性来将里程表修改为任何值。要确保安全,除了进行类似于前面的基本检查外,还需特别注意细节。

5.3 继承

   编写类时,并非总是要从空白开始。如果你要编写的类是另一个现成类的特殊版本,可使用继承继 。一个类继承继 另一个类时,它将自动获得另一个类的所有属性和方法;原有的类称为父类父 ,而新类称为子类子 。子类继承了其父类的所有属性和方法,同时还可以定义自己的属性和方法。

5.3.1 子类的方法 子 init()

   创建子类的实例时,Python首先需要完成的任务是给父类的所有属性赋值。为此,子类的方法__init__() 需要父类施以援手。

   例如,下面来模拟电动汽车。电动汽车是一种特殊的汽车,因此我们可以在前面创建的Car 类的基础上创建新类ElectricCar ,这样我们就只需为电动汽车特有的属性和行为编写代码。

下面来创建一个简单的ElectricCar 类版本,它具备Car 类的所有功能:
electric_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't roll back an odometer!")
    def increment_odometer(self, miles):
        self.odometer_reading += miles
class ElectricCar(Car): # ❷ 
    """电动汽车的独特之处"""
    def __init__(self, make, model, year): # ❸ 
        """初始化父类的属性""" 
        super().__init__(make, model, year) # ❹ 
my_tesla = ElectricCar('tesla', 'model s', 2016) # ❺ 
print(my_tesla.get_descriptive_name())

   首先是Car 类的代码(见❶)。创建子类时,父类必须包含在当前文件中,且位于子类前面。在❷处,我们定义了子类ElectricCar 。定义子类时,必须在括号内指定父类的名称。方法__init__() 接受创建Car 实例所需的信息(见❸)。

   ❹处的super() 是一个特殊函数,帮助Python将父类和子类关联起来。这行代码让Python调用ElectricCar 的父类的方法__init__() ,让ElectricCar 实例包含父类的所有属性。父类也称为超类超 (superclass),名称super因此而得名。

   为测试继承是否能够正确地发挥作用,我们尝试创建一辆电动汽车,但提供的信息与创建普通汽车时相同。在❺处,我们创建ElectricCar 类的一个实例,并将其存储在变量my_tesla 中。这行代码调用ElectricCar 类中定义的方法__init__() ,后者让Python调用父类Car 中定义的方法__init__() 。我们提供了实参’tesla’ 、‘models’ 和2016 。

   除方法__init__() 外,电动汽车没有其他特有的属性和方法。当前,我们只想确认电动汽车具备普通汽车的行为:
2016 Tesla Model S

ElectricCar 实例的行为与Car 实例一样,现在可以开始定义电动汽车特有的属性和方法了。

5.3.2 给子类定义属性和方法

   让一个类继承另一个类后,可添加区分子类和父类所需的新属性和方法。

   下面来添加一个电动汽车特有的属性(电瓶),以及一个描述该属性的方法。我们将存储电瓶容量,并编写一个打印电瓶描述的方法:

class Car():
--snip--
class ElectricCar(Car):
"""Represent aspects of a car, specific to electric vehicles."""
def __init__(self, make, model, year):
""" 电动汽车的独特之处
初始化父类的属性,再初始化电动汽车特有的属性
"""
super().__init__(make, model, year)
self.battery_size = 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()

   在❶处,我们添加了新属性self.battery_size ,并设置其初始值(如70 )。根据ElectricCar 类创建的所有实例都将包含这个属性,但所有Car 实例都不包含它。在❷处,我们还添加了一个名为describe_battery() 的方法,它打印有关电瓶的信息。我们调用这个方法时,将看到一条电动汽车特有的描述:

2016 Tesla Model S
This car has a 70-kWh battery.

   对于ElectricCar 类的特殊化程度没有任何限制。模拟电动汽车时,你可以根据所需的准确程度添加任意数量的属性和方法。如果一个属性或方法是任何汽车都有的,而不是电动汽车特有的,就应将其加入到Car 类而不是ElectricCar 类中。这样,使用Car 类的人将获得相应的功能,而ElectricCar 类只包含处理电动汽车特有属性和行为的代码。

5.3.3 重写父类的方法

   对于父类的方法,只要它不符合子类模拟的实物的行为,都可对其进行重写。为此,可在子类中定义一个这样的方法,即它与要重写的父类方法同名。这样,Python将不会考虑这个父类方法,而只关注你在子类中定义的相应方法。

   假设Car 类有一个名为fill_gas_tank() 的方法,它对全电动汽车来说毫无意义,因此你可能想重写它。下面演示了一种重写方式:

def ElectricCar(Car):
--snip--
def fill_gas_tank():
"""电动汽车没有油箱"""
print("This car doesn't need a gas tank!")

   现在,如果有人对电动汽车调用方法fill_gas_tank() ,Python将忽略Car 类中的方法fill_gas_tank() ,转而运行上述代码。使用继承时,可让子类保留从父类那里继承而来的精华,并剔除不需要的糟粕。

5.3.4 将实例用作属性

   使用代码模拟实物时,你可能会发现自己给类添加的细节越来越多:属性和方法清单以及文件都越来越长。在这种情况下,可能需要将类的一部分作为一个独立的类提取出来。

   你可以将大型类拆分成多个协同工作的小类。

   例如,不断给ElectricCar 类添加细节时,我们可能会发现其中包含很多专门针对汽车电瓶的属性和方法。在这种情况下,我们可将这些属性和方法提取出来,放到另一个名为Battery 的类中,并将一个Battery 实例用作ElectricCar 类的一个属性:

class Car():
--snip--
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.")
class ElectricCar(Car):
"""电动汽车的独特之处"""
def __init__(self, make, model, year):
""" 初始化父类的属性,再初始化电动汽车特有的属性
"""
super().__init__(make, model, year) 
self.battery = Battery() //❹ 
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()

   在❶处,我们定义了一个名为Battery 的新类,它没有继承任何类。❷处的方法__init__() 除self 外,还有另一个形参battery_size 。这个形参是可选的:如果没有给它提供值,电瓶容量将被设置为70。方法describe_battery() 也移到了这个类中(见❸)。

   在ElectricCar 类中,我们添加了一个名为self.battery 的属性(见❹)。这行代码让Python创建一个新的Battery 实例(由于没有指定尺寸,因此为默认值70 ),并将该实例存储在属性self.battery 中。每当方法__init__() 被调用时,都将执行该操作;因此现在每个ElectricCar 实例都包含一个自动创建的Battery 实例。

   我们创建一辆电动汽车,并将其存储在变量my_tesla 中。要描述电瓶时,需要使用电动汽车的属性battery :
my_tesla.battery.describe_battery()
   这行代码让Python在实例my_tesla 中查找属性battery ,并对存储在该属性中的Battery 实例调用方法describe_battery() 。

输出与我们前面看到的相同:

2016 Tesla Model S
This car has a 70-kWh battery.

   这看似做了很多额外的工作,但现在我们想多详细地描述电瓶都可以,且不会导致ElectricCar 类混乱不堪。

下面再给Battery 类添加一个方法,它根据电瓶容量报告汽车的续航里程:

class Car():
--snip--
class Battery():
--snip--def get_range(self):
"""打印一条消息,指出电瓶的续航里程"""
if self.battery_size == 70:
range = 240
elif self.battery_size == 85:
range = 270
message = "This car can go approximately " + str(range)
message += " miles on a full charge."
print(message)
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_range() 做了一些简单的分析:如果电瓶的容量为70kWh,它就将续航里程设置为240英里;如果容量为85kWh,就将续航里程设置为270英里,然后报告
这个值。为使用这个方法,我们也通过汽车的属性battery 来调用它(见❷)。

输出指出了汽车的续航里程(这取决于电瓶的容量):

2016 Tesla Model S
This car has a 70-kWh battery.
This car can go approximately 240 miles on a full charge.

5.4 导入类

   随着你不断地给类添加功能,文件可能变得很长,即便你妥善地使用了继承亦如此。为遵循Python的总体理念,应让文件尽可能整洁。为在这方面提供帮助,Python允许你将类存储在模块中,然后在主程序中导入所需的模块。

5.4.1 导入单个类 导

   下面来创建一个只包含Car 类的模块。这让我们面临一个微妙的命名问题:在本章中,已经有一个名为car.py的文件,但这个模块也应命名为car.py,因为它包含表示汽车的代码。我们将这样解决这个命名问题:将Car 类存储在一个名为car.py的模块中,该模块将覆盖前面使用的文件car.py。从现在开始,使用该模块的程序都必须使用更具体的文件名,如my_car.py。下面是模块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't roll back an odometer!")
def increment_odometer(self, miles):
"""将里程表读数增加指定的量"""
self.odometer_reading += miles

   在❶处,我们包含了一个模块级文档字符串,对该模块的内容做了简要的描述。你应为自己创建的每个模块都编写文档字符串。

下面来创建另一个文件——my_car.py,在其中导入Car 类并创建其实例:
my_car.py

from car import Car #❶ 
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()

   ❶处的import 语句让Python打开模块car ,并导入其中的Car 类。这样我们就可以使用Car 类了,就像它是在这个文件中定义的一样。输出与我们在前面看到的一样:

2016 Audi A4
This car has 23 miles on it.

   导入类是一种有效的编程方式。如果在这个程序中包含了整个Car 类,它该有多长呀!通过将这个类移到一个模块中,并导入该模块,你依然可以使用其所有功能,但主程序文件变得整洁而易于阅读了。这还能让你将大部分逻辑存储在独立的文件中;确定类像你希望的那样工作后,你就可以不管这些文件,而专注于主程序的高级逻辑了。

5.4.2 在一个模块中存储多个类

   虽然同一个模块中的类之间应存在某种相关性,但可根据需要在一个模块中存储任意数量的类。类Battery 和ElectricCar 都可帮助模拟汽车,因此下面将它们都加入模块car.py中:
car.py

class Car():
--snip--
class Battery():
"""一次模拟电动汽车电瓶的简单尝试"""
def __init__(self, battery_size=60):
"""初始化电瓶的属性"""
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 = 240
elif self.battery_size == 85:
range = 270
message = "This car can go approximately " + str(range)
message += " miles on a full charge."
print(message)
class ElectricCar(Car):
"""模拟电动汽车的独特之处"""
def __init__(self, make, model, year):
""" 初始化父类的属性,再初始化电动汽车特有的属性
"""
super().__init__(make, model, year)
self.battery = Battery()

   现在,可以新建一个名为my_electric_car.py的文件,导入ElectricCar 类,并创建一辆电动汽车了:
my_electric_car.py

from car import ElectricCar
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()

   输出与我们前面看到的相同,但大部分逻辑都隐藏在一个模块中:

2016 Tesla Model S
This car has a 70-kWh battery.
This car can go approximately 240 miles on a full charge.

5.4.3 从一个模块中导入多个类

   可根据需要在程序文件中导入任意数量的类。如果我们要在同一个程序中创建普通汽车和电动汽车,就需要将Car 和ElectricCar 类都导入:
my_cars.py

from car import Car, ElectricCar #❶ 
my_beetle = Car('volkswagen', 'beetle', 2016)#❷ 
print(my_beetle.get_descriptive_name())
my_tesla = ElectricCar('tesla', 'roadster', 2016)#❸ 
print(my_tesla.get_descriptive_name())

   在❶处从一个模块中导入多个类时,用逗号分隔了各个类。导入必要的类后,就可根据需要创建每个类的任意数量的实例。
在这个示例中,我们在❷处创建了一辆大众甲壳虫普通汽车,并在❸处创建了一辆特斯拉Roadster电动汽车:

2016 Volkswagen Beetle
2016 Tesla Roadster

5.4.4 导入整个模块

   你还可以导入整个模块,再使用句点表示法访问需要的类。这种导入方法很简单,代码也易于阅读。由于创建类实例的代码都包含模块名,因此不会与当前文件使用的任何名称发生冲突。
下面的代码导入整个car 模块,并创建一辆普通汽车和一辆电动汽车:
my_cars.py

import car #❶ 
my_beetle = car.Car('volkswagen', 'beetle', 2016)#❷ 
print(my_beetle.get_descriptive_name())
my_tesla = car.ElectricCar('tesla', 'roadster', 2016)#❸ 
print(my_tesla.get_descriptive_name())

   在❶处,我们导入了整个car 模块。接下来,我们使用语法 module_name.class_name 访问需要的类。像前面一样,我们在❷处创建了一辆大众甲壳虫汽车,并在❸处创建了一辆特斯拉Roadster汽车。

5.4.5 导入模块中的所有类

要导入模块中的每个类,可使用下面的语法:

from module_name import *

   不推荐使用这种导入方式,其原因有二。首先,如果只要看一下文件开头的import 语句,就能清楚地知道程序使用了哪些类,将大有裨益;但这种导入方式没有明确地指出你使用了模块中的哪些类。这种导入方式还可能引发名称方面的困惑。如果你不小心导入了一个与程序文件中其他东西同名的类,将引发难以诊断的错误。这里之所以介绍这种导入方式,是因为虽然不推荐使用这种方式,但你可能会在别人编写的代码中见到它。

   需要从一个模块中导入很多类时,最好导入整个模块,并使用 module_name.class_name 语法来访问类。这样做时,虽然文件开头并没有列出用到的所有类,但你清楚地知道在程序的哪些地方使用了导入的模块;你还避免了导入模块中的每个类可能引发的名称冲突。

5.4.6 在一个模块中导入另一个模块

   有时候,需要将类分散到多个模块中,以免模块太大,或在同一个模块中存储不相关的类。将类存储在多个模块中时,你可能会发现一个模块中的类依赖于另一个模块中的类。

   在这种情况下,可在前一个模块中导入必要的类。

   例如,下面将Car 类存储在一个模块中,并将ElectricCar 和Battery 类存储在另一个模块中。我们将第二个模块命名为electric_car.py (这将覆盖前面创建的文件electric_car.py),并将Battery 和ElectricCar 类复制到这个模块中:
electric_car.py

"""一组可用于表示电动汽车的类"""
from car import Car //class Battery():
--snip--
class ElectricCar(Car):
--snip--

   ElectricCar 类需要访问其父类Car ,因此在❶处,我们直接将Car 类导入该模块中。如果我们忘记了这行代码,Python将在我们试图创建ElectricCar 实例时引发错误。

   我们还需要更新模块car ,使其包含Car 类:
car.py

"""一个可用于表示汽车的类"""
class Car():
--snip--

现在可以分别从每个模块中导入类,以根据需要创建任何类型的汽车了:
my_cars.py

from car import Car
from electric_car import ElectricCar
my_beetle = Car('volkswagen', 'beetle', 2016)
print(my_beetle.get_descriptive_name())
my_tesla = ElectricCar('tesla', 'roadster', 2016)
print(my_tesla.get_descriptive_name())

   在❶处,我们从模块car 中导入了Car 类,并从模块electric_car 中导入ElectricCar 类。接下来,我们创建了一辆普通汽车和一辆电动汽车。这两种汽车都得以正确地创建:

2016 Volkswagen Beetle
2016 Tesla Roadster

5.4.7 自定义工作流程

   正如你看到的,在组织大型项目的代码方面,Python提供了很多选项。熟悉所有这些选项很重要,这样你才能确定哪种项目组织方式是最佳的,并能理解别人开发的项目。

   一开始应让代码结构尽可能简单。先尽可能在一个文件中完成所有的工作,确定一切都能正确运行后,再将类移到独立的模块中。如果你喜欢模块和文件的交互方式,可在项目开始时就尝试将类存储到模块中。先找出让你能够编写出可行代码的方式,再尝试让代码更为组织有序。

5.5 Python标准库

   Python标准库 标 是一组模块,安装的Python都包含它。你现在对类的工作原理已有大致的了解,可以开始使用其他程序员编写好的模块了。可使用标准库中的任何函数和类,为此只需在程序开头包含一条简单的import 语句。下面来看模块collections 中的一个类——OrderedDict 。
字典让你能够将信息关联起来,但它们不记录你添加键—值对的顺序。要创建字典并记录其中的键—值对的添加顺序,可使用模块collections 中的OrderedDict 类。OrderedDict 实例的行为几乎与字典相同,区别只在于记录了键—值对的添加顺序。

   我们再来看一看第6章的favorite_languages.py示例,但这次将记录被调查者参与调查的顺序:
favorite_languages.py

from collections import OrderedDict //❶ 
favorite_languages = OrderedDict() //❷ 
favorite_languages['jen'] = 'python' //❸ 
favorite_languages['sarah'] = 'c'
favorite_languages['edward'] = 'ruby'
favorite_languages['phil'] = 'python'
for name, language in favorite_languages.items():  //print(name.title() + "'s favorite language is " +
language.title() + ".")

   我们首先从模块collections 中导入了OrderedDict 类(见❶)。在❷处,我们创建了OrderedDict 类的一个实例,并将其存储到favorite_languages 中。请注意,这里没有使用花括号,而是调用OrderedDict() 来创建一个空的有序字典,并将其存储在favorite_languages 中。接下来,我们以每次一对的方式添加名字—语言对(见❸)。在❹处,我们遍历favorite_languages ,但知道将以添加的顺序获取调查结果:

Jen's favorite language is Python.
Sarah's favorite language is C.
Edward's favorite language is Ruby.
Phil's favorite language is Python.

   这是一个很不错的类,它兼具列表和字典的主要优点(在将信息关联起来的同时保留原来的顺序)。等你开始对关心的现实情形建模时,可能会发现有序字典正好能够满足需求。随着你对标准库的了解越来越深入,将熟悉大量可帮助你处理常见情形的模块。

注意 你还可以从其他地方下载外部模块。本书第二部分的每个项目都需要使用外部模块,届时你将看到很多这样的示例。

第6章 文件

6.1 从文件中读取数据

    文本文件可存储的数据量多得难以置信:天气数据、交通数据、社会经济数据、文学作品等。每当需要分析或修改存储在文件中的信息时,读取文件都很有用,对数据分析应用程序来说尤其如此。例如,你可以编写一个这样的程序:读取一个文本文件的内容,重新设置这些数据的格式并将其写入文件,让浏览器能够显示这些内容。

    要使用文本文件中的信息,首先需要将信息读取到内存中。为此,你可以一次性读取文件的全部内容,也可以以每次一行的方式逐步读取。

6.1.1 读取整个文件

    要读取文件,需要一个包含几行文本的文件。下面首先来创建一个文件,它包含精确到小数点后30位的圆周率值,且在小数点后每10位处都换行:
pi_digits.txt

3.1415926535
8979323846
2643383279

要动手尝试后续示例,可在编辑器中输入这些数据行,再将文件保存为pi_digits.txt,也可从本书的配套网站(https://www.nostarch.com/pythoncrashcourse/ )下载该文件。然后,将该
文件保存到本章程序所在的目录中。
下面的程序打开并读取这个文件,再将其内容显示到屏幕上:
file_reader.py

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

在这个程序中,第1行代码做了大量的工作。我们先来看看函数open() 。要以任何方式使用文件——哪怕仅仅是打印其内容,都得先打开打 文件,这样才能访问它。函数open()接受一个参数:要打开的文件的名称。Python在当前执行的文件所在的目录中查找指定的文件。在这个示例中,当前运行的是file_reader.py,因此Python在file_reader.py所在的目录中查找pi_digits.txt。函数open() 返回一个表示文件的对象。在这里,open(‘pi_digits.txt’) 返回一个表示文件pi_digits.txt 的对象;Python将这个对象存储在我们将在后面使用的变量中。

关键字with 在不再需要访问文件后将其关闭。在这个程序中,注意到我们调用了open() ,但没有调用close() ;你也可以调用open() 和close() 来打开和关闭文件,但这样做时,如果程序存在bug,导致close() 语句未执行,文件将不会关闭。这看似微不足道,但未妥善地关闭文件可能会导致数据丢失或受损。如果在程序中过早地调用close() ,你会发现需要使用文件时它已关闭关 (无法访问),这会导致更多的错误。并非在任何情况下都能轻松确定关闭文件的恰当时机,但通过使用前面所示的结构,可让Python去确定:你只管打开文件,并在需要时使用它,Python自会在合适的时候自动将其关闭。

有了表示pi_digits.txt的文件对象后,我们使用方法read() (前述程序的第2行)读取这个文件的全部内容,并将其作为一个长长的字符串存储在变量contents 中。这样,通过打印contents 的值,就可将这个文本文件的全部内容显示出来:

3.1415926535
8979323846
2643383279

相比于原始文件,该输出唯一不同的地方是末尾多了一个空行。为何会多出这个空行呢?因为read() 到达文件末尾时返回一个空字符串,而将这个空字符串显示出来时就是一个空行。要删除多出来的空行,可在print 语句中使用rstrip() :
with open(‘pi_digits.txt’) as file_object:
contents = file_object.read()
print(contents.rstrip())
本书前面说过,Python方法rstrip() 删除(剥除)字符串末尾的空白。现在,输出与原始文件的内容完全相同:

3.1415926535
8979323846
2643383279

6.1.2 文件路径

   当你将类似pi_digits.txt这样的简单文件名传递给函数open() 时,Python将在当前执行的文件(即.py程序文件)所在的目录中查找文件。

   根据你组织文件的方式,有时可能要打开不在程序文件所属目录中的文件。例如,你可能将程序文件存储在了文件夹python_work中,而在文件夹python_work中,有一个名为text_files的文件夹,用于存储程序文件操作的文本文件。虽然文件夹text_files包含在文件夹python_work中,但仅向open() 传递位于该文件夹中的文件的名称也不可行,因为Python 只在文件夹python_work中查找,而不会在其子文件夹text_files中查找。要让Python打开不与程序文件位于同一个目录中的文件,需要提供文件路径 文 ,它让Python到系统的特定位置去查找。

   由于文件夹text_files位于文件夹python_work中,因此可使用相对文件路 相 径来打开该文件夹中的文件。相对文件路径让Python到指定的位置去查找,而该位置是相对于当前运行的程
序所在目录的。
在Linux和OS X中,你可以这样编写代码:
with open(‘text_files/filename.txt’) as file_object:
这行代码让Python到文件夹python_work下的文件夹text_files中去查找指定的.txt文件。在Windows系统中,在文件路径中使用反斜杠(\ )而不是斜杠(/ ):
with open(‘text_files\filename.txt’) as file_object:
你还可以将文件在计算机中的准确位置告诉Python,这样就不用关心当前运行的程序存储在什么地方了。这称为绝对文件路径 绝 。在相对路径行不通时,可使用绝对路径。例如,如果text_files并不在文件夹python_work中,而在文件夹other_files中,则向open() 传递路径’text_files/ filename.txt’ 行不通,因为Python只在文件夹python_work中查找该位置。为明确地指出你希望Python到哪里去查找,你需要提供完整的路径。
绝对路径通常比相对路径更长,因此将其存储在一个变量中,再将该变量传递给open() 会有所帮助。在Linux和OS X中,绝对路径类似于下面这样:
file_path = ‘/home/ehmatthes/other_files/text_files/filename.txt’
with open(file_path) as file_object:
而在Windows系统中,它们类似于下面这样:
file_path = ‘C:\Users\ehmatthes\other_files\text_files\filename.txt’
with open(file_path) as file_object:
通过使用绝对路径,可读取系统任何地方的文件。就目前而言,最简单的做法是,要么将数据文件存储在程序文件所在的目录,要么将其存储在程序文件所在目录下的一个文件夹(如text_files)中。
注意注 Windows系统有时能够正确地解读文件路径中的斜杠。如果你使用的是Windows系统,且结果不符合预期,请确保在文件路径中使用的是反斜杠。

6.1.3 逐行读取

   读取文件时,常常需要检查其中的每一行:你可能要在文件中查找特定的信息,或者要以某种方式修改文件中的文本。例如,你可能要遍历一个包含天气数据的文件,并使用天
气描述中包含字样sunny的行。在新闻报道中,你可能会查找包含标签 的行,并按特定的格式设置它。
要以每次一行的方式检查文件,可对文件对象使用for 循环:
file_reader.py
❶ filename = ‘pi_digits.txt’
❷ with open(filename) as file_object: ❸ for line in file_object:
print(line)
在❶处,我们将要读取的文件的名称存储在变量filename 中,这是使用文件时一种常见的做法。由于变量filename 表示的并非实际文件——它只是一个让Python知道到哪里
去查找文件的字符串,因此可轻松地将’pi_digits.txt’ 替换为你要使用的另一个文件的名称。调用open() 后,将一个表示文件及其内容的对象存储到了变
量file_object 中(见❷)。这里也使用了关键字with ,让Python负责妥善地打开和关闭文件。为查看文件的内容,我们通过对文件对象执行循环来遍历文件中的每一行(见
❸)。
我们打印每一行时,发现空白行更多了:
3.1415926535
8979323846
2643383279
为何会出现这些空白行呢?因为在这个文件中,每行的末尾都有一个看不见的换行符,而print 语句也会加上一个换行符,因此每行末尾都有两个换行符:一个来自文件,另一
个来自print 语句。要消除这些多余的空白行,可在print 语句中使用rstrip() :
filename = ‘pi_digits.txt’
with open(filename) as file_object:
for line in file_object:
print(line.rstrip())
现在,输出又与文件内容完全相同了:
3.1415926535
8979323846
2643383279

6.1.4 创建一个包含文件各行内容的列表

   使用关键字with 时,open() 返回的文件对象只在with 代码块内可用。如果要在with 代码块外访问文件的内容,可在with 代码块内将文件的各行存储在一个列表中,并在with 代码块外使用该列表:你可以立即处理文件的各个部分,也可推迟到程序后面再处理。

   下面的示例在with 代码块中将文件pi_digits.txt的各行存储在一个列表中,再在with 代码块外打印它们:
filename = ‘pi_digits.txt’
with open(filename) as file_object: ❶ lines = file_object.readlines()
❷ for line in lines:
print(line.rstrip())
❶处的方法readlines() 从文件中读取每一行,并将其存储在一个列表中;接下来,该列表被存储到变量lines 中;在with 代码块外,我们依然可以使用这个变量。在❷处,我们使用一个简单的for 循环来打印lines 中的各行。由于列表lines 的每个元素都对应于文件中的一行,因此输出与文件内容完全一致。

10.1.5 使用文件的内容 使

将文件读取到内存中后,就可以以任何方式使用这些数据了。下面以简单的方式使用圆周率的值。首先,我们将创建一个字符串,它包含文件中存储的所有数字,且没有任何空
格:
pi_string.py
filename = ‘pi_digits.txt’
with open(filename) as file_object:
lines = file_object.readlines()
❶ pi_string = ‘’ ❷ for line in lines:
pi_string += line.rstrip()
❸ print(pi_string)
print(len(pi_string))
就像前一个示例一样,我们首先打开文件,并将其中的所有行都存储在一个列表中。在❶处,我们创建了一个变量——pi_string ,用于存储圆周率的值。接下来,我们使用
一个循环将各行都加入pi_string ,并删除每行末尾的换行符(见❷)。在❸处,我们打印这个字符串及其长度:
3.1415926535 8979323846 2643383279
36
在变量pi_string 存储的字符串中,包含原来位于每行左边的空格,为删除这些空格,可使用strip() 而不是rstrip() :
filename = ‘pi_30_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))
这样,我们就获得了一个这样的字符串:它包含精确到30位小数的圆周率值。这个字符串长32字符,因为它还包含整数部分的3和小数点:
3.141592653589793238462643383279
32
注意注 读取文本文件时,Python将其中的所有文本都解读为字符串。如果你读取的是数字,并要将其作为数值使用,就必须使用函数int() 将其转换为整数,或使用
函数float() 将其转换为浮点数。

10.1.6 包含一百万位的大型文件

前面我们分析的都是一个只有三行的文本文件,但这些代码示例也可处理大得多的文件。如果我们有一个文本文件,其中包含精确到小数点后1 000 000位而不是30位的圆周率
值,也可创建一个包含所有这些数字的字符串。为此,我们无需对前面的程序做任何修改,只需将这个文件传递给它即可。在这里,我们只打印到小数点后50位,以免终端为显
示全部1 000 000位而不断地翻滚:
pi_string.py
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))
输出表明,我们创建的字符串确实包含精确到小数点后1 000 000位的圆周率值:
3.14159265358979323846264338327950288419716939937510…
1000002
对于你可处理的数据量,Python没有任何限制;只要系统的内存足够多,你想处理多少数据都可以。
注意注 要运行这个程序(以及后面的众多示例),你需要从https://www.nostarch.com/pythoncra-shcourse/ 下载相关的资源。
10.1.7 圆周率值中包含你的生日吗 圆
我一直想知道自己的生日是否包含在圆周率值中。下面来扩展刚才编写的程序,以确定某个人的生日是否包含在圆周率值的前1 000 000位中。为此,可将生日表示为一个由数字
组成的字符串,再检查这个字符串是否包含在pi_string 中:
filename = ‘pi_million_digits.txt’
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ‘’
for line in lines:
pi_string += line.rstrip()
❶ birthday = input("Enter your birthday, in the form mmddyy: ") ❷ if birthday in pi_string:
print(“Your birthday appears in the first million digits of pi!”)
else:
print(“Your birthday does not appear in the first million digits of pi.”)
在❶处,我们提示用户输入其生日,在接下来的❷处,我们检查这个字符串是否包含在pi_string 中。运行一下这个程序:
Enter your birthdate, in the form mmddyy: 120372
Your birthday appears in the first million digits of pi!
我的生日确实出现在了圆周率值中!读取文件的内容后,就可以以你能想到的任何方式对其进行分析。

10.2 写入文件 写

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

10.2.1 写入空文件 写

   要将文本写入文件,你在调用open() 时需要提供另一个实参,告诉Python你要写入打开的文件。为明白其中的工作原理,我们来将一条简单的消息存储到文件中,而不是将其打印到屏幕上:
write_message.py
filename = ‘programming.txt’
   ❶ with open(filename, ‘w’) as file_object: ❷ file_object.write(“I love programming.”)
在这个示例中,调用open() 时提供了两个实参(见❶)。第一个实参也是要打开的文件的名称;第二个实参(‘w’ )告诉Python,我们要以写入模式 写 打开这个文件。打开文件时,可指定读取模式 读 (‘r’ )、写入模式 写 (‘w’ )、附加模式 附 (‘a’ )或让你能够读取和写入文件的模式(‘r+’ )。如果你省略了模式实参,Python将以默认的只读模式打开文件。

   如果你要写入的文件不存在,函数open() 将自动创建它。然而,以写入(‘w’ )模式打开文件时千万要小心,因为如果指定的文件已经存在,Python将在返回文件对象前清空该文件。

   在❷处,我们使用文件对象的方法write() 将一个字符串写入文件。这个程序没有终端输出,但如果你打开文件programming.txt,将看到其中包含如下一行内容:
programming.txt
I love programming.
   相比于你的计算机中的其他文件,这个文件没有什么不同。你可以打开它、在其中输入新文本、复制其内容、将内容粘贴到其中等。

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

10.2.2 写入多行 写

   函数write() 不会在你写入的文本末尾添加换行符,因此如果你写入多行时没有指定换行符,文件看起来可能不是你希望的那样:
filename = ‘programming.txt’
with open(filename, ‘w’) as file_object:
file_object.write(“I love programming.”)
file_object.write(“I love creating new games.”)
如果你打开programming.txt,将发现两行内容挤在一起:
I love programming.I love creating new games.
要让每个字符串都单独占一行,需要在write() 语句中包含换行符:
filename = ‘programming.txt’
with open(filename, ‘w’) as file_object:
file_object.write(“I love programming.\n”)
file_object.write(“I love creating new games.\n”)
现在,输出出现在不同行中:
I love programming.
I love creating new games.
像显示到终端的输出一样,还可以使用空格、制表符和空行来设置这些输出的格式。

10.2.3 附加到文件 附

   如果你要给文件添加内容,而不是覆盖原有的内容,可以附加模式 附 打开文件。你以附加模式打开文件时,Python不会在返回文件对象前清空文件,而你写入到文件的行都将添加
到文件末尾。如果指定的文件不存在,Python将为你创建一个空文件。
下面来修改write_message.py,在既有文件programming.txt中再添加一些你酷爱编程的原因:
write_message.py
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”)
在❶处,我们打开文件时指定了实参’a’ ,以便将内容附加到文件末尾,而不是覆盖文件原来的内容。在❷处,我们又写入了两行,它们被添加到文件programming.txt末尾:
programming.txt
I love programming.
I love creating new games.
I also love finding meaning in large datasets.
I love creating apps that can run in a browser.
最终的结果是,文件原来的内容还在,它们后面是我们刚添加的内容。

文件与文件路径

   在 Windows 上,路径书写使用倒斜杠作为文件夹之间的分隔符。但在 OS X 和Linux 上,使用正斜杠作为它们的路径分隔符。如果想要程序运行在所有操作系统上,在编写 Python 脚本时,就必须处理这两种情况。

   好在,用 os.path.join()函数来做这件事很简单。如果将单个文件和路径上的文件夹名称的字符串传递给它,os.path.join()就会返回一个文件路径的字符串,包含正确的路径分隔符。在交互式环境中输入以下代码:

>>> import os
>>> os.path.join('usr', 'bin', 'spam')
'usr\\bin\\spam'

   如果需要创建文件名称的字符串,os.path.join()函数就很有用。这些字符串将传递给几个文件相关的函数,本章将进行介绍。例如,下面的例子将一个文件名列表中的名称,添加到文件夹名称的末尾。

>>> myFiles = ['accounts.txt', 'details.csv', 'invite.docx']
>>> for filename in myFiles:
print(os.path.join('C:\\Users\\asweigart', filename))
C:\Users\asweigart\accounts.txt
C:\Users\asweigart\details.csv
C:\Users\asweigart\invite.docx

用 os.getcwd()函数可以取得当前工作路径的字符串;
用 os.makedirs()创建新文件夹;
os.path 模块包含了许多与文件名和文件路径相关的有用函数;
调用 os.path.abspath(path)将返回参数的绝对路径的字符串;
调用 os.path.relpath(path, start)将返回从 start 路径到 path 的相对路径的字符串;

文件读写过程

用 open()函数打开文件

文件的基本方法

读取和写入

使用管道重定向输出

组 织 文 件

shutil模块中包含一些函数,让你在 Python 程序中复制、移动、改名和删除文件。

shutil.copy()将复制一个文件,shutil.copytree()将复制整个文件夹,以及它包含的文件夹和文件。

第7章 异常

7.1 简介

   Python使用被称为异常异 的特殊对象来管理程序执行期间发生的错误。每当发生让Python不知所措的错误时,它都会创建一个异常对象。如果你编写了处理该异常的代码,程序将继续运行;如果你未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告。

   异常是使用try-except 代码块处理的。try-except 代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用了try-except 代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。

7.2 处理处 ZeroDivisionError 异常异

   下面来看一种导致Python引发异常的简单错误。你可能知道不能将一个数字除以0,但我们还是让Python这样做吧:
division.py
print(5/0)
显然,Python无法这样做,因此你将看到一个traceback:
Traceback (most recent call last):
File “division.py”, line 1, in
print(5/0) ❶ ZeroDivisionError: division by zero
在上述traceback中,❶处指出的错误ZeroDivisionError 是一个异常对象。Python无法按你的要求做时,就会创建这种对象。在这种情况下,Python将停止运行程序,并指出
引发了哪种异常,而我们可根据这些信息对程序进行修改。下面我们将告诉Python,发生这种错误时怎么办;这样,如果再次发生这样的错误,我们就有备无患了。
10.3.2 使用使 try-except 代码块 代
当你认为可能发生了错误时,可编写一个try-except 代码块来处理可能引发的异常。你让Python尝试运行一些代码,并告诉它如果这些代码引发了指定的异常,该怎么办。
处理ZeroDivisionError 异常的try-except 代码块类似于下面这样:
try:
print(5/0)
except ZeroDivisionError:
print(“You can’t divide by zero!”)
我们将导致错误的代码行print(5/0) 放在了一个try 代码块中。如果try 代码块中的代码运行起来没有问题,Python将跳过except 代码块;如果try 代码块中的代码导致了
错误,Python将查找这样的except 代码块,并运行其中的代码,即其中指定的错误与引发的错误相同。
在这个示例中,try 代码块中的代码引发了ZeroDivisionError 异常,因此Python指出了该如何解决问题的except 代码块,并运行其中的代码。这样,用户看到的是一条友
好的错误消息,而不是traceback:
You can’t divide by zero!
如果try-except 代码块后面还有其他代码,程序将接着运行,因为已经告诉了Python如何处理这种错误。下面来看一个捕获错误后程序将继续运行的示例。

7.3 使用异常避免崩溃

发生错误时,如果程序还有工作没有完成,妥善地处理错误就尤其重要。这种情况经常会出现在要求用户提供输入的程序中;如果程序能够妥善地处理无效输入,就能再提示用
户提供有效输入,而不至于崩溃。
下面来创建一个只执行除法运算的简单计算器:
division.py
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 ❸ answer = int(first_number) / int(second_number)
print(answer)
在❶处,这个程序提示用户输入一个数字,并将其存储到变量first_number 中;如果用户输入的不是表示退出的q,就再提示用户输入一个数字,并将其存储到变
量second_number 中(见❷)。接下来,我们计算这两个数字的商(即answer ,见❸)。这个程序没有采取任何处理错误的措施,因此让它执行除数为0的除法运算时,它
将崩溃:
Give me two numbers, and I’ll divide them.
Enter ‘q’ to quit.
First number: 5
Second number: 0
Traceback (most recent call last):
File “division.py”, line 9, in
answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero
程序崩溃可不好,但让用户看到traceback也不是好主意。不懂技术的用户会被它们搞糊涂,而且如果用户怀有恶意,他会通过traceback获悉你不希望他知道的信息。例如,他将知
道你的程序文件的名称,还将看到部分不能正确运行的代码。有时候,训练有素的攻击者可根据这些信息判断出可对你的代码发起什么样的攻击。

7.4 else 代码块

通过将可能引发错误的代码放在try-except 代码块中,可提高这个程序抵御错误的能力。错误是执行除法运算的代码行导致的,因此我们需要将它放到try-except 代码块
中。这个示例还包含一个else 代码块;依赖于try 代码块成功执行的代码都应放到else 代码块中:
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: ") ❶ try:
answer = int(first_number) / int(second_number) ❷ except ZeroDivisionError:
print(“You can’t divide by 0!”) ❸ else:
print(answer)
我们让Python尝试执行try 代码块中的除法运算(见❶),这个代码块只包含可能导致错误的代码。依赖于try 代码块成功执行的代码都放在else 代码块中;在这个示例中,如
果除法运算成功,我们就使用else 代码块来打印结果(见❸)。
except 代码块告诉Python,出现ZeroDivisionError 异常时该怎么办(见❷)。如果try 代码块因除零错误而失败,我们就打印一条友好的消息,告诉用户如何避免这种错
误。程序将继续运行,用户根本看不到traceback:
Give me two numbers, and I’ll divide them.
Enter ‘q’ to quit.
First number: 5
Second number: 0
You can’t divide by 0!
First number: 5
Second number: 2
2.5
First number: q
try-except-else 代码块的工作原理大致如下:Python尝试执行try 代码块中的代码;只有可能引发异常的代码才需要放在try 语句中。有时候,有一些仅在try 代码块成功
执行时才需要运行的代码;这些代码应放在else 代码块中。except 代码块告诉Python,如果它尝试运行try 代码块中的代码时引发了指定的异常,该怎么办。
通过预测可能发生错误的代码,可编写健壮的程序,它们即便面临无效数据或缺少资源,也能继续运行,从而能够抵御无意的用户错误和恶意的攻击。
10.3.5 处理处 FileNotFoundError 异常异
使用文件时,一种常见的问题是找不到文件:你要查找的文件可能在其他地方、文件名可能不正确或者这个文件根本就不存在。对于所有这些情形,都可使用try-except 代码
块以直观的方式进行处理。
我们来尝试读取一个不存在的文件。下面的程序尝试读取文件alice.txt的内容,但我没有将这个文件存储在alice.py所在的目录中:
alice.py
filename = ‘alice.txt’
with open(filename) as f_obj:
contents = f_obj.read()
Python无法读取不存在的文件,因此它引发一个异常:
Traceback (most recent call last):
File “alice.py”, line 3, in
with open(filename) as f_obj:
FileNotFoundError: [Errno 2] No such file or directory: ‘alice.txt’
在上述traceback中,最后一行报告了FileNotFoundError 异常,这是Python找不到要打开的文件时创建的异常。在这个示例中,这个错误是函数open() 导致的,因此要处理
这个错误,必须将try 语句放在包含open() 的代码行之前:
filename = ‘alice.txt’
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = “Sorry, the file " + filename + " does not exist.”
print(msg)
在这个示例中,try 代码块引发FileNotFoundError 异常,因此Python找出与该错误匹配的except 代码块,并运行其中的代码。最终的结果是显示一条友好的错误消息,而
不是traceback:
Sorry, the file alice.txt does not exist.
如果文件不存在,这个程序什么都不做,因此错误处理代码的意义不大。下面来扩展这个示例,看看在你使用多个文件时,异常处理可提供什么样的帮助。

7.5 分析文本

你可以分析包含整本书的文本文件。很多经典文学作品都是以简单文本文件的方式提供的,因为它们不受版权限制。本节使用的文本来自项目Gutenberg(http://gutenberg.org/ ),
这个项目提供了一系列不受版权限制的文学作品,如果你要在编程项目中使用文学文本,这是一个很不错的资源。
下面来提取童话 Alicein Wonderland 的文本,并尝试计算它包含多少个单词。我们将使用方法split() ,它根据一个字符串创建一个单词列表。下面是对只包含童话名"Alice
in Wonderland" 的字符串调用方法split() 的结果:

>>> title = "Alice in Wonderland"
>>> title.split()
['Alice', 'in', 'Wonderland']

方法split() 以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中。结果是一个包含字符串中所有单词的列表,虽然有些单词可能包含标点。为计算Alicein Wonderland 包含多少个单词,我们将对整篇小说调用split() ,再计算得到的列表包含多少个元素,从而确定整篇童话大致包含多少个单词:

filename = 'alice.txt'
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
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.")

我们把文件alice.txt移到了正确的目录中,让try 代码块能够成功地执行。在❶处,我们对变量contents (它现在是一个长长的字符串,包含童话 Alicein Wonderland 的全部文本)调用方法split() ,以生成一个列表,其中包含这部童话中的所有单词。当我们使用len() 来确定这个列表的长度时,就知道了原始字符串大致包含多少个单词(见❷)。在❸处,我们打印一条消息,指出文件包含多少个单词。这些代码都放在else 代码块中,因为仅当try 代码块成功执行时才执行它们。输出指出了文件alice.txt包含多少个单词:

The file alice.txt has about 29461 words.

这个数字有点大,因为这里使用的文本文件包含出版商提供的额外信息,但与童话 Alicein Wonderland 的长度相当一致。
10.3.7 使用多个文件 使
下面多分析几本书。这样做之前,我们先将这个程序的大部分代码移到一个名为count_words() 的函数中,这样对多本书进行分析时将更容易:
word_count.py

def count_words(filename):"""计算一个文件大致包含多少个单词"""
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
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_words(filename)

这些代码大都与原来一样,我们只是将它们移到了函数count_words() 中,并增加了缩进量。修改程序的同时更新注释是个不错的习惯,因此我们将注释改成了文档字符串,并稍微调整了一下措辞(见❶)。
现在可以编写一个简单的循环,计算要分析的任何文本包含多少个单词了。为此,我们将要分析的文件的名称存储在一个列表中,然后对列表中的每个文件都调用count_words() 。我们将尝试计算 Alicein Wonderland 、Siddhartha 、Moby Dick 和 Little Women 分别包含多少个单词,它们都不受版权限制。我故意没有将siddhartha.txt放到word_count.py所在的目录中,让你能够看到这个程序在文件不存在时处理得有多出色:

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

文件siddhartha.txt不存在,但这丝毫不影响这个程序处理其他文件:

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

在这个示例中,使用try-except 代码块提供了两个重要的优点:避免让用户看到traceback;让程序能够继续分析能够找到的其他文件。如果不捕获因找不到siddhartha.txt而引发的FileNotFoundError 异常,用户将看到完整的traceback,而程序将在尝试分析 Siddhartha 后停止运行——根本不分析 Moby Dick 和 Little Women 。

7.6 失败时一声不吭

在前一个示例中,我们告诉用户有一个文件找不到。但并非每次捕获到异常时都需要告诉用户,有时候你希望程序在发生异常时一声不吭,就像什么都没有发生一样继续运行。
要让程序在失败时一声不吭,可像通常那样编写try 代码块,但在except 代码块中明确地告诉Python什么都不要做。Python有一个pass 语句,可在代码块中使用它来让Python 什么都不要做:
def count_words(filename):
“”“计算一个文件大致包含多少个单词”“”
try:
–snip–
except FileNotFoundError: ❶ pass
else:
–snip–
filenames = [‘alice.txt’, ‘siddhartha.txt’, ‘moby_dick.txt’, ‘little_women.txt’]
for filename in filenames:
count_words(filename)
相比于前一个程序,这个程序唯一不同的地方是❶处的pass 语句。现在,出现FileNotFoundError 异常时,将执行except 代码块中的代码,但什么都不会发生。这种错误发生时,不会出现traceback,也没有任何输出。用户将看到存在的每个文件包含多少个单词,但没有任何迹象表明有一个文件未找到:
The file alice.txt has about 29461 words.
The file moby_dick.txt has about 215136 words.
The file little_women.txt has about 189079 words.
pass 语句还充当了占位符,它提醒你在程序的某个地方什么都没有做,并且以后也许要在这里做些什么。例如,在这个程序中,我们可能决定将找不到的文件的名称写入到文件missing_files.txt中。用户看不到这个文件,但我们可以读取这个文件,进而处理所有文件找不到的问题。

7.7 决定报告哪些错误

在什么情况下该向用户报告错误?在什么情况下又应该在失败时一声不吭呢?如果用户知道要分析哪些文件,他们可能希望在有文件没有分析时出现一条消息,将其中的原因告诉他们。如果用户只想看到结果,而并不知道要分析哪些文件,可能就无需在有些文件不存在时告知他们。向用户显示他不想看到的信息可能会降低程序的可用性。Python的错误处理结构让你能够细致地控制与用户分享错误信息的程度,要分享多少信息由你决定。
编写得很好且经过详尽测试的代码不容易出现内部错误,如语法或逻辑错误,但只要程序依赖于外部因素,如用户输入、存在指定的文件、有网络链接,就有可能出现异常。凭借经验可判断该在程序的什么地方包含异常处理块,以及出现错误时该向用户提供多少相关的信息。

请注意,通过使用lower() 将字符串转换为小写,可捕捉要查找的单词出现的所有次数,而不管其大小写格式如何。
编写一个程序,它读取你在项目Gutenberg中获取的文件,并计算单词’the’ 在每个文件中分别出现了多少次。

第8章 存储数据

8.1 简介

很多程序都要求用户输入某种信息,如让用户存储游戏首选项或提供要可视化的数据。不管专注的是什么,程序都把用户提供的信息存储在列表和字典等数据结构中。用户关闭程序时,你几乎总是要保存他们提供的信息;一种简单的方式是使用模块json 来存储数据。
模块json 让你能够将简单的Python数据结构转储到文件中,并在程序再次运行时加载该文件中的数据。你还可以使用json 在Python程序之间分享数据。更重要的是,JSON数据格式并非Python专用的,这让你能够将以JSON格式存储的数据与使用其他编程语言的人分享。这是一种轻便格式,很有用,也易于学习。
注意注 JSON(JavaScript Object Notation)格式最初是为JavaScript开发的,但随后成了一种常见格式,被包括Python在内的众多语言采用。

8.2 使用使 json.dump() 和json.load()

我们来编写一个存储一组数字的简短程序,再编写一个将这些数字读取到内存中的程序。第一个程序将使用json.dump() 来存储这组数字,而第二个程序将使用json.load() 。
函数json.dump() 接受两个实参:要存储的数据以及可用于存储数据的文件对象。下面演示了如何使用json.dump() 来存储数字列表:
number_writer.py
import json
numbers = [2, 3, 5, 7, 11, 13]
❶ filename = ‘numbers.json’ ❷ with open(filename, ‘w’) as f_obj: ❸ json.dump(numbers, f_obj)
我们先导入模块json ,再创建一个数字列表。在❶处,我们指定了要将该数字列表存储到其中的文件的名称。通常使用文件扩展名.json来指出文件存储的数据为JSON格式。接
下来,我们以写入模式打开这个文件,让json 能够将数据写入其中(见❷)。在❸处,我们使用函数json.dump() 将数字列表存储到文件numbers.json中。
这个程序没有输出,但我们可以打开文件numbers.json,看看其内容。数据的存储格式与Python中一样:
[2, 3, 5, 7, 11, 13]
下面再编写一个程序,使用json.load() 将这个列表读取到内存中:
number_reader.py
import json
❶ filename = ‘numbers.json’ ❷ with open(filename) as f_obj: ❸ numbers = json.load(f_obj)
print(numbers)
在❶处,我们确保读取的是前面写入的文件。这次我们以读取方式打开这个文件,因为Python只需读取这个文件(见❷)。在❸处,我们使用函数json.load() 加载存储在
numbers.json中的信息,并将其存储到变量numbers 中。最后,我们打印恢复的数字列表,看看它是否与number_writer.py中创建的数字列表相同:
[2, 3, 5, 7, 11, 13]
这是一种在程序之间共享数据的简单方式。
10.4.2 保存和读取用户生成的数据 保
对于用户生成的数据,使用json 保存它们大有裨益,因为如果不以某种方式进行存储,等程序停止运行时用户的信息将丢失。下面来看一个这样的例子:用户首次运行程序时
被提示输入自己的名字,这样再次运行程序时就记住他了。
我们先来存储用户的名字:
remember_me.py
import json
❶ username = input("What is your name? ")
filename = ‘username.json’
with open(filename, ‘w’) as f_obj: ❷ json.dump(username, f_obj) ❸ print("We’ll remember you when you come back, " + username + “!”)
在❶处,我们提示输入用户名,并将其存储在一个变量中。接下来,我们调用json.dump() ,并将用户名和一个文件对象传递给它,从而将用户名存储到文件中(见❷)。然
后,我们打印一条消息,指出我们存储了他输入的信息(见❸):
What is your name? Eric
We’ll remember you when you come back, Eric!
现在再编写一个程序,向其名字被存储的用户发出问候:
greet_user.py
import json
filename = ‘username.json’
with open(filename) as f_obj: ❶ username = json.load(f_obj) ❷ print("Welcome back, " + username + “!”)
在❶处,我们使用json.load() 将存储在username.json中的信息读取到变量username 中。恢复用户名后,我们就可以欢迎用户回来了(见❷):
Welcome back, Eric!
我们需要将这两个程序合并到一个程序(remember_me.py)中。这个程序运行时,我们将尝试从文件username.json中获取用户名,因此我们首先编写一个尝试恢复用户名的try 代
码块。如果这个文件不存在,我们就在except 代码块中提示用户输入用户名,并将其存储在username.json中,以便程序再次运行时能够获取它:
remember_me.py

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 remember you when you come back, " + username + "!")
else:
print("Welcome back, " + username + "!")

这里没有任何新代码,只是将前两个示例的代码合并到了一个程序中。在❶处,我们尝试打开文件username.json。如果这个文件存在,就将其中的用户名读取到内存中(见❷),
再执行else 代码块,即打印一条欢迎用户回来的消息。用户首次运行这个程序时,文件username.json不存在,将引发FileNotFoundError 异常(见❸),因此Python将执
行except 代码块:提示用户输入其用户名(见❹),再使用json.dump() 存储该用户名,并打印一句问候语(见❺)。
无论执行的是except 代码块还是else 代码块,都将显示用户名和合适的问候语。如果这个程序是首次运行,输出将如下:
What is your name? Eric
We’ll remember you when you come back, Eric!
否则,输出将如下:
Welcome back, Eric!
这是程序之前至少运行了一次时的输出。

8.3 重构

    你经常会遇到这样的情况:代码能够正确地运行,但可做进一步的改进——将代码划分为一系列完成具体工作的函数。这样的过程被称为重构重 。重构让代码更清晰、更易于理解、更容易扩展。

    要重构remember_me.py,可将其大部分逻辑放到一个或多个函数中。remember_me.py的重点是问候用户,因此我们将其所有代码都放到一个名为greet_user() 的函数中:
remember_me.py

import json
def greet_user():"""问候用户,并指出其名字"""
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 remember you when you come back, " + username + "!")
else:
print("Welcome back, " + username + "!")
greet_user()

    考虑到现在使用了一个函数,我们删除了注释,转而使用一个文档字符串来指出程序是做什么的(见❶)。这个程序更清晰些,但函数greet_user() 所做的不仅仅是问候用户,还在存储了用户名时获取它,而在没有存储用户名时提示用户输入一个。

下面来重构greet_user() ,让它不执行这么多任务。为此,我们首先将获取存储的用户名的代码移到另一个函数中:

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 greet_user():
"""问候用户,并指出其名字"""
username = get_stored_username() ❸ if username:
print("Welcome back, " + username + "!")
else:
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back, " + username + "!")
greet_user()

    新增的函数get_stored_username() 目标明确,❶处的文档字符串指出了这一点。如果存储了用户名,这个函数就获取并返回它;如果文件username.json不存在,这个函数就返回None (见❷)。这是一种不错的做法:函数要么返回预期的值,要么返回None ;这让我们能够使用函数的返回值做简单测试。在❸处,如果成功地获取了用户名,就打印一条欢迎用户回来的消息,否则就提示用户输入用户名。

我们还需将greet_user() 中的另一个代码块提取出来:将没有存储用户名时提示用户输入的代码放在一个独立的函数中:

import json
def get_stored_username():
"""如果存储了用户名,就获取它"""
--snip--
def get_new_username():
"""提示用户输入用户名"""
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
return username
def greet_user():
"""问候用户,并指出其名字"""
username = get_stored_username()
if username:
print("Welcome back, " + username + "!")
else:
username = get_new_username()
print("We'll remember you when you come back, " + username + "!")
greet_user()

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值