42、Python并发与系统脚本编程全解析

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的强大功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值