Python 入门指南(二)(3)

简介: Python 入门指南(二)

Python 入门指南(二)(2)https://developer.aliyun.com/article/1507369

额外的解包概括

Python 3.5 中引入的最近的新功能之一是能够扩展可迭代(*)和字典(**)解包操作符,以允许在更多位置、任意次数和额外情况下解包。我将给你一个关于函数调用的例子:

# additional.unpacking.py
def additional(*args, **kwargs):
    print(args)
    print(kwargs)
args1 = (1, 2, 3)
args2 = [4, 5]
kwargs1 = dict(option1=10, option2=20)
kwargs2 = {'option3': 30}
additional(*args1, *args2, **kwargs1, **kwargs2)

在前面的例子中,我们定义了一个简单的函数,打印它的输入参数argskwargs。新功能在于我们调用这个函数的方式。注意我们如何解包多个可迭代对象和字典,并且它们在argskwargs下正确地合并。这个功能之所以重要的原因在于,它允许我们不必在代码中合并args1args2,以及kwargs1kwargs2。运行代码会产生:

$ python additional.unpacking.py
(1, 2, 3, 4, 5)
{'option1': 10, 'option2': 20, 'option3': 30}

请参考 PEP 448(www.python.org/dev/peps/pep-0448/)来了解这一新功能的全部内容,并查看更多示例。

避免陷阱!可变默认值

在 Python 中需要非常注意的一件事是,默认值是在def时创建的,因此,对同一个函数的后续调用可能会根据它们的默认值的可变性而表现得不同。让我们看一个例子:

# arguments.defaults.mutable.py
def func(a=[], b={}):
    print(a)
    print(b)
    print('#' * 12)
    a.append(len(a))  # this will affect a's default value
    b[len(a)] = len(a)  # and this will affect b's one
func()
func()
func()

两个参数都有可变的默认值。这意味着,如果你影响这些对象,任何修改都会在后续的函数调用中保留下来。看看你能否理解这些调用的输出:

$ python arguments.defaults.mutable.py
[]
{}
############
[0]
{1: 1}
############
[0, 1]
{1: 1, 2: 2}
############

很有趣,不是吗?虽然这种行为一开始可能看起来很奇怪,但实际上是有道理的,而且非常方便,例如,在使用记忆化技术时(如果你感兴趣,可以搜索一下)。更有趣的是,当我们在调用之间引入一个不使用默认值的调用时,会发生什么:

# arguments.defaults.mutable.intermediate.call.py
func()
func(a=[1, 2, 3], b={'B': 1})
func()

当我们运行这段代码时,输出如下:

$ python arguments.defaults.mutable.intermediate.call.py
[]
{}
############
[1, 2, 3]
{'B': 1}
############
[0]
{1: 1}
############

这个输出告诉我们,即使我们用其他值调用函数,默认值仍然保留。一个让人想到的问题是,我怎样才能每次都得到一个全新的空值?嗯,惯例是这样的:

# arguments.defaults.mutable.no.trap.py
def func(a=None):
    if a is None:
        a = []
    # do whatever you want with `a` ...

请注意,通过使用前面的技术,如果在调用函数时没有传递a,你总是会得到一个全新的空列表。

好了,输入就到此为止,让我们看看另一面,输出。

返回值

函数的返回值是 Python 领先于大多数其他语言的地方之一。通常函数允许返回一个对象(一个值),但在 Python 中,你可以返回一个元组,这意味着你可以返回任何你想要的东西。这个特性允许程序员编写在其他语言中要难得多或者肯定更加繁琐的软件。我们已经说过,要从函数中返回一些东西,我们需要使用return语句,后面跟着我们想要返回的东西。在函数体中可以有多个 return 语句。

另一方面,如果在函数体内部我们没有返回任何东西,或者调用了一个裸的return语句,函数将返回None。这种行为是无害的,尽管我在这里没有足够的空间详细解释为什么 Python 被设计成这样,但我只想告诉你,这个特性允许了几种有趣的模式,并且证实了 Python 是一种非常一致的语言。

我说这是无害的,因为你从来不会被迫收集函数调用的结果。我将用一个例子来说明我的意思:

# return.none.py
def func():
    pass
func()  # the return of this call won't be collected. It's lost.
a = func()  # the return of this one instead is collected into `a`
print(a)  # prints: None

请注意,函数的整个主体只由pass语句组成。正如官方文档告诉我们的那样,pass是一个空操作。当它被执行时,什么都不会发生。当语法上需要一个语句,但不需要执行任何代码时,它是有用的。在其他语言中,我们可能会用一对花括号({})来表示这一点,定义一个空作用域,但在 Python 中,作用域是通过缩进代码来定义的,因此pass这样的语句是必要的。

还要注意,func函数的第一个调用返回一个值(None),我们没有收集。正如我之前所说,收集函数调用的返回值并不是强制性的。

现在,这很好但不是很有趣,那么我们来写一个有趣的函数吧?记住,在第一章中,Python 的初步介绍,我们谈到了函数的阶乘。让我们在这里写一个(为简单起见,我将假设函数总是以适当的值正确调用,因此我不会对输入参数进行健全性检查):

# return.single.value.py
def factorial(n):
    if n in (0, 1):
        return 1
    result = n
    for k in range(2, n):
        result *= k
    return result
f5 = factorial(5)  # f5 = 120

请注意我们有两个返回点。如果n01(在 Python 中通常使用in类型的检查,就像我所做的那样,而不是更冗长的if n == 0 or n == 1:),我们返回1。否则,我们执行所需的计算并返回result。让我们尝试以更简洁的方式编写这个函数:

# return.single.value.2.py from functools import reduce
from operator import mul
def factorial(n):
    return reduce(mul, range(1, n + 1), 1)
f5 = factorial(5)  # f5 = 120

我知道你在想什么:一行?Python 是优雅而简洁的!我认为这个函数即使你从未见过reducemul,也是可读的,但如果你无法阅读或理解它,请花几分钟时间在 Python 文档上做一些研究,直到它的行为对你清晰为止。能够在文档中查找函数并理解他人编写的代码是每个开发人员都需要能够执行的任务,所以把它当作一个挑战。

为此,请确保查找help函数,在控制台探索时会非常有帮助。

返回多个值

与大多数其他语言不同,在 Python 中很容易从函数返回多个对象。这个特性打开了一个全新的可能性世界,并允许你以其他语言难以复制的风格编码。我们的思维受到我们使用的工具的限制,因此当 Python 给予你比其他语言更多的自由时,实际上也在提高你自己的创造力。返回多个值非常容易,你只需使用元组(显式或隐式)。让我们看一个简单的例子,模仿divmod内置函数:

# return.multiple.py
def moddiv(a, b):
    return a // b, a % b
print(moddiv(20, 7))  # prints (2, 6)

我本可以将前面代码中的突出部分包装在括号中,使其成为一个显式的元组,但没有必要。前面的函数同时返回了结果和除法的余数。

在这个示例的源代码中,我留下了一个简单的测试函数的示例,以确保我的代码进行了正确的计算。

一些建议

在编写函数时,遵循指南非常有用,这样你就可以写得更好。我将快速指出其中一些:

  • 函数应该只做一件事:只做一件事的函数很容易用一句简短的话来描述。做多件事的函数可以拆分成做一件事的较小函数。这些较小的函数通常更容易阅读和理解。还记得我们几页前看到的数据科学示例吗?
  • 函数应该尽可能小:它们越小,测试和编写它们就越容易,以便它们只做一件事。
  • 输入参数越少越好:接受大量参数的函数很快就变得难以管理(除其他问题外)。
  • 函数的返回值应该是一致的:返回FalseNone并不相同,即使在布尔上下文中它们都会评估为FalseFalse表示我们有信息(False),而None表示没有信息。尝试编写函数以一致的方式返回,无论其主体发生了什么。
  • 函数不应该有副作用:换句话说,函数不应该影响你调用它们的值。这可能是目前最难理解的陈述,所以我将给你一个使用列表的例子。在下面的代码中,请注意sorted函数没有对numbers进行排序,它实际上返回了一个已排序的numbers的副本。相反,list.sort()方法是在numbers对象本身上操作,这是可以的,因为它是一个方法(属于对象的函数,因此有权修改它):
>>> numbers = [4, 1, 7, 5]
>>> sorted(numbers)  # won't sort the original `numbers` list
[1, 4, 5, 7]
>>> numbers  # let's verify
[4, 1, 7, 5]  # good, untouched
>>> numbers.sort()  # this will act on the list
>>> numbers
[1, 4, 5, 7]

遵循这些准则,你将编写更好的函数,这将为你服务。

递归函数

当一个函数调用自身来产生结果时,它被称为递归。有时,递归函数非常有用,因为它们使编写代码变得更容易。有些算法使用递归范式编写起来非常容易,而其他一些则不是。没有递归函数无法以迭代方式重写,因此通常由程序员选择处理当前情况的最佳方法。

递归函数的主体通常有两个部分:一个是返回值取决于对自身的后续调用,另一个是不取决于后续调用的情况(称为基本情况)。

例如,我们可以考虑(希望现在已经熟悉的)factorial函数,N!。基本情况是当N01时。函数返回1,无需进一步计算。另一方面,在一般情况下,N!返回乘积1 * 2 * … * (N-1) * N。如果你仔细想想,N!可以这样重写:N! = (N-1)! * N。作为一个实际的例子,考虑5! = 1 * 2 * 3 * 4 * 5 = (1 * 2 * 3 * 4) * 5 = 4! * 5

让我们用代码写下来:

# recursive.factorial.py
def factorial(n):
    if n in (0, 1):  # base case
        return 1
    return factorial(n - 1) * n  # recursive case

在编写递归函数时,始终考虑你进行了多少嵌套调用,因为有一个限制。有关此信息的更多信息,请查看sys.getrecursionlimit()sys.setrecursionlimit()

在编写算法时经常使用递归函数,它们编写起来非常有趣。作为练习,尝试使用递归和迭代方法解决一些简单问题。

匿名函数

我想谈谈的最后一种函数类型是匿名函数。这些函数在 Python 中称为lambda,通常在需要一个完全成熟的带有自己名称的函数会显得有些多余时使用,我们只需要一个快速、简单的一行代码来完成任务。

假设你想要一个包含* N *的所有倍数的列表。假设你想使用filter函数进行筛选,该函数接受一个函数和一个可迭代对象,并构造一个筛选对象,你可以对其进行迭代,从可迭代对象中返回True的元素。如果不使用匿名函数,你可以这样做:

# filter.regular.py
def is_multiple_of_five(n):
    return not n % 5
def get_multiples_of_five(n):
    return list(filter(is_multiple_of_five, range(n)))

注意我们如何使用is_multiple_of_five来过滤前n个自然数。这似乎有点多余,任务很简单,我们不需要保留is_multiple_of_five函数以备其他用途。让我们使用 lambda 函数重新编写它:

# filter.lambda.py
def get_multiples_of_five(n):
    return list(filter(lambda k: not k % 5, range(n)))

逻辑完全相同,但现在过滤函数是一个 lambda。定义 lambda 非常简单,遵循这种形式:func_name = lambda [parameter_list]: expression。返回一个函数对象,等同于这个:def func_name([parameter_list]): return expression

请注意,可选参数在常见的语法中用方括号括起来表示。

让我们再看看两种形式定义的等价函数的另外一些例子:

# lambda.explained.py
# example 1: adder
def adder(a, b):
    return a + b
# is equivalent to:
adder_lambda = lambda a, b: a + b
# example 2: to uppercase
def to_upper(s):
    return s.upper()
# is equivalent to:
to_upper_lambda = lambda s: s.upper()

前面的例子非常简单。第一个例子是两个数字相加,第二个例子是生成字符串的大写版本。请注意,我将lambda表达式返回的内容分配给了一个名称(adder_lambdato_upper_lambda),但在我们在filter示例中使用 lambda 时,没有必要这样做。

函数属性

每个函数都是一个完整的对象,因此它们有很多属性。其中一些是特殊的,可以用一种内省的方式在运行时检查函数对象。下面的脚本是一个例子,展示了其中一部分属性以及如何显示它们的值,用于一个示例函数:

# func.attributes.py
def multiplication(a, b=1):
    """Return a multiplied by b. """
    return a * b
special_attributes = [
    "__doc__", "__name__", "__qualname__", "__module__",
    "__defaults__", "__code__", "__globals__", "__dict__",
    "__closure__", "__annotations__", "__kwdefaults__",
]
for attribute in special_attributes:
    print(attribute, '->', getattr(multiplication, attribute))

我使用内置的getattr函数来获取这些属性的值。getattr(obj, attribute)等同于obj.attribute,在我们需要在运行时使用字符串名称获取属性时非常方便。运行这个脚本会产生:

$ python func.attributes.py
__doc__ -> Return a multiplied by b.
__name__ -> multiplication
__qualname__ -> multiplication
__module__ -> __main__
__defaults__ -> (1,)
__code__ -> <code object multiplication at 0x10caf7660, file "func.attributes.py", line 1>
__globals__ -> {...omitted...}
__dict__ -> {}
__closure__ -> None
__annotations__ -> {}
__kwdefaults__ -> None

我省略了__globals__属性的值,因为它太大了。关于这个属性的含义的解释可以在Python 数据模型文档页面的Callable types部分找到(docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy)。如果你想看到一个对象的所有属性,只需调用dir(object_name),你将得到所有属性的列表。

内置函数

Python 自带了很多内置函数。它们可以在任何地方使用,你可以通过检查builtins模块的dir(__builtins__)来获取它们的列表,或者查看官方 Python 文档。不幸的是,我没有足够的空间在这里介绍所有这些函数。我们已经见过其中一些,比如anybinbooldivmodfilterfloatgetattridintlenlistminprintsettupletypezip,但还有很多,你至少应该阅读一次。熟悉它们,进行实验,为每个函数编写一小段代码,并确保你能随时使用它们。

最后一个例子

在我们结束本章之前,最后一个例子怎么样?我在想我们可以编写一个函数来生成一个小于某个限制的质数列表。我们已经见过这个代码了,所以让我们把它变成一个函数,并且为了保持趣味性,让我们稍微优化一下。

事实证明,你不需要将一个数N除以从2N-1 的所有数字来判断它是否是质数。你可以停在√N。此外,你不需要测试从2√N的所有数字的除法,你可以只使用该范围内的质数。如果你感兴趣,我会留给你去弄清楚为什么这样可以,让我们看看代码如何改变:

# primes.py
from math import sqrt, ceil
def get_primes(n):
    """Calculate a list of primes up to n (included). """
    primelist = []
    for candidate in range(2, n + 1):
        is_prime = True
        root = ceil(sqrt(candidate))  # division limit
        for prime in primelist:  # we try only the primes
            if prime > root:  # no need to check any further
                break
            if candidate % prime == 0:
                is_prime = False
                break
        if is_prime:
            primelist.append(candidate)
    return primelist

代码和上一章的一样。我们改变了除法算法,所以我们只使用先前计算出的质数来测试可除性,并且一旦测试除数大于候选数的平方根,我们就停止了。我们使用primelist结果列表来获取除法的质数。我们使用一个花哨的公式来计算根值,即候选数的根的天花板的整数值。虽然一个简单的int(k ** 0.5) + 1也可以满足我们的目的,但我选择的公式更简洁,需要我使用一些导入,我想向你展示。查看math模块中的函数,它们非常有趣!

文档化你的代码

我是一个不需要文档的代码的忠实粉丝。当你正确编程,选择正确的名称并注意细节时,你的代码应该是不言自明的,不需要文档。有时注释是非常有用的,文档也是如此。你可以在PEP 257 - Docstring conventions中找到 Python 文档的指南(www.python.org/dev/peps/pep-0257/),但我会在这里向你展示基础知识。

Python 使用字符串进行文档化,这些字符串被称为docstrings。任何对象都可以被文档化,你可以使用单行或多行 docstrings。单行的非常简单。它们不应该为函数提供另一个签名,而是清楚地说明其目的:

# docstrings.py
def square(n):
    """Return the square of a number n. """
    return n ** 2
def get_username(userid):
    """Return the username of a user given their id. """
    return db.get(user_id=userid).username

使用三个双引号的字符串允许您以后轻松扩展。使用以句点结尾的句子,并且不要在前后留下空行。

多行注释的结构方式类似。应该有一个简短的一行说明对象大意的描述,然后是更详细的描述。例如,我已经使用 Sphinx 符号记录了一个虚构的connect函数,在下面的示例中:

def connect(host, port, user, password):
    """Connect to a database.
    Connect to a PostgreSQL database directly, using the given
    parameters.
    :param host: The host IP.
    :param port: The desired port.
    :param user: The connection username.
    :param password: The connection password.
    :return: The connection object.
    """
    # body of the function here...
    return connection

Sphinx 可能是创建 Python 文档最广泛使用的工具。事实上,官方 Python 文档就是用它编写的。值得花一些时间去了解它。

导入对象

现在您已经对函数有了很多了解,让我们看看如何使用它们。编写函数的整个目的是能够以后重用它们,在 Python 中,这意味着将它们导入到需要它们的命名空间中。有许多不同的方法可以将对象导入命名空间,但最常见的是import module_namefrom module_name import function_name。当然,这些都是相当简单的例子,但请暂时忍耐。

import module_name 形式会找到module_name模块,并在执行import语句的本地命名空间中为其定义一个名称。from module_name import identifier 形式比这略微复杂一些,但基本上做的是相同的事情。它找到module_name并搜索属性(或子模块),并在本地命名空间中存储对identifier的引用。

两种形式都可以使用as子句更改导入对象的名称:

from mymodule import myfunc as better_named_func 

只是为了让您了解导入的样子,这里有一个来自我的一个项目的测试模块的示例(请注意,导入块之间的空行遵循 PEP 8 的指南:标准库、第三方库和本地代码):

from datetime import datetime, timezone  # two imports on the same line
from unittest.mock import patch  # single import
import pytest  # third party library
from core.models import (  # multiline import
    Exam,
    Exercise,
    Solution,
)

当您有一个从项目根目录开始的文件结构时,您可以使用点符号来获取您想要导入到当前命名空间的对象,无论是包、模块、类、函数还是其他任何东西。from module import语法还允许使用一个通配符子句,from module import *,有时用于一次性将模块中的所有名称导入当前命名空间,但出于多种原因,如性能和潜在的静默屏蔽其他名称的风险,这是不被赞成的。您可以在官方 Python 文档中阅读关于导入的所有内容,但在我们离开这个主题之前,让我给您一个更好的例子。

假设您在lib文件夹中的模块funcdef.py中定义了一对函数:square(n)cube(n)。您希望在与lib文件夹处于相同级别的几个模块中使用它们,这些模块称为func_import.pyfunc_from.py。显示该项目的树结构会产生以下内容:

├── func_from.py
├── func_import.py
├── lib
 ├── funcdef.py
 └── __init__.py

在我展示每个模块的代码之前,请记住,为了告诉 Python 它实际上是一个包,我们需要在其中放置一个__init__.py模块。

关于__init__.py文件有两件事需要注意。首先,它是一个完整的 Python 模块,因此您可以像对待任何其他模块一样将代码放入其中。其次,从 Python 3.3 开始,不再需要它的存在来使文件夹被解释为 Python 包。

代码如下:

# funcdef.py
def square(n): 
    return n ** 2 
def cube(n): 
    return n ** 3 
# func_import.py import lib.funcdef 
print(lib.funcdef.square(10)) 
print(lib.funcdef.cube(10)) 
# func_from.py
from lib.funcdef import square, cube 
print(square(10)) 
print(cube(10)) 

这两个文件在执行时都会打印1001000。您可以看到我们如何根据当前范围中导入的内容以及导入的方式和内容来访问squarecube函数的不同方式。

相对导入

到目前为止,我们所见过的导入被称为绝对导入,即它们定义了我们要导入的模块的整个路径,或者我们要从中导入对象的模块。在 Python 中还有另一种导入对象的方式,称为相对导入。在我们想要重新排列大型包的结构而不必编辑子包时,或者当我们想要使包内的模块能够自我导入时,这种方式非常有帮助。相对导入是通过在模块前面添加与我们需要回溯的文件夹数量相同数量的前导点来完成的,以便找到我们正在搜索的内容。简而言之,它就是这样的。

from .mymodule import myfunc 

有关相对导入的完整解释,请参阅 PEP 328 (www.python.org/dev/peps/pep-0328/)。

总结

在本章中,我们探讨了函数的世界。它们非常重要,从现在开始,我们基本上会在任何地方使用它们。我们谈到了使用它们的主要原因,其中最重要的是代码重用和实现隐藏。

我们看到函数对象就像一个接受可选输入并产生输出的盒子。我们可以以许多不同的方式向函数提供输入值,使用位置参数和关键字参数,并对两种类型都使用变量语法。

现在你应该知道如何编写一个函数,对它进行文档化,将它导入到你的代码中,并调用它。

在下一章中,我们将看到如何处理文件以及如何以多种不同的方式和格式持久化数据。

第五章:文件和数据持久性

“持久性是我们称之为生活的冒险的关键。” - Torsten Alexander Lange

在之前的章节中,我们已经探索了 Python 的几个不同方面。由于示例具有教学目的,我们在简单的 Python shell 中运行它们,或者以 Python 模块的形式运行它们。它们运行,可能在控制台上打印一些内容,然后终止,留下了它们短暂存在的痕迹。

然而,现实世界的应用通常大不相同。它们当然仍然在内存中运行,但它们与网络、磁盘和数据库进行交互。它们还使用适合情况的格式与其他应用程序和设备交换信息。

在本章中,我们将开始逐渐接近现实世界,探索以下内容:

  • 文件和目录
  • 压缩
  • 网络和流量
  • JSON 数据交换格式
  • 使用 pickle 和 shelve 进行数据持久化,来自标准库
  • 使用 SQLAlchemy 进行数据持久化

和往常一样,我会努力平衡广度和深度,这样在本章结束时,你将对基本原理有扎实的理解,并且知道如何在网络上获取更多信息。

处理文件和目录

在处理文件和目录时,Python 提供了许多有用的工具。特别是在以下示例中,我们将利用osshutil模块。因为我们将在磁盘上读写数据,我将使用一个名为fear.txt的文件,其中包含了《恐惧》(Fear)的节选,作者是 Thich Nhat Hanh,作为我们一些示例的实验对象。

打开文件

在 Python 中打开文件非常简单和直观。实际上,我们只需要使用open函数。让我们看一个快速的例子:

# files/open_try.py
fh = open('fear.txt', 'rt')  # r: read, t: text
for line in fh.readlines():
    print(line.strip())  # remove whitespace and print
fh.close()

前面的代码非常简单。我们调用open,传递文件名,并告诉open我们要以文本模式读取它。在文件名之前没有路径信息;因此,open会假定文件在运行脚本的同一文件夹中。这意味着如果我们从files文件夹外部运行此脚本,那么fear.txt将找不到。

一旦文件被打开,我们就会得到一个文件对象fh,我们可以用它来处理文件的内容。在这种情况下,我们使用readlines()方法来迭代文件中的所有行,并打印它们。我们对每一行调用strip()来去除内容周围的任何额外空格,包括末尾的行终止字符,因为print会为我们添加一个。这是一个快速而粗糙的解决方案,在这个例子中有效,但是如果文件的内容包含需要保留的有意义的空格,你将需要在清理数据时更加小心。在脚本的结尾,我们刷新并关闭流。

关闭文件非常重要,因为我们不希望冒着释放文件句柄的风险。因此,我们需要采取一些预防措施,并将之前的逻辑包装在try/finally块中。这样做的效果是,无论我们尝试打开和读取文件时可能发生什么错误,我们都可以放心close()会被调用:

# files/open_try.py
try:
    fh = open('fear.txt', 'rt')
    for line in fh.readlines():
        print(line.strip())
finally:
    fh.close()

逻辑完全相同,但现在也是安全的。

如果你现在不理解try/finally,不要担心。我们将在后面的章节中探讨如何处理异常。现在,只需说将代码放在try块的主体内会在该代码周围添加一个机制,允许我们检测错误(称为异常)并决定发生错误时该怎么办。在这种情况下,如果发生错误,我们实际上并不做任何事情,但通过在finally块中关闭文件,我们确保该行被执行,无论是否发生了任何错误。

我们可以这样简化前面的例子:

# files/open_try.py
try:
    fh = open('fear.txt')  # rt is default
    for line in fh:  # we can iterate directly on fh
        print(line.strip())
finally:
    fh.close()

正如你所看到的,rt是打开文件的默认模式,因此我们不需要指定它。此外,我们可以直接在fh上进行迭代,而不需要显式调用readlines()。Python 非常好,给了我们简化代码的快捷方式,使我们的代码更短、更容易阅读。

所有前面的例子都在控制台上打印文件(查看源代码以阅读整个内容):

An excerpt from Fear - By Thich Nhat Hanh
The Present Is Free from Fear
When we are not fully present, we are not really living. We’re not really there, either for our loved ones or for ourselves. If we’re not there, then where are we? We are running, running, running, even during our sleep. We run because we’re trying to escape from our fear.
...

使用上下文管理器打开文件

让我们承认吧:不得不用try/finally块来传播我们的代码并不是最好的选择。像往常一样,Python 给了我们一个更好的方式以安全的方式打开文件:使用上下文管理器。让我们先看看代码:

# files/open_with.py
with open('fear.txt') as fh:
    for line in fh:
        print(line.strip())

前面的例子等同于之前的例子,但读起来更好。with语句支持由上下文管理器定义的运行时上下文的概念。这是使用一对方法__enter____exit__来实现的,允许用户定义的类定义在语句体执行之前进入的运行时上下文,并在语句结束时退出。open函数在由上下文管理器调用时能够生成一个文件对象,但它真正的美妙之处在于fh.close()会自动为我们调用,即使出现错误也是如此。

上下文管理器在几种不同的场景中使用,比如线程同步、文件或其他对象的关闭,以及网络和数据库连接的管理。您可以在contextlib文档页面中找到有关它们的信息(docs.python.org/3.7/library/contextlib.html)。

读写文件

现在我们知道如何打开文件了,让我们看看我们有几种不同的方式来读取和写入文件:

# files/print_file.py
with open('print_example.txt', 'w') as fw:
    print('Hey I am printing into a file!!!', file=fw)

第一种方法使用了print函数,你在前几章中已经见过很多次。在获取文件对象之后,这次指定我们打算写入它(“w”),我们可以告诉print调用将其效果定向到文件,而不是默认的sys.stdout,当在控制台上执行时,它会映射到它。

前面的代码的效果是:如果print_example.txt文件不存在,则创建它,或者如果存在,则将其截断,并将行Hey I am printing into a file!!!写入其中。

这很简单易懂,但不是我们通常写文件时所采用的方式。让我们看一个更常见的方法:

# files/read_write.py
with open('fear.txt') as f:
    lines = [line.rstrip() for line in f]
with open('fear_copy.txt', 'w') as fw:
    fw.write('\n'.join(lines))

在前面的例子中,我们首先打开fear.txt并将其内容逐行收集到一个列表中。请注意,这次我调用了一个更精确的方法rstrip(),作为一个例子,以确保我只去掉每行右侧的空白。

在代码片段的第二部分中,我们创建了一个新文件fear_copy.txt,并将原始文件中的所有行写入其中,用换行符\n连接起来。Python 很慷慨,并且默认使用通用换行符,这意味着即使原始文件的换行符与\n不同,它也会在返回行之前自动转换为\n。当然,这种行为是可以自定义的,但通常它正是你想要的。说到换行符,你能想到副本中可能缺少的换行符吗?

以二进制模式读写

请注意,通过在选项中传递t来打开文件(或者省略它,因为它是默认值),我们是以文本模式打开文件。这意味着文件的内容被视为文本并进行解释。如果您希望向文件写入字节,可以以二进制模式打开它。当您处理不仅包含原始文本的文件时,这是一个常见的要求,比如图像、音频/视频和一般的任何其他专有格式。

要处理二进制模式的文件,只需在打开文件时指定b标志,就像下面的例子一样:

# files/read_write_bin.py
with open('example.bin', 'wb') as fw:
    fw.write(b'This is binary data...')
with open('example.bin', 'rb') as f:
    print(f.read())  # prints: b'This is binary data...'

在这个例子中,我仍然使用文本作为二进制数据,但它可以是任何你想要的东西。你可以看到它被视为二进制数据的事实,因为在输出中你得到了b'This ...'前缀。

防止覆盖现有文件

Python 让我们有能力打开文件进行写入。通过使用w标志,我们打开一个文件并截断其内容。这意味着文件被覆盖为一个空文件,并且原始内容丢失。如果您希望仅在文件不存在时打开文件进行写入,可以改用x标志,如下例所示:

# files/write_not_exists.py
with open('write_x.txt', 'x') as fw:
    fw.write('Writing line 1')  # this succeeds
with open('write_x.txt', 'x') as fw:
    fw.write('Writing line 2')  # this fails

如果您运行前面的片段,您将在您的目录中找到一个名为write_x.txt的文件,其中只包含一行文本。实际上,片段的第二部分未能执行。这是我在控制台上得到的输出:

$ python write_not_exists.py
Traceback (most recent call last):
 File "write_not_exists.py", line 6, in <module>
 with open('write_x.txt', 'x') as fw:
FileExistsError: [Errno 17] File exists: 'write_x.txt'

检查文件和目录是否存在

如果您想确保文件或目录存在(或不存在),则需要使用os.path模块。让我们看一个小例子:

# files/existence.py
import os
filename = 'fear.txt'
path = os.path.dirname(os.path.abspath(filename))
print(os.path.isfile(filename))  # True
print(os.path.isdir(path))  # True
print(path)  # /Users/fab/srv/lpp/ch5/files

前面的片段非常有趣。在使用相对引用声明文件名之后(因为缺少路径信息),我们使用abspath来计算文件的完整绝对路径。然后,我们通过调用dirname来获取路径信息(删除末尾的文件名)。结果如您所见,打印在最后一行。还要注意我们如何通过调用isfileisdir来检查文件和目录的存在。在os.path模块中,您可以找到处理路径名所需的所有函数。

如果您需要以不同的方式处理路径,可以查看pathlib。虽然os.path使用字符串,但pathlib提供了表示适合不同操作系统的文件系统路径的类。这超出了本章的范围,但如果您感兴趣,请查看 PEP428(www.python.org/dev/peps/pep-0428/)及其在标准库中的页面。

操作文件和目录

让我们看一些关于如何操作文件和目录的快速示例。第一个示例操作内容:

# files/manipulation.py
from collections import Counter
from string import ascii_letters
chars = ascii_letters + ' '
def sanitize(s, chars):
    return ''.join(c for c in s if c in chars)
def reverse(s):
    return s[::-1]
with open('fear.txt') as stream:
    lines = [line.rstrip() for line in stream]
with open('raef.txt', 'w') as stream:
    stream.write('\n'.join(reverse(line) for line in lines))
# now we can calculate some statistics
lines = [sanitize(line, chars) for line in lines]
whole = ' '.join(lines)
cnt = Counter(whole.lower().split())
print(cnt.most_common(3))

前面的例子定义了两个函数:sanitizereverse。它们是简单的函数,其目的是从字符串中删除任何不是字母或空格的内容,并分别生成字符串的反转副本。

我们打开fear.txt,并将其内容读入列表。然后我们创建一个新文件raef.txt,其中将包含原始文件的水平镜像版本。我们使用join在新行字符上写入lines的所有内容。也许更有趣的是最后的部分。首先,我们通过列表推导将lines重新分配为其经过清理的版本。然后我们将它们放在whole字符串中,最后将结果传递给Counter。请注意,我们拆分字符串并将其转换为小写。这样,每个单词都将被正确计数,而不管其大小写,而且由于split,我们不需要担心任何额外的空格。当我们打印出最常见的三个单词时,我们意识到真正的 Thich Nhat Hanh 的重点在于其他人,因为we是文本中最常见的单词:

$ python manipulation.py
[('we', 17), ('the', 13), ('were', 7)]

现在让我们看一个更加面向磁盘操作的操作示例,其中我们使用shutil模块:

# files/ops_create.py
import shutil
import os
BASE_PATH = 'ops_example'  # this will be our base path
os.mkdir(BASE_PATH)
path_b = os.path.join(BASE_PATH, 'A', 'B')
path_c = os.path.join(BASE_PATH, 'A', 'C')
path_d = os.path.join(BASE_PATH, 'A', 'D')
os.makedirs(path_b)
os.makedirs(path_c)
for filename in ('ex1.txt', 'ex2.txt', 'ex3.txt'):
    with open(os.path.join(path_b, filename), 'w') as stream:
        stream.write(f'Some content here in {filename}\n')
shutil.move(path_b, path_d)
shutil.move(
    os.path.join(path_d, 'ex1.txt'),
os.path.join(path_d, 'ex1d.txt')
)

在前面的代码中,我们首先声明一个基本路径,该路径将安全地包含我们将要创建的所有文件和文件夹。然后我们使用makedirs创建两个目录:ops_example/A/Bops_example/A/C。(您能想到使用map来创建这两个目录的方法吗?)。

我们使用os.path.join来连接目录名称,因为使用/会使代码专门在目录分隔符为/的平台上运行,但是在具有不同分隔符的平台上,代码将失败。让我们委托给join来确定哪个是适当的分隔符的任务。

在创建目录之后,在一个简单的for循环中,我们放入一些代码,创建目录B中的三个文件。然后,我们将文件夹B及其内容移动到另一个名称D,最后,我们将ex1.txt重命名为ex1d.txt。如果你打开那个文件,你会看到它仍然包含来自for循环的原始文本。在结果上调用tree会产生以下结果:

$ tree ops_example/
ops_example/
└── A
 ├── C
 └── D
 ├── ex1d.txt
 ├── ex2.txt
 └── ex3.txt 

操作路径名

让我们通过一个简单的例子来更多地探索os.path的能力:

# files/paths.py
import os
filename = 'fear.txt'
path = os.path.abspath(filename)
print(path)
print(os.path.basename(path))
print(os.path.dirname(path))
print(os.path.splitext(path))
print(os.path.split(path))
readme_path = os.path.join(
    os.path.dirname(path), '..', '..', 'README.rst')
print(readme_path)
print(os.path.normpath(readme_path))

阅读结果可能是对这个简单例子的足够好的解释:

/Users/fab/srv/lpp/ch5/files/fear.txt           # path
fear.txt                                        # basename
/Users/fab/srv/lpp/ch5/files                    # dirname
('/Users/fab/srv/lpp/ch5/files/fear', '.txt')   # splitext
('/Users/fab/srv/lpp/ch5/files', 'fear.txt')    # split
/Users/fab/srv/lpp/ch5/files/../../README.rst   # readme_path
/Users/fab/srv/lpp/README.rst                   # normalized

临时文件和目录

有时,在运行一些代码时,能够创建临时目录或文件是非常有用的。例如,在编写影响磁盘的测试时,你可以使用临时文件和目录来运行你的逻辑并断言它是正确的,并确保在测试运行结束时,测试文件夹中没有任何剩余物。让我们看看在 Python 中如何做到这一点:

# files/tmp.py
import os
from tempfile import NamedTemporaryFile, TemporaryDirectory
with TemporaryDirectory(dir='.') as td:
    print('Temp directory:', td)
    with NamedTemporaryFile(dir=td) as t:
        name = t.name
        print(os.path.abspath(name))

上面的例子非常简单:我们在当前目录(.)中创建一个临时目录,并在其中创建一个命名的临时文件。我们打印文件名,以及它的完整路径:

$ python tmp.py
Temp directory: ./tmpwa9bdwgo
/Users/fab/srv/lpp/ch5/files/tmpwa9bdwgo/tmp3d45hm46 

运行这个脚本将每次产生不同的结果。毕竟,我们在这里创建的是一个临时的随机名称,对吧?

目录内容

使用 Python,你也可以检查目录的内容。我将向你展示两种方法:

# files/listing.py
import os
with os.scandir('.') as it:
    for entry in it:
        print(
            entry.name, entry.path,
            'File' if entry.is_file() else 'Folder'
        )

这个片段使用os.scandir,在当前目录上调用。我们对结果进行迭代,每个结果都是os.DirEntry的一个实例,这是一个暴露有用属性和方法的好类。在代码中,我们访问了其中的一部分:namepathis_file()。运行代码会产生以下结果(为了简洁起见,我省略了一些结果):

$ python listing.py
fixed_amount.py ./fixed_amount.py File
existence.py ./existence.py File
...
ops_example ./ops_example Folder
...

扫描目录树的更强大的方法是由os.walk提供的。让我们看一个例子:

# files/walking.py
import os
for root, dirs, files in os.walk('.'):
    print(os.path.abspath(root))
    if dirs:
        print('Directories:')
        for dir_ in dirs:
            print(dir_)
        print()
    if files:
        print('Files:')
        for filename in files:
            print(filename)
        print()

运行上面的片段将产生当前所有文件和目录的列表,并且对每个子目录都会执行相同的操作。

Python 入门指南(二)(4)https://developer.aliyun.com/article/1507373

相关文章
|
2天前
|
数据采集 运维 API
适合所有编程初学者,豆瓣评分8.6的Python入门手册开放下载!
Python是一种跨平台的计算机程序设计语言,它可以用来完成Web开发、数据科学、网络爬虫、自动化运维、嵌入式应用开发、游戏开发和桌面应用开发。 Python上手很容易,基本有其他语言编程经验的人可以在1周内学会Python最基本的内容(PS:没有基础的人也可以直接学习,速度会慢一点) 今天给小伙伴们分享一份Python语言及其应用的手册,这份手册主要介绍 Python 语言的基础知识及其在各个领域的具体应用,基于最新版本 3.x。
|
5天前
|
数据可视化 API Python
Python零基础“圣经”!300W小白从入门到精通首选!
今天分享的这本书在让你尽快学会 Python基础知识的同时,能够编写并正确的运行程序(游戏、数据可视化、Web应用程序) 最大的特色在于,在为初学者构建完整的 Python 语言知识体系的同时,面向实际应用情境编写代码样例,而且许多样例还是 后续实践项目部分的伏笔。实践项目部分的选题经过精心设计,生动详尽 又面面俱到。相信这本书能够得到更多 Python 初学者的喜爱。
|
6天前
|
Python
小白入门必备!计科教授的Python精要参考PDF开放下载!
随着互联网产业的高速发展,在网络上早已积累了极其丰富的Python学习资料,任何人都可以基于这些资源,自学掌握 Python。 但实际上,网络上充斥的资源太多、太杂且不成体系,在没有足够的编程/工程经验之前,仅靠“看”线上资源自学,的确是一件非常困难的事。
|
6天前
|
数据可视化 API Python
豆瓣评分9.4!堪称经典的Python入门圣经,你还没看过吗?
最理想的新人入门书应该满足两个特点:第一就是内容通俗易懂;第二就是要有实战,能够让读者在学完之后知道具体怎么用。 今天给小伙伴们分享的这份Python入门手册,在为初学者构建完整的Python语言知识体系的同时,面向实际应用情境编写代码样例,而且许多样例还是后续实践项目部分的伏笔。实践项目部分的选题经过精心设计,生动详尽又面面俱到。
|
8天前
|
数据采集 运维 API
适合所有编程初学者,豆瓣评分8.6的Python入门手册开放下载!
Python是一种跨平台的计算机程序设计语言,它可以用来完成Web开发、数据科学、网络爬虫、自动化运维、嵌入式应用开发、游戏开发和桌面应用开发。 Python上手很容易,基本有其他语言编程经验的人可以在1周内学会Python最基本的内容(PS:没有基础的人也可以直接学习,速度会慢一点)
|
9天前
|
数据采集 SQL 数据可视化
使用Python和Pandas库进行数据分析的入门指南
使用Python和Pandas库进行数据分析的入门指南
73 0
|
9天前
|
Linux iOS开发 MacOS
Python入门指南
Python入门指南
32 0
|
10天前
|
数据采集 前端开发 JavaScript
Python爬虫入门
网络爬虫是自动抓取网页数据的程序,通过URL获取网页源代码并用正则表达式提取所需信息。反爬机制是网站为防止爬取数据设置的障碍,而反反爬是对这些机制的对策。`robots.txt`文件规定了网站可爬取的数据。基础爬虫示例使用Python的`urllib.request`模块。HTTP协议涉及请求和响应,包括状态码、头部和主体。`Requests`模块是Python中常用的HTTP库,能方便地进行GET和POST请求。POST请求常用于隐式提交表单数据,适用于需要发送复杂数据的情况。
16 1
|
13天前
|
机器学习/深度学习 人工智能 数据可视化
Python编程入门:从零开始探索编程的奇妙世界
这篇教程引导初学者入门Python编程,从安装Python开始,逐步讲解基本语法,如`print()`、变量、条件判断、循环以及自定义函数。文章强调了Python在数据处理、数据分析、人工智能和机器学习等领域的重要性,并鼓励学习者探索Python的广泛应用,开启编程之旅。
|
14天前
|
数据可视化 API Python
Python零基础“圣经”!300W小白从入门到精通首选!
今天分享的这本书在让你尽快学会 Python基础知识的同时,能够编写并正确的运行程序(游戏、数据可视化、Web应用程序) 最大的特色在于,在为初学者构建完整的 Python 语言知识体系的同时,面向实际应用情境编写代码样例,而且许多样例还是 后续实践项目部分的伏笔。实践项目部分的选题经过精心设计,生动详尽 又面面俱到。相信这本书能够得到更多 Python 初学者的喜爱。