23、Python 输入输出及相关操作全解析

Python 输入输出及相关操作全解析

1. 输入操作

在 Python 中, input() 函数用于获取用户输入。例如:

name = input("Enter your name: ")

需要注意的是, input() 读取的行不包含末尾的换行符,这与直接从 sys.stdin 读取不同,后者会包含输入文本中的换行符。必要时, sys.stdout sys.stdin sys.stderr 的值可以替换为其他文件对象,此时 print() input() 函数将使用新的值。若要恢复 sys.stdout 的原始值,需先保存它,解释器启动时 sys.stdout sys.stdin sys.stderr 的原始值分别存储在 sys.__stdout__ sys.__stdin__ sys.__stderr__ 中。

2. 目录操作
  • 获取目录列表 :使用 os.listdir(pathname) 函数可以获取目录列表。示例代码如下:
import os
names = os.listdir('dirname')
for name in names:
    print(name)

listdir() 返回的名称通常根据 sys.getfilesystemencoding() 返回的编码进行解码。若将初始路径指定为字节,则文件名将作为未解码的字节字符串返回,示例如下:

import os
# Return raw undecoded names
names = os.listdir(b'dirname')
  • 文件名匹配 :与目录列表相关的一个有用操作是根据模式匹配文件名,即通配符匹配。可以使用 pathlib 模块实现此功能。例如,匹配特定目录中所有 .txt 文件的示例代码如下:
import pathlib
for filename in path.Path('dirname').glob('*.txt'):
    print(filename)

如果使用 rglob() 代替 glob() ,它将递归搜索所有子目录以查找匹配模式的文件名。 glob() rglob() 函数都返回一个生成器,通过迭代生成文件名。

3. print() 函数的使用

print() 函数用于打印一系列由空格分隔的值,示例如下:

print('The values are', x, y, z)

还可以通过关键字参数对 print() 函数进行更多操作:
- 抑制或更改行尾 :使用 end 关键字参数,示例如下:

# Suppress the newline
print('The values are', x, y, z, end='')
  • 将输出重定向到文件 :使用 file 关键字参数,示例如下:
# Redirect to file object f
print('The values are', x, y, z, file=f)
  • 更改项之间的分隔字符 :使用 sep 关键字参数,示例如下:
# Put commas between the values
print('The values are', x, y, z, sep=',')
4. 生成输出

除了直接处理文件,生成器函数也可用于将 I/O 流作为一系列数据片段发出。使用 yield 语句,就像使用 write() print() 函数一样。示例代码如下:

def countdown(n):
    while n > 0:
        yield f'T-minus {n}\n'
        n -= 1
    yield 'Kaboom!\n'

以这种方式生成输出流具有灵活性,因为它与实际将流导向其预期目标的代码解耦。例如,将上述输出路由到文件 f 的示例如下:

lines = countdown(5)
f.writelines(lines)

若要将输出重定向到套接字 s ,示例如下:

for chunk in lines:
    s.sendall(chunk.encode('utf-8'))

若要将所有输出捕获到单个字符串中,示例如下:

out = ''.join(lines)

更高级的应用可以使用这种方法实现自己的 I/O 缓冲。例如,一个生成器可以发出小的文本片段,另一个函数则将这些片段收集到更大的缓冲区中,以创建更高效的单个 I/O 操作。示例代码如下:

chunks = []
buffered_size = 0
for chunk in count:
    chunks.append(chunk)
    buffered_size += len(chunk)
    if buffered_size >= MAXBUFFERSIZE:
        outf.write(''.join(chunks))
        chunks.clear()
        buffered_size = 0
outf.write(''.join(chunks))

对于将输出路由到文件或网络连接的程序,生成器方法还可以显著减少内存使用,因为整个输出流通常可以以小片段的形式生成和处理,而不是先收集到一个大的输出字符串或字符串列表中。

5. 消费输入

对于消费零碎输入的程序,增强生成器可用于解码协议和 I/O 的其他方面。以下是一个接收字节片段并将其组装成行的增强生成器示例:

def line_receiver():
    data = bytearray()
    line = None
    linecount = 0
    while True:
        part = yield line
        linecount += part.count(b'\n')
        data.extend(part)
        if linecount > 0:
            index = data.index(b'\n')
            line = bytes(data[:index+1])
            data = data[index+1:]
            linecount -= 1
        else:
            line = None

以下是该生成器的使用示例:

>>> r = line_receiver()
>>> r.send(None)
# Necessary first step
>>> r.send(b'hello')
>>> r.send(b'world\nit ')
b'hello world\n'
>>> r.send(b'works!')
>>> r.send(b'\n')
b'it works!\n'

这种方法的一个有趣的副作用是,它将获取输入数据必须执行的实际 I/O 操作外部化。具体来说, line_receiver() 的实现根本不包含 I/O 操作,这意味着它可以在不同的上下文中使用,例如与套接字、文件或异步代码结合使用。以下是不同上下文的使用示例:
- 与套接字结合使用

r = line_receiver()
data = None
while True:
    while not (line:=r.send(data)):
        data = sock.recv(8192)
    # Process the line
    ...
  • 与文件结合使用
r = line_receiver()
data = None
while True:
    while not (line:=r.send(data)):
        data = file.read(10000)
    # Process the line
    ...
  • 在异步代码中使用
async def reader(ch):
    r = line_receiver()
    data = None
    while True:
        while not (line:=r.send(data)):
            data = await ch.receive(8192)
        # Process the line
        ...
6. 对象序列化

有时需要对对象的表示进行序列化,以便通过网络传输、保存到文件或存储在数据库中。一种方法是将数据转换为标准编码,如 JSON 或 XML,还有一种常见的 Python 特定数据序列化格式是 Pickle
pickle 模块将对象序列化为字节流,以便在以后的某个时间点重建对象。 pickle 的接口很简单,包括 dump() load() 两个操作。以下是将对象写入文件的示例代码:

import pickle
obj = SomeObject()
with open(filename, 'wb') as file:
    pickle.dump(obj, file)
# Save object on f

恢复对象的示例代码如下:

with open(filename, 'rb') as file:
    obj = pickle.load(file)
# Restore the object

pickle 使用的数据格式有自己的记录框架,因此可以通过依次发出一系列 dump() 操作来保存对象序列。要恢复这些对象,只需使用类似的 load() 操作序列。
在网络编程中,通常使用 pickle 创建字节编码的消息。可以使用 dumps() loads() 函数,这两个函数处理字节字符串。示例如下:

obj = SomeObject()
# Turn an object into bytes
data = pickle.dumps(obj)
...
# Turn bytes back into an object
obj = pickle.loads(data)

通常,用户定义的对象无需额外操作即可与 pickle 一起使用。但是,某些类型的对象无法被 pickle 序列化,这些对象通常包含运行时状态,如打开的文件、线程、闭包、生成器等。为了处理这些棘手的情况,类可以定义特殊方法 __getstate__() __setstate__()
__getstate__() 方法(如果定义)将被调用来创建表示对象状态的值,返回的值通常是字符串、元组、列表或字典。 __setstate__() 方法在反序列化期间接收此值,并应从中恢复对象的状态。
需要注意的是, pickle 在编码对象时不包含底层源代码本身,而是对定义类进行名称引用。在反序列化时,使用此名称在系统上进行源代码查找。为了使反序列化正常工作,接收 pickle 的一方必须已经安装了适当的源代码。此外, pickle 本质上是不安全的,反序列化不可信的数据是已知的远程代码执行途径,因此只有在能够完全保护运行时环境的情况下才应使用 pickle

7. 阻塞操作和并发

I/O 的一个基本概念是阻塞。由于 I/O 与现实世界相关,通常涉及等待输入或设备准备好。例如,从网络读取数据的代码可能会在套接字上执行接收操作:

data = sock.recv(8192)

当此语句执行时,如果有数据可用,它可能会立即返回;否则,它将停止,等待数据到达,这就是阻塞。在程序阻塞期间,其他操作无法进行。
对于数据分析脚本或简单程序,阻塞通常不是问题。但如果希望程序在操作阻塞时执行其他操作,则需要采用不同的方法,这就是并发的基本问题,即让程序同时处理多个任务。一个常见的问题是让程序同时从两个或多个不同的网络套接字读取数据,示例代码如下:

def reader1(sock):
    while (data := sock.recv(8192)):
        print('reader1 got:', data)
def reader2(sock):
    while (data := sock.recv(8192)):
        print('reader2 got:', data)
# Problem: How to make reader1() and reader2()
# run at the same time?

以下是几种解决此问题的方法:
- 非阻塞 I/O :通过启用非阻塞模式可以避免阻塞,例如在套接字上启用非阻塞模式的示例代码如下:

sock.setblocking(False)

启用后,如果操作会阻塞,则会引发异常,示例如下:

try:
    data = sock.recv(8192)
except BlockingIOError as e:
    # No data is available
    ...

以下是同时从两个套接字读取数据的示例代码:

def reader1(sock):
    try:
        data = sock.recv(8192)
        print('reader1 got:', data)
    except BlockingIOError:
        pass
def reader2(sock):
    try:
        data = sock.recv(8192)
        print('reader2 got:', data)
    except BlockingIOError:
        pass
def run(sock1, sock2):
    sock1.setblocking(False)
    sock2.setblocking(False)
    while True:
        reader1(sock1)
        reader2(sock2)

但实际上,仅依赖非阻塞 I/O 既笨拙又低效,因为程序的核心 run() 函数会在一个低效的忙循环中运行,不断尝试从套接字读取数据。
- I/O 轮询 :可以使用 select selectors 模块对 I/O 通道进行轮询,以查看是否有数据可用。以下是 run() 函数的改进版本示例:

from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
def run(sock1, sock2):
    selector = DefaultSelector()
    selector.register(sock1, EVENT_READ, data=reader1)
    selector.register(sock2, EVENT_READ, data=reader2)
    # Wait for something to happen
    while True:
        for key, evt in selector.select():
            func = key.data
            func(key.fileobj)

在这段代码中,每当在相应的套接字上检测到 I/O 时,循环会将 reader1() reader2() 函数作为回调进行调度。 selector.select() 操作本身会阻塞,等待 I/O 发生,因此不会使 CPU 疯狂旋转。这种 I/O 方法是许多所谓“异步”框架(如 asyncio )的基础。
- 线程 :可以使用线程编程和 threading 模块实现并发。以下是同时从两个套接字读取数据的示例代码:

import threading
def reader1(sock):
    while (data := sock.recv(8192)):
        print('reader1 got:', data)
def reader2(sock):
    while (data := sock.recv(8192)):
        print('reader2 got:', data)
t1 = threading.Thread(target=reader1, args=[sock1])
t2 = threading.Thread(target=reader2, args=[sock2])
# Start the threads
t1.start()
t2.start()
# Wait for the threads to finish
t1.join()
t2.join()

在这个程序中, reader1() reader2() 函数并发执行,由主机操作系统管理,因此无需了解其具体工作原理。如果一个线程中发生阻塞操作,不会影响其他线程。
- 使用 asyncio 进行并发执行 asyncio 模块提供了一种替代线程的并发实现。它基于一个使用 I/O 轮询的事件循环,但通过使用特殊的异步函数,其高级编程模型与线程非常相似。以下是一个示例:

import asyncio
async def reader1(sock):
    loop = asyncio.get_event_loop()
    while (data := await loop.sock_recv(sock, 8192)):
        print('reader1 got:', data)
async def reader2(sock):
    loop = asyncio.get_event_loop()
    while (data := await loop.sock_recv(sock, 8192)):
        print('reader2 got:', data)
async def main(sock1, sock2):
    loop = asyncio.get_event_loop()
    t1 = loop.create_task, reader1(sock1))
    t2 = loop.create_task(reader2(sock2))
    # Wait for the tasks to finish
    await t1
    await t2
...
# Run it
asyncio.run(main(sock1, sock2))

使用 asyncio 的详细信息需要专门的书籍介绍,但需要知道的是,许多库和框架都支持异步操作,通常意味着通过 asyncio 或类似模块支持并发执行,大部分代码可能涉及异步函数和相关特性。

8. 标准库模块

Python 有大量标准库模块用于各种 I/O 相关任务,以下是一些常用模块的简要概述及示例:
- asyncio 模块 asyncio 模块通过 I/O 轮询和底层事件循环支持并发 I/O 操作,主要用于涉及网络和分布式系统的代码。以下是一个使用低级套接字的 TCP 回显服务器示例:

import asyncio
from socket import *
async def echo_server(address):
    loop = asyncio.get_event_loop()
    sock = socket(AF_INET, SOCK_STREAM)
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    sock.setblocking(False)
    print('Server listening at', address)
    with sock:
        while True:
            client, addr = await loop.sock_accept(sock)
            print('Connection from', addr)
            loop.create_task(echo_client(loop, client))
async def echo_client(loop, client):
    with client:
        while True:
            data = await loop.sock_recv(client, 10000)
            if not data:
                break
            await loop.sock_sendall(client, b'Got:' + data)
            print('Connection closed')
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.create_task(echo_server(('', 25000)))
    loop.run_forever()

要测试此代码,可以使用 nc telnet 等程序连接到机器上的 25000 端口,代码应回显你输入的文本。如果使用多个终端窗口多次连接,会发现代码可以同时处理所有连接。大多数使用 asyncio 的应用程序可能在比套接字更高的级别上操作,但仍需使用特殊的异步函数并以某种方式与底层事件循环交互。
- binascii 模块 binascii 模块具有将二进制数据转换为各种基于文本的表示形式(如十六进制和 Base64)的函数,示例如下:

>>> import binascii
>>> binascii.b2a_hex(b'hello')
b'68656c6c6f'
>>> binascii.a2b_hex(_)
b'hello'
>>> binascii.b2a_base64(b'hello')
b'aGVsbG8=\n'
>>> binascii.a2b_base64(_)
b'hello'

类似的功能也可以在 base64 模块以及 bytes hex() fromhex() 方法中找到,示例如下:

>>> a = b'hello'
>>> a.hex()
'68656c6c6f'
>>> bytes.fromhex(_)
b'hello'
>>> import base64
>>> base64.b64encode(a)
b'aGVsbG8='
  • cgi 模块 :如果想在网站上放置一个基本表单,例如每周“Cats and Categories”时事通讯的注册表单,可以使用 cgi 模块编写一个基本的 CGI 脚本。假设网页上有以下表单片段:
<form method="POST" action="cgi-bin/register.py">
    <p>
        To register, please provide a contact name and email address.
    </p>
    <div>
        <input name="name" type="text">Your name:</input>
    </div>
    <div>
        <input name="email" type="email">Your email:</input>
    </div>
    <div class="modal-footer justify-content-center">
        <input type="submit" name="submit" value="Register"></input>
    </div>
</form>

以下是接收表单数据的 CGI 脚本示例:

#!/usr/bin/env python
import cgi
try:
    form = cgi.FieldStorage()
    name = form.getvalue('name')
    email = form.getvalue('email')
    # Validate the responses and do whatever
    ...
    # Produce an HTML result (or redirect)
    print("Status: 302 Moved\r")
    print("Location: https://www.mywebsite.com/thanks.html\r")
    print("\r")
except Exception as e:
    print("Status: 501 Error\r")
    print("Content-type: text/plain\r")
    print("\r")
    print("Some kind of error occurred.\r")

编写这样的 CGI 脚本可能不会让你在互联网初创公司找到工作,但可能会解决实际问题。

综上所述,Python 提供了丰富的输入输出及相关操作的功能和模块,通过合理运用这些知识,可以高效地处理各种 I/O 任务和并发问题。在实际应用中,需要根据具体需求选择合适的方法和模块,同时注意一些操作的安全性和性能问题。

Python 输入输出及相关操作全解析

9. 各模块功能总结与对比

为了更清晰地了解上述各模块的功能和适用场景,下面通过表格进行总结对比:
| 模块名称 | 主要功能 | 适用场景 | 示例代码 |
| — | — | — | — |
| asyncio | 通过 I/O 轮询和底层事件循环支持并发 I/O 操作 | 涉及网络和分布式系统的代码,如 TCP 回显服务器 |

import asyncio
from socket import *
async def echo_server(address):
    loop = asyncio.get_event_loop()
    sock = socket(AF_INET, SOCK_STREAM)
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    sock.setblocking(False)
    print('Server listening at', address)
    with sock:
        while True:
            client, addr = await loop.sock_accept(sock)
            print('Connection from', addr)
            loop.create_task(echo_client(loop, client))
async def echo_client(loop, client):
    with client:
        while True:
            data = await loop.sock_recv(client, 10000)
            if not data:
                break
            await loop.sock_sendall(client, b'Got:' + data)
            print('Connection closed')
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.create_task(echo_server(('', 25000)))
    loop.run_forever()
``` |
| `binascii` | 将二进制数据转换为各种基于文本的表示形式,如十六进制和 Base64 | 需要对二进制数据进行编码和解码的场景 | 
```python
import binascii
binascii.b2a_hex(b'hello')
binascii.a2b_hex(_)
binascii.b2a_base64(b'hello')
binascii.a2b_base64(_)
``` |
| `cgi` | 用于编写基本的 CGI 脚本,处理网页表单数据 | 简单的网页表单数据处理,如注册表单 | 
```python
#!/usr/bin/env python
import cgi
try:
    form = cgi.FieldStorage()
    name = form.getvalue('name')
    email = form.getvalue('email')
    # Validate the responses and do whatever
    ...
    # Produce an HTML result (or redirect)
    print("Status: 302 Moved\r")
    print("Location: https://www.mywebsite.com/thanks.html\r")
    print("\r")
except Exception as e:
    print("Status: 501 Error\r")
    print("Content-type: text/plain\r")
    print("\r")
    print("Some kind of error occurred.\r")
``` |
| `pickle` | 对象序列化,将对象转换为字节流以便传输、保存或存储 | 需要在网络传输对象、将对象保存到文件或数据库的场景 | 
```python
import pickle
obj = SomeObject()
# 保存对象
with open(filename, 'wb') as file:
    pickle.dump(obj, file)
# 恢复对象
with open(filename, 'rb') as file:
    obj = pickle.load(file)
``` |

#### 10. 并发操作流程梳理
下面通过 mermaid 流程图来梳理并发操作中几种方法的执行流程:
```mermaid
graph TD;
    A[开始] --> B{选择并发方法};
    B --> C[非阻塞 I/O];
    B --> D[I/O 轮询];
    B --> E[线程];
    B --> F[asyncio];
    C --> C1[设置套接字为非阻塞模式];
    C1 --> C2[尝试接收数据];
    C2 --> C3{是否有数据};
    C3 -- 是 --> C4[处理数据];
    C3 -- 否 --> C5[处理其他任务];
    C5 --> C2;
    D --> D1[创建选择器并注册套接字];
    D1 --> D2[等待 I/O 事件];
    D2 --> D3{是否有事件};
    D3 -- 是 --> D4[执行回调函数处理数据];
    D3 -- 否 --> D2;
    E --> E1[创建线程];
    E1 --> E2[启动线程];
    E2 --> E3{线程是否结束};
    E3 -- 否 --> E4[线程持续接收和处理数据];
    E3 -- 是 --> E5[结束线程];
    F --> F1[创建异步任务];
    F1 --> F2[运行事件循环];
    F2 --> F3{任务是否完成};
    F3 -- 否 --> F4[异步接收和处理数据];
    F3 -- 是 --> F5[结束事件循环];
11. 输入输出操作的最佳实践建议
  • 输入操作
    • 当使用 input() 函数获取用户输入时,要注意对输入进行验证和处理,避免因用户输入异常导致程序出错。例如,如果需要用户输入一个整数,可以使用 try-except 语句进行验证:
try:
    num = int(input("请输入一个整数: "))
    print(f"你输入的整数是: {num}")
except ValueError:
    print("输入不是有效的整数,请重新输入。")
- 在替换 `sys.stdout`、`sys.stdin` 和 `sys.stderr` 的值时,一定要先保存原始值,以便在需要时恢复。
  • 输出操作
    • 在使用 print() 函数时,合理使用 end sep file 关键字参数,以满足不同的输出需求。例如,当需要将日志信息输出到文件时,可以使用 file 参数:
with open('log.txt', 'a') as f:
    print('这是一条日志信息', file=f)
- 使用生成器函数生成输出时,要注意内存的使用情况,避免一次性生成大量数据导致内存溢出。可以结合 I/O 缓冲技术,将小片段数据收集成大缓冲区进行处理。
  • 并发操作
    • 对于简单的并发需求,可以先考虑使用 asyncio 模块,它提供了简洁的异步编程模型。例如,在处理多个网络请求时,使用 asyncio 可以提高程序的效率:
import asyncio

async def fetch_url(url):
    # 模拟网络请求
    await asyncio.sleep(1)
    print(f"获取到 {url} 的数据")

async def main():
    urls = ['http://example.com', 'http://example.org', 'http://example.net']
    tasks = [fetch_url(url) for url in urls]
    await asyncio.gather(*tasks)

asyncio.run(main())
- 当需要处理复杂的并发场景,如多线程共享资源时,要注意线程安全问题,可以使用锁机制来避免数据竞争。
  • 对象序列化
    • 尽量避免使用 pickle 处理不可信的数据,因为它存在安全风险。如果需要在网络传输对象,可以考虑使用更安全的序列化格式,如 JSON。
    • 在自定义类中使用 __getstate__() __setstate__() 方法时,要确保正确处理对象的状态,避免丢失重要信息。
12. 总结与展望

Python 提供的丰富的输入输出及相关操作功能和模块,为开发者处理各种 I/O 任务和并发问题提供了强大的支持。通过合理运用这些知识,我们可以高效地开发出各种类型的程序,从简单的命令行工具到复杂的网络应用。

在未来的 Python 开发中,随着技术的不断发展,并发编程和异步操作将变得越来越重要。 asyncio 模块也将不断完善和扩展,提供更多强大的功能和更简洁的编程接口。同时,对于数据的序列化和传输,也会有更多安全、高效的解决方案出现。开发者需要不断学习和掌握这些新技术,以跟上时代的步伐,开发出更优质的 Python 程序。

总之,掌握好 Python 的输入输出及相关操作知识,是成为一名优秀 Python 开发者的重要基础。希望本文能为大家在实际开发中提供一些帮助和参考。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值