Python并发与系统脚本编程全解析
在Python编程中,并发编程和系统脚本编程是两个重要的领域。并发编程可以提高程序的执行效率,而系统脚本编程则可以帮助我们自动化一些常见的系统任务。下面将详细介绍这两个领域的相关知识和技巧。
1. 并发编程
1.1 绕过全局解释器锁(GIL)
在Python中,全局解释器锁(GIL)是一个需要关注的问题。GIL会导致同一时刻只有一个线程可以执行Python字节码,这在多线程CPU密集型任务中会成为性能瓶颈。不过,我们可以通过一些方法来绕过GIL:
-
使用进程池
:使用进程池时,需要进行数据序列化,并与另一个Python解释器进行通信。要确保操作被封装在通过
def
定义的函数中,返回值要与
pickle
兼容,且工作量要足够大以覆盖额外通信的开销。例如:
import multiprocessing
def task(x):
return x * x
if __name__ == '__main__':
with multiprocessing.Pool(processes=4) as pool:
results = pool.map(task, [1, 2, 3, 4])
print(results)
- 使用C扩展 :对于C扩展,要确保C代码独立于Python运行,不使用Python数据结构和API。同时,C扩展要执行足够的工作量,以证明接入它的努力是值得的。
- 其他解决方案 :对于一些不适合上述方法的应用程序,可以考虑自定义解决方案,如多个进程访问共享内存区域、多个解释器在一个进程中工作等。也可以考虑使用其他Python解释器实现,如PyPy。
1.2 定义演员任务
“演员模型”是一种简单且古老的并发和分布式计算方法。演员是并发执行的任务,它接收消息并对其进行操作,还可以向其他演员发送消息。演员之间的通信是单向且异步的。
以下是一个简单的演员实现示例:
from queue import Queue
from threading import Thread, Event
# 用于关闭的哨兵
class ActorExit(Exception):
pass
class Actor:
def __init__(self):
self._mailbox = Queue()
def send(self, msg):
'''
向演员发送消息
'''
self._mailbox.put(msg)
def recv(self):
'''
接收传入的消息
'''
msg = self._mailbox.get()
if msg is ActorExit:
raise ActorExit()
return msg
def close(self):
'''
关闭演员并停用它
'''
self.send(ActorExit)
def start(self):
'''
启动并发执行
'''
self._terminated = Event()
t = Thread(target=self._bootstrap)
t.daemon = True
t.start()
def _bootstrap(self):
try:
self.run()
except ActorExit:
pass
finally:
self._terminated.set()
def join(self):
self._terminated.wait()
def run(self):
'''
启动用户实现的方法
'''
while True:
msg = self.recv()
# 示例ActorTask
class PrintActor(Actor):
def run(self):
while True:
msg = self.recv()
print('Got:', msg)
# 示例使用
p = PrintActor()
p.start()
p.send('Hello')
p.send('World')
p.close()
p.join()
如果对并发和异步消息传递的要求不高,也可以使用生成器来定义类似演员的对象:
def print_actor():
while True:
try:
msg = yield # 获取消息
print('Got:', msg)
except GeneratorExit:
print('Actor terminating')
# 示例使用
p = print_actor()
next(p) # 推进到yield(准备接收)
p.send('Hello')
p.send('World')
p.close()
1.3 实现发布/订阅消息系统
在基于通信线程的程序中,实现发布/订阅消息系统可以通过添加一个“交换点”对象来实现。交换点作为所有消息的中介,消息发送到交换点后,由它将消息传递给一个或多个订阅的任务。
以下是一个简单的交换点实现示例:
from collections import defaultdict
class Exchange:
def __init__(self):
self._subscribers = set()
def attach(self, task):
self._subscribers.add(task)
def detach(self, task):
self._subscribers.remove(task)
def send(self, msg):
for subscriber in self._subscribers:
subscriber.send(msg)
# 所有创建的交换点的字典
_exchanges = defaultdict(Exchange)
# 返回与给定名称关联的Exchange实例
def get_exchange(name):
return _exchanges[name]
# 示例任务。任何具有send()方法的对象
class Task:
def send(self, msg):
pass
task_a = Task()
task_b = Task()
# 示例获取交换点
exc = get_exchange('name')
# 示例订阅任务
exc.attach(task_a)
exc.attach(task_b)
# 示例发送消息
exc.send('msg1')
exc.send('msg2')
# 示例取消订阅
exc.detach(task_a)
exc.detach(task_b)
使用交换点的好处包括简化线程通信的设置、开启新的通信模式以及可以与不同的“任务类”对象一起工作。但需要注意正确管理订阅者的附加和分离,可以使用上下文管理器来简化这一过程。
1.4 使用生成器作为线程的替代方案
使用生成器(协程)可以实现并发,这种方法有时被称为用户级线程或绿色线程。其核心思想是利用
yield
语句使生成器暂停执行,从而实现任务的协作切换。
以下是一个简单的任务调度器示例:
from collections import deque
# 两个简单的生成器
def countdown(n):
while n > 0:
print('T-minus', n)
yield
n -= 1
print('Blastoff!')
def countup(n):
x = 0
while x < n:
print('Counting up', x)
yield
x += 1
class TaskScheduler:
def __init__(self):
self._task_queue = deque()
def new_task(self, task):
'''
允许新的启动任务进入调度器
'''
self._task_queue.append(task)
def run(self):
'''
运行直到没有任务为止
'''
while self._task_queue:
task = self._task_queue.popleft()
try:
# 运行到下一个yield语句
next(task)
self._task_queue.append(task)
except StopIteration:
# 生成器不再执行
pass
# 示例使用
sched = TaskScheduler()
sched.new_task(countdown(10))
sched.new_task(countdown(5))
sched.new_task(countup(15))
sched.run()
在实际应用中,生成器可以用于替代线程来实现演员或网络服务器。但需要注意,使用生成器编程有一些基本限制,如无法获得线程提供的优势,大多数Python库可能无法很好地与基于生成器的线程配合工作。
1.5 轮询多线程队列
要轮询多个线程队列中的元素,可以使用一个隐藏的循环网络连接技巧。为每个要轮询的队列创建一对连接的套接字,通过向其中一个套接字写入数据来信号数据的存在,然后将另一个套接字传递给
select()
或类似函数进行轮询。
以下是一个示例:
import queue
import socket
import os
class PollableQueue(queue.Queue):
def __init__(self):
super().__init__()
# 创建一对连接的套接字
if os.name == 'posix':
self._putsocket, self._getsocket = socket.socketpair()
else:
# 为了与非POSIX系统兼容
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 0))
server.listen(1)
self._putsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._putsocket.connect(server.getsockname())
self._getsocket, _ = server.accept()
server.close()
def fileno(self):
return self._getsocket.fileno()
def put(self, item):
super().put(item)
self._putsocket.send(b'x')
def get(self):
self._getsocket.recv(1)
return super().get()
import select
import threading
def consumer(queues):
'''
消费者,同时从多个队列中读取数据
'''
while True:
can_read, _, _ = select.select(queues, [], [])
for r in can_read:
item = r.get()
print('Got:', item)
q1 = PollableQueue()
q2 = PollableQueue()
q3 = PollableQueue()
t = threading.Thread(target=consumer, args=([q1, q2, q3],))
t.daemon = True
t.start()
# 向队列提供数据
q1.put(1)
q2.put(10)
q3.put('hello')
q2.put(15)
这种方法可以解决普通轮询方法的一些问题,如性能问题和与其他对象轮询混合的问题。
1.6 在Unix上启动守护进程
在Unix或类Unix系统上创建正确的守护进程需要精确遵循一系列系统调用。以下是一个示例代码:
#!/usr/bin/env python3
# daemon.py
import os
import sys
import atexit
import signal
def daemonize(pidfile, *, stdin='/dev/null',
stdout='/dev/null',
stderr='/dev/null'):
if os.path.exists(pidfile):
raise RuntimeError('Already running')
# 第一个fork(与父进程分离)
try:
if os.fork() > 0:
raise SystemExit(0) # 父进程退出
except OSError as e:
raise RuntimeError('fork #1 failed.')
os.chdir('/')
os.umask(0)
os.setsid()
# 第二个fork(放弃会话领导权)
try:
if os.fork() > 0:
raise SystemExit(0)
except OSError as e:
raise RuntimeError('fork #2 failed.')
# 刷新输入/输出缓冲区
sys.stdout.flush()
sys.stderr.flush()
# 替换stdin、stdout和stderr的文件描述符
with open(stdin, 'rb', 0) as f:
os.dup2(f.fileno(), sys.stdin.fileno())
with open(stdout, 'ab', 0) as f:
os.dup2(f.fileno(), sys.stdout.fileno())
with open(stderr, 'ab', 0) as f:
os.dup2(f.fileno(), sys.stderr.fileno())
# 写入PID文件
with open(pidfile, 'w') as f:
print(os.getpid(), file=f)
# 安排在退出/信号时删除PID文件
atexit.register(lambda: os.remove(pidfile))
# 终止信号处理程序(必需)
def sigterm_handler(signo, frame):
raise SystemExit(1)
signal.signal(signal.SIGTERM, sigterm_handler)
def main():
import time
sys.stdout.write('Daemon started with pid {}\n'.format(os.getpid()))
while True:
sys.stdout.write('Daemon Alive! {}\n'.format(time.ctime()))
time.sleep(10)
if __name__ == '__main__':
PIDFILE = '/tmp/daemon.pid'
if len(sys.argv) != 2:
print('Usage: {} [start|stop]'.format(sys.argv[0]), file=sys.stderr)
raise SystemExit(1)
if sys.argv[1] == 'start':
try:
daemonize(PIDFILE,
stdout='/tmp/daemon.log',
stderr='/tmp/dameon.log')
except RuntimeError as e:
print(e, file=sys.stderr)
raise SystemExit(1)
main()
elif sys.argv[1] == 'stop':
if os.path.exists(PIDFILE):
with open(PIDFILE) as f:
os.kill(int(f.read()), signal.SIGTERM)
else:
print('Not running', file=sys.stderr)
raise SystemExit(1)
else:
print('Unknown command {!r}'.format(sys.argv[1]), file=sys.stderr)
raise SystemExit(1)
启动守护进程的命令如下:
bash % daemon.py start
bash % cat /tmp/daemon.pid
2882
bash % tail -f /tmp/daemon.log
Daemon started with pid 2882
Daemon Alive! Fri Oct 12 13:45:37 2012
Daemon Alive! Fri Oct 12 13:45:47 2012
...
停止守护进程的命令如下:
bash % daemon.py stop
bash %
2. 系统脚本编程
2.1 接受输入的脚本
使用
fileinput
模块可以让脚本通过重定向、管道或文件接受输入。以下是一个示例:
#!/usr/bin/env python3
import fileinput
with fileinput.input() as f_input:
for line in f_input:
print(line, end='')
可以通过以下方式使用该脚本:
$ ls | ./filein.py # 将目录内容输出到stdout。
$ ./filein.py /etc/passwd # 读取/etc/passwd到stdout。
$ ./filein.py < /etc/passwd # 读取/etc/passwd到stdout。
2.2 带错误消息终止程序
通过抛出
SystemExit
异常并传递错误消息作为参数,可以使程序输出错误消息并返回非零状态码终止。例如:
raise SystemExit('It failed!')
2.3 解析命令行参数
使用
argparse
模块可以解析命令行参数。以下是一个示例:
# search.py
'''
假设的命令行工具,用于在文件集合中搜索一个或多个文本模式。
'''
import argparse
parser = argparse.ArgumentParser(description='Search some files')
parser.add_argument(dest='filenames', metavar='filename', nargs='*')
parser.add_argument('-p', '--pat', metavar='pattern', required=True,
dest='patterns', action='append',
help='text pattern to search for')
parser.add_argument('-v', dest='verbose', action='store_true',
help='verbose mode')
parser.add_argument('-o', dest='outfile', action='store',
help='output file')
parser.add_argument('--speed', dest='speed', action='store',
choices={'slow', 'fast'}, default='slow',
help='search speed')
args = parser.parse_args()
# 输出收集的参数
print(args.filenames)
print(args.patterns)
print(args.verbose)
print(args.outfile)
print(args.speed)
该程序的使用示例如下:
bash % python3 search.py -h
usage: search.py [-h] [-p pattern] [-v] [-o OUTFILE]
[--speed {slow,fast}] [filename [filename ...]]
Search some files
positional arguments:
filename
optional arguments:
-h, --help show this help message and exit
-p pattern, --pat pattern text pattern to search for
-v verbose mode
-o OUTFILE output file
--speed {slow,fast} search speed
bash % python3 search.py foo.txt bar.txt
usage: search.py [-h] -p pattern [-v] [-o OUTFILE] [--speed {fast,slow}]
[filename [filename ...]]
search.py: error: the following arguments are required: -p/--pat
bash % python3 search.py -v -p spam --pat=eggs foo.txt bar.txt
filenames = ['foo.txt', 'bar.txt']
patterns = ['spam', 'eggs']
verbose = True
outfile = None
speed = slow
bash % python3 search.py -v -p spam --pat=eggs foo.txt bar.txt -o results
filenames = ['foo.txt', 'bar.txt']
patterns = ['spam', 'eggs']
verbose = True
outfile = results
speed = slow
bash % python3 search.py -v -p spam --pat=eggs foo.txt bar.txt -o results
--speed=fast
filenames = ['foo.txt', 'bar.txt']
patterns = ['spam', 'eggs']
verbose = True
outfile = results
speed = fast
2.4 在执行时请求密码
使用
getpass
模块可以在不显示用户输入字符的情况下提示用户输入密码。示例如下:
import getpass
user = getpass.getuser()
passwd = getpass.getpass()
if svc_login(user, passwd): # 你需要编写svc_login()
print('Yay!')
else:
print('Boo!')
如果需要明确提示用户输入用户名,可以使用
input
函数:
user = input('Enter your username: ')
2.5 获取终端窗口大小
使用
os.get_terminal_size()
函数可以获取终端窗口的大小。示例如下:
import os
sz = os.get_terminal_size()
print(sz)
print(sz.columns)
print(sz.lines)
2.6 执行外部命令并获取其输出
使用
subprocess.check_output()
函数可以执行外部命令并将其输出收集到Python字符串中。示例如下:
import subprocess
out_bytes = subprocess.check_output(['netstat', '-a'])
out_text = out_bytes.decode('utf-8')
如果命令执行返回非零退出码,会抛出异常。可以捕获异常并获取输出和退出码:
try:
out_bytes = subprocess.check_output(['cmd', 'arg1', 'arg2'])
except subprocess.CalledProcessError as e:
out_bytes = e.output # 错误发生前生成的输出
code = e.returncode # 返回码
默认情况下,
check_output()
只返回写入标准输出的内容。如果需要收集标准输出和标准错误输出,可以使用
stderr
参数:
out_bytes = subprocess.check_output(['cmd', 'arg1', 'arg2'],
stderr=subprocess.STDOUT)
如果需要执行带超时的命令,可以使用
timeout
参数:
try:
out_bytes = subprocess.check_output(['cmd', 'arg1', 'arg2'], timeout=5)
except subprocess.TimeoutExpired as e:
...
如果需要让命令在shell中执行,可以添加
shell=True
参数,但要注意安全问题,可以使用
shlex.quote()
函数正确传递参数。
2.7 复制或移动文件和目录
使用
shutil
模块可以复制或移动文件和目录。示例如下:
import shutil
# 复制src到dst。(cp src dst)
shutil.copy(src, dst)
# 复制数据,保留元数据 (cp -p src dst)
shutil.copy2(src, dst)
# 复制目录树 (cp -R src dst)
shutil.copytree(src, dst)
# 移动src到dst (mv src dst)
shutil.move(src, dst)
默认情况下,这些命令会跟随符号链接。如果需要复制符号链接本身,可以提供
follow_symlinks=False
参数。在复制目录时,可以使用
ignore
参数忽略某些文件和目录。
2.8 创建和解压存档
使用
shutil
模块的
make_archive()
和
unpack_archive()
函数可以创建和解压常见格式的存档。示例如下:
import shutil
shutil.unpack_archive('Python-3.3.0.tgz')
shutil.make_archive('py33', 'zip', 'Python-3.3.0')
2.9 按名称查找文件
使用
os.walk()
函数可以按名称查找文件。以下是一个示例:
#!/usr/bin/env python3.3
import os
def findfile(start, name):
for relpath, dirs, files in os.walk(start):
if name in files:
full_path = os.path.join(start, relpath, name)
print(os.path.normpath(os.path.abspath(full_path)))
if __name__ == '__main__':
import sys
findfile(sys.argv[1], sys.argv[2])
可以通过以下命令运行该脚本:
bash % ./findfile.py . myfile.txt
通过以上介绍,我们了解了Python并发编程和系统脚本编程的多个方面,包括绕过GIL的方法、演员模型的实现、发布/订阅消息系统的搭建、生成器的使用、多线程队列的轮询、守护进程的启动,以及系统脚本编程中接受输入、解析参数、请求密码、获取终端大小、执行外部命令、复制移动文件和目录、创建解压存档、查找文件等操作。这些知识和技巧可以帮助我们更好地编写高效、实用的Python程序。
Python并发与系统脚本编程全解析
2. 系统脚本编程(续)
在前面我们已经介绍了系统脚本编程的部分内容,接下来继续深入探讨一些相关的操作和技巧。
2.10 复制或移动文件和目录(深入探讨)
在使用
shutil
模块进行文件和目录操作时,还有一些细节需要注意。
-
保留符号链接
:
-
默认情况下,
shutil.copy、shutil.copy2和shutil.copytree会跟随符号链接,复制链接指向的文件。若要复制符号链接本身,可在copy2中使用follow_symlinks=False参数,在copytree中使用symlinks=True参数。 - 示例代码如下:
-
默认情况下,
import shutil
# 复制文件时保留符号链接
shutil.copy2(src, dst, follow_symlinks=False)
# 复制目录树时保留符号链接
shutil.copytree(src, dst, symlinks=True)
-
忽略文件和目录
:
-
在复制目录时,可使用
copytree的ignore参数忽略某些文件和目录。可以自定义一个函数,该函数接受目录名和文件名列表作为输入,返回需要忽略的文件名列表。 - 示例代码如下:
-
在复制目录时,可使用
import shutil
def ignore_pyc_files(dirname, filenames):
return [name for name in filenames if name.endswith('.pyc')]
shutil.copytree(src, dst, ignore=ignore_pyc_files)
- 也可使用`shutil.ignore_patterns`函数,它能更方便地根据模式忽略文件和目录。
shutil.copytree(src, dst, ignore=shutil.ignore_patterns('*~', '*.pyc'))
-
错误处理
:
-
在复制目录时,可能会遇到各种错误,如损坏的符号链接、权限问题等。所有异常会被收集到一个列表中,并在操作结束时抛出一个异常。可以通过捕获
shutil.Error异常来处理这些错误。 - 示例代码如下:
-
在复制目录时,可能会遇到各种错误,如损坏的符号链接、权限问题等。所有异常会被收集到一个列表中,并在操作结束时抛出一个异常。可以通过捕获
try:
shutil.copytree(src, dst)
except shutil.Error as e:
for src, dst, msg in e.args[0]:
print(dst, src, msg)
- 若要忽略损坏的符号链接,可在`copytree`中使用`ignore_dangling_symlinks=True`参数。
2.11 创建和解压存档(深入探讨)
shutil
模块的
make_archive
和
unpack_archive
函数在创建和解压存档时,还有一些使用技巧。
-
获取支持的存档格式
:
-
使用
shutil.get_archive_formats函数可以获取支持的存档格式列表。 - 示例代码如下:
-
使用
import shutil
formats = shutil.get_archive_formats()
print(formats)
-
创建存档
:
-
make_archive函数的第二个参数指定输出的存档格式,常见的有'zip'、'tar'、'gztar'等。 - 示例代码如下:
-
import shutil
shutil.make_archive('archive_name', 'zip', 'source_directory')
-
解压存档
:
-
unpack_archive函数可自动识别存档格式并进行解压。 - 示例代码如下:
-
import shutil
shutil.unpack_archive('archive_name.zip')
2.12 按名称查找文件(扩展功能)
使用
os.walk
函数按名称查找文件时,还可以扩展其功能。
-
查找最近修改的文件
:
- 可以编写一个函数,查找指定目录下最近在一定时间内修改过的文件。
- 示例代码如下:
import os
import time
def modified_within(top, seconds):
now = time.time()
for path, dirs, files in os.walk(top):
for name in files:
full_path = os.path.join(path, name)
mtime = os.path.getmtime(full_path)
if now - mtime < seconds:
print(full_path)
# 查找最近60秒内修改过的文件
modified_within('.', 60)
3. 总结与应用场景分析
3.1 并发编程总结
- 绕过GIL :在CPU密集型任务中,可使用进程池或C扩展来绕过GIL,提高程序性能。对于不适合这些方法的应用,可考虑自定义解决方案或使用其他解释器实现。
- 演员模型 :用于实现并发任务,适用于消息传递和异步通信场景,如分布式系统中的任务调度。
- 发布/订阅消息系统 :简化线程通信,可用于构建具有多任务通信需求的系统,如消息队列、事件驱动系统等。
- 生成器替代线程 :在一些对并发要求不高的场景中,使用生成器可以实现轻量级的并发,避免线程带来的开销。
- 多线程队列轮询 :解决多线程队列轮询的性能问题,可用于监控多个队列的状态。
- 守护进程 :在Unix系统上,守护进程可用于实现后台服务,如日志记录、定时任务等。
3.2 系统脚本编程总结
-
接受输入
:使用
fileinput模块可让脚本灵活接受输入,适用于处理多种输入源的脚本。 -
错误处理
:通过抛出
SystemExit异常并传递错误消息,可使程序优雅地终止并输出错误信息。 -
命令行参数解析
:
argparse模块方便解析命令行参数,使脚本具有更好的交互性。 -
密码输入
:
getpass模块可安全地获取用户密码,适用于需要身份验证的脚本。 -
终端窗口大小
:
os.get_terminal_size函数可获取终端窗口大小,用于优化脚本输出格式。 -
外部命令执行
:
subprocess模块可执行外部命令并获取输出,适用于与外部程序交互的脚本。 -
文件和目录操作
:
shutil模块提供了复制、移动、创建和解压存档等功能,方便进行文件管理。 -
文件查找
:
os.walk函数可按名称查找文件,还可扩展功能查找最近修改的文件。
3.3 应用场景示例
- 网络爬虫 :可使用并发编程提高爬取效率,使用系统脚本编程实现输入输出处理、文件管理等功能。
- 自动化部署脚本 :利用系统脚本编程解析命令行参数、执行外部命令、复制文件等,实现自动化部署。
- 分布式计算系统 :采用演员模型和发布/订阅消息系统实现任务调度和消息传递,提高系统的并发性能。
3.4 流程图总结
下面是一个简单的Python脚本开发流程的mermaid流程图:
graph LR
A[需求分析] --> B[选择并发或脚本编程方向]
B --> C{并发编程}
B --> D{系统脚本编程}
C --> C1[绕过GIL]
C --> C2[定义演员任务]
C --> C3[实现发布/订阅系统]
C --> C4[使用生成器替代线程]
C --> C5[轮询多线程队列]
C --> C6[启动守护进程]
D --> D1[接受输入]
D --> D2[带错误消息终止程序]
D --> D3[解析命令行参数]
D --> D4[请求密码]
D --> D5[获取终端窗口大小]
D --> D6[执行外部命令]
D --> D7[复制或移动文件和目录]
D --> D8[创建和解压存档]
D --> D9[按名称查找文件]
C1 --> E[编写代码]
C2 --> E
C3 --> E
C4 --> E
C5 --> E
C6 --> E
D1 --> E
D2 --> E
D3 --> E
D4 --> E
D5 --> E
D6 --> E
D7 --> E
D8 --> E
D9 --> E
E --> F[测试与调试]
F --> G[优化与部署]
通过对Python并发编程和系统脚本编程的深入学习,我们掌握了一系列实用的知识和技巧。在实际应用中,可根据具体需求灵活运用这些方法,开发出高效、稳定的Python程序。无论是处理大规模数据、构建分布式系统,还是实现自动化脚本,这些知识都将发挥重要作用。希望大家在实践中不断探索和创新,充分发挥Python的强大功能。
超级会员免费看
2万+

被折叠的 条评论
为什么被折叠?



