PyCon-2017-会议笔记-全-

PyCon 2017 会议笔记(全)

001:PyCon 2017 演讲精讲

在本教程中,我们将学习Python字典在近年来的重要演进。从Python 2.7到3.6,字典增加了许多强大特性,如字典推导式、视图对象、键共享字典、哈希随机化以及紧凑字典等。这些改进不仅提升了性能,也增强了安全性和易用性。


P1:1:引言与背景

女士们,先生们,字典在Python中非常强大且有用。在Python 3.6中,添加了许多新特性,使其功能远超以往。本次教程基于布兰登·罗兹在PyCon 2017的演讲,将详细介绍这些增强功能。

正如许多人猜测的,这是对2010年演讲的后续。自那时起,Python字典发生了许多变化,尤其是在Python 3.6版本中。雷蒙德·赫廷格曾指出,3.6.1是首个比2.7更优秀的版本,因此现在是讨论字典发展的好时机。

最初的演讲在2010年2月进行,恰好在2.7最终发布前几个月,因此只关注了哈希表的基础原理。而2.7版本引入了来自Python 3系列的几项创新,这些内容将在本教程中涵盖。


P1:2:Python 2.7中的字典改进

上一节我们介绍了本次教程的背景,本节中我们来看看首先被引入Python 2.7的一些重要特性。

以下是三项从Python 3回溯到2.7的关键改进:

  1. 字典推导式
    列表推导式自Python 2.0就已存在,但创建字典需要先构建元组列表,效率较低。字典推导式解决了这个问题。

    # 旧方法:通过元组列表创建字典
    dict([(i, i*2) for i in range(5)])
    # 字典推导式
    {i: i*2 for i in range(5)}
    

    它更高效、易读,并且让语言更具对称性,方便学习者推断。

  2. 字典视图对象
    在Python 3.0引入并添加到2.7。之前,keys()values()items()方法返回列表的副本。视图对象则提供了字典条目的动态视图,支持集合操作和高效成员检查。

    d = {'a': 1, 'b': 2}
    keys_view = d.keys()   # 这是一个视图对象,不是列表
    # 支持集合操作
    other_keys = {'a', 'c'}
    print(keys_view & other_keys)  # 输出: {'a'}
    
  3. collections.OrderedDict
    在PEP 372中提出,于Python 3.1添加并回溯到2.7。它保持键的插入顺序,但在3.6之前,它比普通字典更大更慢,因此仅用于需要顺序的特殊场景。


P1:3:Python 3.3:键共享字典

上一节我们介绍了2.7中的改进,本节中我们来看看Python 3.3引入的键共享字典,它主要优化了内存使用。

PEP 412提出了键共享字典。考虑一个类,其实例的 __dict__ 存储着相同的属性名(键)。传统上,每个实例的字典都独立存储键和哈希值,造成重复。

键共享字典将键和哈希值分开存储。所有同类的实例共享同一份键结构,实例字典只存储值的引用。这为拥有大量相似对象的程序节省了10%-20%的内存。

核心机制

  • 第一个实例创建时,会建立键结构。
  • 后续同类型实例共享此结构,只存储自己的值数组。
  • 如果在 __init__ 外动态添加新属性,该实例会退化为传统字典,失去共享优势。

因此,最佳实践是在 __init__ 方法中初始化所有可能用到的属性。


P1:4:哈希洪水攻击与随机化

上一节我们讨论了内存优化,本节中我们来看看与字典安全相关的哈希洪水攻击及Python的应对策略。

字典插入的平均时间复杂度是O(1),但恶意攻击者可以精心构造一批哈希值全部冲突的键。这会导致每次插入都发生碰撞,使插入时间退化为O(n²),从而对Web服务发起拒绝服务攻击。

在2011年的安全会议上,此问题被公开,影响了包括Python在内的多种语言。

Python的应对分为两个阶段:

  1. Python 3.3:哈希随机化
    通过为哈希函数引入随机种子(进程启动时生成),使得攻击者无法预计算碰撞键。这通过环境变量 PYTHONHASHSEED=random-R 参数启用。

  2. Python 3.4:SipHash
    默认采用加密安全的SipHash算法替代原有哈希函数。它速度快,且能有效防止哈希洪水攻击,从此字典的哈希值完全随机且不可预测。

需要注意的是,哈希随机化也导致字典的迭代顺序在每次运行时都不同(在3.6引入紧凑字典并保证顺序之前)。


P1:5:Python 3.6:紧凑字典与顺序保证

上一节我们探讨了安全问题,本节中我们来看看Python 3.6中最革命性的变化:紧凑字典及其带来的顺序保证。

PEP 509为字典添加了版本号,为优化提供了可能。但更重要的变化是紧凑字典的实现。

传统字典结构

  • 一个稀疏表(例如8行),每行存储哈希值、键引用、值引用。
  • 当表达到2/3满时,会分配一个更大的新表并重新插入所有条目,旧表中的空行浪费了空间。

紧凑字典结构

  • 引入一个独立的索引数组(例如8字节),记录条目在“紧凑数组”中的位置。
  • 键和值被紧密地存储在两个单独的、连续的数组中。
  • 插入新键时,只需追加到数组末尾,并更新索引。

带来的好处

  1. 内存更高效:消除了稀疏表中的空位浪费。
  2. 迭代更快:可以直接遍历紧凑数组,无需跳过空槽。
  3. 天然保持插入顺序:因为键值对是按插入顺序追加到数组的。

由于紧凑字典天然有序,Python 3.6决定将字典的插入顺序作为官方语言特性。这意味着关键字参数顺序、类命名空间顺序等都得到了保证,符合人类直觉。

愿你的哈希是唯一的,你的键很少冲突,而你的字典永远是有序的。


P1:6:总结

在本教程中,我们一起学习了Python字典从2.7到3.6的重大演进:

  1. Python 2.7:引入了字典推导式、视图对象和OrderedDict,提升了表达能力和功能。
  2. Python 3.3:通过键共享字典优化了面向对象程序的内存使用。
  3. Python 3.3/3.4:通过哈希随机化和SipHash算法,有效防御了哈希洪水攻击,增强了安全性。
  4. Python 3.6:革命性的紧凑字典结构大幅提升了内存利用率和迭代性能,并正式保证了字典的插入顺序。

这些改进使得Python字典更加强大、高效和安全,同时保持了其对开发者友好的核心哲学。

002:2017年部署你的Python Web应用的五种方式

在本教程中,我们将学习五种在2017年将本地Python Web应用部署到互联网上的不同方法。我们将使用一个简单的Flask应用作为示例,并逐一演示每种部署方式,同时分析其优缺点,帮助你根据项目需求选择最合适的方案。

技术一:ngrok 🚀

上一节我们介绍了本教程的目标,本节中我们来看看第一种技术:ngrok。ngrok并非真正的部署,而是一个极其便捷的工具,可以为本地服务器创建安全的公共隧道。

ngrok的官方描述是“为本地主机提供安全隧道”。其核心功能是将运行在NAT或防火墙后的本地服务器暴露到互联网上。

以下是使用ngrok的步骤:

  1. 从官网下载并安装ngrok。
  2. 在本地启动Flask应用(例如运行在localhost:5000)。
  3. 在终端运行命令 ngrok http 5000
  4. ngrok会生成一个随机的公共URL(如 https://xxxx.ngrok.io),访问该URL即可看到你的应用。

ngrok还提供了一个内置的Web界面(通常位于 http://localhost:4040),用于实时查看所有传入的HTTP请求和响应,方便调试。

优点分析:

  • 速度极快,操作简单:没有比这更快让应用上线的方法。
  • 适合演示和临时分享:在会议或团队内快速展示本地开发成果。
  • 非常适合Webhook开发:当需要接收外部服务(如Twilio)的回调请求时,ngrok提供了临时的公网地址。

缺点分析:

  • 非持久化:关闭本地电脑或ngrok进程,服务即中断。
  • 域名随机:免费版每次启动都会获得不同的URL。
  • 无法扩展:免费版有并发连接数限制,不适合正式生产环境。

技术二:Heroku ☁️

上一节我们了解了用于临时暴露服务的ngrok,本节中我们来看看如何让应用持久在线。Heroku是一种平台即服务(PaaS),它能让你的代码在云端7x24小时运行,且过程非常简单。

使用Heroku部署,你无需直接管理服务器。其核心思想是:你提供代码和运行指令,Heroku负责其余的一切。

以下是部署到Heroku的关键步骤:

  1. 创建 Procfile 文件,定义启动命令。例如:web: gunicorn hello:app --log-file -
  2. 安装Heroku CLI并登录。
  3. 在项目目录下运行 heroku create 命令创建应用。
  4. 使用 git push heroku master 将代码推送到Heroku。
  5. 使用 heroku open 命令打开应用。

优点分析:

  • 简单快捷:是让应用免费、持久在线的最简单方式之一。
  • 无需服务器管理:完全抽象了底层基础设施。
  • 丰富的插件生态:可轻松添加数据库、日志、监控等服务。
  • 扩展简单:通过配置可以轻松增加服务器实例(但成本会增加)。

缺点分析:

  • 成本随规模增长:当应用需要多个服务器实例时,费用可能变得昂贵。
  • 服务器定制困难:如果需要特定的操作系统库或深度定制,会受到限制。
  • 插件质量参差:第三方插件的可靠性和支持水平不一。

技术三:无服务器架构(以AWS Lambda为例) ⚡

上一节我们介绍了全托管的PaaS平台Heroku,本节中我们来看看一种更前沿的部署模式:无服务器架构。这里的“无服务器”并非没有服务器,而是指你无需管理服务器,代码只在被请求时执行。

我们使用Zappa框架来简化将Flask应用部署到AWS Lambda的过程。Zappa封装了AWS的API Gateway和Lambda服务。

以下是使用Zappa部署的步骤:

  1. 安装Zappa:pip install zappa
  2. 初始化Zappa配置:zappa init,并回答相关问题(如环境名、AWS配置等)。
  3. 部署应用:zappa deploy production
  4. 部署完成后,Zappa会提供一个URL,访问即可看到应用。

部署后,你可以在AWS控制台中查看自动创建的API Gateway、Lambda函数和S3存储桶。

优点分析:

  • 按需付费,经济高效:只为代码实际执行的时间付费,非常适合低流量或突发流量场景。
  • 自动扩展:云服务商自动处理流量激增,无需人工干预。
  • 零服务器配置:比Heroku更彻底地抽象了服务器概念。

缺点分析:

  • 新兴技术:生态系统和最佳实践仍在快速发展中。
  • 调试复杂:问题可能涉及多个AWS服务的交互,排查需要熟悉AWS控制台。
  • 冷启动延迟:函数一段时间未被调用后再次启动,可能会有短暂的延迟。

技术四:虚拟机(以Google Compute Engine为例) 🖥️

上一节我们探讨了抽象程度很高的无服务器架构,本节我们将接触更底层的部署方式:虚拟机。这是互联网上最主流的运行代码的方式,它让你在云端获得一台完整的虚拟计算机。

我们以Google Compute Engine为例。使用虚拟机意味着你需要像设置一台全新物理服务器一样,手动配置一切。

以下是部署到虚拟机的基本步骤:

  1. 在云平台控制台创建虚拟机实例,选择配置(CPU、内存、操作系统如Ubuntu)。
  2. 通过SSH连接到虚拟机。
  3. 在虚拟机内手动设置环境:安装Python、pip、创建虚拟环境。
    sudo apt-get update
    sudo apt-get install python3-pip
    pip3 install virtualenv
    virtualenv venv
    source venv/bin/activate
    
  4. 拉取项目代码并安装依赖。
    git clone <your-repo>
    cd <your-project>
    pip install -r requirements.txt
    
  5. 使用Gunicorn启动应用,并绑定到外部IP和80端口。
    gunicorn hello:app -b 0.0.0.0:80
    

优点分析:

  • 完全控制:你可以完全掌控操作系统、安装任何软件、进行任何配置。
  • 扩展潜力大:性能上限取决于你选择的机器配置和钱包深度。
  • 成本可能更优:对于长期运行、负载稳定的应用,精心配置的虚拟机可能比PaaS更经济。

缺点分析:

  • 工作量大:需要手动完成所有运维工作,是今天所有方法中最繁琐的。
  • 学习曲线陡峭:需要掌握系统管理、网络、安全、监控、自动化配置(如Ansible)等知识。
  • 成本预测复杂:结合负载均衡器、存储等附加服务后,账单可能难以精确预估。

技术五:Docker 🐳

上一节我们体验了需要大量手动操作的虚拟机部署,本节我们介绍一种折中方案:Docker。Docker通过容器技术,试图在控制力和简便性之间取得平衡。

容器比虚拟机更轻量,它打包了应用及其所有依赖,确保环境一致性。你可以像在虚拟机中一样定义环境,但容器更容易创建、分发和运行。

以下是使用Docker部署的步骤:

  1. 创建 Dockerfile,定义如何构建镜像。
    FROM python:3-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    COPY . .
    EXPOSE 5000
    CMD ["gunicorn", "hello:app", "-b", "0.0.0.0:5000"]
    
  2. 构建Docker镜像:docker build -t yourname/five-ways .
  3. 本地运行测试:docker run -p 5000:5000 yourname/five-ways
  4. 将镜像推送到Docker Hub:docker push yourname/five-ways
  5. 在云服务器(可通过docker-machine创建)上拉取并运行该镜像。

优点分析:

  • 环境一致性:完美解决了“在我机器上能运行”的问题,确保开发、测试、生产环境一致。
  • 轻量高效:容器共享主机内核,启动速度快,资源开销远小于虚拟机。
  • 适合微服务:是构建和部署微服务架构的理想选择。
  • 镜像易于分发:构建一次,随处运行。

缺点分析:

  • 学习曲线:需要学习Dockerfile语法、镜像、容器、网络、存储卷等概念。
  • 最佳实践仍在演进:虽然已相对成熟,但生态系统和工具链仍在快速发展中。
  • 团队协作要求高:要充分发挥Docker的优势,需要团队在开发流程上做出相应调整。

总结 📝

本节课中我们一起学习了在2017年部署Python Web应用的五种主要方式:

  1. ngrok:用于快速创建临时公共隧道的利器,适合演示和Webhook开发。
  2. Heroku:全托管的PaaS平台,是让应用简单、持久上线的首选。
  3. 无服务器(AWS Lambda + Zappa):按需执行的架构,适合事件驱动和突发流量的场景,经济高效。
  4. 虚拟机(GCE/AWS EC2):提供完全控制,是互联网的基石,需要较强的运维能力。
  5. Docker:容器化技术,在环境一致性和部署便利性方面表现出色,是现代应用部署的重要趋势。

每种技术都在控制力、易用性、成本和复杂性之间有着不同的权衡。对于初学者,可以从Heroku或简单的Docker部署开始;当需要更多控制或优化成本时,可以逐步探索虚拟机和更深入的无服务器或容器化方案。选择最适合你当前项目阶段和团队技能的那一种。

003:Async/Await与Asyncio在Python 3.6及以后版本

概述

在本教程中,我们将学习Python 3.6及以后版本中的async/await语法和asyncio库。我们将探讨它们出现的原因、核心概念、生态系统、未来发展方向,并解答一些常见问题。本教程旨在让初学者能够理解异步编程的基本原理和应用。


为什么需要Async/Await?🚀

在开始学习async/await之前,我们首先需要理解为什么需要它。Python中已经存在多种实现并发的方式,例如线程、回调和Promise、GEvent、事件驱动编程、无栈Python,或者仅使用带有yield from语法的生成器。

那么,为什么还需要async/await呢?答案是可读性效率

  • 可读性async/await代码比基于回调或Promise的代码更易于阅读、调试和重构。它清晰地标明了代码中可能因I/O操作而切换上下文的点,并且促进了更好的编程模式(如消息传递),而不是依赖全局共享数据结构。
  • 效率:由于Python的全局解释器锁(GIL),线程并不总是最佳解决方案。即使在没有GIL的语言(如C#)中,async/await依然存在,因为线程是一种有限的系统资源。使用async/await,可以轻松处理成千上万个甚至数百万个长期存活的服务器连接。

什么是Async/Await?🔧

async/await不仅仅是一种语法,它还定义了一套协议。

语法层面

在Python 3.5中首次引入了async/await语法,用于定义协程(异步函数)、异步上下文管理器、异步迭代器等。在Python 3.6中,又进一步增加了异步生成器、异步列表推导式和异步生成器表达式。

目前,几乎所有的Python同步编程模式都可以用async/await来实现。

协议层面

常见的误解是async/await专为asyncio设计。实际上,async/await基于迭代器协议,通过一系列魔术方法(如__await__)使对象变得“可等待”。你可以为自己的框架实现async/await,但这通常需要大量工作。


异步编程的生态系统🌐

一个典型的现代async/await应用包含一个技术栈。

以下是这个技术栈的组成部分:

  1. 操作系统
  2. Python解释器
  3. 异步框架:例如asyncioTornadoTwistedCurioTrio
  4. 应用框架:例如支持异步的HTTP框架(aiohttpSanic)或Web框架的异步版本。
  5. 你的应用程序

主流框架介绍

  • Twisted 和 Tornado:这两个老牌异步框架现在都已支持async/await语法,并且拥有庞大的生态系统和市场份额。这意味着你可以在它们的代码中调用asyncio库,反之亦然。
  • Curio 和 Trio:这两个是较新的框架,致力于探索使异步编程更简单、更安全的新方法。它们虽然尚未成为主流,但其设计思想非常有价值。
  • Asyncio:这是Python标准库中的异步I/O框架。它提供了从低级回调API到高级async/await API的完整工具集。从Python 3.6开始,asyncio不再是临时模块,而是Python社区承诺长期维护的核心库。

深入Asyncio⚙️

asyncio是Python异步编程的核心基础。

核心特性

  • 低级与高级API:它既提供了基于回调的低级API(用于调度、网络传输等),也提供了基于async/await的高级API(用于协程、流、子进程等)。
  • 可插拔的事件循环:这是asyncio的关键设计,允许集成其他框架(如Twisted)或替换为更高性能的实现。
  • 健康的生态系统:围绕asyncio已经形成了丰富的库生态,涵盖了HTTP服务器、数据库驱动(PostgreSQL、MySQL、Redis等)、缓存等几乎所有主要组件。

性能优化:uvloop

uvloop是一个用Cython编写的高性能asyncio事件循环替代品。它基于libuv库,在实际生产代码中通常能带来15%到50%的性能提升。uvloop稳定且适用于生产环境。

未来探索:PyO3 Asyncio

这是一个实验性项目,旨在探索用Rust语言实现asyncio事件循环的可能性。目标是让asyncio成为Python与Rust世界之间的桥梁,结合Rust的安全性和高性能优势。


Asyncio的未来规划🔮

社区为asyncio的未来发展设定了一些目标,特别是在Python 3.7及以后版本中。

以下是asyncio未来的主要发展方向:

  1. 更好的框架集成:确保Twisted等框架的代码能无缝在asyncio上运行,并探索Curio/Trioasyncio上重构的可能性,以增加用户基数和修复潜在问题。
  2. 改进事件循环集成:简化第三方事件循环(即使用其他语言编写的事件循环)与asyncio的集成过程。
  3. 提升可用性与文档:当前asyncio的文档偏重底层细节。未来的重点是编写更友好的教程,教开发者如何高效使用和维护asyncio代码库。
  4. 简化API设计:鼓励设计不需要显式传递事件循环参数的高级API。开发者应在需要时通过asyncio.get_event_loop()获取当前循环。
  5. 添加新功能:计划添加的功能包括:
    • StartTLS支持:用于协议从明文升级到加密连接。
    • 上下文变量API:解决在深层异步代码中传递上下文(如请求对象)的难题。
    • 异步REPL:允许通过python -m asyncio直接进入交互式环境编写和测试异步代码。

社区鼓励开发者通过Python的bug追踪系统、邮件列表或GitHub提交问题和拉取请求,共同参与asyncio的改进。


常见问题解答❓

上一节我们展望了asyncio的未来,现在来看看开发者常见的一些疑问。

Q1: async/await与其他语言(如JavaScript、C#)的实现有何异同?

A: 核心思想基本相同。Python面临的一个独特挑战是其大量内置API是同步的,容易造成阻塞。除此之外,Python的async/await与JavaScript的非常相似。Python在某些方面甚至更优,例如率先支持了异步上下文管理器异步生成器

Q2: 既然uvloop更快,为什么它不是asyncio的默认事件循环?

A: 主要因为uvloop依赖的libuv库是一个庞大且独立的C库。Python标准库希望保持轻量和最小依赖。asyncio从一开始就设计了可插拔的事件循环架构,让用户能轻松安装和替换uvloop

Q3: 启动和运行asyncio程序的过程似乎很繁琐,有改进计划吗?

A: 是的。计划为Python 3.7添加像asyncio.run()这样的高级辅助函数。它将作为简单异步程序的入口点,自动处理事件循环的创建、运行和清理,极大简化启动流程。

Q4: PyO3 Asyncio项目的具体用例是什么?

A: 主要目标有两个:

  1. 促进集成:作为Python异步世界与Rust高性能、安全代码之间的桥梁。
  2. 提升性能:允许用Rust编写关键的低级组件(如HTTP解析器、数据库驱动),并以异步方式在Python中调用,从而获得安全性和性能的双重好处。

总结🎯

本节课我们一起学习了Python中的async/awaitasyncio

我们首先了解了引入异步编程是为了提升代码的可读性和运行效率。接着,我们剖析了async/await既是语法糖也是基于协议的编程模型。然后,我们浏览了丰富的异步编程生态系统,包括asyncioTornadoTwisted等框架。

我们深入探讨了asyncio的核心特性,如可插拔事件循环,并介绍了性能利器uvloop和前沿项目PyO3 Asyncio。最后,我们展望了asyncio的未来发展方向,并解答了若干常见问题。

asyncio为Python高性能网络服务和并发应用提供了坚实、灵活且充满活力的基础,是每一位现代Python开发者值得掌握的重要工具。

004:为完全初学者讲解异步 Python

在本节课中,我们将要学习异步编程的核心概念。我们将从基本定义开始,通过生动的比喻理解其工作原理,并了解它与进程、线程等传统并发方式的区别。最后,我们会探讨异步编程的优势、适用场景以及需要注意的常见陷阱。

什么是异步编程? 🧐

上一节我们介绍了课程概述,本节中我们来看看异步编程的基本定义。

异步是一个通用术语,指的是一种并发编程的方式,意味着程序可以同时处理多件事情。这并非特指 Python 的 asyncio 库,而是实现并发的一种模式。

其核心思想是:正在运行的任务在进入等待期(如等待网络响应)时,主动释放 CPU 控制权,让其他需要 CPU 的任务得以运行

实现并发的几种方式 ⚙️

在深入了解异步之前,我们先看看几种常见的并发实现方式。

以下是三种主要的并发模型:

  1. 多进程:启动多个独立的 Python 解释器进程。操作系统负责在多个 CPU 核心间分配资源。这是 CPython 中真正利用多核 CPU 的唯一方式。
  2. 多线程:在一个进程内创建多个执行线程。它们共享内存空间,但编写线程安全的代码较为复杂。在 Python 中,由于全局解释器锁(GIL)的存在,任何时候只有一个线程可以执行 Python 字节码,这限制了多线程在 CPU 密集型任务上的性能。
  3. 异步编程:在单个进程、单个线程内实现并发。它通过“协作式多任务”来管理多个任务,由程序自身(而非操作系统)来调度任务在何时运行。

异步如何工作:国际象棋表演的比喻 ♟️

上一节我们对比了不同的并发方式,本节中我们通过一个比喻来形象地理解异步的工作原理。

想象一位国际象棋大师同时与 24 位业余棋手对弈。

  • 同步方式:大师与第一位棋手对弈直至终局(假设30分钟),然后再与第二位对弈,如此循环。完成全部对局需要 24 * 30分钟 = 12小时
  • 异步方式:大师走到第一张棋盘,走一步(5秒),然后立即移动到第二张棋盘走一步,以此类推。在两分钟内,她可以在所有24张棋盘上走出第一步。当她回到第一张棋盘时,对手已经想好了应手,她可以立即走下一步。通过这种方式,她可以在大约1小时内完成所有对局。

在这个比喻中:

  • 国际象棋大师 相当于 CPU
  • 走棋 相当于 执行计算
  • 等待对手思考 相当于 I/O 等待
  • 异步的秘诀 就是 避免 CPU 空闲等待,充分利用等待时间去处理其他任务

异步编程的技术要件 🔧

理解了核心思想后,我们来看看在代码层面实现异步需要哪些技术组件。

实现异步编程主要需要两样东西:

  1. 可暂停和恢复的函数:我们需要一种机制,让函数在遇到 I/O 等待时能够“暂停”,并在等待结束后从暂停点“恢复”执行。在 Python 中,有几种方式可以实现:

    • 生成器函数:使用 yieldyield from 关键字。
    • async/await 关键字:Python 3.5 引入的语法,使代码更清晰。
    • 第三方库:如 greenlet
    • 回调函数:一种较原始且复杂的方式。
  2. 事件循环:这是一个调度器,负责管理所有待运行的任务(协程)。它从可运行的任务中选择一个执行,当该任务主动暂停(await)时,事件循环收回控制权,并选择另一个任务执行,如此循环往复。这种模式称为 协作式多任务处理

代码示例对比 📝

上一节我们介绍了异步的技术基础,本节中我们通过具体代码来感受其写法。

以下是一个简单的“打印 Hello, 等待3秒, 打印 World”的任务。

同步阻塞版本

import time

def hello():
    print(‘Hello’)
    time.sleep(3)  # 阻塞调用,整个线程停住
    print(‘World’)

# 运行10次将耗时约30秒
for _ in range(10):
    hello()

使用 asyncio 的异步版本

import asyncio

async def hello():
    print(‘Hello’)
    await asyncio.sleep(3)  # 非阻塞暂停,事件循环可在此期间运行其他任务
    print(‘World’)

async def main():
    # 创建10个并发任务
    tasks = [asyncio.create_task(hello()) for _ in range(10)]
    await asyncio.gather(*tasks)

asyncio.run(main())  # 总耗时仅略多于3秒

在异步版本中,当第一个任务执行到 await asyncio.sleep(3) 时,它会暂停并将控制权交还给事件循环。事件循环随即执行第二个、第三个...任务。因此,所有10个任务的“等待3秒”是并发进行的,总运行时间远短于30秒。

异步编程的陷阱与注意事项 ⚠️

编写异步代码时,有一些关键的注意事项,否则可能无法获得预期的性能提升,甚至导致程序阻塞。

以下是初学者常遇到的几个陷阱:

  1. CPU 密集型任务会阻塞事件循环:由于是单线程协作,如果一个任务长时间占用 CPU 进行计算而不暂停(await),其他所有任务都会被阻塞。解决方案是在计算循环中定期 await asyncio.sleep(0) 来主动让出控制权。
  2. 不能使用标准的阻塞式 I/O 函数:例如 time.sleep()socket.recv()、同步的文件读写等。这些函数会阻塞整个线程。必须使用异步框架提供的替代品,如 asyncio.sleep()aiofiles 等。
  3. “猴子补丁”的兼容性问题:一些异步框架(如 geventeventlet)通过“猴子补丁”替换标准库的阻塞函数,使得同步代码无需修改就能获得异步行为。但这可能带来隐蔽的兼容性问题,并且 asyncio 不采用这种方式,它要求显式地使用异步库。

如何选择:进程、线程还是异步? 📊

最后,我们通过一个对比表格来总结,帮助你根据实际场景做出技术选型。

特性 多进程 多线程 异步
利用多核 CPU (最佳) 受 GIL 限制 受单线程限制
可扩展性 低 (内存开销大) (可处理数千连接)
I/O 等待时不阻塞 是 (OS 调度) 是 (OS 调度) (协作式调度)
使用标准库阻塞函数 可以 可以 不可以 (需用异步版本)
编程复杂度 高 (线程安全)

总结与建议

  • 选择异步的最佳理由需要极高的 I/O 并发能力,例如构建高性能网络服务器、爬虫或微服务,需要处理成千上万的并发连接。
  • 如果需要充分利用多核进行CPU 密集型计算,应选择多进程,或结合“多进程+异步/线程”的架构。
  • 如果只是简单的后台任务或已有同步代码库,多线程可能更易于集成。
  • 如果你喜欢异步的编程模式,它本身也是一个完全有效的应用开发框架。

本节课中我们一起学习了异步 Python 的核心思想、工作原理、代码实现以及适用场景。关键要记住:异步通过让任务在等待时主动让出 CPU 来实现高并发,其威力在于 I/O 密集型场景,但编写时需避免阻塞调用并注意任务间的友好协作

005:Cython作为效率的游戏规则改变者 🚀

在本教程中,我们将学习如何利用Cython来显著提升Python代码的执行效率。我们将从Python的性能瓶颈谈起,逐步介绍Cython的核心概念、基本语法和实际应用,帮助你理解如何在不重写大量代码或改变现有运行时环境的情况下,为你的Python项目带来性能飞跃。


1:Python的效率挑战

我们知道Python是一门适合多种场景的伟大语言,但它在执行速度方面存在不足。开发者效率高,但在CPU或内存使用率方面表现不佳。

对于典型网络公司的后端工程师,扩展初期面临的挑战通常是数据库或缓存问题。无状态的Web服务器层可以通过增加机器来简单扩展。

但当机器数量增长到一定程度时,节省成本、减少机器数量就变得重要。即便如此,Python的执行速度本身可能并非首要关注点,问题可能是CPU密集型、内存密集型或I/O密集型。


2:性能分析与优化起点

假设你和Instagram一样面临CPU问题,第一步是进行性能分析。根据帕累托原则,20%的代码通常负责80%的CPU使用率。因此,应避免过早优化,先找出需要优化的关键代码部分。

找到关键代码后,下一步是阅读代码。代码可能执行了不必要的操作,或存在数据结构误用等问题。

例如,以下代码使用列表推导式生成列表,然后在循环中检查元素是否存在:

# 低效的 O(N²) 算法
my_list = [x for x in some_iterable]
for item in another_iterable:
    if item in my_list:  # 列表的`in`操作是O(N)
        # 执行操作

列表数据结构并非为快速成员查询设计,这导致了O(N²)的算法复杂度。修复方法很简单,将列表推导式改为集合推导式,复杂度即降为线性:

# 高效的 O(N) 算法
my_set = {x for x in some_iterable}
for item in another_iterable:
    if item in my_set:  # 集合的`in`操作平均是O(1)
        # 执行操作

在进行任何重大更改前,应先尝试阅读代码并检查算法。但如果代码逻辑正确却依然很慢,则需考虑其他方案。


3:性能优化方案对比

当代码逻辑正确但性能不足时,你有多种选择。

方案A:微服务
将关键代码提取为独立的微服务,并用性能更好的语言(如Go、Rust)重写。缺点是工作量非轻,且会增加系统架构的复杂性和维护成本。

方案B:经典的C扩展
将代码用C/C++重写,并创建Python绑定。许多高性能库(如NumPy)正是如此构建。但要求开发者掌握C/C++,门槛较高。

方案C:更换Python运行时
Python有多种实现,如PyPy、Jython。但切换运行时可能不简单,特别是当项目依赖大量C扩展时,迁移会变得棘手。

方案D:升级Python版本
从Python 2升级到Python 3可能带来显著的性能提升(例如Instagram获得了12%的整体CPU使用率下降)。如果可行,这应作为优先选项。

方案E:使用Cython
Cython是Python的超集,它允许你编写类似Python的代码,并将其编译为C/C++扩展模块,从而获得接近原生代码的性能,同时保持与现有CPython运行时的完全兼容。


4:Cython初体验与核心优势

Cython是什么?根据定义,Cython是为提高性能而设计的Python编程语言超集。大部分代码用Python编写,但它提供了可选的额外语法。它编译为C或C++,并与现有运行时完美兼容,无需更改基础设施。

考虑以下实例:Instagram发现Django的URL调度器消耗了系统4%的CPU。他们所做的第一步仅仅是使用Cython编译了这个模块。这个简单的操作使该模块性能提升了3倍,CPU消耗从4%降至1%。

关键在于,他们没有更改任何一行Django源代码,也无需学习Cython的新语法就获得了显著的性能收益。这是一个轻松的胜利。


5:Cython语法入门:类型声明

上一节我们看到了Cython的威力。本节中,我们来看看如何通过添加类型注解来让Cython发挥更大作用。

Cython引入的主要关键字是cdef,用于声明变量和函数的C类型。

变量类型声明

cdef int i
cdef str s = ""
cdef list data = []

这声明了一个C整数i,一个Python字符串s和一个Python列表data

函数类型声明

def transform(int x) -> int:
    return x * x

def plot(int n) -> int:
    cdef int result = 0
    cdef int i
    for i in range(n):
        result += transform(i)
    return result

在函数签名和局部变量中添加类型后,Cython能生成更高效的C代码。对于大型函数,性能提升可达数百倍。

函数声明类型
Cython有三种函数声明方式:

  1. def: 普通的Python函数。
  2. cdef: 纯C函数,只能被Cython或C代码调用,无Python调用开销。
  3. cpdef: 混合声明,既生成高效的C函数供内部调用,也生成一个Python包装器供外部Python代码调用。

6:Cython支持的类型系统

Cython支持丰富的类型系统,这是其性能优化的基础。

基本C类型
支持所有基本的C类型,如intlongfloatdoublechar

字符串类型
支持字节字符串和Unicode字符串,在Python 2和Python 3中都能正确处理。例如,str类型在Python 3中是Unicode,在Python 2中是字节串。

Python集合
支持所有常用的Python集合类型,如listdicttuple。在大多数情况下,使用这些类型并添加注解就能获得2到5倍的性能提升。

低级类型(进阶)
对于追求极致性能的场景,Cython支持C数组、原始指针、枚举、C结构和联合体。例如,你可以使用C++标准模板库(STL)中的容器:

from libcpp.vector cimport vector
cdef vector[int] vec

使用这些低级类型需要格外小心。


7:使用扩展类型(cdef类)

除了基本类型,Cython的“扩展类型”(使用cdef class声明)是另一个强大的性能优化工具。它们看起来像普通Python类,但属性存储在类型化的C结构而非Python字典中。

以下是一个扩展类型的例子:

cdef class PyConSpeaker:
    cdef str name
    cdef int age
    cdef str bio

    def __init__(self, str name, int age, str bio):
        self.name = name
        self.age = age
        self.bio = bio

    @property
    def info(self):
        return f"{self.name}, {self.age}"

优势

  • 内存占用少:属性存储在C结构体中。
  • 访问速度快:属性查找和方法调用更快。
  • 可作为静态类型:可以在Cython的类型系统中使用。
  • 完全兼容:可以从Python代码中创建和继承。

8:Cython优化工作流程

结合以上知识,一个典型的Cython优化工作流程如下:

  1. 定位与编译:识别出性能关键模块,直接用Cython编译(不修改代码),测试性能。
  2. 逐步添加类型:如果性能不达标,为关键函数和变量添加类型注解(cdef),重新编译测试。
  3. 迭代优化:重复步骤2,逐步添加更多类型,直到达到性能目标。
  4. 深度优化(可选):在极少数情况下,如果类型化后仍需要更高性能,可以考虑使用C数组、C++ STL容器等低级结构替换Python数据结构。

辅助工具:注解报告
在编译时使用-a标志(如cythonize -a your_module.pyx),Cython会生成一个HTML报告。报告中:

  • 黄色线条表示与Python虚拟机的交互。
  • 颜色越亮,交互成本越高。
  • 点击行可以查看生成的C代码,帮助你理解性能瓶颈所在。

9:实践成果与总结

Instagram的实践表明,仅将约10-15个关键模块转换为Cython,就使整个Web服务栈的全局CPU消耗降低了30%。这是一个以较小投入获得巨大回报的典型案例。

回顾与总结
在本教程中,我们一起学习了如何利用Cython优化Python性能:

  1. 勿过早优化:像Instagram一样,在规模达到一定程度后再针对性优化。
  2. 分析先行:使用性能分析工具定位关键代码。
  3. 首选Cython:与其他方案(微服务、C扩展、更换运行时)相比,Cython优势明显:
    • 避免重写:无需用其他语言重写大量代码。
    • 渐进式:可以从编译现有代码开始,逐步添加类型。
    • 语法亲和:本质上是“带类型的Python”,学习曲线平缓。
    • 运行时兼容:完全兼容现有CPython环境和基础设施。

Cython不仅适用于包装C库或数据科学项目,对于典型的网络服务也同样高效。如果你想深入探索,可以访问 cython.org 查看详细文档。

本节课中,我们一起学习了Python的性能瓶颈、Cython的核心概念与语法、以及如何通过渐进式类型注解来显著提升代码执行效率。 现在,你可以尝试在自己的项目中应用这些知识,开启性能优化之旅。

006:进展报告 - PyCon 2017

概述

在本教程中,我们将学习 Larry Hastings 在 PyCon 2017 上关于“Gilectomy”项目的演讲内容。Gilectomy 是一个旨在移除 CPython 全局解释器锁(GIL)的项目,目标是让现有的多线程 Python 程序能够在多个 CPU 核心上并行运行,同时尽可能减少对现有 C API 的破坏。


项目背景与目标

在上一节中,我们了解了 Gilectomy 项目的存在。本节中,我们来看看项目的具体目标和面临的挑战。

Gilectomy 的目标是让使用 threading 模块编写的现有多线程 Python 程序能够在多个核心上同时运行。实现方式需要尽可能减少对 C API 的破坏。虽然无法完全保证 GIL 提供的所有特性,但会尽力减少破坏。

项目的成功标准是:在墙钟时间(wall-clock time)上,Gilectomy 下的程序运行速度要比带 GIL 的标准 CPython 更快。

为实现这一目标,Larry 采取的主要方法包括:

  • 将 Python 用于跟踪对象生命周期的引用计数操作切换为原子操作
  • 在 Python 内部所有可变对象(如字典、列表)的数据结构上添加锁,以确保操作安全。
  • 在 CPython 内部使用的许多数据结构(如小块分配器 obmalloc、各种空闲列表)周围添加锁,或使它们按线程分配。
  • 暂时禁用垃圾收集器,因为实现线程安全的垃圾收集算法是一项独立且复杂的工作。

总体策略是:先让程序运行起来,然后通过性能分析找出瓶颈并优化。


性能基准测试的挑战

在深入技术细节之前,我们需要了解一个前提:在现代计算机上进行精确的基准测试非常困难。

现代 CPU(如 Intel Xeon)具有动态调整频率的技术。这意味着 CPU 核心的运行速度会不断变化,你无法确切知道某个核心在某一时刻的运行速度。这种不确定性使得有意义的基准测试变得几乎不可能,基准结果可能存在高达 5% 的误差。

因此,在评估 Gilectomy 的性能数据时,需要考虑到这种背景噪音。


演进历程与性能对比

Gilectomy 项目自启动以来经历了几个主要的技术阶段。以下是每个阶段的简要介绍和其对性能的影响。

我们通过一个运行递归斐波那契数列生成器的基准测试来对比性能。图表中,X 轴代表使用的线程(核心)数,Y 轴代表程序运行时间(秒)。红线代表标准 CPython(带 GIL),其他颜色的线代表不同阶段的 Gilectomy。

  1. 原子引用计数(2016年6月):初始版本,将引用计数改为原子操作。这导致性能严重下降,在7个核心上比 CPython 慢了 18.9 倍。主要原因是原子操作导致 CPU 核心间通信总线饱和。
  2. 缓冲引用计数(2016年10月):引入“缓冲引用计数”技术,大幅减少了核心间的争用,性能得到显著提升。
  3. 优化小块分配器(2017年4月):对 obmalloc 进行优化,采用分级锁和每线程空闲列表,进一步提升了性能。
  4. 移除线程局部存储访问(2017年5月):减少在关键函数路径中从线程局部存储(TLS)获取数据的次数,带来了又一次性能提升。

经过这些优化,Gilectomy 的性能(墙钟时间)已经非常接近单线程的 CPython,达到了“触手可及”的范围。


核心技术:缓冲引用计数

上一节我们看到了缓冲引用计数带来的巨大性能提升。本节中,我们来深入了解这项核心技术的原理。

问题:传统的原子引用计数操作(Py_INCREF/Py_DECREF)会导致多个线程频繁争用同一内存地址,造成性能瓶颈。

解决方案:缓冲引用计数。其核心思想是引入一个“引用计数提交线程”,并让工作线程将引用计数的增减操作记录到日志中,而非直接修改内存。

以下是该方案的工作流程:

  1. 每个工作线程拥有本地的引用计数日志缓冲区。
  2. 当线程需要增加或减少某个对象的引用计数时,它只是将“对象指针 + 操作类型(INC/DEC)”写入自己的本地日志。
  3. 一个专用的提交线程会定期收集并处理这些日志,真正地执行引用计数的修改。

由于只有提交线程会修改实际的引用计数值,因此它可以使用普通的非原子操作,从而避免了争用。

挑战与解决:日志提交的顺序必须正确,否则可能导致对象过早被销毁。例如,线程A增加了对象O的引用,线程B减少了对象O的最后一个引用。如果先提交线程B的日志,对象O会被销毁,再提交线程A的日志时就会访问已释放的内存。

解决方案是,将日志分为 INC(增加)DEC(减少) 两个独立的队列。只需保证在提交一个 DEC 操作时,所有时间上早于它的 INC 操作都已提交即可。这可以通过一种轻量级的、常数时间的排队算法来实现。

这项技术的实现使得 Py_INCREFPy_DECREF 宏变得更复杂,但通过缓存和预分配缓冲区等优化,可以使其运行得足够快。


其他技术挑战与解决方案

缓冲引用计数解决了主要矛盾,但也带来了新的问题。以下是其他几个关键挑战及其解决方案。

  • 弱引用问题:弱引用机制需要知道对象的实时存活状态。解决方案是引入一个次要的、原子操作的引用计数,专用于弱引用系统。
  • 对象复活问题:在 __del__ 方法中复活对象(将 self 赋值给外部变量)在 Gilectomy 下会出问题,因为无法实时感知引用计数变化。建议是避免这种编程实践。
  • 性能分析工具:开发过程中,可逆调试器 UndoDB 是不可或缺的工具,它可以像倒带一样运行程序,帮助定位复杂的并发 Bug。

当前工作与未来方向

目前,性能分析表明内存分配器 obmalloc 仍然是主要的性能瓶颈。未来的优化方向包括:

  1. 重写 obmalloc:尝试将其改造成更彻底的每线程结构,减少锁争用。
  2. 私有锁:大多数对象从不跨线程共享。可以为对象设计一种“预锁定”模式,当对象仅被创建它的线程访问时,使用极低开销的“锁”;只有当其他线程试图访问时,才将其升级为真正的锁。
  3. 分离引用计数:将引用计数值从对象本身分离存储。这可以避免修改不可变对象(如小整数)的引用计数时,使所有 CPU 核心的缓存行失效,从而提升多核效率。
  4. 终极方案:切换到追踪式垃圾回收:如果上述优化均无法达到目标,最后的备选方案是彻底将 CPython 的垃圾回收机制从引用计数改为追踪式垃圾回收(如 Java、Go 所用)。这可以利用 PyPy 项目的 cpyext 经验,在追踪式 GC 上模拟 CPython 的 C API 和引用计数语义。但这将是一项浩大的工程,并且会严重破坏现有 C 扩展的语义。

关于兼容性的重要说明

Gilectomy 不可避免地会破坏现有 C 扩展的语义

虽然源代码级别的 API(如 Py_INCREF)保持不变,可以重新编译,但 GIL 所提供的隐式保证将消失。例如:

  • 对象销毁的非确定性:依赖“对象在最后一个引用消失后立即销毁”的代码将无法正常工作。
  • 丢失的线程安全:过去依赖 GIL 保护的非线程安全代码(如静态变量的惰性初始化)将出现竞争条件。

Python 语言规范从未保证这些行为,其他实现(如 Jython、IronPython)也未提供。这是移除 GIL 必须付出的代价。


总结

本节课中,我们一起学习了 Larry Hastings 的 Gilectomy 项目在 PyCon 2017 时的进展。我们了解了项目的目标、性能基准测试的挑战,以及项目演进的几个关键阶段,特别是缓冲引用计数这一核心技术。我们还探讨了项目面临的其他技术挑战、当前的优化方向,以及移除 GIL 对 C 扩展语义兼容性带来的必然影响。

Gilectomy 项目证明,在 C 语言中编写一个无 GIL 的多线程 Python 解释器是可行的(Jython 和 IronPython 已做到)。真正的挑战在于,如何在提升多核性能的同时,最小化对庞大 CPython 生态系统的破坏。

007:是的,学习正则表达式的时间到了

在本教程中,我们将学习正则表达式的基础知识。正则表达式是一种强大的文本模式匹配工具,可以帮助我们在字符串中查找、验证或提取特定模式的文本。我们将从基本概念开始,逐步了解其核心语法和在Python中的使用方法。


正则表达式:1:什么是正则表达式?🔍

正则表达式,常简称为“regex”,是一种用于描述文本模式的字符串。它允许我们指定一个搜索模式,而无需知道确切的文本内容。例如,识别一个电话号码,是因为我们知道其模式(如区号、短横线等),而不是因为记住了每一个号码。

正则表达式的作用就是让我们能够定义这种模式,并在文本中查找匹配该模式的字符串。


正则表达式:2:Python中的基本用法🐍

在Python中使用正则表达式非常简单,主要涉及三个步骤:导入模块、编译模式和搜索匹配。

以下是核心代码流程:

import re
phone_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
match_object = phone_regex.search('我的电话是 415-555-4242。')
if match_object:
    print(match_object.group())

代码解释

  1. import re:导入Python的正则表达式模块。
  2. re.compile():将表示模式的字符串(如r‘\d\d\d-\d\d\d-\d\d\d\d’)编译成一个正则表达式对象。使用原始字符串(r‘’)可以避免转义反斜杠的麻烦。
  3. .search():在目标字符串(“干草堆”)中搜索编译好的模式(“针”)。如果找到,返回一个匹配对象;否则返回None
  4. .group():从匹配对象中提取实际匹配到的文本。

虽然看起来比简单的字符串查找方法复杂,但对于复杂的模式匹配,正则表达式能极大简化代码。


正则表达式:3:理解字符类🔠

上一节我们介绍了基本流程,本节中我们来看看构成模式的核心——字符类。字符类定义了你要匹配的单个字符属于哪个集合。

以下是一些预定义的常用字符类:

  • \d:匹配任意一个数字(0-9)。
  • \w:匹配任意一个单词字符(字母、数字、下划线)。
  • \s:匹配任意一个空白字符(空格、制表符、换行符)。
  • \D\W\S:分别匹配对应小写字符类之外的任意一个字符。

你也可以创建自定义的字符类:

  • [aeiouAEIOU]:匹配任意一个元音字母。
  • [^aeiouAEIOU]:匹配任意一个元音字母。^在方括号内表示“非”。
  • [a-zA-Z0-9]:匹配任意一个字母或数字。-表示一个范围。
  • [\(\\)]:匹配左圆括号或右圆括号。在字符类内,大多数特殊字符(如()会失去特殊含义,但为了清晰,有时仍会转义。

字符类是你告诉正则表达式引擎“我要匹配这类字符”的方式。


正则表达式:4:指定匹配数量🔢

仅仅匹配一个字符往往不够,我们经常需要匹配连续出现的多个字符。这时就需要在字符类后面加上表示数量的符号。

以下是常用的数量符号:

  • {3}:精确匹配前面的元素3次。例如,\d{3}匹配三个连续数字。
  • {1,3}:匹配前面的元素1到3次。
  • ?:匹配前面的元素0次或1次(即可选)。
  • *:匹配前面的元素0次或多次。
  • +:匹配前面的元素1次或多次。

因此,电话号码模式可以更简洁地写为:r‘\d{3}-\d{3}-\d{4}’。模式结构通常是:字符类 + 数量符号


正则表达式:5:分组与选择⚖️

有时我们需要将多个元素视为一个整体进行操作,或者提供多种匹配选择。这就需要用到分组和管道符号。

分组使用圆括号()。例如,日语假名通常由辅音和元音组合而成。要匹配一个假名序列,我们可以将(辅音+元音)作为一个组,然后匹配这个组多次:

pattern = r‘([^aeiouAEIOU]+[aeiouAEIOU]+)+’

这个模式会匹配像“さようなら”(Sayonara)这样的单词。

管道符号|表示“或”的关系。例如,想匹配《蒙提·派森》短剧中的一些特定单词(如egg, bacon, sausage, ham),可以使用:

pattern = r‘(egg|bacon|sausage|ham)+’

这将匹配像“egghambacon”这样的组合。管道符号让你可以在多个备选模式中进行选择。


正则表达式:6:通配符与贪婪匹配🌐

点号.是一个强大的通配符,它可以匹配除换行符外的任意单个字符。

当点号与数量符号结合时,功能非常强大:

  • .*:匹配任意长度的任意字符(贪婪模式)。它会尽可能多地匹配字符。
  • .*?:匹配任意长度的任意字符(非贪婪模式)。它会尽可能少地匹配字符。

例如,在字符串‘<服务人类>吃晚餐>’中:

  • 模式r‘<.*>’(贪婪)会匹配整个字符串‘<服务人类>吃晚餐>’
  • 模式r‘<.*?>’(非贪婪)只会匹配第一个‘<服务人类>’

理解贪婪与非贪婪的区别,对于精确提取文本至关重要。


正则表达式:7:最佳实践与局限性🚧

正则表达式虽然强大,但也有其适用边界和最佳实践。

以下是需要注意的几点:

  1. 不要用正则表达式解析HTML/XML/JSON:这些结构化语言有嵌套、属性等复杂语法,正则表达式难以可靠处理。应使用专门的解析库(如Beautiful Soupjson模块)。
  2. 避免过于复杂的正则表达式:如果一个正则表达式变得极其冗长和难以理解(例如,用于验证密码强度),考虑将其拆分为多个简单的正则表达式或使用普通代码逻辑。
  3. 了解其理论限制:正则表达式基于“正则语言”,无法处理需要匹配嵌套结构(如平衡的括号((())))的问题,因为这需要更强大的“上下文无关文法”。

总结📝

本节课中我们一起学习了正则表达式的核心概念。我们从Python中使用正则表达式的基本三步(编译、搜索、分组)开始,逐步深入到字符类、数量符号、分组、管道符以及通配符。记住,学习正则表达式的关键是理解“模式”而非具体文本,并善用在线测试工具进行练习和调试。虽然它有一些局限性,但在文本模式匹配领域,正则表达式无疑是一个极其高效和强大的工具。现在,是时候开始在实践中使用它了。

008:一个Python分布式数据科学框架

在本教程中,我们将学习Dask,一个用于Python并行和分布式计算的库。我们将了解它如何帮助扩展NumPy、Pandas等流行库,以及如何并行化更通用的Python代码。课程内容将从基础概念开始,逐步深入到任务调度和系统架构。

概述

Dask旨在解决Python科学计算栈(如NumPy、Pandas)在单核和内存限制下的瓶颈。它通过并行和分布式计算,使这些库能够处理超出单机内存的大型数据集。本教程将介绍Dask的核心概念、使用方法及其在生态系统中的位置。


章节 1: 并行化NumPy与Pandas 🧮

上一节我们概述了Dask的目标。本节中,我们来看看Dask如何为NumPy和Pandas用户提供熟悉的并行接口。

Dask提供了两个高级集合:Dask数组和Dask DataFrame。

  • Dask数组 由许多小的NumPy数组块组成,形成一个逻辑上的大型多维数组。
  • Dask DataFrame 由许多Pandas DataFrame组成,形成一个逻辑上的大型表格。

这些集合的API与NumPy和Pandas高度相似,但操作是并行执行的,可以在多核机器、磁盘或集群上运行。

以下是Dask数组的一个简单示例:

import dask.array as da
# 创建一个由100个1000x1000的NumPy数组块组成的Dask数组
x = da.random.random((10000, 1000), chunks=(1000, 1000))
# 计算总和,操作是并行执行的
result = x.sum()
result.compute()

章节 2: 并行化通用Python代码 ⚙️

仅仅并行化数组和DataFrame还不够。许多科学计算涉及更复杂、自定义的算法。Dask通过其延迟执行(delayed)功能来处理通用Python代码。

dask.delayed装饰器可以将普通的Python函数转换为延迟执行的任务。Dask会构建一个任务图,并仅在需要结果时才并行执行。

以下是如何使用delayed并行化自定义函数的示例:

from dask import delayed
import time

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/c756025e33a1d78115431bb7e1f1596e_13.png)

@delayed
def slow_inc(x):
    time.sleep(1)
    return x + 1

@delayed
def slow_add(x, y):
    time.sleep(1)
    return x + y

# 构建任务图,此时并未实际计算
a = slow_inc(1)
b = slow_inc(2)
c = slow_add(a, b)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/c756025e33a1d78115431bb7e1f1596e_15.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/c756025e33a1d78115431bb7e1f1596e_17.png)

# 触发并行计算
result = c.compute()
print(result)  # 输出 5

章节 3: 任务图与调度器 🗺️

上一节我们看到了如何创建任务。本节中我们来看看Dask如何管理和执行这些任务。

Dask的核心是一个动态任务调度器。它主要做两件事:

  1. 生成任务图:将高级操作(如数组求和)分解为许多低级任务(如对每个数据块求和)。
  2. 调度执行:在可用的工作线程或集群节点上高效地并行执行这些任务。

任务图是一个有向无环图(DAG),其中节点代表任务(函数),边代表数据依赖关系。调度器遍历此图,决定任务的执行顺序和位置。


章节 4: Dask与其他并行计算系统对比 ⚖️

在深入Dask调度器细节之前,了解Python生态中其他并行计算选项的优缺点有助于理解Dask的定位。

以下是几种常见系统的简要对比:

  • multiprocessing / concurrent.futures

    • 优点:Python标准库,轻量,易于使用。
    • 缺点:进程间通信开销大,难以处理复杂的任务依赖图。
  • 大数据集合系统(如Spark)

    • 优点:提供丰富的API(map、reduce、join等),扩展性好,在企业中受信任。
    • 缺点:通常基于JVM,对Python支持是二等公民;API固定,难以处理任意、动态的任务图。
  • 工作流调度系统(如Airflow, Luigi)

    • 优点:能处理复杂的任务依赖图,通常是Python原生。
    • 缺点:任务开销高(~100毫秒),不为低延迟计算优化,缺乏工作节点间高效的数据通信。

Dask试图结合以上系统的优点:它像Airflow一样能处理任意任务图,又像Spark一样为计算性能优化,并且是纯Python、轻量级的。


章节 5: Dask调度器架构 🏗️

Dask主要有两种调度器:

  1. 单机调度器:轻量级,开销极低(约50微秒),仅依赖Python标准库。适用于单机多核环境。
  2. 分布式调度器:基于Tornado的TCP应用,支持在集群上运行。包含一个中心调度器进程和多个工作器进程,工作器之间可以点对点通信。

分布式调度器的典型架构如下:

客户端 (Client) -> 调度器 (Scheduler) <-> 工作器 (Worker 1, Worker 2, ...)

客户端提交任务图给调度器,调度器将任务分发给空闲的工作器执行。


章节 6: 熟悉的API与协议 🤝

为了降低学习成本和促进采用,Dask尽可能地复用现有的、流行的Python API和协议。

Dask支持多种熟悉的接口:

  • NumPy/Pandas API:如前所述,Dask数组和DataFrame模仿了原生接口。
  • concurrent.futures接口:可以像使用线程池/进程池执行器一样使用Dask。
  • Async/Await:支持原生的Python异步编程模式。
  • Joblib:可以替换scikit-learn的默认并行后端,使其代码能在Dask集群上运行。

以下是一个使用concurrent.futures接口的示例:

from dask.distributed import Client
client = Client() # 连接到本地集群

# 使用类似concurrent.futures的submit方法
future = client.submit(lambda x: x * 2, 10)
result = future.result()
print(result) # 输出 20

章节 7: Python生态的优势与总结 🌟

构建Dask的过程相对顺利,这很大程度上得益于Python生态系统既拥有强大的数值计算栈(NumPy, SciPy),又拥有成熟的并发网络栈(Tornado, asyncio)。这种结合是罕见的,使得在Python中构建高性能的分布式计算框架成为可能。

数值计算栈:通过C/Fortran扩展,能以接近硬件的速度执行计算,并且GIL(全局解释器锁)在此类操作中通常不是问题。
并发网络栈:提供了高效的事件循环和异步编程支持,使得构建响应式、高并发的调度器成为可能。

总结

在本教程中,我们一起学习了:

  1. Dask如何通过Dask数组Dask DataFrame提供并行化的NumPy和Pandas体验。
  2. 如何使用 dask.delayed 来并行化任意、自定义的Python函数和算法。
  3. Dask的核心是一个动态任务调度器,它构建并高效执行任务图。
  4. Dask在生态系统中的定位,它填补了轻量级并行库与重型大数据框架之间的空白。
  5. Dask的分布式架构以及它如何利用现有Python API和协议来降低使用门槛。
  6. Python生态在数值计算和并发网络两方面的强大实力,是Dask等项目成功的基础。

Dask是一个灵活、强大且易于上手的工具,无论是想在笔记本电脑上加速计算,还是需要在集群上处理TB级数据,它都能提供有效的解决方案。

009:Python如何成为科学家的首选工具

在本节课中,我们将一起探讨Python语言如何从一个教学工具,发展成为驱动现代天文学等前沿科学研究的核心力量。我们将通过Jake Vanderplas在PyCon 2017上的主题演讲内容,了解Python在科学领域的应用、优势及其背后的理念。

演讲者介绍与背景

我叫Jake Vanderplas。这是我连续第六次参加PyCon。五年前,我作为一名博士生,通过PSF的旅行奖学金第一次参加了PyCon。那次经历让我深受震撼,不仅因为演讲的高质量,更因为我有机会与NumPy、SciPy和IPython等库的作者们交流。这些工具正是我日常工作中不可或缺的。

Python社区:一幅多元的马赛克

PyCon就像一个马赛克,汇聚了Python社区的各个片段。无论是SciPy、DjangoCon还是JupyterCon,每个会议都代表了社区的一个特定方面。但在PyCon,你能接触到所有人。这让我意识到,社区中的每个小群体都有自己使用语言、工具和解决问题的方式。这种多样性是Python生态系统的巨大财富。

现代天文学:数据驱动的科学

我是一名天文学家。但现代天文学家的形象,已不再是透过目镜观察星空。我们看待世界的方式,更多是通过数据库查询和数据分析。

哈勃太空望远镜与斯隆数字巡天

哈勃太空望远镜自1990年起,为我们带来了宇宙深处的惊人图像,例如揭示早期星系的“超深场”。而斯隆数字巡天则采取了互补的策略,它扫描了整个天空,绘制出星系的三维分布图,帮助我们理解宇宙的结构和组成。

开普勒任务与系外行星搜寻

开普勒项目通过监测恒星的亮度变化来寻找系外行星。当行星从恒星前方经过时,会导致恒星亮度发生极其微弱的下降(通常低于1%)。从充满仪器噪声和恒星自身活动的数据中提取这些信号,依赖于复杂的统计建模和数据处理管道。

核心概念:行星凌星导致的光变曲线下降。

# 简化的光变曲线模型示例
def transit_model(time, depth, duration, period):
    # 计算行星凌星导致的亮度下降模型
    # depth: 凌星深度, duration: 凌星持续时间, period: 轨道周期
    ...

詹姆斯·韦伯太空望远镜的未来

即将发射的詹姆斯·韦伯太空望远镜将在红外波段工作。它可能首次直接对系外行星成像,并分析其大气光谱,寻找如氧气、臭氧等可能由生命活动产生的生物标志物。

大型综合巡天望远镜的数据洪流

LSST项目将建造有史以来最大的数码相机(32亿像素),计划在十年内持续拍摄整个南天星空,最终产生数百PB的数据。这将彻底改变天文学的各个领域。

Python在科学中的崛起

过去十年,Python在天文学论文中的提及率呈指数级增长,而传统的封闭商业软件(如IDL)则在衰退。那么,Python为何能成为科学家的首选?

1. 卓越的互操作性(胶水语言)

科学家需要整合各种工具:模拟代码、数据库、可视化软件等。Python最初的角色就是作为“胶水”,将这些不同的组件连接在一起。正如早期倡导者所说,Python解决了使不同组件协同工作的难题。

2. “内置电池”与丰富的生态系统

Python标准库功能强大,而围绕它构建的科学软件生态(如NumPy, SciPy, Matplotlib, pandas, scikit-learn等)更是极其丰富。科学家几乎可以为任何问题找到现成的工具包。

核心概念:科学Python软件栈层次。

应用层 (如 scikit-learn, AstroPy)
|
领域工具层 (如 pandas, Matplotlib)
|
科学计算核心 (SciPy, NumPy)
|
Python语言与标准库
|
底层C/Fortran库

3. 简单性与动态特性

Python语法清晰,像“可执行的伪代码”,极大地降低了学习门槛和开发启动成本。对于探索性的科学研究而言,开发速度往往比执行速度更重要。

4. 开放的精神

开放科学是解决当前科学研究“可重复性危机”的关键。Python社区的开源文化与科学的需求完美契合。如今,像LIGO(激光干涉引力波天文台)这样的重大发现都会随论文一起发布其分析代码和Jupyter Notebook,确保了研究的透明度和可重复性。

总结与展望

本节课中我们一起学习了Python在科学计算,特别是天文学领域扮演的关键角色。它凭借卓越的互操作性丰富的生态系统简单易用的特性以及强大的开源精神,成功地将科学家和开发者凝聚在同一个工具集周围。

Python社区就像一幅由不同碎片组成的马赛克,每个领域的使用方式都独具特色。这种多样性正是其生命力的源泉。作为初学者或来自其他领域的开发者,理解并借鉴科学家的这种探索性、迭代式的工作流程(常借助Jupyter Notebook等工具),或许能为你自己的项目带来新的灵感。

最终,Python不仅仅是一种编程语言,更是一个推动科学发现、促进开放协作的社区和理念。

010:不再悲伤的pandas

概述

在本教程中,我们将学习如何优化pandas代码以提高其运行速度和效率。我们将从如何对代码进行基准测试开始,然后探讨几种常见的优化策略,包括避免循环、使用向量化操作、利用NumPy数组以及使用Cython进行加速。本教程旨在让初学者能够理解并应用这些技巧。

如何进行基准测试

在开始优化之前,我们需要知道如何衡量代码的性能。我们将使用Jupyter Notebook中的“魔法命令”来对函数进行计时和分析。

使用 %timeit 命令

%timeit 命令会多次运行一个函数,并计算其平均运行时间和标准差,这为我们提供了一个可靠的性能基准。

示例代码:

%timeit df['new_column'] = normalize_function(df['high_rate'])

运行上述代码会输出类似 2.84 ms ± 7.29 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 的结果,告诉我们函数的平均执行时间。

使用行分析器 %lprun

行分析器 (%lprun) 可以逐行分析函数,显示每行代码被调用的次数以及执行时间所占的百分比。这有助于我们定位代码中的性能瓶颈。

示例代码:

%lprun -f normalize_function df['new_column'] = normalize_function(df['high_rate'])

分析结果会以表格形式展示,其中“% Time”列清晰地指出了最耗时的代码行。

常见的低效模式与优化方法

上一节我们介绍了如何测量性能,本节中我们来看看pandas中几种常见的低效操作模式及其优化方案。

1. 避免使用循环遍历行

许多初学者会尝试使用循环(如 for 循环或 iterrows())来逐行处理数据框。这是pandas中最慢的操作之一。

低效示例(使用 iterrows):

distances = {}
for index, row in df.iterrows():
    distances[row['hotel_id']] = haversine(row['latitude'], row['longitude'], brooklyn_point)
# 执行时间:184 ms

优化方法:使用 apply 函数
apply 函数沿着数据框的轴应用函数,比显式循环高效得多。

优化后代码:

df['distance'] = df.apply(lambda row: haversine(row['latitude'], row['longitude'], brooklyn_point), axis=1)
# 执行时间:78 ms

仅通过将 iterrows 替换为 apply,性能就提升了约2.5倍。

2. 拥抱向量化操作

向量化是pandas高效的核心。它意味着对整个数组(Series或DataFrame列)执行操作,而不是对单个标量值进行循环。

向量化优化示例:

df['distance'] = haversine(df['latitude'], df['longitude'], brooklyn_point)
# 执行时间:1.8 ms

通过直接对整个 latitudelongitude 序列进行操作,执行时间从78毫秒大幅降至1.8毫秒,提升了43倍。

3. 使用NumPy数组进行底层优化

Pandas Series在提供丰富功能的同时也带来了一些开销。对于纯数值计算,可以将其转换为NumPy数组以获得更快的速度。

使用 .values 转换为NumPy数组:

df['distance'] = haversine(df['latitude'].values, df['longitude'].values, brooklyn_point)
# 执行时间:0.37 ms

这比在pandas Series上操作又快了近5倍,相比最初的循环,总提升超过500倍。

4. 当必须循环时:使用Cython加速

有时函数过于复杂,难以向量化。在这种情况下,可以使用Cython将关键循环编译成C代码来加速。

基础Cython使用(提升有限):

%load_ext Cython
%%cython
import numpy as np
def haversine_cython(lat, lon):
    # ... 函数体(与Python版相同)
    return distances
# 执行时间:76 ms

优化Cython代码(添加类型和C数学库):
通过为变量声明C数据类型(如 cdef float)并使用 libc.math 替代 numpy 中的数学函数,可以显著提升Cython性能。

%%cython
cimport cython
from libc.math cimport sin, cos, asin, sqrt, pi

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/344784ece4961f2de8d0773170a4c965_26.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/344784ece4961f2de8d0773170a4c965_28.png)

@cython.boundscheck(False)
@cython.wraparound(False)
def haversine_cython_optimized(float lat1, float lon1, float[:] lat2, float[:] lon2):
    cdef int n = lat2.shape[0]
    cdef float[:] dist = np.empty(n, dtype=np.float32)
    cdef float r = 3959.0
    cdef float phi1 = lat1 * pi / 180.0
    # ... 优化后的计算逻辑
    return np.asarray(dist)
# 执行时间:50 ms

经过优化的Cython代码比普通Python循环快约3.7倍,但通常仍不如向量化方法高效。

性能优化总结

以下是本节课中我们一起学习的各种方法及其性能对比的总结:

方法 执行时间 相对初始性能提升
初始循环 (iterrows) 184 ms 1x (基准)
使用 apply 循环 78 ms ~2.4x
向量化 (Pandas Series) 1.8 ms ~102x
向量化 (NumPy Arrays) 0.37 ms ~497x
Cython (基础) 76 ms ~2.4x
Cython (优化后) 50 ms ~3.7x

核心优化原则:

  1. 优先向量化:尽可能避免显式循环,使用pandas和NumPy的数组操作。
  2. 善用 apply:如果必须按行处理,applyiterrowsitertuples 更高效。
  3. 深入NumPy:对于计算密集型任务,将数据转换为NumPy数组(.values)可以移除pandas开销。
  4. 谨慎使用Cython:仅在循环无法避免且成为瓶颈时考虑使用Cython,并确保进行充分的类型注解和库优化以获得收益。

重要提示:

  • 避免过早优化:首先确保代码功能正确,然后再针对已证实的瓶颈进行优化。
  • 结果因情况而异:本教程中的性能提升基于特定数据集和函数。实际效果会因数据规模、操作类型和运行环境而有所不同。

通过应用这些策略,你可以显著提升pandas代码的效率,将运行时间从分钟级缩短到秒级甚至毫秒级。

011:布雷特·卡农 Python 3.6 的新变化

在本教程中,我们将一起学习 Python 3.6 版本引入的一系列重要新特性。Python 3.6 是一个包含大量改进的版本,涵盖了语法增强、性能优化、安全性提升以及开发体验的改善。我们将按照 Python 增强提案(PEP)的顺序,逐一解析这些变化,帮助你快速掌握 Python 3.6 的核心更新。

PEP 468:保留关键字参数顺序 🗂️

上一节我们介绍了本教程的概述,本节中我们来看看第一个重要特性:保留关键字参数顺序。在 Python 3.6 之前,使用 **kwargs 收集关键字参数时,其顺序是随机的。现在,Python 保证在函数调用中传入的关键字参数的顺序会被保留。

核心概念**kwargs 现在是一个保持插入顺序的映射。

def func(**kwargs):
    for key in kwargs:
        print(key, kwargs[key])

func(a=1, b=2, c=3)  # 输出顺序将保证是 a, b, c

重要提示:虽然 Python 3.6 的字典实现保持了插入顺序,但这在语言规范中仅针对此特定场景(函数 **kwargs)得到保证。请不要依赖从普通字典迭代中输出的键值顺序,因为它仍被视为实现细节。

PEP 487:简化的类创建定制 🏗️

之前,定制类创建主要依赖元类或类装饰器,两者各有其复杂性或局限性。PEP 487 引入了一个新的类创建钩子 __init_subclass__,它提供了一个更简单、更直接的介入点。

核心概念:使用 __init_subclass__ 方法在子类创建时进行定制。

class Base:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        # 在类创建时添加一个方法
        cls.hello = lambda self: print(f"Hello from {cls.__name__}")

class MyClass(Base):
    pass

obj = MyClass()
obj.hello()  # 输出: Hello from MyClass

这个钩子在元类的完全控制和类装饰器的后期修改之间提供了一个理想的中间地带。

PEP 495:本地时间消歧义 ⏰

处理夏令时转换期间的模糊时间一直是个难题。PEP 495 为 datetime 对象添加了一个 fold 属性,用于消除本地时间在时钟回拨时产生的歧义。

核心概念datetime 实例的 fold 属性(值为 0 或 1)指示时间是否处于重复的“回拨”小时内。

from datetime import datetime, timezone, timedelta

# 模拟一个模糊时间(例如,夏令时结束时的 01:30)
dt_ambiguous = datetime(2016, 11, 6, 1, 30, tzinfo=timezone(timedelta(hours=-5)))
print(dt_ambiguous)  # 输出可能包含 fold 信息

最佳实践是始终使用时区感知的 datetime 对象。但当无法获取时区信息时,fold 属性提供了关键的消歧义手段。

PEP 498:格式化字符串字面量(f-string)✨

格式化字符串字面量,通常称为 f-string,是 Python 3.6 中最受瞩目的特性之一。它提供了一种更简洁、更直观且性能更高的字符串格式化方式。

核心概念:在字符串前加前缀 fF,即可在字符串内直接嵌入表达式。

name = "World"
age = 30
greeting = f"Hello, {name}. You are {age} years old."
print(greeting)  # 输出: Hello, World. You are 30 years old.

# 支持表达式
print(f"Next year you will be {age + 1}.")

f-string 在编译时被解析并转换为高效的字节码,因此其性能通常优于传统的 % 格式化或 str.format() 方法。

PEP 506 与 524:增强的安全性模块 🛡️

Python 3.6 引入了两个与安全相关的 PEP,旨在提供更可靠的随机数生成。

PEP 506:secrets 模块
secrets 模块专为生成加密级安全的随机数(如令牌、密钥)而设计。

核心概念:使用 secrets 替代 random 模块进行安全敏感操作。

import secrets

# 生成一个安全的随机令牌
token = secrets.token_hex(16)
print(token)

# 从序列中安全地选择一项
safe_choice = secrets.choice(['apple', 'banana', 'cherry'])

PEP 524:os.getrandom()
为了解决 os.urandom() 在系统熵不足时可能阻塞的问题,Python 3.6 恢复了 os.urandom() 的阻塞行为,并新增了 os.getrandom()。后者在熵不足时会抛出 BlockingIOError 异常,让开发者可以自主选择处理策略。

import os

try:
    random_bytes = os.getrandom(16)  # 非阻塞,可能抛出异常
except BlockingIOError:
    # 处理熵不足的情况
    pass

# 或者使用会阻塞直到熵足够的 os.urandom()
random_bytes_blocking = os.urandom(16)

PEP 509:为字典添加私有版本 🔧

这个特性主要面向 CPython 解释器的内部优化和工具开发者(如 JIT、调试器)。它在可变字典上添加了一个私有版本号,C 扩展可以借此快速判断字典自上次查看后是否被修改,从而为未来的性能优化铺平道路。普通用户通常无需直接使用此功能。

PEP 515:数字字面量中的下划线 📊

为了提高长数字的可读性,Python 3.6 允许在数字字面量中使用下划线进行分组。

核心概念:在整数和浮点数中使用 _ 作为千位分隔符(或任意分组)。

# 提高大数字的可读性
billion = 1_000_000_000
hex_value = 0xDEAD_BEEF
bytes_value = 0b_1101_0010

print(billion)  # 输出: 1000000000

下划线的放置位置非常灵活,但不能放在数字的开头或结尾,也不能紧邻小数点。

PEP 519:文件系统路径协议 🗺️

为了让 pathlib.Path 对象能在更多接受字符串路径的地方使用,PEP 519 定义了一个 os.PathLike 协议。任何实现了 __fspath__() 方法的对象都可以被识别为文件系统路径。

核心概念:对象通过实现 __fspath__() 方法来提供其路径的字符串表示。

from pathlib import Path

p = Path('/home/user')
print(os.fspath(p))  # 输出: /home/user

# 现在许多标准库函数可以直接接受 Path 对象
with open(p / 'file.txt') as f:
    content = f.read()

标准库(如 os.path 模块)已广泛支持此协议,使得 pathlib 的使用更加无缝。

PEP 520:保留类属性定义顺序 📝

与保留关键字参数顺序类似,PEP 520 保证了类体中属性定义的顺序会被保留在类的 __dict____slots__ 中。

核心概念:类命名空间(如 __dict__)现在是一个保持插入顺序的映射。

class OrderedClass:
    x = 1
    y = 2
    z = 3

print(list(OrderedClass.__dict__.keys()))
# 输出将包含 'x', 'y', 'z' 等,且其定义顺序被保留。

再次强调:这是语言规范为类命名空间提供的保证,不要依赖普通字典的迭代顺序。

PEP 523:CPython 的帧评估 API ⚙️

这是一个面向高级用户和工具开发者(如 JIT、调试器、性能分析器)的特性。它允许在 C 级别钩住 Python 帧的执行过程,为动态替换执行引擎(例如,从解释器切换到 JIT 编译器)提供了可能。

核心概念:通过 PyEval_SetProfilePyEval_SetTrace 等 API 的扩展,实现了更灵活的帧评估控制。

此特性本身不改变 Python 代码的写法,但它为像 PyPy 的 JIT 或 PyCharm 调试器(据说性能提升了 20%)这样的工具提供了强大的底层支持。

PEP 525 与 530:异步生成器与推导式 ⚡

Python 3.5 引入了 async/await 语法,但生成器还不能使用 await。PEP 525 引入了异步生成器,PEP 530 则进一步允许在异步上下文管理器、推导式等中使用 async forawait

核心概念:定义使用 async def 且包含 yield 语句的函数来创建异步生成器。

import asyncio

async def async_gen():
    for i in range(3):
        await asyncio.sleep(0.1)  # 可以在生成器中等待
        yield i

async def main():
    # 异步列表推导式 (PEP 530)
    results = [i async for i in async_gen()]
    print(results)  # 输出: [0, 1, 2]

    # 在推导式中使用 await
    values = [await asyncio.sleep(0.1, i) for i in range(3)]
    print(values)  # 输出: [0, 1, 2]

asyncio.run(main())

这使得异步编程在 Python 中更加完整和流畅。

PEP 526:变量注释语法 📄

PEP 484 引入了函数类型提示,PEP 526 将其扩展到了变量(包括类变量和实例变量)。这为静态类型检查器提供了更多信息,但运行时不会强制执行这些类型。

核心概念:使用冒号 : 语法为变量添加类型注释。

from typing import List, Dict

# 类变量注释
class Node:
    children: List['Node'] = []
    metadata: Dict[str, str]

    def __init__(self, value: int):
        # 实例变量注释
        self.value: int = value
        # 局部变量注释(主要用于静态检查,运行时无作用)
        local_var: str = "temp"

# 类型信息可通过 __annotations__ 访问
print(Node.__annotations__)  # 输出: {'children': typing.List[__main__.Node], 'metadata': typing.Dict[str, str]}

变量注释主要服务于像 mypy 这样的静态类型检查工具,以提升代码的健壮性和可维护性。

PEP 528 与 529:Windows 控制台与文件系统编码改进 💻

这两个 PEP 显著改善了 Python 在 Windows 平台上的使用体验。

PEP 528:Windows 控制台使用 UTF-8
Python 交互式解释器(REPL)现在在 Windows 上默认使用 UTF-8 编码,这意味着可以正确显示 Emoji 和其他 Unicode 字符。

PEP 529:Windows 文件系统编码改为 UTF-8
Python 现在将 Windows 文件系统编码默认视为 UTF-8。当接收到字节路径时,会将其重新编码为 UTF-16 供 Windows API 使用。这使得处理从 Python 2 迁移过来的、使用字节路径的代码更加容易。

# 现在在 Windows 上,这类操作更加可靠
with open('café.txt', 'w', encoding='utf-8') as f:  # 路径包含非ASCII字符
    f.write('Hello')

其他重要改进 🎁

  • PEP 529:添加了 PYTHONMALLOC 环境变量,允许在不重新编译调试版 Python 的情况下启用内存分配调试器。
  • DTrace 和 SystemTap 支持:为 CPython 添加了基本的 DTrace 和 SystemTap 探针支持,便于系统级性能分析和跟踪。
  • 性能提升:Python 3.6 包含了许多内部优化。根据 speed.python.org 的基准测试,Python 3.6 在多项指标上比 Python 2.7 和 3.5 更快。
  • 语法警告:对无效的转义序列(如 \m)会发出 SyntaxWarning,帮助开发者避免错误。

总结 🎯

本节课中我们一起学习了 Python 3.6 版本带来的众多新特性。我们从保留关键字参数和类属性顺序开始,探讨了更安全的 secrets 模块、革命性的 f-string、更易读的数字字面量、对 pathlib 的更好支持,以及强大的异步生成器和变量注释语法。我们还看到了针对 Windows 平台的编码改进和为高级用户准备的底层钩子。

Python 3.6 是一个功能丰富且性能卓越的版本,这些改进共同使 Python 语言更加强大、安全和易用。建议开发者查阅 官方文档 以获取更详细的信息,并开始在新项目中使用这些特性。

012:十几个伟大思想的汇聚 🧠

在本教程中,我们将跟随Raymond Hettinger的演讲,探索Python字典从诞生到现代实现的演进历程。我们将看到,现代Python字典是跨越约50年、汇聚了十几个伟大思想的结晶。从最初的简单设计到如今高效、紧凑的实现,每一步创新都解决了特定问题并提升了性能。

概述 📖

Python字典是语言的核心组件。全局变量、局部变量、模块、类和实例都依赖于字典。它们无处不在且至关重要。本教程将带你从“恐龙时代”的简单关联列表开始,逐步了解哈希表、分离链接、开放寻址、缓存哈希、快速匹配、键共享字典等关键概念,最终抵达Python 3.6中更小、更快、有序的现代字典实现。


1:起点与现状 🦕➡️🤖

上一节我们概述了本教程的内容,本节中我们来看看Python字典的起点和我们最终要达到的现代形态。

一个简单的Python类实例,其属性背后就是字典。

class Person:
    def __init__(self, name, color, city, fruit):
        self.name = name
        self.color = color
        self.city = city
        self.fruit = fruit

quito = Person('Quito', 'blue', 'Austin', 'apple')

实例quito的属性存储在一个字典中。不同Python版本中,这个字典的实现和特性差异巨大。

以下是不同Python版本中字典特性的对比:

  • Python 2.7:
    • 大小:280字节
    • 键顺序:确定性但杂乱(与插入顺序无关)
  • Python 3.5:
    • 大小:196字节
    • 键顺序:随机化(每次运行顺序不同)
  • Python 3.6+:
    • 大小:112字节
    • 键顺序:确定性(保持插入顺序)

现代Python 3.6+的字典更小、更快,并且是隐式的“有序字典”。这是升级到Python 3.6+的一个重要理由。


2:最初的尝试——线性搜索 📜

上一节我们看到了现代字典的优越性,本节中我们回溯历史,看看最初人们是如何解决问题的。

在早期,人们使用类似数据库表的结构存储关联数据。在Python中,可以表示为一个元组列表。

# 模拟一个简单的“数据库表”
data = [
    ('Quito', 'blue'),
    ('Sarah', 'red'),
    ('Barry', 'yellow'),
    ('Rachel', 'green'),
    ('Tim', 'purple')
]

要查找‘Barry’对应的颜色,必须进行线性搜索(Linear Search),即从头到尾遍历列表,直到找到匹配的键。

def linear_search(key, data):
    for k, v in data:
        if k == key:
            return v
    return None

当数据量增大时,线性搜索的性能会急剧下降。有趣的是,即使只有2到3个条目,现代字典的性能通常也已优于线性搜索的列表。


3:哈希表的诞生——分离链接法 🪣

上一节我们介绍了低效的线性搜索,本节中我们来看看第一个伟大创新:哈希表,具体来说是分离链接法(Separate Chaining)。

哈希表的核心思想是减少搜索空间。与其在一个大列表中搜索,不如先将所有条目分散到多个更小的“桶”(bucket)中。查找时,先确定目标在哪个桶,然后只在该桶内进行小范围的线性搜索。

确定条目属于哪个桶的过程叫做哈希(Hashing)。我们使用一个哈希函数来计算键的哈希值,然后根据哈希值决定其桶的索引。

哈希值 = hash(键)
桶索引 = 哈希值 % 桶的数量

以下是分离链接法的演进思路:

  1. 两个桶:将条目分散到两个桶中,平均搜索长度减半。
  2. 四个桶:进一步分散条目,大多数查找只需一次探测(probe)即可命中。
  3. 更多桶:当桶的数量足够多,使得每个桶内平均只有很少甚至一个条目时,查找效率接近常数时间O(1)。

然而,随着字典中条目数量增加,如果桶的数量固定,每个桶内的链会变长,性能会退化。解决方案是动态调整大小(Resizing):当字典的负载因子(条目数/桶数)超过某个阈值(如2/3)时,就创建一个更大的新表(通常是原大小的两倍),然后将所有条目重新哈希并插入新表。


4:性能优化——缓存哈希与快速匹配 ⚡

上一节我们学习了哈希表的基本原理,本节中我们深入两个关键的优化细节,这些在学校教科书中往往被省略。

缓存哈希值(Caching Hashes)
在调整大小时,需要为每个键重新计算哈希值。对于某些对象(如长字符串、复杂对象),计算哈希值可能非常昂贵。因此,Python字典在存储条目时,会缓存该键的哈希值。调整大小时,只需读取缓存的哈希值,无需重新计算,这使得调整大小的操作非常快。

快速匹配(Fast Matching)
在桶内查找匹配的键时,最直接的方法是调用键的__eq__方法进行相等性比较。但在面向对象语言中,__eq__操作可能非常复杂和耗时(例如比较两个包含许多字段的对象)。

Python字典使用了一个包含两个步骤的快速匹配算法来尽可能避免昂贵的相等性比较:

  1. 身份比较(Identity Comparison):首先检查两个对象是否是同一个对象(即key is target_key)。如果是,则它们必然相等。这只需要一次快速的指针比较。
  2. 哈希值比较(Hash Comparison):然后比较缓存的哈希值。如果哈希值不相等,根据哈希不变式(相等的对象必须有相等的哈希值),可以断定这两个对象不相等。

只有在以上两步都无法确定时,才会进行最终的、可能昂贵的相等性测试(key == target_key)。由于64位哈希值冲突的概率极低,在实践中几乎永远不会进行不必要的相等性测试。

# 快速匹配的伪代码逻辑
if key is target_key:
    return True  # 身份即相等,快速返回
if cached_hash != target_hash:
    return False # 哈希不同,必然不等
# 最后的手段:进行完整的相等性比较
return key == target_key

这两行优化代码对Python字典的性能至关重要。


5:空间优化——开放寻址与线性探测 🗺️

上一节我们关注了查找速度的优化,本节中我们看看如何优化哈希表的空间利用率。

分离链接法需要为每个桶维护一个链表(或列表),这引入了额外的指针开销。开放寻址(Open Addressing) 是另一种策略,它将所有条目都存储在一个大的连续数组中,从而消除了指针开销。

当发生哈希冲突时(两个键被哈希到同一个数组索引),开放寻址会按照某种探测序列(Probing Sequence)寻找下一个可用的空槽。最简单的是线性探测(Linear Probing):如果索引i被占用,则尝试i+1, i+2,依此类推。

# 线性探测的简化示例
index = hash(key) % table_size
while table[index] is not None and table[index].key != key:
    index = (index + 1) % table_size  # 线性移动到下一个槽

删除的问题与虚位条目(Dummy Entry)
开放寻址的一个挑战是删除。如果直接清空一个槽,可能会切断后续条目的探测路径,导致它们变得“不可达”。解决方案是使用一个特殊的虚位条目(如<dummy> 来标记被删除的位置。查找时,遇到虚位条目可以跳过它继续探测;插入时,则可以复用虚位槽。

更优的探测序列——扰动(Perturbation)
单纯的线性探测容易导致“聚集”(Clustering),降低性能。Python采用了一种更聪明的方法,结合了扰动技术:在发生冲突时,不仅使用哈希值的低位,还逐步引入哈希值的高位来生成新的探测索引。这通常与一个线性同余生成器结合(如 index = (5*index + 1) % table_size),确保能探测到表中的每一个槽,避免死循环。


6:现代字典——紧凑布局与键共享 🧩

上一节我们介绍了开放寻址,本节中我们来到现代Python字典的核心创新:紧凑布局。

传统的开放寻址哈希表(如Python 3.5之前)是一个稀疏数组,每个槽存储三个字段:哈希值、键、值。这造成了大量的空间浪费(空槽)。

紧凑字典(Compact Dict)
Raymond Hettinger提出的创新是将存储结构一分为二:

  1. 密集数组:一个按插入顺序紧密排列的数组,依次存储所有条目的(哈希值, 键, 值)。这个数组没有空位。
  2. 索引表:一个大小与哈希表桶数相同的稀疏数组。它不直接存储数据,而是存储指向密集数组中对应条目的索引
# 传统布局(稀疏)
索引表: [空, 空, 指向(哈希A, 键A, 值A), 空, 指向(哈希B, 键B, 值B), ...]

# 紧凑布局
密集数组: [(哈希A, 键A, 值A), (哈希B, 键B, 值B), ...] # 紧密排列
索引表: [空, 空, 0, 空, 1, ...] # 存储的是密集数组的索引

优势

  • 空间高效:消除了稀疏数组中的空槽浪费。
  • 迭代更快:遍历密集数组就是按插入顺序遍历,非常高效。
  • 保持顺序:作为紧凑布局的副产品,字典自然地保持了键的插入顺序。

键共享字典(Key-Sharing Dict)
对于拥有大量相同键集合的字典(例如许多同类实例的__dict__),PEP 412引入了进一步的优化。这些字典可以共享相同的键(和哈希值)数组,每个实例只需存储自己独有的值数组。这极大地减少了内存占用。

# 多个实例的字典共享键数组
共享键表: [‘name‘, ‘color‘, ‘city‘, ‘fruit‘]
实例1值表: [‘Quito‘, ‘blue‘, ‘Austin‘, ‘apple‘]
实例2值表: [‘Sarah‘, ‘red‘, ‘Dallas‘, ‘banana‘]

7:其他要点与未来展望 🔮

上一节我们揭晓了现代字典的最终形态,本节中我们补充一些其他要点并展望未来。

安全性——哈希随机化
为防止一种称为“哈希洪水拒绝服务(HashDoS)”的攻击(攻击者故意构造大量哈希冲突的键来拖慢字典性能),Python在启动时会生成一个随机盐(salt)并与对象的哈希值混合。这使得攻击者无法预测键的哈希分布,从而无法轻易构造冲突。

集合(Set)的不同策略
Python的集合(set)虽然也基于哈希表,但其优化策略与字典略有不同。因为集合的主要操作是判断成员是否存在,而字典的主要操作是通过已知键查找值。两者在负载因子、探测策略上可能进行不同的权衡。

关于布谷鸟哈希(Cuckoo Hashing)
布谷鸟哈希是另一种哈希表冲突解决策略,它使用两个(或多个)哈希函数和两个表。虽然它能保证最坏情况下的查找次数,但现代Python的紧凑字典通过增加索引表的稀疏性(用很小的空间代价)已能基本消除冲突,因此目前并未采用布谷鸟哈希。

迭代安全
在迭代字典或集合时修改其结构(如增删键)会导致未定义行为或运行时错误。Python内部有机制来检测这种操作并抛出RuntimeError,以保证程序不会崩溃。

未来:更稀疏的索引表
一个未来的优化方向是进一步增加索引表的稀疏性(例如,让索引表大小是条目数的两倍)。由于索引表本身很小(存储字节),增加其大小的代价很低,但却能几乎完全消除哈希冲突,使绝大多数查找在第一次探测时就命中。


总结 🎯

本节课中我们一起学习了Python字典波澜壮阔的演进史。我们从最基础的线性搜索和关联列表出发,逐步探索了:

  1. 哈希表与分离链接法:通过分桶减少搜索空间。
  2. 动态调整大小:维持哈希表的高性能。
  3. 缓存哈希:加速调整大小操作。
  4. 快速匹配:利用身份比较和哈希比较避免昂贵的相等性测试。
  5. 开放寻址与线性探测:提高空间利用率,并引入虚位条目处理删除。
  6. 扰动技术:优化探测序列,减少聚集。
  7. 版本号:允许缓存字典查找结果。
  8. 紧凑布局:将数据存储与索引分离,实现空间高效和有序迭代。
  9. 键共享字典:为大量相似字典节省内存。
  10. 哈希随机化:增强安全性,抵御HashDoS攻击。

最终,这些跨越数十年的伟大思想汇聚成了Python 3.6及以后版本中我们使用的更小、更快、有序且内存高效的现代字典。这个演进过程完美体现了软件工程中持续的优化、权衡与创新精神。

013:从Hello World到Kubernetes编排

概述

在本节课中,我们将学习如何将一个简单的Python应用(例如一个Flask“Hello World”服务)从本地开发环境,通过容器化技术,最终部署到Kubernetes集群中。我们将探讨传统部署方式的痛点,理解容器(特别是Docker)如何解决依赖和环境一致性问题,并初步了解Kubernetes如何实现应用的自动化编排与高可用部署。内容将尽可能简单直白,适合初学者理解现代应用部署的基本流程。


传统部署的挑战 🚧

上一节我们概述了课程目标,本节中我们来看看在容器技术普及之前,部署一个Python应用通常会遇到哪些挑战。

最初,运行一个“Hello World”应用似乎很简单。你编写几行代码,例如一个基本的Flask应用:

# app.py
from flask import Flask
app = Flask(__name__)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_19.png)

@app.route('/')
def hello():
    return "Hello, World!"

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_21.png)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

在本地,你可以直接运行 python app.py 并访问 http://localhost:5000 看到结果。这创造了“我是一个真正的开发者”的错觉。

然而,问题始于你试图在另一台机器(例如服务器)上运行它。你需要确保那台机器上安装了正确版本的Python、Flask以及其他可能的依赖。这个过程可能非常耗时且容易出错,尤其是在处理不同操作系统或需要编译C扩展时。我们实际上是在要求每个运行我们软件的人去解决复杂的系统配置问题。


引入容器:解决依赖与环境问题 📦

上一节我们看到了环境不一致带来的麻烦,本节中我们来看看容器技术如何提供一个解决方案。

容器技术的核心思想是将应用程序及其所有依赖项(运行时、系统工具、库)打包成一个独立的、可移植的“镜像”。这个镜像可以在任何支持容器运行时的环境中一致地运行。这类似于移动设备上的应用商店体验:用户下载一个完整的、立即可用的包。

对于Python开发者,Docker是最常用的容器工具。它通过一个名为 Dockerfile 的文本文件来定义如何构建镜像。

以下是一个简单的Dockerfile示例,用于打包上述Flask应用:

# Dockerfile
# 1. 指定基础镜像,包含一个完整的操作系统(如Ubuntu)和Python环境
FROM ubuntu:latest

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_41.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_43.png)

# 2. 在容器内安装Python3和pip
RUN apt-get update && apt-get install -y python3 python3-pip

# 3. 将本地的依赖文件复制到容器内
COPY requirements.txt /app/requirements.txt

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_45.png)

# 4. 安装Python依赖
RUN pip3 install -r /app/requirements.txt

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_47.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_49.png)

# 5. 将应用代码复制到容器内
COPY app.py /app/app.py

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_51.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_53.png)

# 6. 设置工作目录
WORKDIR /app

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_55.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_57.png)

# 7. 定义容器启动时执行的命令
ENTRYPOINT ["python3", "app.py"]

requirements.txt 文件内容很简单:

Flask==2.0.1

构建并运行此镜像的命令如下:

# 构建镜像,命名为 hello-world
docker build -t hello-world:1.0 .

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/9619658629589a41f3618dae125dd6d7_68.png)

# 运行容器,将容器的5000端口映射到主机的5000端口
docker run -p 5000:5000 hello-world:1.0

现在,无论你的开发机、测试服务器或生产服务器是什么配置,只要安装了Docker,都可以通过完全相同的命令来运行这个应用。它解决了“在我机器上能运行”的经典问题。


超越单机:Kubernetes 编排 🤖

上一节我们学会了如何将应用打包成容器,本节中我们来看看当应用需要运行在多台机器上,并实现高可用和弹性伸缩时,该如何管理。

在笔记本电脑上运行一两个容器是简单的。但在生产环境中,我们通常需要在多台服务器(节点)上运行应用的多个副本,以确保高可用性和负载能力。手动管理这些容器的放置、启动、停止、监控和网络连接是极其复杂的。

这就是编排系统的作用。Kubernetes (K8s) 是一个开源的容器编排平台,它可以将多台服务器抽象成一个统一的“集群”资源池。

在Kubernetes中,你不再直接命令某台具体机器运行容器,而是通过声明式的配置文件(通常是YAML格式)来描述你期望的应用状态。例如,你声明“我需要运行3个hello-world应用的副本”。Kubernetes的调度器会自动决定在哪些节点上启动这些容器(Pod),并持续监控,确保实际状态始终符合你的声明。

以下是一个简化的Kubernetes部署(Deployment)配置文件示例:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world-deployment
spec:
  replicas: 3 # 期望运行3个副本
  selector:
    matchLabels:
      app: hello-world
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
      - name: hello-world
        image: hello-world:1.0 # 使用我们构建的Docker镜像
        ports:
        - containerPort: 5000
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"

使用 kubectl (Kubernetes命令行工具) 可以应用这个配置:

kubectl apply -f deployment.yaml

Kubernetes会自动完成以下工作:

  1. 调度:根据各节点的资源(CPU、内存)情况,将3个Pod调度到合适的节点上。
  2. 拉取镜像:从指定的仓库拉取 hello-world:1.0 镜像。
  3. 运行容器:在节点上启动容器。
  4. 自愈:如果某个Pod所在的节点宕机或容器崩溃,Kubernetes会自动在新的节点上重新启动一个Pod,始终保持3个副本运行。
  5. 滚动更新:当你将镜像版本更新为 hello-world:2.0 并重新应用配置时,Kubernetes可以逐步用新Pod替换旧Pod,实现零停机部署。
  6. 服务发现与负载均衡:你可以创建另一个Kubernetes Service 资源,为这3个Pod提供一个统一的访问入口,并自动将流量负载均衡到它们。


总结 🎯

本节课中我们一起学习了现代Python应用部署的演进路径。

我们从编写一个简单的“Hello World” Flask应用开始,经历了传统部署方式中依赖管理和环境一致性的痛苦。接着,我们引入了Docker容器技术,通过编写Dockerfile将应用及其环境打包成一个可移植的镜像,彻底解决了“它在我机器上能运行”的问题。

最后,为了管理生产环境中大规模、高可用的容器化应用,我们初步了解了Kubernetes编排平台。Kubernetes通过声明式配置,自动化了容器的调度、部署、扩展和故障恢复,让开发者能够专注于应用逻辑本身,而非底层基础设施的复杂性。

将Python应用与容器化、编排技术结合,是构建可靠、可扩展现代服务的关键一步。

014:Amjith Ramanujam 在 PyCon 2017 的演讲 🚀

在本教程中,我们将学习如何设计出色的命令行工具。我们将探讨三个核心设计原则:可发现性用户关注最小化配置。通过分析 PGCLI、MyCLI、Fish Shell 和 Bpython 等优秀工具,我们将了解如何将这些原则付诸实践。最后,我们将使用 Python 的 prompt_toolkit 库,快速构建一个功能丰富的交互式 REPL。


精彩的命令行工具:1:引言与背景

我叫 Amjith Ramanujam。本次演讲的主题是命令行工具。我的日常工作是在 Netflix 的流量工程团队。但今天,我将讨论我的两个副项目:PGCLI 和 MyCLI,它们分别是 PostgreSQL 和 MySQL 的命令行客户端。

本次演讲将分享我们作为核心团队,为克服命令行界面固有局限而做出的一些设计决策。我们从许多设计精良的现有命令行应用程序中获得了灵感。


精彩的命令行工具:2:可发现性的重要性 🕵️

故事始于大约二十年前。一个孩子在 MS-DOS 终端上输入命令,因打错字而沮丧。直到老师教他使用上箭头键调出历史命令并编辑,问题才得以解决。这带来了巨大的喜悦。

后来,在 Linux 中,Bash 保留了通过上下箭头导航历史的惯例。更令人惊喜的是Tab 键补全功能,它能自动补全文件名或路径名。

然而,这些强大功能并不容易被新用户发现。用户需要阅读手册页,或依赖他人指导。相比之下,GUI 应用的新功能通常有明确的图标或菜单项,易于探索。

那么,如何将这种可发现性引入命令行应用呢?我们将以历史导航和 Tab 补全为例。


提升 Tab 补全的可发现性

在 PGCLI 中,我们改进了 Tab 补全。用户开始输入时,所有补全选项会立即显示,无需先按 Tab 键。例如,输入 SELECT 时,相关关键字会自动建议。

# 在 PGCLI 中,补全建议是即时且上下文相关的。
输入: SEL
建议: SELECT


提升历史导航的可发现性

在 Bash 中,可以通过 Ctrl+R 搜索历史命令,但这功能不易被发现。Fish Shell 采取了更主动的方式:用户一开始输入,它就会自动搜索历史并给出建议,用户按右箭头即可采纳。

# Fish Shell 风格的历史建议
输入: git comm
建议: git commit -m “fix typo” (来自历史记录)


可发现性的核心理念

其背后的理念是坦诚。不要将功能隐藏在特殊按键组合之后。为了让程序更具可发现性,应该更直接地向用户展示其功能。


精彩的命令行工具:3:以用户为中心的设计 👥

上一节我们探讨了可发现性,本节我们来看看如何将用户放在首位。设计时应首先考虑什么对用户最直观、最强大,而不是实现起来有多困难。


案例对比:MySQL vs MyCLI

以下是 MySQL 默认客户端与 MyCLI 在补全体验上的对比:

  1. 大小写敏感性问题:在 MySQL 客户端中,输入 sel 后按 Tab 无反应,必须输入 SEL(大写)才会触发补全。MyCLI 则实现了不区分大小写的补全。
  2. 上下文无关的补全:在 MySQL 中,输入 SELECT * FROM 后按 Tab,会列出所有可能的关键字(可能超过800个),而不是当前数据库中的表。MyCLI 则提供上下文敏感的补全,FROM 关键字后只建议当前数据库中的表名。
-- MyCLI 的上下文敏感补全
输入: SELECT * FROM users WHERE
补全建议: id, name, email... (仅限 users 表的列)

最初实现 MyCLI 的智能补全引擎非常困难,但坚持用户优先的原则,最终创造了更强大的工具。


用户关注点的总结

始终将用户置于首位。在设计新功能时,思考用户的需求,追求极致的直观和强大。实现细节的困难应该放在第二位去解决。


精彩的命令行工具:4:警惕过度配置 ⚙️

我们已将最好的留到最后。Bpython 是一个出色的交互式 Python Shell,它展示了如何通过智能默认值减少配置。


Bpython 的强大功能

在标准 Python REPL 中,输入 impo 后按 Tab,只会插入一个制表符。而在 Bpython 中:

  • 输入时自动显示补全建议。
  • 显示函数签名和文档字符串。
  • 无需离开终端即可了解方法用法。

# 在 Bpython 中
输入: requests.get(
显示: get(url, params=None, **kwargs)  # 自动显示签名和文档

配置是“万恶之源”

一个常见的反对意见是:“我可以通过配置标准 Python REPL 来获得类似功能。”然而,过多的配置选项往往意味着程序无法智能地判断什么对用户最好

配置应仅限于主观偏好,例如配色方案。对于客观的功能,程序应该提供明智的默认值,让大多数用户开箱即用,无需配置。


精彩的命令行工具:5:快速构建卓越 REPL 🛠️

我们探讨了命令行工具的三大问题:可发现性、用户关注和配置最小化。现在你可能会想,实现这些高级功能是否非常困难?

如果我告诉你,用大约 10 行 Python 代码,在 10 分钟内就能实现一个具备所有这些功能的 REPL,你会怎么想?

以下是构建一个优秀交互式 REPL 的检查清单:

  1. 持久化历史记录
  2. 历史搜索(如 Ctrl+R 或 Fish 风格建议)
  3. Emacs 风格键绑定(如 Ctrl+A 到行首,Ctrl+E 到行尾)
  4. 输出分页
  5. 自动触发补全(无需按 Tab)
  6. 最小化配置
  7. 语法高亮

我们将使用 prompt_toolkit 库来实现它们。


第一步:基础 REPL

首先,我们创建一个简单的回声 REPL,它读取用户输入并打印出来。

from prompt_toolkit import prompt

while True:
    user_input = prompt(‘> ‘)
    print(user_input)


第二步:添加持久化历史记录

使用 prompt_toolkitFileHistory 为 REPL 添加历史记录功能。

from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory

while True:
    user_input = prompt(‘> ‘, history=FileHistory(‘./history.txt‘))
    print(user_input)

现在,REPL 已经支持上下箭头导航历史、Ctrl+R 搜索,以及 Emacs 键绑定。


第三步:添加 Fish 风格自动建议

让 REPL 能够根据历史记录自动建议。

from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory

while True:
    user_input = prompt(
        ‘> ‘,
        history=FileHistory(‘./history.txt‘),
        auto_suggest=AutoSuggestFromHistory(), # 添加自动建议
    )
    print(user_input)

第四步:添加自动补全

我们创建一个 SQL 关键字补全器,并在用户输入时自动触发补全。

from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import WordCompleter

# 定义 SQL 补全器
sql_completer = WordCompleter([‘select‘, ‘from‘, ‘where‘, ‘show‘], ignore_case=True)

while True:
    user_input = prompt(
        ‘> ‘,
        history=FileHistory(‘./history.txt‘),
        auto_suggest=AutoSuggestFromHistory(),
        completer=sql_completer, # 添加补全器
        complete_while_typing=True, # 输入时自动补全
    )
    print(user_input)

第五步:添加语法高亮

使用 Pygments 库为 SQL 语句添加语法高亮。

from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import WordCompleter
from pygments.lexers.sql import SqlLexer # 导入 SQL 词法分析器

sql_completer = WordCompleter([‘select‘, ‘from‘, ‘where‘, ‘show‘], ignore_case=True)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/446f7186f960c62a6a2abb0c835f6cf5_54.png)

while True:
    user_input = prompt(
        ‘> ‘,
        history=FileHistory(‘./history.txt‘),
        auto_suggest=AutoSuggestFromHistory(),
        completer=sql_completer,
        complete_while_typing=True,
        lexer=SqlLexer, # 添加语法高亮
    )
    print(user_input)

至此,我们用一个简短的脚本实现了一个支持持久历史、智能补全、自动建议和语法高亮的强大 REPL。输出分页可以使用 click 库轻松实现。


精彩的命令行工具:6:总结与资源 📚

本节课中,我们一起学习了设计优秀命令行工具的三大原则:

  1. 可发现性:主动展示功能,减少隐藏操作。
  2. 用户关注:优先考虑用户体验,再解决实现难题。
  3. 最小化配置:提供明智的默认值,仅在处理主观偏好时提供配置选项。

我们还使用 prompt_toolkit 库快速构建了一个功能丰富的 REPL,演示了如何轻松应用这些原则。

相关资源

  • PGCLI: PostgreSQL 命令行客户端
  • MyCLI: MySQL 命令行客户端
  • Fish Shell: 现代化的命令行 Shell
  • Bpython: 功能丰富的 Python REPL
  • prompt_toolkit: 用于构建强大命令行应用程序的 Python 库
  • Click: Python 命令行工具创建库,支持输出分页

感谢阅读,希望这些理念能帮助你构建更用户友好的命令行工具!

015:Python 可视化工具生态概览 🗺️

在本节课中,我们将一起梳理 Python 世界中庞大而多样的数据可视化工具生态系统。我们将了解不同工具库的起源、核心优势、适用场景以及它们之间的关系,帮助你根据具体任务选择最合适的工具。

概述:Python 可视化的“宇宙” 🌌

Python 的可视化领域并非只有一个“正确答案”。相反,它拥有一个由众多专业化工具组成的丰富生态系统。每个工具都有其独特的应用场景和优势。本节课的目标是帮助你建立一张“心智地图”,让你在面对一个可视化任务时,能够快速判断在 Python 中应该使用哪个包。

核心基石:Matplotlib 及其衍生工具 🏛️

一切始于 Matplotlib。它已存在近二十年,是 Python 科学计算可视化的基石。许多其他工具都构建在它的基础之上。

Matplotlib 的优势在于其强大的灵活性和广泛的输出格式支持。它的设计灵感来源于 MATLAB,这对于早期从 MATLAB 转向 Python 的科学家和工程师至关重要。它支持 PNG、PDF、SVG、EPS 等多种渲染后端,几乎可以复现任何你能想象到的静态图表。

然而,Matplotlib 也有其弱点。它的 API 有时较为冗长,进行统计可视化时可能需要编写大量样板代码。例如,用花瓣长度对比萼片长度,并按物种着色绘制散点图,在 Matplotlib 中可能需要多行代码:

# 示例:使用 Matplotlib 绘制分组散点图
fig, ax = plt.subplots()
for species, group in iris.groupby('species'):
    ax.scatter(group['petal_length'], group['sepal_length'], label=species)
ax.legend()
ax.set_xlabel('Petal Length')
ax.set_ylabel('Sepal Length')
plt.show()

此外,其默认样式曾略显陈旧(尽管在 2.0 版本中已大幅改进),并且原生对交互式网页图形和大数据集的支持有限。

为了改善这些弱点,同时保留 Matplotlib 的强大功能,一个庞大的工具集群应运而生。它们通过在 Matplotlib 之上构建更高级、更简洁的 API 来解决问题。

以下是围绕 Matplotlib 生态的一些关键工具:

  • Pandas:其 DataFrame 对象内置了便捷的绘图方法,通常只需一行代码即可完成基础可视化。
  • Seaborn:专门为统计可视化设计,提供了美观的默认样式和高级绘图函数(如 pairplot),能极大简化复杂图表的创建。
  • ggplot:为喜欢 R 语言 ggplot2 语法风格的用户提供了类似的接口。
  • Cartopy / Basemap:用于地理空间数据可视化。
  • NetworkX:用于网络图可视化。
  • Yellowbrick / scikit-plot:专注于机器学习模型的可视化诊断。

交互式浪潮:基于 JavaScript 的工具 🌐

随着对交互式图表需求的增长,许多 Python 库开始与 JavaScript 结合,利用浏览器的能力创建动态、可交互的可视化。这类工具的共同点是:在 Python 中生成图表的序列化规范(如 JSON),然后由前端的 JavaScript 库渲染。

在这个集群中,BokehPlotly 是两个最成熟和流行的代表。

  • Bokeh:擅长创建复杂的交互式仪表盘,支持平移、缩放、悬停提示等丰富的交互功能。其优势在于强大的交互性和灵活的 API 层次。
  • Plotly:同样提供出色的交互性,并以其易用的 3D 绘图和动画功能著称。它采用开源核心加商业服务的模式。

这些工具的优势是带来了 Matplotlib 难以企及的交互体验。但相对的,它们在输出印刷质量的矢量图形(如 PDF)方面可能不如 Matplotlib 成熟。

应对大数据:高效渲染工具 ⚡

Matplotlib 在处理海量数据点时可能效率低下。为此,出现了一批专注于高性能渲染的工具。

  • Datashader:其核心思想是“先聚合,后渲染”。当你有上亿个数据点时,直接绘制每个点没有意义(屏幕像素有限)。Datashader 会先根据视图范围动态聚合数据(如生成热图),再将聚合后的图像发送到前端,从而实现大数据集的流畅交互浏览。
  • VisPy / Glumpy:基于 OpenGL,利用 GPU 加速进行高性能科学可视化,尤其适合大型 3D 数据集。

未来方向:声明式可视化与规范语言 🚀

这是目前非常活跃和令人兴奋的一个领域。其理念是将“图表是什么”(声明)与“如何绘制图表”(执行)分离开来。

  • Altair:是本课程重点介绍的一个库。它基于 Vega-Lite 规范。你只需用简洁的 Python 代码声明数据和想要的可视化编码关系(如“x是花瓣长度,y是萼片宽度,颜色按物种区分”),Altair 会将其转换为 Vega-Lite 的 JSON 规范,最后由 JavaScript 引擎渲染。这使得代码极其简洁且专注于数据本身。
    # 示例:使用 Altair 实现相同的分组散点图
    import altair as alt
    chart = alt.Chart(iris).mark_circle().encode(
        x='petal_length',
        y='sepal_length',
        color='species'
    )
    
  • Vega / Vega-Lite:本身是一种声明式的可视化语法规范(JSON 格式),正在被越来越多的大型项目(如维基百科)采用,用于存储可交互的图表数据。
  • HoloViews:另一个声明式库,旨在让数据集“懂得如何可视化自己”,并支持对接 MatplotlibBokeh 等多个后端。

声明式可视化的优势在于提高了代码的表达力和可移植性,同一个图表规范可以在不同渲染器中使用,是促进工具间互操作性的重要方向。

总结与选择指南 🧭

本节课我们一起学习了 Python 可视化工具的四大集群:

  1. Matplotlib 核心生态:灵活、稳定、输出格式全,是静态出版级图表的基石。需要更多便利时,可选用 PandasSeaborn 等上层工具。
  2. JavaScript 交互生态:以 BokehPlotly 为代表,为创建网页交互式图表和仪表盘而生。
  3. 大数据渲染生态:如 Datashader,专门用于高效可视化海量数据集。
  4. 声明式规范生态:以 AltairVega-Lite 为代表,代表了更简洁、更专注、更具互操作性的未来趋势。

如何选择?请遵循以下思路:

  • 需要快速探索数据或制作简单静态图?从 Pandas .plot()Seaborn 开始。
  • 需要完全控制细节或出版高质量静态图?深入使用 Matplotlib
  • 需要创建交互式网页应用或仪表盘?选择 BokehPlotly
  • 需要可视化数亿以上的数据点?研究 Datashader
  • 希望以最简洁的代码描述数据关系,并看重未来的互操作性?尝试 Altair

Python 的可视化世界丰富多彩,没有唯一的最佳工具,只有最适合当前场景的工具。希望本教程能帮助你在这个生态中找到方向,更高效地将数据转化为洞察。

016:如何构建优秀数据集预测奥斯卡与票房 🎬

在本节课中,我们将跟随黛博拉·哈努斯的演讲,学习如何从互联网上抓取数据,并通过数据分析来预测电影的票房成功与奥斯卡获奖情况。我们将从定义问题开始,逐步完成数据获取、探索分析,并最终得出结论。


数据科学实战:P16-1:定义可回答的问题 🎯

上一节我们介绍了课程的整体目标,本节中我们来看看如何将模糊的商业目标转化为一个具体、可回答的数据科学问题。

假设我们是一名电影制片人,拥有制作资金,并希望获得投资回报。我们真正想知道的是:“这部电影是否会成为票房热门?” 但这个问题过于模糊,难以量化。

因此,我们可以将其重新表述为更具体的问题:

  • 问题一(票房):在考虑到电影具有某些特征(如预算、类型)的情况下,它成为票房热门的可能性有多大?
  • 问题二(奥斯卡):一部电影的哪些属性与其赢得奥斯卡奖相关?

通过这种方式,我们将一个开放性问题转化为了可以通过数据进行分析和建模的具体问题。


数据科学实战:P16-2:寻找与获取优质数据 📊

在明确了问题之后,我们需要寻找能够回答这些问题的数据。优质数据通常具备相关性、结构化和相对完整的特点。

以下是一些获取电影相关数据的优质来源:

  • Box Office Mojo:提供历年票房排行榜,数据以表格形式呈现,结构清晰。
  • 烂番茄 (Rotten Tomatoes):包含电影评分、评论等结构化信息。
  • 维基百科 (Wikipedia):电影信息框格式统一,便于抓取。
  • IMDb:丰富的电影元数据,如评分、演员、票房等。

获取这些数据主要有两种方法:

  1. 使用官方API:例如 IMDb API,通过预定义的函数(如 get_movies())获取数据。
  2. 编写网络爬虫:当没有现成API时,我们需要从网页中直接抓取数据。

编写爬虫的基本步骤是:发送HTTP请求获取网页HTML,然后解析HTML以提取所需信息。我们可以使用Python的 requests 库来获取网页,然后用 BeautifulSouppyquery 等工具来解析HTML。

例如,抓取Box Office Mojo的代码片段如下:

import requests
from time import sleep

base_url = “http://www.boxofficemojo.com/yearly/chart/?page=1&view=releasedate&view2=domestic&yr=2017&p=.htm”
all_movies = []

for page in range(1, 6): # 假设抓取5页
    url = base_url.replace(“page=1”, f“page={page}”)
    response = requests.get(url)
    # 将HTML内容存入字典
    all_movies.append(response.text)
    sleep(1) # 礼貌性延迟,避免被网站封禁

在抓取数据时,需要注意“速率限制”。网站会识别快速、自动的请求。因此,在请求间添加延迟(如 time.sleep(1))是必要的,这模拟了人类浏览的速度,是一种良好的网络礼仪。


数据科学实战:P16-3:探索性数据分析与票房预测 📈

获取数据后,下一步是探索数据,以发现潜在的模式和关系。这对于构建预测模型至关重要。

我们可以基于数据集探索多个可能影响票房的因素:

  • 电影预算
  • IMDb评分
  • 发行公司
  • 首映周末票房
  • 开画影院数量
  • 上映季节(如12月)
  • MPA分级(G, PG, PG-13等)

以下是探索数据时的一些关键发现:

  • 开画影院数量:存在一个“甜蜜点”(约3500家影院),超过或低于这个数量都可能影响总票房。
  • 电影质量(IMDb评分):与总票房收入没有明显相关性。好电影不一定最卖座。
  • 首映周末票房:与电影总票房高度相关,是一个很强的预测指标。

基于这些发现,我们可以构建一个多变量线性回归模型来预测票房。模型可能会揭示:

  • 预算对票房有正面影响,但并非决定性因素。
  • 上映时机(如12月)非常重要。
  • G级和PG级电影更赚钱。
  • 首映周末票房是最强的预测因子。

数据科学实战:P16-4:调整问题与预测奥斯卡奖 🏆

预测奥斯卡奖的思路与预测票房类似,但需要调整问题和数据来源。

我们扮演演员,想知道参演的电影能否获奖。问题可定义为:“考虑到电影具有某些特征,它赢得奥斯卡奖的可能性有多大?”或“哪些电影属性与获奖相关?”

在数据获取上遇到了挑战:IMDb API速度很慢,无法在短时间内获取大量现代电影数据。因此,我们调整了策略,使用了一个包含1980年至1996年奥斯卡提名电影的CSV数据集。问题也随之调整为:“在一部电影已获提名的前提下,哪些属性能预测它最终获奖?”

我们探索了新的因素:

  • 提名类别(如最佳影片、最佳摄影)
  • 电影主题(是否关于家庭、战争、是否包含暴力、吸烟等)
  • 电影类型(剧情片、恐怖片等)
  • 制作国家
  • 上映年份/月份

分析发现:

  • 国家:来自意大利或西班牙的提名电影获奖几率很高。
  • 上映月份:单纯看获奖数量,12月最多。但考虑上映电影总数后,下半年(7-9月)上映的电影获奖率更高。
  • 主题:暴力电影通常表现不佳,但如果是战争片,则例外(奥斯卡青睐战争片)。
  • 提名类别:获得“最佳影片”提名对获奖是强信号,而“最佳摄影”提名则关联性较弱。

通过这些分析,我们可以构建一个分类器(如逻辑回归),来预测已提名电影的获奖概率。


数据科学实战:P16-5:总结与最佳实践 ✅

本节课中,我们一起学习了构建数据科学项目的完整流程:

  1. 定义问题:将模糊目标转化为具体、可量化的问题。
  2. 获取数据:寻找相关、结构化的数据源,使用API或编写礼貌的网络爬虫进行抓取。
  3. 探索数据:通过可视化(图表)和统计分析,理解数据分布和变量间的关系。
  4. 得出结论:基于探索结果构建模型(如线性回归、分类器),并解释发现。

关键工具与技巧

  • 工具requests(获取网页),BeautifulSoup(解析HTML),Jupyter Notebook(分析与可视化),scikit-learn/statsmodels(建模)。
  • 技巧:爬虫中设置延迟(time.sleep);模型评估时与简单基准(如“全部猜输”)进行比较;从开源项目中学习。

预测奥斯卡和票房是一个生动的例子,展示了如何将公开数据转化为有价值的见解。你可以应用同样的方法,从定义问题开始,探索你感兴趣的任何领域。

总结:数据科学始于一个好问题,成于一份好数据。通过系统性的获取、探索和分析,我们可以从数据中讲述故事,并做出预测。现在,你可以尝试寻找自己的数据集,开始你的数据探索之旅了。

017:在 Django 和 Python 上运行 Instagram

概述

在本教程中,我们将学习 Instagram 如何在其庞大的社交平台上使用 Django 和 Python 技术栈,并深入了解他们从 Python 2 迁移到 Python 3 的完整过程、面临的挑战以及取得的成果。这对于希望了解如何在大规模生产环境中应用和扩展 Python 的开发者非常有价值。


章节 1:Instagram 与 Python 的渊源

大家好。我是 Huie,负责 Instagram 的基础设施工程。今天,我和我的同事郭丽莎将分享我们如何在 Django 和 Python 技术栈上运行 Instagram。

Instagram 自 2012 年起就开始使用 Python。我们的第一位基础设施工程师曾讨论过如何在 Python 中构建发布/订阅系统。我们很高兴成为这个社区的一部分。

首先,你可能想知道 Instagram 是什么。对许多人来说,Instagram 是一个分享早午餐照片的应用。它始于 2010 年,并迅速发展成为全球最大的社交平台之一。如今,每月有超过 7 亿活跃用户使用 Instagram。


章节 2:为什么选择 Python?

那么,Instagram 为什么选择 Python 呢?要回答这个问题,我们需要回到 2010 年,了解两位联合创始人 Kevin 和 Mike。作为当时没有太多后端经验的产品经理,他们自然在开始时寻找能帮助他们构建产品的最佳可用框架。

他们发现了 Django 和 Python 框架,这是当时最受欢迎的框架之一。在观看了一些教程和阅读文档后,他们便朝着服务 7 亿用户的目标前进了。

如今,Instagram 喜欢 Python 的原因如下:

  • 开发者友好:Python 以其易用性和高生产力著称。
  • 人才储备丰富:流行的语言意味着有大量优秀的工程师渴望使用 Python 工作。
  • 框架成熟:尽管长期使用旧版 Django,但我们能够利用其成熟性,专注于业务逻辑和产品发布。

例如,五年来,我们一直使用 Django 的工作流来管理所有 Instagram 的用户信息。我们在能力达到极限之前,就已经用完了 32 位用户 ID,而 Django 框架本身的能力尚未耗尽。


章节 3:扩展 Python 与 Django

我们很早就采用了 Django 和 Python 技术栈,并已经走得很远。在早期,我们在 Django 的 ORM 层中添加了分片支持,以满足社交图谱数据的存储需求。出人意料的是,我们禁用了 Python 中的垃圾回收机制,以提高内存利用率。我们还扩展了 Instagram,使其能够在多个地理分布的数据中心运行。

选择 Python 作为编程语言,也帮助影响和塑造了 Instagram 的工程文化。我们倾向于使用经过验证的技术。Python 简单实用的特性,使我们的工程师能够专注于为用户解决真正的问题,而不是被语言细节所困扰。

你可能会想,Python 仍然很慢,对吧?但在 Instagram,我们相信更大的瓶颈是开发者的速度,而不仅仅是纯粹的代码执行速度。我们的结论是,你可以用 Python 在不担心框架和语言性能的情况下,获得数亿用户。


章节 4:应对增长与效率挑战

这并不意味着我们不关心扩展 Python。在某个时刻,我们意识到,随着团队和产品的不断扩展,服务器数量的增长远远超过了用户的增长。随着我们增加更多工程师和推出更多产品,这种情况只会变得更糟。

因此,我们制定了解决 Python 效率的策略:

  1. 建立分析工具:我们开始建立广泛的工具,以分析和理解性能瓶颈。
  2. 使用本地语言:我们主动将关键但稳定的组件用 C 和 C++ 等本地语言实现,例如 memcache 访问库。
  3. 利用科学化:我们利用科学化作为武器,显著提升性能。
  4. 探索未来方案:随着我们寻找下一个 7 亿用户,我们将考虑更大的想法,例如让整个 Django 技术栈完全异步,或编写一个新的 Python 运行时。

我们承诺将继续推动在 Instagram 上运行 Python 的边界。


章节 5:迈向 Python 3 的决策

但有一个小问题。很长一段时间以来,像其他人一样,我们一直在使用 Python 2.7 和 Django 1.3。我们甚至在代码库中还有一些复杂的小补丁。我们心想,难道我们会永远停留在这个设置上吗?

经过多轮调查和讨论,我们最终决定将整个代码库升级到 Python 3。经过一些努力后,今天我很高兴地宣布,Instagram 的整个 Django 网络服务和异步工作任务队列已经运行在 Python 3.6 上好几个月了。

那么,发生了什么,我们是如何走到这一步的?接下来,我的同事 Lisa 将告诉大家我们的 Python 3 迁移之路。


章节 6:迁移到 Python 3 的动机

谢谢 Huie。没错,Instagram 已经完全运行在 Python 3 上好几个月了。在接下来的内容中,我想回到一年前,分享我们为什么转向 Python 3,达到这个目标所采取的步骤,解决的具体挑战,以及最终我们今天所处的位置。

那么,为什么要迁移到 Python 3 呢?在去年的 PyCon 大会上,Guido 宣布 Python 2 的支持将在 2020 年结束。作为基础设施团队,我们的使命是确保 Instagram 在今天和明天都能继续运行,所以我们总是在寻找未来可能出现的瓶颈。

我们的动机主要有三点:

  1. 提升开发速度:Python 3 引入了标准化的类型提示,这有助于提高代码的可读性、可维护性,并方便工具进行静态检查。
  2. 提升服务器性能:Python 3 原生支持 asyncio,这有助于我们更高效地处理并发请求,优化服务器资源利用率。
  3. 融入活跃社区:Python 拥有一个非常活跃的开发社区,但创新只会继续在 Python 3 上进行。我们希望成为其中的一部分,并更容易地回馈社区。

章节 7:迁移策略与约束

我们得到了继续推进项目的绿灯。但我们有两个必须遵循的约束:
第一,不能影响用户。升级不能有服务停机时间。
第二,产品发布不能放缓。

那么,我们如何做到这一点呢?首先,了解一下 Instagram 如何将代码发布到生产环境。我们在主分支上开发代码,不使用功能分支。我们的理念是进行小而集中的代码变更。更改被提交到主分支后,通常在一个小时内就会被推送到生产环境。

考虑到这一点,我们评估了几个迁移选项:

  • 长期功能分支:由于我们每天在主分支上有大量提交,且 Python 3 的更改与整个代码库重叠,分支同步的开销会很大,容易出错。
  • 按端点逐步迁移:创建单独的服务器池运行 Python 3。这不适合我们,因为我们只有一个二进制文件,许多端点共享公共模块,管理不同池的开销很大。
  • 微服务重构:将公共模块提取为微服务。这需要大规模重构,会增加延迟和部署复杂度。

最终,我们决定遵循我们擅长的方式:通过小步骤交付大型变更,确保代码同时兼容 Python 2 和 Python 3。我们将在主干上进行小的、兼容的更改。当所有代码准备就绪时,我们将虚拟机切换到 Python 3。


章节 8:迁移的具体阶段

我们的迁移工作主要分为几个并行和连续的阶段:

第一阶段:处理依赖和代码现代化
我们制定的第一条规则是:新添加的依赖必须兼容 Python 3。我们清理了不再使用的包,并升级了关键依赖,其中最大的一项是将 Django 从 1.3 升级到 1.8。同时,我们使用现代化工具对源代码进行大规模修改,一次只进行一种类型的修复,以便于代码审查。

第二阶段:修复单元测试
单元测试在我们的迁移中尤其有帮助。我们添加了一个“已知通过测试”的列表,并纳入流程。每个合并到主分支的提交都必须通过 Python 2 和 Python 3 的测试。随着我们修复测试,这个列表越来越长,最终转变为“已知失败测试”的排除列表。此时,任何新测试都必须默认兼容 Python 3。

第三阶段:渐进式生产发布
在单元测试覆盖后,我们开始渐进式发布:

  1. 开发者沙箱:将开发者的测试环境切换到 Python 3,像剥洋葱一样层层发现和修复问题。
  2. 内部员工测试:将 Python 3 暴露给 Facebook 员工使用,他们使用的功能比开发者更广泛。
  3. 逐步用户发布:慢慢向越来越多的用户推出,此阶段更多关注性能调优。


章节 9:遇到的主要挑战与解决方案

在迁移过程中,我们遇到了几类主要问题:

1. Unicode/字符串处理
这是最常见的问题。在 Python 3 中,字符串和字节串的区分更严格。

# 问题代码
hmac.new(‘my_secret_key’, ‘my_message’, hashlib.sha256)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/pycon-2017/img/711a2f93a070477c7fb7502d1b7813fa_51.png)

# 修复代码
hmac.new(b‘my_secret_key’, b‘my_message’, hashlib.sha256)

我们创建了辅助函数(如 to_bytes, to_text)来简化转换,使代码更清晰且易于理解。

2. 数据兼容性(特别是 Pickle)
我们使用 Pickle 在 memcache 中存储数据。Python 2 和 Python 3 的 Pickle 协议默认版本不同,且对 Unicode 的处理方式不同,导致无法互相解码。
解决方案:我们将 memcache 的键空间按 Python 版本进行划分,使 Python 2 和 Python 3 的服务器不共享缓存数据。

3. 迭代器行为变化
在 Python 3 中,mapfilterdict.keys() 等方法返回的是迭代器,而非列表。迭代器只能消费一次。

# Python 2: map 返回列表
# Python 3: map 返回迭代器
files_to_compile = list(map(Compiler, py_files))  # 修复:转换为列表

建议在迁移初期,使用工具将所有这类调用显式转换为列表,然后再根据需要优化。

4. 字典迭代顺序
在 Python 3.6 之前,字典的迭代顺序是未定义的。如果你的应用程序依赖于 JSON 输出的键顺序,需要明确指出。

import json
data = {‘c’: 1, ‘b’: 2, ‘a’: 3}
# 顺序可能不同
print(json.dumps(data))
# 指定顺序
print(json.dumps(data, sort_keys=True))


章节 10:性能调优与最终成果

经过所有修复,我们准备切换。但我们仍关心性能。我们使用两个指标:每个请求的 CPU 指令数(越少越好)和服务器每秒支持的最大请求数(越多越好)。

我们发现,在切换到 Python 3 后,第一个指标(CPU指令数)下降了 12%,但第二个指标(吞吐量)没有相应提升。经过调查,问题出在一行配置检查代码上,一个 Unicode 字符串的比较在 Python 3 中行为不同,导致内存配置未被正确应用。修复这个“最重要的一封信”后,我们获得了全面的性能提升。

最终,在 2017 年 2 月,我们将流量完全切换到 Python 3.5,随后又升级到 Python 3.6。

我们得到了什么?

  • 性能提升:在 Django 层面实现了 12% 的 CPU 提升,在异步任务层面节省了 30% 的内存。
  • 业务未受影响:在迁移期间,我们的月活跃用户增长速度加快了,并发布了包括“实时故事”在内的众多新功能。
  • 未来基础:早期采用者已经开始使用类型提示;我们在各个端点加速了 asyncio 的采用,将用户请求延迟减少了 30% 到 40%;我们也在与社区合作进行性能基准测试和优化。


总结

本节课中,我们一起学习了 Instagram 如何利用 Python 和 Django 支撑其海量业务,并深入探讨了他们从 Python 2 到 Python 3 的大规模迁移之旅。关键要点包括:选择成熟技术并专注于解决业务问题的重要性;通过渐进式、兼容性驱动的策略进行大规模技术升级的可行性;以及面对 Unicode、数据序列化、迭代器等具体挑战时的解决方案。最终,这次迁移为 Instagram 带来了显著的性能收益,提升了开发效率,并为未来的增长奠定了坚实的基础。这表明,让 Python 3 在生产环境中运行,不仅是可行的,而且是值得的。

posted @ 2026-03-29 09:24  布客飞龙II  阅读(2)  评论(0)    收藏  举报