<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>小白维基</title>
  
  <subtitle>语言，框架，架构，技术有关的一切都在这里。</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://wiki.blanc.site/"/>
  <updated>2024-11-10T15:39:52.936Z</updated>
  <id>https://wiki.blanc.site/</id>
  
  <author>
    <name>ryomahan</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>「翻译」使用 Llama-index 实现的 Agentic RAG-Router Query Engine</title>
    <link href="https://wiki.blanc.site/archives/ed42c0e4.html"/>
    <id>https://wiki.blanc.site/archives/ed42c0e4.html</id>
    <published>2024-06-14T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://medium.com/aimonks/agentic-rag-with-llama-index-router-query-engine-01-381e83a418af" target="_blank" rel="external nofollow noopener noreferrer">Agentic RAG With Llama-index | Router Query Engine #01</a></p><p>作者：Prince Krampah</p><p>翻译：<a href="https://github.com/ryomahan" target="_blank" rel="external nofollow noopener noreferrer">RyomaHan | 小白</a></p><p>提示：本文有对应的 <a href="https://www.youtube.com/watch?v=fnDYXE7BFto" target="_blank" rel="external nofollow noopener noreferrer">YouTube 视频</a>，本文中的我代指原作者</p></blockquote><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>你是否也厌倦了我在博文中经常提到的老式 RAG(Retrieval Augmented Generation | 检索增强生成) 系统？反正我是对此感到厌倦了。但我们可以做一些有趣的事情，让它更上一层楼。接下来就跟我一起将 agents 概念引入传统的 RAG 工作流，重新构建自己的 Agentic RAG 系统吧。</p><p>去年大模型领域最流行的关键词就是 RAG 了，而今年，一代新人换旧人，agents 已经取代 RAG 成为大模型领域的新宠。你大可不必为错过 RAG 的风口而沮丧，因为将 agents 引入 RAG 系统后会有更好的效果，所以现在开始学习也不晚。</p><p>在本文中，我会介绍<strong>如何使用 <a href="https://www.llamaindex.ai/" target="_blank" rel="external nofollow noopener noreferrer">Llama-index</a> 实现基本的 Agentic RAG 应用</strong>。我将在接下来的几周发布一系列（共四篇）有关 Agentic RAG 架构的文章，本文是这个系列的第一篇。</p><h2 id="RAG-基础工作流"><a href="#RAG-基础工作流" class="headerlink" title="RAG 基础工作流"></a>RAG 基础工作流</h2><p>在开始新篇章前，让我们快速回顾一下传统的 RAG 架构的组织形式和工作原理。这部分知识在后续过程也会用到，那些对 RAG 没有任何经验的初学者务必仔细学习。</p><p><img src="https://img.blanc.site/wiki/img/202406140456782.png" alt="传统的 RAG 架构的组织形式和工作原理"></p><p>在上面简单的 RAG 架构图中，我们至少需要理解以下概念：</p><ol><li>文档（Documents）：用于增强 LLM 理解特定领域的上下文。可以是 PDF 或者任何文本文档，对于多模态的 LLM 来说甚至可以是图片；</li><li>块（Chunks）/ 节点（nodes）：将较大的文档通过合适的算法分割成尺寸合适的块（或节点）；</li><li>特征映射（Embedings）：在将文档分割成块后，我们需要为每个块创建与之对应的特征映射（一般以向量的形式存储）。当系统收到来自用户的查询时，RAG 系统会通过相似性搜索找到与查询内容相关性最高的文档块。这些被检索出来的文档块会与用户查询内容一起发送给 LLM，检索出来的文档块会充当此次 LLM 调用的上下文。最终，LLM 会根据以上内容生成响应。</li></ol><p>以上就是典型的传统 RAG 系统的组织形式和工作原理。</p><h2 id="为什么要创建-Agentic-RAG"><a href="#为什么要创建-Agentic-RAG" class="headerlink" title="为什么要创建 Agentic RAG"></a>为什么要创建 Agentic RAG</h2><p>通过上一章节我们了解了传统 RAG 的实现，这种实现方案适用于少量文档的简单 QA 任务，不适合复杂的 QA 任务和对较大文档集的总结。</p><p>而这恰好是 agents 的强势领域，将 agents 与 RAG 结合能够将传统 RAG 系统提升到一个全新的水平。通过 Agentic RAG 系统，可以轻松地执行更复杂的任务，例如文档集摘要、复杂 QA 以及其他复杂任务。Agentic RAG 还能使 RAG 系统具有工具调用的能力，并且这些工具可以是自定义函数。</p><p>在本系列文章中我将讨论以下内容：</p><ol><li>路由式查询引擎（Router Query Engines）：这是最简单的 Agentic RAG 实现方式，它提供了添加逻辑声明的能力。这种能力可以帮助 LLM <strong>根据需要执行的任务以及提供的工具确定通过何种路径能够达到最终目的</strong>；</li><li>工具调用（Tool Calling）：在这篇文章中，我将介绍如何将自己定义的工具（方法）添加到 Agentic RAG 架构中。我会为 agents 实现一些接口，以便从我们提供的工具中选择合适的工具并通过 LLM 生成调用这些工具（这里我们默认工具是自定义的 Python 函数）所需要的参数；</li><li>具有多步推理能力的 Agentic RAG；</li><li>在文档集中具有多步推理能力的 Agentic RAG。</li></ol><h2 id="路由式查询引擎-Router-Query-Engine"><a href="#路由式查询引擎-Router-Query-Engine" class="headerlink" title="路由式查询引擎 | Router Query Engine"></a>路由式查询引擎 | Router Query Engine</h2><p>这是 Llama-index 中最简单的 Agentic RAG 实现方案。在这种方案中，我们只需要创建一个路由式查询引擎。<strong>它能够在 LLM 帮助下，（从提供的工具和查询引擎列表中）确定具体使用什么工具或查询引擎来解决用户查询的</strong>。</p><p>下图是本文要实现的路由式查询引擎的基本结构：</p><p><img src="https://img.blanc.site/wiki/img/202406140456803.png" alt="本文要实现的路由式查询引擎的基本结构"></p><h2 id="项目环境初始化"><a href="#项目环境初始化" class="headerlink" title="项目环境初始化"></a>项目环境初始化</h2><p>创建一个名为 <code>agentic_rag</code> 的目录作为本系列文章的项目目录，再在 <code>agentic_rag</code> 内部创建一个名为 <code>basics</code> 的目录作为本文代码实践的工作目录。创建完成后进入 <code>basics</code> 目录中进行环境初始化：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> /root/to/agentic_rag/basics</span></span><br><span class="line">poetry init</span><br></pre></td></tr></table></figure><p>在正式开始前，你需要先准备好你的 OpenAI API 密钥，如果你还没有密钥，可以从 <a href="https://platform.openai.com/api-keys" target="_blank" rel="external nofollow noopener noreferrer">此处</a> 获取。准备好密钥后，将其添加到你的 <code>.env</code> 文件中：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># &#x2F;root&#x2F;to&#x2F;agentic_rag&#x2F;basics&#x2F;.env</span><br><span class="line">OPENAI_API_KEY&#x3D;sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span><br></pre></td></tr></table></figure><p><strong>译者注：也可以使用其他平台的 LLM 接口，你可以在 <a href="https://docs.llamaindex.ai/en/latest/api_reference/llms/" target="_blank" rel="external nofollow noopener noreferrer">此链接</a> 下确认 Llama-index 是否支持你期望的平台。</strong></p><p><strong>译者注：如果没有能力购买平台提供的 LLM 能力，也可以使用 <a href="https://ollama.com/" target="_blank" rel="external nofollow noopener noreferrer">Ollama</a> 在本地运行指定模型，并参考 <a href="https://docs.llamaindex.ai/en/latest/api_reference/llms/ollama/" target="_blank" rel="external nofollow noopener noreferrer">文档</a> 在 Llama-index 中调用模型能力。</strong></p><p><strong>译者注：如果使用了不同平台的接口需要将后文中提到的 OpenAI 相关的接口替换成你所使用的平台的接口。</strong></p><p><img src="https://img.blanc.site/wiki/img/202406140456816.png" alt="代码截图-1"></p><p><img src="https://img.blanc.site/wiki/img/202406140456830.png" alt="代码截图-2"></p><h3 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h3><p>我们会在本项目中使用到 Llama-index 以及一些其他的依赖库，你可以通过如下命令进行安装：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> /root/to/agentic_rag/basics</span></span><br><span class="line">poetry add python-dotenv ipykernel llama-index nest_asyncio</span><br></pre></td></tr></table></figure><h3 id="下载数据集"><a href="#下载数据集" class="headerlink" title="下载数据集"></a>下载数据集</h3><p>我们需要一个 PDF 文件用于后续代码实践，你可以点击 <a href="https://medium.com/aimonks/agentic-rag-with-llama-index-router-query-engine-01-381e83a418af#:~:text=this%20PDF%20from-,here,-.%20Again%2C%20feel%20free" target="_blank" rel="external nofollow noopener noreferrer">此链接</a> 下载我所使用的 PDF。当然你也可以使用你手中的任何一个 PDF 文件（<strong>译者注：对于初学者来说最好是一个纯文本的 PDF 文件</strong>）。将它保存到 <code>/path/to/agentic_rag/basic/datasets</code> 目录下。</p><p><img src="https://img.blanc.site/wiki/img/202406140456843.png" alt="代码截图-3"></p><h2 id="加载文档并将其处理成块"><a href="#加载文档并将其处理成块" class="headerlink" title="加载文档并将其处理成块"></a>加载文档并将其处理成块</h2><p><strong>译者注：原文作者是使用 <code>.ipynb</code> 格式来编写并运行代码的，如果你不熟悉这个文件格式可以使用正常的 <code>.py</code> 文件。</strong></p><p>现在我们已经将代码运行的基础环境准备好了，然我们先通过 <code>python-dotenv</code> 库加载环境变量：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> dotenv</span><br><span class="line">%load_ext dotenv</span><br><span class="line">%dotenv</span><br></pre></td></tr></table></figure><p>随后我们需要引入 <code>nest_asyncio</code> 库，因为 Llama-index 在后台使用大量 asyncio 功能：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> nest_asyncio</span><br><span class="line">nest_asyncio.apply()</span><br></pre></td></tr></table></figure><p>现在，然我们加载准备好的 PDF 文档：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index.core <span class="keyword">import</span> SimpleDirectoryReader  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># load lora_paper.pdf documents  </span></span><br><span class="line">documents = SimpleDirectoryReader(input_files=[<span class="string">"./datasets/lora_paper.pdf"</span>]).load_data()</span><br></pre></td></tr></table></figure><h3 id="创建文档块"><a href="#创建文档块" class="headerlink" title="创建文档块"></a>创建文档块</h3><p>成功加载文档后，我们需要将其分解成合适大小的块：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index.core.node_parser <span class="keyword">import</span> SentenceSplitter  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># chunk_size of 1024 is a good default value  </span></span><br><span class="line">splitter = SentenceSplitter(chunk_size=<span class="number">1024</span>)  </span><br><span class="line"><span class="comment"># Create nodes from documents  </span></span><br><span class="line">nodes = splitter.get_nodes_from_documents(documents)</span><br></pre></td></tr></table></figure><p>可以使用以下方法获取有关每个块的详细信息：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">node_metadata = nodes[<span class="number">1</span>].get_content(metadata_mode=<span class="literal">True</span>)  </span><br><span class="line">print(node_metadata)</span><br></pre></td></tr></table></figure><p><img src="https://img.blanc.site/wiki/img/202406140456857.png" alt="代码截图-4"></p><h2 id="创建所需的-LLM-和-Embedding-模型对象"><a href="#创建所需的-LLM-和-Embedding-模型对象" class="headerlink" title="创建所需的 LLM 和 Embedding 模型对象"></a>创建所需的 LLM 和 Embedding 模型对象</h2><p><strong>译者注：这里你需要将用到的模型类替换为你所使用的平台所对应的类，有些类需要添加额外的第三方依赖，详情请查看 Llama-index 文档。</strong></p><p>在本次代码实践中，我将使用 OpenAI 的 <code>gpt-3.5-turbo</code> 模型作为 LLM 模型，使用 OpenAI 的 <code>text-embedding-ada-002</code> 模型作为 Embedding 模型。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index.core <span class="keyword">import</span> Settings  </span><br><span class="line"><span class="keyword">from</span> llama_index.llms.openai <span class="keyword">import</span> OpenAI  </span><br><span class="line"><span class="keyword">from</span> llama_index.embeddings.openai <span class="keyword">import</span> OpenAIEmbedding  </span><br><span class="line"></span><br><span class="line"><span class="comment"># LLM model  </span></span><br><span class="line">Settings.llm = OpenAI(model=<span class="string">"gpt-3.5-turbo"</span>)  </span><br><span class="line"><span class="comment"># embedding model  </span></span><br><span class="line">Settings.embed_model = OpenAIEmbedding(model=<span class="string">"text-embedding-ada-002"</span>)</span><br></pre></td></tr></table></figure><h3 id="创建索引"><a href="#创建索引" class="headerlink" title="创建索引"></a>创建索引</h3><p>正如前文基本结构图片所示，在本次代码实践中，我们将使用两个主要的索引：</p><ol><li>摘要索引（Summary Index）：根据 Llama-index <a href="https://docs.llamaindex.ai/en/stable/api_reference/indices/summary/" target="_blank" rel="external nofollow noopener noreferrer">对应文档</a> 所示，摘要索引是一种简单的数据结构，其中节点按顺序存储。在索引构建过程中，文档文本被分块、转换为节点并存储在列表中。在查询期间，摘要索引使用一些可选的过滤器参数迭代节点，并综合所有节点的答案。</li><li>向量索引（Vector Index）：一个通过 Embedding 创建的常规索引存储，可以执行相似性搜索，以获得与搜索条件最相似的 <code>n</code> 个的索引。</li></ol><p>可以使用下面的代码来创建这两个索引：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index.core <span class="keyword">import</span> SummaryIndex, VectorStoreIndex  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># summary index  </span></span><br><span class="line">summary_index = SummaryIndex(nodes)  </span><br><span class="line"><span class="comment"># vector store index  </span></span><br><span class="line">vector_index = VectorStoreIndex(nodes)</span><br></pre></td></tr></table></figure><h2 id="将向量索引转换为查询引擎"><a href="#将向量索引转换为查询引擎" class="headerlink" title="将向量索引转换为查询引擎"></a>将向量索引转换为查询引擎</h2><p>在创建并存储向量索引后，我们需要继续创建查询引擎，后续会将它们转换为 agnets 使用的工具（又名查询工具）。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># summary query engine  </span></span><br><span class="line">summary_query_engine = summary_index.as_query_engine(  </span><br><span class="line">response_mode=<span class="string">"tree_summarize"</span>,  </span><br><span class="line">use_async=<span class="literal">True</span>,  </span><br><span class="line">)  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># vector query engine  </span></span><br><span class="line">vector_query_engine = vector_index.as_query_engine()</span><br></pre></td></tr></table></figure><p>在上面的例子中，我们创建了两个不同的查询引擎。后续我们会将它们都挂载到路由式查询引擎下，然后路由式查询引擎会根据用户的查询内容决定具体使用哪个查询引擎。</p><p><img src="https://img.blanc.site/wiki/img/202406140456872.png" alt="路由式查询引擎功能示意"></p><p>在上面的代码中，我们指定了 <code>use_async</code> 参数以加快查询速度，这是我们必须使用 <code>nest_asyncio</code> 库的原因之一。</p><p><strong>译者注：我刚去看 nest_asyncio 的代码库，发现被 archive 了，我还以为是 Python 官方支持了，后面在阅读 <a href="https://github.com/python/cpython/issues/66435#issuecomment-2003904906" target="_blank" rel="external nofollow noopener noreferrer">相关 issue</a> 的时候才发现原来是代码库的作者年初去世了，RIP.</strong></p><h2 id="创建查询工具"><a href="#创建查询工具" class="headerlink" title="创建查询工具"></a>创建查询工具</h2><p>查询工具是一个带有元数据（例如存储当前查询工具可以用来做什么）的查询引擎。这有助于路由式查询引擎能够根据传入的用户查询来决定具体使用哪个查询引擎。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index.core.tools <span class="keyword">import</span> QueryEngineTool  </span><br><span class="line">  </span><br><span class="line">  </span><br><span class="line">summary_tool = QueryEngineTool.from_defaults(  </span><br><span class="line">query_engine=summary_query_engine,  </span><br><span class="line">description=(  </span><br><span class="line"><span class="string">"Useful for summarization questions related to the Lora paper."</span>  </span><br><span class="line">),  </span><br><span class="line">)  </span><br><span class="line">  </span><br><span class="line">vector_tool = QueryEngineTool.from_defaults(  </span><br><span class="line">query_engine=vector_query_engine,  </span><br><span class="line">description=(  </span><br><span class="line"><span class="string">"Useful for retrieving specific context from the the Lora paper."</span>  </span><br><span class="line">),</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h2 id="创建路由式查询引擎"><a href="#创建路由式查询引擎" class="headerlink" title="创建路由式查询引擎"></a>创建路由式查询引擎</h2><p>最后，我们需要创建最终用于查询的路由式查询引擎。它能够帮我们聚合上文所创建的所有查询工具，即 <code>summary_tool</code> 和 <code>vector_tool</code>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index.core.query_engine.router_query_engine <span class="keyword">import</span> RouterQueryEngine  </span><br><span class="line"><span class="keyword">from</span> llama_index.core.selectors <span class="keyword">import</span> LLMSingleSelector  </span><br><span class="line">  </span><br><span class="line">  </span><br><span class="line">query_engine = RouterQueryEngine(  </span><br><span class="line">selector=LLMSingleSelector.from_defaults(),  </span><br><span class="line">query_engine_tools=[  </span><br><span class="line">summary_tool,  </span><br><span class="line">vector_tool,  </span><br><span class="line">],  </span><br><span class="line">verbose=<span class="literal">True</span>  </span><br><span class="line">)</span><br></pre></td></tr></table></figure><p><strong>LLMSingleSelector</strong>：是一个使用 LLM 从选项列表中选择出单个选项的选择器。你可以在 <a href="https://docs.llamaindex.ai/en/stable/module_guides/querying/router/#defining-a-selector" target="_blank" rel="external nofollow noopener noreferrer">这个链接</a> 中查看更多信息。</p><h3 id="测试路由式查询引擎"><a href="#测试路由式查询引擎" class="headerlink" title="测试路由式查询引擎"></a>测试路由式查询引擎</h3><p><strong>译者注：在执行如下代码是记得把问题换成与你使用的 PDF 相关的问题。</strong></p><p>可以通过如下代码测试我们创建的路由式查询引擎：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">response = query_engine.query(<span class="string">"What is the summary of the document?"</span>)  </span><br><span class="line">print(str(response))</span><br></pre></td></tr></table></figure><p><img src="https://img.blanc.site/wiki/img/202406140456902.png" alt="代码截图-5"></p><p>以上是论文的摘要，总结了我们传递给查询引擎的 Lora 论文的所有上下文。</p><p>由于我们使用的摘要索引是将所有块存储在顺序列表中，因此在生成摘要时会访问所有块并从中生成一个总摘要，然后再以此生成最终摘要。</p><p>可以通过检查响应中 <code>source_nodes</code> 列表的长度来确认这一点，<code>source_nodes</code> 属性是这次响应中用到的所有块的列表。</p><p><img src="https://img.blanc.site/wiki/img/202406140456926.png" alt="代码截图-6"></p><p>可以看到最终的结果 38 与我们前面创建的块的数量相同，这意味着所有的块都用于本次摘要生成。</p><p>再提问一个不涉及总结的问题：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">response = query_engine.query(<span class="string">"What is the long from of Lora?"</span>)  </span><br><span class="line">print(str(response))</span><br></pre></td></tr></table></figure><p><img src="https://img.blanc.site/wiki/img/202406140456941.png" alt="代码截图-7"></p><p>这次回答使用了向量索引，尽管响应内容不是很准确。</p><h2 id="将代码合并"><a href="#将代码合并" class="headerlink" title="将代码合并"></a>将代码合并</h2><p>现在你已经大致理解了这套 Agentic RAG 工作流，让我们将其抽象成函数方便后续调用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index.core.query_engine.router_query_engine <span class="keyword">import</span> RouterQueryEngine  </span><br><span class="line"><span class="keyword">from</span> llama_index.core.selectors <span class="keyword">import</span> LLMSingleSelector  </span><br><span class="line"><span class="keyword">from</span> llama_index.core.tools <span class="keyword">import</span> QueryEngineTool  </span><br><span class="line"><span class="keyword">from</span> llama_index.core <span class="keyword">import</span> SummaryIndex, VectorStoreIndex  </span><br><span class="line"><span class="keyword">from</span> llama_index.core <span class="keyword">import</span> Settings  </span><br><span class="line"><span class="keyword">from</span> llama_index.llms.openai <span class="keyword">import</span> OpenAI  </span><br><span class="line"><span class="keyword">from</span> llama_index.embeddings.openai <span class="keyword">import</span> OpenAIEmbedding  </span><br><span class="line"><span class="keyword">from</span> llama_index.core.node_parser <span class="keyword">import</span> SentenceSplitter  </span><br><span class="line"><span class="keyword">from</span> llama_index.core <span class="keyword">import</span> SimpleDirectoryReader</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">create_router_query_engine</span><span class="params">(  </span></span></span><br><span class="line"><span class="function"><span class="params">document_fp: str,  </span></span></span><br><span class="line"><span class="function"><span class="params">verbose: bool = True,  </span></span></span><br><span class="line"><span class="function"><span class="params">)</span> -&gt; RouterQueryEngine:</span>  </span><br><span class="line"><span class="comment"># load lora_paper.pdf documents  </span></span><br><span class="line">documents = SimpleDirectoryReader(input_files=[document_fp]).load_data()  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># chunk_size of 1024 is a good default value  </span></span><br><span class="line">splitter = SentenceSplitter(chunk_size=<span class="number">1024</span>)  </span><br><span class="line"><span class="comment"># Create nodes from documents  </span></span><br><span class="line">nodes = splitter.get_nodes_from_documents(documents)  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># LLM model  </span></span><br><span class="line">Settings.llm = OpenAI(model=<span class="string">"gpt-3.5-turbo"</span>)  </span><br><span class="line"><span class="comment"># embedding model  </span></span><br><span class="line">Settings.embed_model = OpenAIEmbedding(model=<span class="string">"text-embedding-ada-002"</span>)  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># summary index  </span></span><br><span class="line">summary_index = SummaryIndex(nodes)  </span><br><span class="line"><span class="comment"># vector store index  </span></span><br><span class="line">vector_index = VectorStoreIndex(nodes)  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># summary query engine  </span></span><br><span class="line">summary_query_engine = summary_index.as_query_engine(  </span><br><span class="line">response_mode=<span class="string">"tree_summarize"</span>,  </span><br><span class="line">use_async=<span class="literal">True</span>,  </span><br><span class="line">)  </span><br><span class="line">  </span><br><span class="line"><span class="comment"># vector query engine  </span></span><br><span class="line">vector_query_engine = vector_index.as_query_engine()  </span><br><span class="line">  </span><br><span class="line">summary_tool = QueryEngineTool.from_defaults(  </span><br><span class="line">query_engine=summary_query_engine,  </span><br><span class="line">description=(  </span><br><span class="line"><span class="string">"Useful for summarization questions related to the Lora paper."</span>  </span><br><span class="line">),  </span><br><span class="line">)  </span><br><span class="line">  </span><br><span class="line">vector_tool = QueryEngineTool.from_defaults(  </span><br><span class="line">query_engine=vector_query_engine,  </span><br><span class="line">description=(  </span><br><span class="line"><span class="string">"Useful for retrieving specific context from the the Lora paper."</span>  </span><br><span class="line">),  </span><br><span class="line">)  </span><br><span class="line">  </span><br><span class="line">  </span><br><span class="line">query_engine = RouterQueryEngine(  </span><br><span class="line">selector=LLMSingleSelector.from_defaults(),  </span><br><span class="line">query_engine_tools=[  </span><br><span class="line">summary_tool,  </span><br><span class="line">vector_tool,  </span><br><span class="line">],  </span><br><span class="line">verbose=verbose  </span><br><span class="line">)  </span><br><span class="line">  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">return</span> query_engine</span><br></pre></td></tr></table></figure><p>然后我们就可以通过如下方法便捷的调用次函数：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">query_engine = <span class="keyword">await</span> create_router_query_engine(<span class="string">"./datasets/lora_paper.pdf"</span>)  </span><br><span class="line">response = query_engine.query(<span class="string">"What is the summary of the document?"</span>)  </span><br><span class="line">print(str(response))</span><br></pre></td></tr></table></figure><p><img src="https://img.blanc.site/wiki/img/202406140456956.png" alt="代码截图-7"></p><p>我们可以在当前目录下创建一个 <code>utils.py</code> 文件，用于存放刚刚抽象出来的函数：</p><p><img src="https://img.blanc.site/wiki/img/202406140456972.png" alt="代码截图-8"></p><p>之后我们就可以通过如下代码便捷的调用此函数：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> utils <span class="keyword">import</span> create_router_query_engine  </span><br><span class="line">  </span><br><span class="line">query_engine = <span class="keyword">await</span> create_router_query_engine(<span class="string">"./datasets/lora_paper.pdf"</span>)  </span><br><span class="line">response = query_engine.query(<span class="string">"What is the summary of the document?"</span>)  </span><br><span class="line">print(str(response))</span><br></pre></td></tr></table></figure><p><img src="https://img.blanc.site/wiki/img/202406140456988.png" alt="代码截图-9"></p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>恭喜你走到这一步。以上就是本文的全部内容了，在下一篇文章中，我将介绍如何使用工具调用（也称函数调用）来进一步加强我们的 RAG 系统。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&quot;https://medium.com/aimonks/agentic-rag-with-llama-index-router-query-engine-01-381e83a418af&quot; target=&quot;_blank&quot; rel
      
    
    </summary>
    
    
      <category term="1-网络笔记" scheme="https://wiki.blanc.site/categories/1-%E7%BD%91%E7%BB%9C%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="翻译" scheme="https://wiki.blanc.site/tags/%E7%BF%BB%E8%AF%91/"/>
    
  </entry>
  
  <entry>
    <title>WeeklyPEP-8-PEP 492-使用 async 和 await 语法的协程-overview</title>
    <link href="https://wiki.blanc.site/archives/7eaf0f5f.html"/>
    <id>https://wiki.blanc.site/archives/7eaf0f5f.html</id>
    <published>2024-03-06T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文的主体内容大部分来自对 <a href="https://peps.python.org/pep-0492/" target="_blank" rel="external nofollow noopener noreferrer">PEP 492</a> 原文的翻译，剩余部分是本人对原文的理解，在整理过程中我没有刻意地区分二者，这两部分被糅杂在一起形成了本文。因此请<strong>不要带着</strong>「本文的内容是百分百正确」的想法阅读。如果文中的某些内容让你产生疑惑，你可以给我留言与我讨论或者对比 <a href="https://peps.python.org/pep-0492/" target="_blank" rel="external nofollow noopener noreferrer">PEP 492</a> 的原文加以确认。</p><p><strong>注：<a href="https://peps.python.org/pep-0492/" target="_blank" rel="external nofollow noopener noreferrer">PEP 492</a> 创建于 2015-04-09，Python 3.5</strong><br><strong>注：文中的「当前版本」指的是本提案生效之前的版本</strong><br><strong>注：本文过长，虽然已经自我校对过一次，但还是难免存在错别字或语句不通顺的地方，如果您发现了问题欢迎留言给我</strong></p><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>网络请求爆发性地增长引发了对低延时、可拓展代码的相关需求。本提案旨在让<strong>显式地编写异步、并发 Python 代码更容易、更 Pythoinc</strong>，并以此满足前述需求。</p><p>提案建议使协程成为 Python 中完全独立的新概念，并引入新的支持语法。最终的目的是<strong>在 Python 中建立一个简洁通用的异步编程心智模型，并使它尽可能接近同步编程</strong>。</p><p>在本提案中，<strong>假设</strong>异步任务都使用类似内置模块 <code>asyncio.events.AbstractEventLoop</code> 中的事件循环进行编排和协调。但是，本提案与任何特定的事件循环实现<strong>无关</strong>，只与使用 <code>yield</code> 作为调度信号的协程相关，也就是说协程会在事件（例如 IO）完成前保持等待。</p><p>我们相信，本提案能够让 Python 在快速增长的异步编程领域中继续保持竞争力，因为很多其他语言已经或计划采用近似的特性：<a href="http://wiki.ecmascript.org/doku.php?id=strawman:async_functions" target="_blank" rel="external nofollow noopener noreferrer">2</a>，<a href="https://msdn.microsoft.com/en-us/library/hh191443.aspx" target="_blank" rel="external nofollow noopener noreferrer">5</a>，<a href="https://www.dartlang.org/articles/await-async/" target="_blank" rel="external nofollow noopener noreferrer">7</a>，<a href="http://docs.scala-lang.org/sips/pending/async.html" target="_blank" rel="external nofollow noopener noreferrer">8</a>，<a href="http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3722.pdf" target="_blank" rel="external nofollow noopener noreferrer">10</a>。</p><h2 id="接口设计与实施修订"><a href="#接口设计与实施修订" class="headerlink" title="接口设计与实施修订"></a>接口设计与实施修订</h2><p>注：<strong>这部分是修订内容，可以放在最后阅读。</strong></p><ol><li>以初始 Python 3.5 beta 版本的反馈为依据，重构本提案设置的对象模型。这次重构的目的是<strong>更明确地将原生协程与生成器分离</strong>，而不是将原生协程作为一种新的生成器，<strong>原生协程要设计成完全独立的类型</strong>（具体实施在 <a href="https://hg.python.org/cpython/rev/7a0a1a4ac639" target="_blank" rel="external nofollow noopener noreferrer">引用 17</a>）。这么做的主要原因是在尝试为 <a href="https://www.tornadoweb.org/en/stable/" target="_blank" rel="external nofollow noopener noreferrer">Tornado Web Server</a> 集成原生协程时遇到了问题（记录在 <a href="http://bugs.python.org/issue24400" target="_blank" rel="external nofollow noopener noreferrer">引用 18</a>）。</li><li>CPython 3.5.2 更新了 <code>__aiter__</code> 协议。在 3.5.2 之前，<code>__aiter__</code> 返回一个可以被解析成 <a href="#异步迭代器">异步迭代器</a> 的 <a href="https://docs.python.org/zh-cn/3/glossary.html#term-awaitable" target="_blank" rel="external nofollow noopener noreferrer">可等待对象（awaitable）</a>。从 3.5.2 开始，<code>__aiter__</code> <strong>直接返回异步迭代器</strong>。如果在 3.5.2 中使用旧协议，会抛出一个 <code>PendingDeprecatioWarning</code> 异常。而在 3.6 中，旧的 <code>__aiter__</code> 协议仍旧会被支持，但会抛出一个 <code>DeprecationWarning</code> 异常。最终，在 3.7 中，旧的 <code>__aiter__</code> 协议将被废弃，如果 <code>__aiter__</code> 返回的不是异步迭代器，则会引发 <code>RuntimeError</code>。可以通过 <a href="http://bugs.python.org/issue27243" target="_blank" rel="external nofollow noopener noreferrer">引用 19</a> 和 <a href="https://docs.python.org/3/reference/datamodel.html#async-iterators" target="_blank" rel="external nofollow noopener noreferrer">引用 20</a> 来获取更多细节。</li></ol><h2 id="理由和目标"><a href="#理由和目标" class="headerlink" title="理由和目标"></a>理由和目标</h2><p>当前版本的 Python 支持通过生成器来实现协程（<a href="https://peps.python.org/pep-0342/" target="_blank" rel="external nofollow noopener noreferrer">PEP 342</a>），<a href="https://peps.python.org/pep-0380/" target="_blank" rel="external nofollow noopener noreferrer">PEP 380</a> 中引入的 <code>yield from</code> 语法进一步增强了这一特性。但是这种方案有很多缺点：</p><ol><li>生成器实现的协程和正常生成器的语法相同，因此很<strong>容易被混淆</strong>，对于新用户来说尤其如此；</li><li>一个函数是否是协程取决于函数体中是否存在 <code>yield</code> 或 <code>yield from</code> 语句。在重构这些函数时，如果删除或新增了 <code>yield</code> 相关语句就可能会导致一些不明显的错误；</li><li>只能在 <code>yield</code> 语法支持的地方进行异步调用，无法异步调用类似 with 或 for 这样的语句，限制了可用性。</li></ol><p>本提案使协程成为 Python 语言的一种原生特性，并且清晰地将其与生成器区分开。这样做不仅<strong>消除了生成器与协程之间的歧义</strong>，还<strong>可以不依赖特定库直接定义协程</strong>。同时也提升了 linters 或 IDE 静态代码分析和重构的能力。</p><p>原生协程以及相关新语法使得在异步操作中定义上下文管理器和可迭代协议成为可能。稍后会在提案中提及：新的 <code>async with</code> 语句允许 Python 程序在进入或退出上下文上时执行异步调用，而新的 <code>async for</code> 语句可以在迭代器中执行异步调用。</p><h2 id="规范"><a href="#规范" class="headerlink" title="规范"></a>规范</h2><p>规范章节引入了新的语法和语义，以增强 Python 对协程的支持。</p><p>本规范假定阅读者已经了解此前 Python 中协程的实现（ <a href="https://peps.python.org/pep-0342/" target="_blank" rel="external nofollow noopener noreferrer">PEP 342</a> 和 <a href="https://peps.python.org/pep-0380/" target="_blank" rel="external nofollow noopener noreferrer">PEP 380</a>）。本规范涉及的语法修改动机来自 asyncio 模块提案（<a href="https://peps.python.org/pep-3156/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3156</a>）和 Cofunctions 提案（<a href="https://peps.python.org/pep-3152/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3152</a>，现已被本规范否决）。</p><p>在后文中，将使用「原生协程」来指代使用新语法声明的协程，使用「生成器式协程」指代基于生成器语法的协程。</p><h3 id="原生协程声明语法"><a href="#原生协程声明语法" class="headerlink" title="原生协程声明语法"></a>原生协程声明语法</h3><p>原生协程声明语法如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">read_data</span><span class="params">(db)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>它的主要特性有：</p><ol><li>使用 <code>async def</code> 声明的函数一定是协程，即使内部不包含 <code>await</code>；</li><li><strong>在 <code>async</code> 函数中使用 <code>yield</code> 或 <code>yield from</code> 会引发 <code>SyntaxError</code> 异常；</strong></li><li>在内部，引入了两个新的 <a href="https://docs.python.org/3/library/inspect.html#code-objects-bit-flags" target="_blank" rel="external nofollow noopener noreferrer">code object flags</a>：<ol><li><code>CO_COROUTINE</code>：用于标记原生协程；</li><li><code>CO_ITERABLE_COROUTINE</code>：使生成器式协程与原生协程兼容（由 <a href="https://docs.python.org/zh-cn/3/library/types.html#types.coroutine" target="_blank" rel="external nofollow noopener noreferrer">types.coroutine</a> 函数设置）。</li></ol></li><li>常规生成器返回一个生成器对象，类似的，<strong>协程返回一个协程对象</strong>；</li><li>在协程中 <code>StopIteration</code> 会被 <code>RuntimeError</code> 代替，对于常规生成器来说，这种行为会在后续过程中支持（详情请看 <a href="https://peps.python.org/pep-0479/" target="_blank" rel="external nofollow noopener noreferrer">PEP 479</a>）；</li><li>如果不使用 <code>await</code> 直接调用原生协程，当它被垃圾回收时会抛出一个 <code>RuntimeWarning</code>（点击 <a href="#用于调试的特性">用于调试的特性</a> 了解更多）；</li><li>更多特性请看：<a href="#协程对象">协程对象</a> 章节。</li></ol><h3 id="types-coroutine"><a href="#types-coroutine" class="headerlink" title="types.coroutine()"></a>types.coroutine()</h3><p><code>types</code> 模块中新增了一个名为 <code>coroutine(fn)</code> 的函数。它能够帮助「asyncio 中现有的生成器式协程」与「本提案引入的原生协程」实现相互兼容：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@types.coroutine</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">process_data</span><span class="params">(db)</span>:</span></span><br><span class="line">data = <span class="keyword">yield</span> <span class="keyword">from</span> read_data(db)</span><br><span class="line">...</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># CPython 3.10 Lib.types.py</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">coroutine</span><span class="params">(func)</span>:</span>  </span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> callable(func):  </span><br><span class="line">        <span class="keyword">raise</span> TypeError(<span class="string">'types.coroutine() expects a callable'</span>)  </span><br><span class="line">  </span><br><span class="line">    <span class="keyword">if</span> (</span><br><span class="line">    func.__class__ <span class="keyword">is</span> FunctionType <span class="keyword">and</span> </span><br><span class="line">    getattr(func, <span class="string">'__code__'</span>, <span class="literal">None</span>).__class__ <span class="keyword">is</span> CodeType</span><br><span class="line">):  </span><br><span class="line">        co_flags = func.__code__.co_flags  </span><br><span class="line"></span><br><span class="line"><span class="comment"># 0x20 == CO_GENERATOR 生成器标识</span></span><br><span class="line"><span class="comment"># 0x180 == CO_COROUTINE | CO_ITERABLE_COROUTINE 原生协程标识 or 生成式协程标识</span></span><br><span class="line"><span class="comment"># 0x100 == CO_ITERABLE_COROUTINE 生成式协程标识</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果确定传入函数是一个原生协程则直接返回</span></span><br><span class="line">        <span class="keyword">if</span> co_flags &amp; <span class="number">0x180</span>:  </span><br><span class="line">            <span class="keyword">return</span> func  </span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果传入函数是一个生成器函数，</span></span><br><span class="line"><span class="comment"># 则将 CO_ITERABLE_COROUTINE 标记附加在此函数上，随后返回</span></span><br><span class="line">        <span class="keyword">if</span> co_flags &amp; <span class="number">0x20</span>:</span><br><span class="line">            co = func.__code__</span><br><span class="line">            func.__code__ = co.replace(co_flags=co.co_flags | <span class="number">0x100</span>)</span><br><span class="line">            <span class="keyword">return</span> func  </span><br><span class="line"></span><br><span class="line"><span class="comment"># 下面的代码主要用于兼容「返回类似生成器对象」的函数</span></span><br><span class="line"><span class="comment"># 例如使用 Cython 编译的生成器</span></span><br><span class="line"><span class="keyword">import</span> functools</span><br><span class="line">    <span class="keyword">import</span> _collections_abc</span><br><span class="line"><span class="meta">    @functools.wraps(func)</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">wrapped</span><span class="params">(*args, **kwargs)</span>:</span>  </span><br><span class="line">        coro = func(*args, **kwargs)  </span><br><span class="line">        <span class="keyword">if</span> (coro.__class__ <span class="keyword">is</span> CoroutineType <span class="keyword">or</span>  </span><br><span class="line">            coro.__class__ <span class="keyword">is</span> GeneratorType <span class="keyword">and</span> coro.gi_code.co_flags &amp; <span class="number">0x100</span>):  </span><br><span class="line">            <span class="keyword">return</span> coro  </span><br><span class="line">        <span class="keyword">if</span> (isinstance(coro, _collections_abc.Generator) <span class="keyword">and</span>  </span><br><span class="line">            <span class="keyword">not</span> isinstance(coro, _collections_abc.Coroutine)):  </span><br><span class="line">            <span class="keyword">return</span> _GeneratorWrapper(coro)  </span><br><span class="line">        <span class="keyword">return</span> coro  </span><br><span class="line">  </span><br><span class="line">    <span class="keyword">return</span> wrapped</span><br></pre></td></tr></table></figure><p>如果 <code>fn</code> 是生成器函数， <code>types.coroutine()</code> 会在它的 code object 中添加 <code>CO_ITERABLE_COROUTINE</code> 标志，使其返回一个协程对象。<br>如果 <code>fn</code> 不是生成器函数，<code>types.coroutine()</code> 会对齐进行包装。如果 <code>fn</code> 返回一个生成器函数，返回的函数会被 <code>_GeneratorWrapper</code> 包装。</p><p><code>type.coroutine()</code> 不会为生成器函数附加 <code>CO_COROUTINE</code> 标志，以便区分「原生协程」和「生成器式协程」。</p><h3 id="await-表达式"><a href="#await-表达式" class="headerlink" title="await 表达式"></a>await 表达式</h3><p><code>await</code> 表达式用来获取一个协程执行的结果：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">read_data</span><span class="params">(db)</span>:</span></span><br><span class="line">data = <span class="keyword">await</span> db.fetch(<span class="string">"SELECT ..."</span>)</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p><code>await</code> 与 <code>yield from</code> 近似，会暂停 read_data 函数的执行直到可等待对象 <code>db.fetch</code> 完成并返回结果。<code>await</code> 使用 <code>yield from</code> 实现，但是多了一个验证参数的步骤。<code>await</code> 后只能跟一个 <a href="https://docs.python.org/zh-cn/3/glossary.html#term-awaitable" target="_blank" rel="external nofollow noopener noreferrer">可等待对象（awaitable）</a>，可以是以下选项之一：</p><ol><li>原生协程函数返回的原生协程对象；</li><li>被 <code>types.coroutine()</code> 装饰的函数中返回的生成式协程对象；</li><li>一个拥有 <code>__await__</code> 方法的对象，且该方法需要返回一个迭代器；</li><li>使用 CPython C API 定义的带有 <code>tp_as_async.am_await</code> 函数的对象，该函数返回一个迭代器（类似 <code>__await__</code> 方法）。</li></ol><p>关于第三点一些延伸内容：任何 <code>yield from</code> 调用链都会以 <code>yield</code> 收尾，这是 <a href="https://docs.python.org/3/library/asyncio-future.html" target="_blank" rel="external nofollow noopener noreferrer">Futures</a> 执行的必要条件。由于<strong>协程本质上是一个特殊的生成器</strong>，因此每个 <code>await</code> 都会被 <code>await</code> 调用链上的某个 <code>yield</code> 挂起（详情请参考 <a href="https://peps.python.org/pep-3156/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3156</a>）。为了在协程上实现这种行为，一个名为 <code>__await__</code> 的新魔术方法被添加进来。例如，在 asyncio 中， 要想在 <code>await</code> 语句中使用 Future，唯一要做的就是在 <code>asyncio.Future</code> 类中添加 <code>__await__ = __iter__</code>。后续章节中，称<strong>带有 <code>__await__</code> 方法的对象为类 Future 对象</strong>。如果 <code>__await__</code> 返回迭代器之外的东西，会抛出 <code>TypeError</code> 异常。</p><p>在原生协程外部使用 <code>await</code> 会抛出 <code>SyntaxError</code> 异常（就像在一般函数外调用 <code>yield</code> 一样）。</p><p>不在 <code>await</code> 关键字后使用可等待对象会抛出 <code>TypeError</code> 异常。</p><h4 id="更新运算符优先级"><a href="#更新运算符优先级" class="headerlink" title="更新运算符优先级"></a>更新运算符优先级</h4><p><code>await</code> 关键字被定义为：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">power ::= <span class="keyword">await</span> [<span class="string">"**"</span> u_expr]</span><br><span class="line"><span class="keyword">await</span> ::= [<span class="string">"await"</span>] primary</span><br></pre></td></tr></table></figure><p>其中「primary」代表语言中最主要的操作，其语法为：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">primary ::= atom | attributerf | subscription | slicing | call</span><br></pre></td></tr></table></figure><p>如需理解上述表达式含义可参考 <a href="https://docs.python.org/3/reference/expressions.html#primaries" target="_blank" rel="external nofollow noopener noreferrer">引用 12</a> 和 <a href="#语法更新">语法更新</a>。</p><p>与 <code>yield</code> 和 <code>yield from</code> 不同，大多数情况下 <code>await</code> 表达式不需要被圆括号包裹。此外，<code>yield from</code> 允许将<strong>任何表达式</strong>作为参数，甚至可以 <code>yield from a() + b()</code>，它会被解析为 <code>yield from (a() + b())</code>。这看起来很反常识就像一个 BUG 一样，因为一般来说，算数操作不会产生可等待对象。为了避免此类问题在 <code>await</code> 表达式中再次出现，<strong><code>await</code> 的优先级被设定为低于 <code>[]</code>, <code>()</code>, <code>.</code> 但高于 `</strong>`**。</p><p>具体优先级如下（<strong>从上到下，从低到高</strong>）：</p><table><thead><tr><th>Operator</th><th>Description</th></tr></thead><tbody><tr><td><code>yield x, yield from x</code></td><td>Yield 表达式</td></tr><tr><td><code>lambda</code></td><td>Lambda 表达式</td></tr><tr><td><code>if - else</code></td><td>条件语句</td></tr><tr><td><code>or</code></td><td>布尔 或</td></tr><tr><td><code>and</code></td><td>布尔 且</td></tr><tr><td><code>not x</code></td><td>布尔 非</td></tr><tr><td><code>in, not in, is, is not, &lt;, &lt;=, &gt;, &gt;=, !=, ==</code></td><td>比较，包括成员测试和身份测试</td></tr><tr><td><code>|</code></td><td>位运算 或</td></tr><tr><td><code>^</code></td><td>位运算 异或</td></tr><tr><td><code>&amp;</code></td><td>位运算 且</td></tr><tr><td><code>&lt;&lt;, &gt;&gt;</code></td><td>位运算 左移和右移</td></tr><tr><td><code>+, -</code></td><td>加减运算</td></tr><tr><td><code>*, @, /, //, %</code></td><td>乘、矩阵乘、除、地板除、取余</td></tr><tr><td><code>+x, -x, ~x</code></td><td>正、负、按位取反</td></tr><tr><td><code>**</code></td><td>幂运算</td></tr><tr><td><code>await x</code></td><td>await 表达式</td></tr><tr><td><code>x[index], x[index:index], x(args...), x.attribute</code></td><td>索引、切片、调用、属性</td></tr><tr><td><code>(expressions...), [expressions...], {key: value...}, {expressions...}</code></td><td>元组生成器、列表生成器、字典生成器、集合生成器</td></tr></tbody></table><h4 id="使用-await-关键字的示例"><a href="#使用-await-关键字的示例" class="headerlink" title="使用 await 关键字的示例"></a>使用 await 关键字的示例</h4><p><strong>有效调用：</strong></p><table><thead><tr><th>Expression</th><th>Will be parsed as</th></tr></thead><tbody><tr><td><code>if await fut: pass</code></td><td><code>if (await fut): pass</code></td></tr><tr><td><code>if await fut + 1: poass</code></td><td><code>if (await fut) + 1: pass</code></td></tr><tr><td><code>pair = await fut, &#39;spam&#39;</code></td><td><code>pair = (await fut), &#39;spam&#39;</code></td></tr><tr><td><code>with await fut, open(): pass</code></td><td><code>with (await fut), open(): pass</code></td></tr><tr><td><code>await foo()[&#39;spam&#39;].baz()()</code></td><td><code>await ( foo()[&#39;spam&#39;].baz()() )</code></td></tr><tr><td><code>return await coro()</code></td><td><code>return ( await coro() )</code></td></tr><tr><td><code>res = await coro() ** 2</code></td><td><code>res = (await coro()) ** 2</code></td></tr><tr><td><code>func(a1=await coro(), a2=0)</code></td><td><code>func(a1=(await coro()), a2=0)</code></td></tr><tr><td><code>await foo() + await bar()</code></td><td><code>(await foo()) + (await bar())</code></td></tr><tr><td><code>-await foo()</code></td><td><code>-(await foo())</code></td></tr></tbody></table><p><strong>无效调用：</strong></p><table><thead><tr><th>Expression</th><th>Shoud be written as</th></tr></thead><tbody><tr><td><code>await await coro()</code></td><td><code>await (await coro())</code></td></tr><tr><td><code>await -coro()</code></td><td><code>await (-coro())</code></td></tr></tbody></table><h3 id="异步上下文管理器与-async-with"><a href="#异步上下文管理器与-async-with" class="headerlink" title="异步上下文管理器与 async with"></a>异步上下文管理器与 async with</h3><p><strong>注：关于上下文管理器的内容可以参考：<a href="https://wiki.blanc.site/archives/5823d5df.html">WeeklyPEP-2-PEP343-with 语句-overview</a></strong></p><p>所谓异步上下文管理器，是一种能够在进入或退出上下文时调用异步代码的上下文管理器。为了实现它，本规范单独为异步上下文提出了一个新协议，此协议由两个新的魔术方法组成：<code>__aenter__</code> 和 <code>__aexit__</code>。<strong>它们都必须返回一个可等待对象</strong>。</p><p>异步上下文管理器的示例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AsyncContextManager</span>:</span></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">__aenter__</span><span class="params">(self)</span>:</span></span><br><span class="line"><span class="keyword">await</span> log(<span class="string">"entering context"</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">__aexit__</span><span class="params">(self, exc_type, exc, tb)</span>:</span></span><br><span class="line"><span class="keyword">await</span> log(<span class="string">"exiting context"</span>)</span><br></pre></td></tr></table></figure><h4 id="新语法"><a href="#新语法" class="headerlink" title="新语法"></a>新语法</h4><p>本规范规定的异步上下文管理器声明方式如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">with</span> EXPR <span class="keyword">as</span> VAR:</span><br><span class="line">BLOCK</span><br></pre></td></tr></table></figure><p>语义上等同于：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">mgr = (EXPR)</span><br><span class="line">aexit = type(mgr).__aexit__</span><br><span class="line">aenter = type(mgr).__aenter__</span><br><span class="line"></span><br><span class="line">VAR = <span class="keyword">await</span> aenter(mgr)</span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">BLOCK</span><br><span class="line"><span class="keyword">except</span>:</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> <span class="keyword">await</span> aexit(mgr, *sys.exc_info()):</span><br><span class="line"><span class="keyword">raise</span></span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line"><span class="keyword">await</span> aexit(mgr, <span class="literal">None</span>, <span class="literal">None</span>, <span class="literal">None</span>)</span><br></pre></td></tr></table></figure><p>与普通的 <code>with</code> 语句一样，可以在单个 <code>async with</code> 语句中指定多个上下文管理器。</p><p>不能将没有实现 <code>__aenter__</code> 和 <code>__aexit__</code> 的普通上下文管理器传递给 <code>async with</code>。在 <code>async def</code> 函数之外使用 <code>async with</code> 会抛出 <code>SyntaxError</code> 异常。</p><h4 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h4><p>通过异步上下文管理器可以很方便的在协程中实现数据库事务管理器：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">commit</span><span class="params">(session, data)</span>:</span></span><br><span class="line">...</span><br><span class="line"><span class="keyword">async</span> <span class="keyword">with</span> session.transaction():</span><br><span class="line">...</span><br><span class="line"><span class="keyword">await</span> session.update(data)</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>也可以很简洁的使用锁：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">with</span> lock:</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>代替之前的：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> (<span class="keyword">yield</span> <span class="keyword">from</span> lock):</span><br><span class="line">...</span><br></pre></td></tr></table></figure><h3 id="异步迭代器和-async-for"><a href="#异步迭代器和-async-for" class="headerlink" title="异步迭代器和 async for"></a>异步迭代器和 async for</h3><p>所谓异步迭代器，是一种可以在 iter 和 next 方法中调用异步代码的迭代器。要想实现它：</p><ol><li>指定对象必须实现一个返回异步迭代器对象的 <code>__aiter__</code> 方法（如果是通过 CPython  C API 定义则需要定义 <code>tp_as_async.am_aiter</code> slot 代替 <code>__aiter__</code>）；</li><li>异步迭代器对象必须实现一个返回可等待对象的 <code>__anext__</code> 方法（如果是通过 CPython  C API 定义则需要定义 <code>tp_as_async.am_anext</code> slot 代替 <code>__anext__</code>）；</li><li>为了使迭代过程不会无限进行下去，<code>__anext__</code> 必须在适当的时候抛出 <code>StopAsyncIteration</code> 异常。</li></ol><p>异步迭代器示例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AsyncIterable</span>:</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__aiter__</span><span class="params">(self)</span>:</span></span><br><span class="line"><span class="keyword">return</span> self</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">__anext__</span><span class="params">(self)</span>:</span></span><br><span class="line">data = <span class="keyword">await</span> self.fetch_data()</span><br><span class="line"><span class="keyword">if</span> data:</span><br><span class="line"><span class="keyword">return</span> data</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line"><span class="keyword">raise</span> StopAsyncIteration</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">fetch_data</span><span class="params">(self)</span>:</span></span><br><span class="line">...</span><br></pre></td></tr></table></figure><h4 id="新语法-1"><a href="#新语法-1" class="headerlink" title="新语法"></a>新语法</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">for</span> TARGET <span class="keyword">in</span> ITER:</span><br><span class="line">BLOCK</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">BLOCK2</span><br></pre></td></tr></table></figure><p>语义上等同于：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">iter = (ITER)</span><br><span class="line">iter = type(iter).__aiter__(iter)</span><br><span class="line">running = <span class="literal">True</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> running:</span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">TARGET = <span class="keyword">await</span> type(iter).__anext__(iter)</span><br><span class="line"><span class="keyword">except</span> StopAsyncIteration:</span><br><span class="line">runnint = <span class="literal">False</span></span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">BLOCK</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">BLOCK2</span><br></pre></td></tr></table></figure><p>在 <code>async for</code> 后使用未实现 <code>__aiter__</code> 方法的常规迭可迭代对象会抛出 <code>TypeError</code> 异常，在 <code>async def</code> 外使用 <code>async for</code> 会抛出 <code>SyntaxError</code> 异常。</p><p>与常规 <code>for</code> 语句一样，<code>async for</code> 也有一个可选的 <code>else</code> 字句。</p><h4 id="示例-1"><a href="#示例-1" class="headerlink" title="示例 1"></a>示例 1</h4><p>通过异步迭代器可以在迭代过程中异步缓冲数据：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">for</span> data <span class="keyword">in</span> cursor:</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>其中，<code>cursor</code> 是一个异步迭代器，每迭代 N 次就会从数据库中预取 N 行数据。</p><p>下面的代码实现了异步迭代协议：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Cursor</span>:</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self)</span>:</span></span><br><span class="line">self.buffer = collections.deque()</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">_prefetch</span><span class="params">(self)</span>:</span></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__aiter__</span><span class="params">(self)</span>:</span></span><br><span class="line"><span class="keyword">return</span> self</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">__anext__</span><span class="params">(self)</span>:</span></span><br><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> self.buffer:</span><br><span class="line">self.buffer = <span class="keyword">await</span> self._prefetch()</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> self.buffer:</span><br><span class="line"><span class="keyword">raise</span> StopAsyncIteration</span><br><span class="line"><span class="keyword">return</span> self.buffer.popleft()</span><br></pre></td></tr></table></figure><p>然后 <code>Cursor</code> 类可以像这样被使用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">for</span> row <span class="keyword">in</span> Cursor():</span><br><span class="line">print(row)</span><br></pre></td></tr></table></figure><p>等同于下面这段代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">i = Cursor().__aiter__()</span><br><span class="line"><span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">row = <span class="keyword">await</span> i.__anext__()</span><br><span class="line"><span class="keyword">except</span> StopAsyncIteration:</span><br><span class="line"><span class="keyword">break</span>:</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">print(row)</span><br></pre></td></tr></table></figure><h4 id="示例-2"><a href="#示例-2" class="headerlink" title="示例 2"></a>示例 2</h4><p>下面的示例是一个通用的工具类，它能够将常规迭代器转换为异步迭代器。虽然这不是一个会经常使用的操作，但是这个示例代码说明了常规迭代器和异步迭代器之间的关系：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AsyncIteratorWrapper</span>:</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, obj)</span>:</span></span><br><span class="line">self._it = iter(obj)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__aiter__</span><span class="params">(self)</span>:</span></span><br><span class="line"><span class="keyword">return</span> self</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">__anext__</span><span class="params">(self)</span>:</span></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">value = next(self._it)</span><br><span class="line">expect StopIteration:</span><br><span class="line"><span class="keyword">raise</span> StopAsyncIteration</span><br><span class="line"><span class="keyword">return</span> value</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">for</span> letter <span class="keyword">in</span> AsyncIteratorWrapper(<span class="string">"abc"</span>):</span><br><span class="line">print(letter)</span><br></pre></td></tr></table></figure><h4 id="为什么需要-StopAsyncIteration"><a href="#为什么需要-StopAsyncIteration" class="headerlink" title="为什么需要 StopAsyncIteration"></a>为什么需要 StopAsyncIteration</h4><p>为什么需要 <code>StopAsyncIteration</code> 也就是为什么不继续使用 <code>StopIteration</code>。协程的本质是生成器，所以在 <a href="https://peps.python.org/pep-0479/" target="_blank" rel="external nofollow noopener noreferrer">PEP 479</a> 之前，下面的两段代码没有本质上的不同：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">g1</span><span class="params">()</span>:</span></span><br><span class="line"><span class="keyword">yield</span> <span class="keyword">from</span> fut</span><br><span class="line"><span class="keyword">return</span> <span class="string">"spam"</span></span><br></pre></td></tr></table></figure><p>和 </p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">g2</span><span class="params">()</span>:</span></span><br><span class="line"><span class="keyword">yield</span> <span class="keyword">from</span> fut</span><br><span class="line"><span class="keyword">raise</span> StopIteration(<span class="string">"spam"</span>)</span><br></pre></td></tr></table></figure><p>由于 <a href="https://peps.python.org/pep-0479/" target="_blank" rel="external nofollow noopener noreferrer">PEP 479</a> 被接受并且在协程中默认启用，下面的示例代码将会使用 <code>RuntimeError</code> 包裹 <code>StopIteration</code>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">a1</span><span class="params">()</span>:</span></span><br><span class="line"><span class="keyword">await</span> fut</span><br><span class="line"><span class="keyword">raise</span> StopIteration(<span class="string">"spam"</span>)</span><br></pre></td></tr></table></figure><p>因此通知外部代码迭代结束的唯一方案就是抛出一个 <code>StopIteration</code> 以外的异常，也正是因为这样才需要新增一个内置的 <code>StopAsyncIteration</code> 异常。此外，根据 <a href="https://peps.python.org/pep-0479/" target="_blank" rel="external nofollow noopener noreferrer">PEP 479</a> 中的定义，<strong>所有在协程中抛出的 <code>StopIteration</code> 异常都会被封装在 <code>RuntimeError</code> 中</strong>。</p><h3 id="协程对象"><a href="#协程对象" class="headerlink" title="协程对象"></a>协程对象</h3><h4 id="与生成器的不同之处"><a href="#与生成器的不同之处" class="headerlink" title="与生成器的不同之处"></a>与生成器的不同之处</h4><p>本小节仅适用于带有 <code>CO_COROUTINE</code> 的原生协程，即通过 <code>async def</code> 语法定义的协程。<strong>asyncio 中现有的生成器式协程的行为保持不变。</strong></p><p>为了确保协程与生成器作为不同的概念处理需要付出很大的努力：</p><ol><li>原生协程对象没有实现 <code>__iter__</code> 和 <code>__next__</code> 方法。因此，它不能通过 <code>iter()</code>，<code>list()</code>，<code>tuple()</code> 或其他内置方法迭代，同样不能在 <code>for .. in</code> 中使用。若要强行在原生协程中实现 <code>__iter__</code> 或 <code>__next__</code> 会抛出 <code>TypeError</code> 异常；</li><li>不能使用 <code>yield from</code> 加原生协程返回正常的生成器，这个行为会抛出 <code>TypeError</code> 异常；</li><li>可以使用 <code>yield from</code> 加原生协程返回生成器式协程（在 asyncio 代码中必须使用 <a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine" target="_blank" rel="external nofollow noopener noreferrer"><code>@asyncio.coroutine</code></a>）；</li><li><code>inspect.isgenerator()</code> 和 <code>inspect.isgeneratorfunction()</code> 在接收原生协对象和原生协程方法时需要返回 <code>False</code>。</li></ol><h4 id="协程对象的内置方法"><a href="#协程对象的内置方法" class="headerlink" title="协程对象的内置方法"></a>协程对象的内置方法</h4><p>在底层实现上，协程继承自生成器共享实现代码。所以，协程类似生成器拥有 <code>throw()</code>，<code>send()</code> 和 <code>close()</code> 方法，<code>StopIteration</code> 和 <code>GeneratorExit</code> 在协程中也起相同的作用（尽管 <a href="https://peps.python.org/pep-0479/" target="_blank" rel="external nofollow noopener noreferrer">PEP 479</a> 默认在协程中启用）。协程的 <code>throw()</code> 和 <code>send()</code> 方法被用来将值或异常传递给类 Future 对象。</p><p>更多细节请看 <a href="https://peps.python.org/pep-0342/" target="_blank" rel="external nofollow noopener noreferrer">PEP 342</a>，<a href="https://peps.python.org/pep-0380/" target="_blank" rel="external nofollow noopener noreferrer">PEP 380</a> 和 <a href="https://docs.python.org/3/reference/expressions.html#generator-iterator-methods" target="_blank" rel="external nofollow noopener noreferrer">Python 文档相关章节</a>。</p><h3 id="用于调试的特性"><a href="#用于调试的特性" class="headerlink" title="用于调试的特性"></a>用于调试的特性</h3><p>注：<code>asyncio.coroutine</code> <strong>在 Python 3.8 之后被标记为废弃，并在 Python 3.11 正式删除。</strong><br>注：被标记为废弃的是 <code>asyncio.coroutine</code> 而不是 <code>types.coroutine</code><br>注：这一小节的内容我看完之后有点犯迷糊，不知道他在表述什么事情。</p><p>新手容易犯的一个错误是忘记可以在协程中使用 <code>yield from</code>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@asyncio.coroutine</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">useful</span><span class="params">()</span>:</span></span><br><span class="line"><span class="comment"># 如果没有 yield from 语句，这段代码将不会起作用</span></span><br><span class="line">asyncio.sleep(<span class="number">1</span>)</span><br></pre></td></tr></table></figure><p>为了调试这类错误，asyncio 中有一种特殊的调试模式，其中 <code>@coroutine</code> 装饰器使用一个特殊的对象包装所有传递进来的函数，这个对象的析构函数会记录警告日志。每当被包装的<strong>生成器</strong>被 GC 进行垃圾回收时，就会产生一条详细的日志信息，其中包含该装饰器确切的定义位置、被回收位置的堆栈跟踪等信息。封装对象还提供了一个方便的 <code>__repr__</code> 函数，一种包含有关生成器的详细信息。</p><p>问题是如何启动这些调试功能。调试功能在生产环境下应该是不可用的，所以 <code>@coroutine</code> 装饰器根据操作系统环境变量 <code>PYTHONSYNCIODEBUG</code> 来判断是否起作用。这样就可以在运行 asyncio 程序时使用 asyncio 自带的函数。<code>EventLoop.set_debug</code>（一种不用的调试工具）对 <code>@coroutine</code> 装饰器的行为没有影响。</p><p>为了使协程就成为与生成器不同的原生概念：</p><ol><li>如果协程未被 await 直接调用会抛出 <code>RuntimeWarning</code> 异常；</li><li>还建议在 <code>sys</code> 模块中添加两个新函数：<code>set_coroutine_wrapper</code> 和 <code>get_coroutine_wrapper</code>。它们的作用是在 asyncio 或其他框架中启用高级调试功能（例如显示创建协程的具体位置，以及更详细的垃圾回收堆栈跟踪）。</li></ol><h3 id="新内置函数"><a href="#新内置函数" class="headerlink" title="新内置函数"></a>新内置函数</h3><ol><li><code>types.coroutine(gen)</code>：点击 <a href="#types.coroutine()">types.coroutine()</a> 了解更多；</li><li><code>inspect.iscoroutine(obj)</code>：如果 <code>obj</code> 是原生协程对象，返回 <code>True</code>；</li><li><code>inspect.iscoroutinefunction(obj)</code>：如果 <code>obj</code> 是原生协程函数，返回 <code>Ture</code>；</li><li><code>inspect.isawaitable(obj)</code>：如果 <code>obj</code> 是可等待对象，返回 <code>True</code>；</li><li><code>inspect.getcoroutinestate(coro)</code>：返回原生协程对象的当前状态（<code>inspect.getfgeneratorstate(gen)</code> 的逆向函数）；</li><li><code>inspect.getfgeneratorstate(gen)</code>：返回本地协程独享的局部变量与其值的映射（<code>inspect.getcoroutinestate(coro)</code> 的逆向函数）；</li><li><code>sys.set_coroutine_wrapper(wrapper)</code>：允许拦截原生协程的创建，在原生协程创建时调用 <code>wrapper</code>。<code>wrapper</code> 可以是「一个接受一个参数（一个协程对象）的可调用对象」或是 <code>None</code>。如果是 <code>None</code> 则会重置之前定义的 <code>wrapper</code>，如果调用多次，新的 wrpaaer 将取代之前的。该函数是线程绑定的；</li><li><code>sys.get_coroutine_wrapper()</code>：返回通过 <code>sys.set_coroutine_wrapper</code> 设置的 <code>wrapper</code>，如果没设置则返回 <code>None</code>。该函数是线程绑定的。</li></ol><h3 id="新的抽象基类"><a href="#新的抽象基类" class="headerlink" title="新的抽象基类"></a>新的抽象基类</h3><p>为了更好的与现有框架（如 Tornado，参考 <a href="https://mail.python.org/pipermail/python-dev/2015-May/139851.html" target="_blank" rel="external nofollow noopener noreferrer">引用 13</a>）和编译器（如 Cython，参考 <a href="https://github.com/python/asyncio/issues/233" target="_blank" rel="external nofollow noopener noreferrer">引用 16</a>）集成，新增了两个抽象基类：</p><ol><li><code>collections.abc.Awaitable</code>：为类 Future 对象创建的基类，实现了 <code>__await__</code> 方法；</li><li><code>collection.abc.Coroutine</code>：为协程对象创建的基类，实现了 <code>send(value)</code>，<code>throw(type, exc, tb)</code>，<code>close</code> 和 <code>__await__()</code> 方法。</li></ol><p>注意，带有 <code>CO_ITERABLE_COROUTINE</code> 标志的生成器式协程没有实现 <code>__await__</code> 方法，因此不是 <code>collections.abc.Coroutine</code> 或 <code>collections.abc.Awaitable</code> 基类的实例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@types.coroutine</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">gencoro</span><span class="params">()</span>:</span></span><br><span class="line"><span class="keyword">yield</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">assert</span> <span class="keyword">not</span> isinstance(gencoro(), collections.abc.Coroutine)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 应该如何识别：</span></span><br><span class="line"><span class="keyword">assert</span> inspect.isawaitable(gencoro())</span><br></pre></td></tr></table></figure><p>为了能更简单地测试指定对象是否支持异步迭代，又引入了另外两个基类：</p><ol><li><code>collections.abc.AsyncIterable</code>：测试是否存在 <code>__aiter__</code> 方法；</li><li><code>collections.abs.AsyncIterator</code>：测试是否存在 <code>__aiter__</code> 和 <code>__anext__</code> 方法。</li></ol><h2 id="术语表"><a href="#术语表" class="headerlink" title="术语表"></a>术语表</h2><h3 id="原生协程函数"><a href="#原生协程函数" class="headerlink" title="原生协程函数"></a>原生协程函数</h3><p>Navite coroutine function，通过 <code>async def</code> 定义的协程函数，点击 <a href="#原生协程声明语法">原生协程声明语法</a> 了解更多。</p><h3 id="原生协程"><a href="#原生协程" class="headerlink" title="原生协程"></a>原生协程</h3><p>Navite coroutine，从原生协程函数返回的内容，点击 [await 表达式](#await 表达式) 了解更多。</p><h3 id="生成器式协程函数"><a href="#生成器式协程函数" class="headerlink" title="生成器式协程函数"></a>生成器式协程函数</h3><p>Generator-based coroutine function，基于生成器语法的协程，更常见的示例是使用 <code>@asyncio.coroutine</code> 定义的函数。</p><h3 id="生成器式协程"><a href="#生成器式协程" class="headerlink" title="生成器式协程"></a>生成器式协程</h3><p> Generator-based coroutine，通过生成器式协程函数返回的内容。</p><h3 id="协程"><a href="#协程" class="headerlink" title="协程"></a>协程</h3><p>Coroutine，原生协程或生成器式协程。</p><h3 id="协程对象-1"><a href="#协程对象-1" class="headerlink" title="协程对象"></a>协程对象</h3><p>Coroutine object，原生协程对象或生成器式协程对象。</p><h3 id="类-Future-对象"><a href="#类-Future-对象" class="headerlink" title="类 Future 对象"></a>类 Future 对象</h3><p>Future-like object，拥有 <code>__await__</code> 方法的对象或拥有 <code>tp_as_async-&gt;am_await</code> 函数的 C Object，且该函数或方法返回一个迭代器。可以在协程中作为 <code>await</code> 表达式的参数。在协程中 <code>await</code> 类 Future 对象时，协程会被推迟直到类 Future 对象的 <code>__await__</code> 完成并且返回结果，点击 [await 表达式](#await 表达式) 了解更多。</p><h3 id="可等待对象"><a href="#可等待对象" class="headerlink" title="可等待对象"></a>可等待对象</h3><p>Awaitable，类 Future 对象或协程对象。点击 [await 表达式](#await 表达式) 了解更多。</p><h3 id="异步上下文管理器"><a href="#异步上下文管理器" class="headerlink" title="异步上下文管理器"></a>异步上下文管理器</h3><p>Asynchronous context manager，拥有 <code>__aenter__</code> 和 <code>__aexit__</code> 方法的对象，可以搭配 <code>async with</code> 使用，点击 [异步上下文管理器与 async with](#异步上下文管理器与 async await) 了解更多。</p><h3 id="异步可迭代对象"><a href="#异步可迭代对象" class="headerlink" title="异步可迭代对象"></a>异步可迭代对象</h3><p>Asynchronous iterable，拥有 <code>__aiter__</code> 方法的对象，该方法返回一个异步迭代器对象。可以搭配 <code>async for</code> 一起使用，点击 [异步迭代器和 async for](#异步迭代器和 async for) 了解更多。</p><h3 id="异步迭代器"><a href="#异步迭代器" class="headerlink" title="异步迭代器"></a>异步迭代器</h3><p>Asynchronos iterator，拥有 <code>__anext__</code> 方法的对象，点击 [异步迭代器和 async for](#异步迭代器和 async for) 了解更多。</p><h2 id="过渡计划"><a href="#过渡计划" class="headerlink" title="过渡计划"></a>过渡计划</h2><blockquote><p><code>tokenizer.c</code> 文件是 CPython 源码中的一个文件，主要负责实现 Python 解释器中的词法分析器。</p></blockquote><p>为了解决 <code>async</code> 和 <code>await</code> 的向后兼容性问题，需要对 <code>tokenizer.c</code> 进行如下修改：</p><ol><li>识别 <code>async def</code> <code>NAME</code> 标记组合；</li><li>在对 <code>async def</code> 块进行词法分析时，会将 <code>async</code> <code>NAME</code> 标记替换为 <code>ASYNC</code>，将 <code>await</code> <code>NAME</code> 标记替换为 <code>AWAIT</code>；</li><li>在对 <code>def</code> 块进行词法分析时，会保持 <code>async</code> 和 <code>await</code> <code>NAME</code> 不变。</li></ol><p>这种实现方式能够让新语法（只能在 <code>async</code> 函数中使用）与现有代码无缝结合。一个既包含 <code>async def</code> 又包含 <code>async</code> 属性的示例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Spam</span>:</span></span><br><span class="line"><span class="keyword">async</span> = <span class="number">42</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 协程函数能够被执行并且打印 42</span></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">ham</span><span class="params">()</span>:</span></span><br><span class="line">print(getattr(Spam, <span class="string">"async"</span>))</span><br></pre></td></tr></table></figure><h3 id="向后兼容性"><a href="#向后兼容性" class="headerlink" title="向后兼容性"></a>向后兼容性</h3><p>为了兼容新语法，需要确保在现有的内置模块中不存在与 <code>async</code> 和 <code>await</code> 关键字冲突的命名，且新的原生协程需要兼容之前存在的生成器式协程。</p><h4 id="asyncio"><a href="#asyncio" class="headerlink" title="asyncio"></a>asyncio</h4><p><strong>注：在本 PEP 实施之前，asyncio 库中已经存在了一个名为 <code>async</code> 的函数。</strong></p><p><code>asyncio</code> 模块进行了调整和测试，使现有协程方案与新语法保持兼容，保证 100% 向后兼容，即现有代码能够在新版本中正常运行。</p><p>进行调整的主要有：</p><ol><li>使 <code>@asyncio.coroutine</code> 装饰器使用新的 <code>types.coroutine()</code> 函数；</li><li>向 <code>asyncio.Future</code> 类添加 <code>__await__ = __iter__</code>；</li><li>将 <code>ensure_future()</code> 作为 <code>async()</code> 函数的别名，废弃 <code>asyncio</code> 中的 <code>async()</code> 函数。</li></ol><h4 id="asyncio-迁移策略"><a href="#asyncio-迁移策略" class="headerlink" title="asyncio 迁移策略"></a>asyncio 迁移策略</h4><p> <code>yield from</code> 原生协程对象不能返回普通的生成器（点击 <a href="#与生成器的不同之处">与生成器的不同之处</a> 了解更多），因此建议在开始使用新语法之前，确保所有生成器式协程都使用 <code>@asyncio.coroutine</code> 进行装饰。</p><h4 id="CPython-代码库中的-async-await"><a href="#CPython-代码库中的-async-await" class="headerlink" title="CPython 代码库中的 async/await"></a>CPython 代码库中的 async/await</h4><p>在 CPython 中没有使用 <code>await</code>。</p><p><code>async</code> 关键字主要是被 asyncio 模块占用。为了解决这个问题，需要将 asyncio 模块中的 <code>async()</code> 函数重命名为 <code>ensure_future()</code>（点击 <a href="#asyncio">asyncio</a> 了解更多）。</p><p><code>async</code> 关键字的另一个占用场景是 <code>Lib/xml/dom/xmlbuilder.py</code> 中为 <code>DocumentLS</code> 类定义的 <code>async = False</code> 属性。没有针对这一属性的文档或测试文件，CPython 中的其他地方也没有使用这个属性。现在它被一个 getter 取代，调用 getter 会引发一个 <code>DeprecationWarning</code> 异常并通过异常信息建议使用 <code>async_</code> 属性代替此属性。 除此以外，CPython 代码库中没有其他的 async 属性被记录或使用。</p><h3 id="语法更新"><a href="#语法更新" class="headerlink" title="语法更新"></a>语法更新</h3><p>语法的变化相当小：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">decorated: decorators (classdef | funcdef | async_funcdef)</span><br><span class="line">async_funcdef: ASYNC funcdef</span><br><span class="line"></span><br><span class="line">compound_stmt: (if_stmt | while_stmt | for_stmt | try_stmt | with_stmt</span><br><span class="line">| funcdef | classdef | decorated | async_stmt)</span><br><span class="line"></span><br><span class="line">async_stmt: ASYNC (funcdef | with_stmt | for_stmt)</span><br><span class="line"></span><br><span class="line">power: atom_expr ['**' factor]</span><br><span class="line">atom_expr: [AWAIT] atom tarilter*</span><br><span class="line"></span><br><span class="line"># 这段代码定义了 Python 中异步语法的各个组成部分，</span><br><span class="line"># 包括异步函数定义、异步语句以及与异步操作相关的表达式。 </span><br><span class="line"># 这些语法元素共同构成了 Python 异步编程的基础。</span><br><span class="line"># 来自 Google Gemini <span class="number">1.5</span> Pro</span><br></pre></td></tr></table></figure><h3 id="废弃计划"><a href="#废弃计划" class="headerlink" title="废弃计划"></a>废弃计划</h3><p><strong>注：根据原文，本来计划在 Python 3.5 或 3.6 中废弃 <code>async</code> 和 <code>await</code>，并在 3.7 中过渡到一个更合适的关键字，但从当下来看这个计划应该是没有实施。</strong></p><h2 id="决策过程"><a href="#决策过程" class="headerlink" title="决策过程"></a>决策过程</h2><h3 id="PEP-3152"><a href="#PEP-3152" class="headerlink" title="PEP 3152"></a>PEP 3152</h3><p>Gregory Ewing 提出的 <a href="https://peps.python.org/pep-3152/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3152</a> 提供了另一种机制来实现协程（或者称为 cofunctions），其中一些关键的要素：</p><ol><li>用于声明 cofunction 的新关键字 <code>codef</code>。Cofunction 总是一个生成器，即使没有 <code>cocall</code> 表达式在其内部。类比 <code>async def</code>；</li><li>用于调用 cofunction 的新关键字 <code>cocall</code>。只能够被用在 cofunction 内部。类比 <code>await</code>；</li><li>Cofunction 只能通过 <code>cocall</code> 关键字调用；</li><li><code>cocall</code> 语法需要在其后方使用圆括号；</li><li><code>cocall f(*args, **kwargs)</code> 在语义上等同于 <code>yield from f.__cocall__(*args, **kwds)</code>。</li></ol><p>相关语法定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">atom: cocall | &lt;existing alternatives <span class="keyword">for</span> atom&gt;</span><br><span class="line">cocall: 'cocall' atom cotrailer* '(' [arglist] ')'</span><br><span class="line">cotrailer: <span class="string">'['</span> subscriptlist <span class="string">']'</span> | <span class="string">'.'</span> NAME</span><br></pre></td></tr></table></figure><p>与本提案的不同之处：</p><ol><li>没有与 <code>__cocall__</code> 一致的方法。<code>__cocall__</code> 方法会被 <code>cocall</code> 表达式会调用并将其结果传递给 <code>yield from</code>，虽然 <code>__await__</code> 方法与 <code>__cocall__</code> 类似，但 <code>__await__</code> 只用于定义类 Future 对象。</li><li>在语法中，<code>await</code> 的定义几乎与 <code>yield from</code> 相同（后来强制规定 <code>await</code> 只能出现在 <code>async def</code> 中）。但 <code>await</code> 可以很简洁地使用 <code>await future</code> 的方式调用，而 <code>cocall</code> 总是需要圆括号辅助； </li><li>要使 asyncio 与 <a href="https://peps.python.org/pep-3152/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3152</a> 兼容，需要重构 <code>@asyncio.coroutine</code> 装饰器，来将所有函数封装在一个带有 <code>__cocal__</code> 方法的对象中，或在生成器上实现 <code>__cocall__</code>。要在生成器式协程中调用 cofunctions，需要使用内置的 <code>costart(cofunc, *args, **kwargs)</code>；</li><li>因为 cofunction 必须使用 <code>cocall</code> 关键字调用 ，因此自动避免在生成器式协程中忘记使用 <code>yield from</code> 的常见错误。本提案是使用其他方法来解决这一问题的，点击 <a href="#用于调试的特性">用于调试的特性</a> 了解更多。</li><li>使用 <code>cocall</code> 调用 cofunction 的一个缺点是，如果决定实现协程生成器（使用 <code>yield</code> 或 <code>async yield</code> 表达式的协程），就不需要 <code>cocall</code> 关键字来调用。因此最终会使得协程拥有 <code>__cocall__</code> 而没有 <code>__call__</code>，协程生成器拥有 <code>__call__</code> 而没有 <code>__cocall__</code>。</li><li>在 <a href="https://peps.python.org/pep-3152/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3152</a> 中， 没有类似 <code>async for</code> 和 <code>async with</code> 的设计。</li><li>括号语法会带来很多问题：</li></ol><p>下面这段代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> fut</span><br><span class="line"><span class="keyword">await</span> function_returning_future()</span><br><span class="line"><span class="keyword">await</span> asyncio.gather(coro1(arg1, arg2), coro2(arg1, arg2))</span><br></pre></td></tr></table></figure><p>需要这样表达：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cocall fut()</span><br><span class="line">cocall (function_returning_future())</span><br><span class="line">cocall asyncio.gather(costart(coro1, arg1, arg2), costar(coro2, arg1, arg2))</span><br></pre></td></tr></table></figure><h3 id="协程生成器"><a href="#协程生成器" class="headerlink" title="协程生成器"></a>协程生成器</h3><p>通过 <code>async for</code> 关键字可以实现一种协程生成器的概念，即一个带有 <code>yield</code> 或 <code>yield from</code> 的协程。为了避免与一般的生成器混淆，可能需要在 <code>yield</code> 关键字前加上 <code>async</code> 关键字，而 <code>async yield from</code> 会抛出 <code>StopAsyncIteration</code> 异常。</p><p>虽然协程生成器的概念可能实现，但是不应该在本提案中讨论。这是一个高阶的概念，会使当前生成器的实现发生巨大的变动，应该权衡利弊，仔细考虑。这个问题应该由一个单独的 PEP 进行讨论。 </p><h3 id="为什么选择-async-和-await-关键字"><a href="#为什么选择-async-和-await-关键字" class="headerlink" title="为什么选择 async 和 await 关键字"></a>为什么选择 async 和 await 关键字</h3><p>在众多编程语言中，async/await 已经不是一个新鲜的概念了：</p><ol><li>C# 很久以前就是使用它们，请看 <a href="https://msdn.microsoft.com/en-us/library/hh191443.aspx" target="_blank" rel="external nofollow noopener noreferrer">引用 5</a>；</li><li>ECMAScript 7 中也提议键入 async/await，在 Traceur 项目也是，请看 <a href="http://wiki.ecmascript.org/doku.php?id=strawman:async_functions" target="_blank" rel="external nofollow noopener noreferrer">引用 2</a> 和 <a href="https://github.com/google/traceur-compiler/wiki/LanguageFeatures#async-functions-experimental" target="_blank" rel="external nofollow noopener noreferrer">引用 9</a>；</li><li>Facebook’s Hack/HHVM，请看 <a href="http://docs.hhvm.com/manual/en/hack.async.php" target="_blank" rel="external nofollow noopener noreferrer">引用 6</a>；</li><li>Googles Dart 语言，请看 <a href="https://www.dartlang.org/articles/await-async/" target="_blank" rel="external nofollow noopener noreferrer">引用 7</a>；</li><li>Scala，请看 <a href="http://docs.scala-lang.org/sips/pending/async.html" target="_blank" rel="external nofollow noopener noreferrer">引用 8</a>；</li><li>提议在 C++ 添加 async/await，请看 <a href="http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3722.pdf" target="_blank" rel="external nofollow noopener noreferrer">引用 10</a>；</li><li>还有很多其他语言…</li></ol><p>这是一个巨大的优势，因为这些语言的使用者已经有了使用 async/await 的经验，而这使得在一个项目中使用多种语言（例如在 Python 中使用 ECMAScript 7）变得更加容易。</p><h3 id="为什么-aiter-返回的不是可等待对象"><a href="#为什么-aiter-返回的不是可等待对象" class="headerlink" title="为什么 __aiter__ 返回的不是可等待对象"></a>为什么 <code>__aiter__</code> 返回的不是可等待对象</h3><p><a href="https://peps.python.org/pep-0492/" target="_blank" rel="external nofollow noopener noreferrer">PEP 492</a> 在 CPython 3.5.0 被接受，并且新增了 <code>__aiter__</code> 方法，该方法返回一个解析为异步迭代器的可等待对象。</p><p>在 3.5.2 中（PEP 492 被临时接受），<code>__aiter__</code> 协议被更新为直接返回异步迭代器。</p><p>这么做的目的是在 Python 中实现异步生成器，点击 <a href="http://bugs.python.org/issue27243" target="_blank" rel="external nofollow noopener noreferrer">引用 19</a> 和 <a href="https://docs.python.org/3/reference/datamodel.html#async-iterators" target="_blank" rel="external nofollow noopener noreferrer">引用 20</a> 了解更多。</p><h3 id="async-关键字的重要性"><a href="#async-关键字的重要性" class="headerlink" title="async 关键字的重要性"></a>async 关键字的重要性</h3><p>虽然可以只实现 <code>await</code> 表达式，并且将至少拥有一个 <code>await</code> 的函数视为协程，但这样做会增加 API 设计、代码重构和长期支持的难度。</p><p>假设 Python 只有 <code>await</code> 关键字：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">useful</span><span class="params">()</span>:</span></span><br><span class="line">...</span><br><span class="line"><span class="keyword">await</span> log(...)</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">important</span><span class="params">()</span>:</span></span><br><span class="line"><span class="keyword">await</span> useful()</span><br></pre></td></tr></table></figure><p>如果对 <code>useful()</code> 函数进行重构，删除其内部所有 <code>await</code> 表达式，它就会变成一个普通 Python 函数，所有依赖于它的代码（例如 <code>important()</code>）都会产生异常。要想解决这个问题，必须引入类似 <code>@asyncio.coroutine</code> 的装饰器。</p><h3 id="为什么使用-async-def"><a href="#为什么使用-async-def" class="headerlink" title="为什么使用 async def"></a>为什么使用 async def</h3><p>直接使用 <code>async name(): pass</code> 可能比 <code>async def name(): pass</code> 更有吸引力，因为这种方式输入的字符更少。但它打破了 <code>async def</code>、<code>async with</code> 和 <code>async for</code> 之间的一致性，其中 <code>async</code> 都是修饰符，表示语句是异步的。此外，它与现有语法更契合。</p><h3 id="为什么不使用-await-for-和-await-with"><a href="#为什么不使用-await-for-和-await-with" class="headerlink" title="为什么不使用 await for 和 await with"></a>为什么不使用 await for 和 await with</h3><p><code>async</code> 是一个形容词，因此更适合作为修饰词与其他关键字搭配。await for/with 看起来更像是等待 <code>for</code> 或 <code>with</code> 语句执行完成。</p><h3 id="为什么使用-async-def-而不是-def-async"><a href="#为什么使用-async-def-而不是-def-async" class="headerlink" title="为什么使用 async def 而不是 def async"></a>为什么使用 async def 而不是 def async</h3><p><code>async</code> 关键字是一个语句的修饰符。在其他编程语言中常见的 <code>static</code>、<code>public</code>、<code>unsafe</code> 等关键字是一个很形象的类比。<code>async for</code> 是异步的 <code>for</code> 语句，<code>async with</code> 是异步的 <code>with</code> 语句，<code>async def</code> 是异步函数。</p><p>将 <code>async</code> 放在其他关键字后面很可能会引起混淆，例如 <code>for async item in iterator</code> 可以理解为从 iterator 中遍历异步的 item。</p><p>将 <code>async</code> 放在 <code>def</code>、<code>with</code> 和 <code>for</code> 前面，还能使语言语法更加简单。同时，<code>async def</code> 也能让使用者更方便地区分协程与普通函数。</p><h3 id="为什么不导入-future"><a href="#为什么不导入-future" class="headerlink" title="为什么不导入 __future__"></a>为什么不导入 <code>__future__</code></h3><blockquote><p><code>from __future__ import feature</code> 形式的导入被称为 future 语句。 它们会被 Python 编译器当作特例，通过包含 future 语句来允许新的 Python 特性在该特性成为语言标准之前发布的模块中使用。</p></blockquote><p><a href="#过渡计划">过渡计划</a> 章节介绍了词法分析器做了哪些修改使其仅在 <code>async def</code> 块才中将 <code>async</code> 和<code>await</code> 作为关键字处理。因此，<code>async def</code> 发挥了模块级编译器声明（类似 <code>from  __future__ import async_await</code>）的作用。</p><h3 id="为什么异步魔术方法都用-a-开头"><a href="#为什么异步魔术方法都用-a-开头" class="headerlink" title="为什么异步魔术方法都用 a 开头"></a>为什么异步魔术方法都用 a 开头</h3><p>一个备选方案是使用 <code>async</code> 前缀，但是为了让新的魔术方法和原有魔术方法保持尽可能高的相似性，最终选择了方法名更短的方案。</p><h3 id="为什么不复用现有魔术方法"><a href="#为什么不复用现有魔术方法" class="headerlink" title="为什么不复用现有魔术方法"></a>为什么不复用现有魔术方法</h3><p>存在一个异步迭代器和异步上下文管理器的备选方案，提议在声明中添加 <code>async</code> 关键字来复用现有的魔术方法：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CM</span>:</span></span><br><span class="line"><span class="comment"># 代替 __aenter__</span></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">__enter__</span><span class="params">(self)</span>:</span></span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>这个方案有以下缺点：</p><ol><li>不能创建一个既可以在 <code>with</code> 中使用，又可以在 <code>async with</code> 中使用的对象；</li><li>会破坏兼容性，因为在版本低于 3.4 的 Python 代码中没有规定禁止从 <code>__enter__</code> 或 <code>__exit__</code> 中返回类 Future 对象；</li><li>让原生协程简洁无歧义是本提案主要目的之一，因此将异步协议用的魔术方法做了区分处理。</li></ol><h3 id="为什么复用-for-和-with-语句"><a href="#为什么复用-for-和-with-语句" class="headerlink" title="为什么复用 for 和 with 语句"></a>为什么复用 for 和 with 语句</h3><p>无论是现有的生成器式协程还是本提案提出的原生协程都更希望使用者能够明显地看到代码可能阻塞的位置。让现有的 <code>for</code> 和 <code>with</code> 语句识别异步迭代器和异步上下文管理器会不可避免地引入隐式阻塞点，从而导致代码变得更难理解。</p><h3 id="异步推导式"><a href="#异步推导式" class="headerlink" title="异步推导式"></a>异步推导式</h3><p>可以提供异步推导式，但是这个语法不在本提案的讨论范围内。</p><p><strong>注：<a href="https://wiki.blanc.site/archives/5823d5df.html">PEP 530</a> 定义了异步推导式，可以在 3.6 之后的版本使用。</strong></p><h3 id="异步-lambda-函数"><a href="#异步-lambda-函数" class="headerlink" title="异步 lambda 函数"></a>异步 lambda 函数</h3><p>可以提供异步 lambda 函数，但这个语法不在本提案的讨论范围内。</p><p><strong>注：目前 Python 还没有解锁异步 lambda。</strong></p><h2 id="性能影响"><a href="#性能影响" class="headerlink" title="性能影响"></a>性能影响</h2><h3 id="整体影响"><a href="#整体影响" class="headerlink" title="整体影响"></a>整体影响</h3><p>本提案不会对 Pyhton 本身的性能产生明显影响。下面是 <a href="https://hg.python.org/benchmarks" target="_blank" rel="external nofollow noopener noreferrer">Python 官方基准测试</a> 的输出结果：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">python perf.py -r -b default ..&#x2F;cpython&#x2F;python.exe ..&#x2F;cpython-aw&#x2F;python.exe</span><br><span class="line"></span><br><span class="line">[skipped]</span><br><span class="line"></span><br><span class="line">Report on Darwin ysmac 14.3.0 Darwin Kernel Version 14.3.0:</span><br><span class="line">Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5&#x2F;RELEASE_X86_64</span><br><span class="line">x86_64 i386</span><br><span class="line"></span><br><span class="line">Total CPU cores: 8</span><br><span class="line"></span><br><span class="line">### etree_iterparse ###</span><br><span class="line">Min: 0.365359 -&gt; 0.349168: 1.05x faster</span><br><span class="line">Avg: 0.396924 -&gt; 0.379735: 1.05x faster</span><br><span class="line">Significant (t&#x3D;9.71)</span><br><span class="line">Stddev: 0.01225 -&gt; 0.01277: 1.0423x larger</span><br><span class="line"></span><br><span class="line">The following not significant results are hidden, use -v to show them:</span><br><span class="line">django_v2, 2to3, etree_generate, etree_parse, etree_process, fastpickle,</span><br><span class="line">fastunpickle, json_dump_v2, json_load, nbody, regex_v8, tornado_http.</span><br></pre></td></tr></table></figure><h3 id="词法分析器的影响"><a href="#词法分析器的影响" class="headerlink" title="词法分析器的影响"></a>词法分析器的影响</h3><p>使用修改后的词法分析起解析 Python 文件没有明显的速度减慢：解析一个 12MB 的文件（<code>Lib/test/test_binop.py</code> 重复 1000 次）所需时间（与之前）相同。</p><h3 id="async-await-的影响"><a href="#async-await-的影响" class="headerlink" title="async/await 的影响"></a>async/await 的影响</h3><p>下面的微型基准测试用于确定异步函数和生成器之间的性能差异：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">binary</span><span class="params">(n)</span>:</span></span><br><span class="line"><span class="keyword">if</span> n &lt;= <span class="number">0</span>:</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span></span><br><span class="line">l = <span class="keyword">yield</span> <span class="keyword">from</span> binary(n - <span class="number">1</span>)</span><br><span class="line">r = <span class="keyword">yield</span> <span class="keyword">from</span> binary(n - <span class="number">1</span>)</span><br><span class="line"><span class="keyword">return</span> l + <span class="number">1</span> + r</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">abinary</span><span class="params">(n)</span>:</span></span><br><span class="line"><span class="keyword">if</span> n &lt; <span class="number">0</span>:</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span></span><br><span class="line">l = <span class="keyword">await</span> abinary(n - <span class="number">1</span>)</span><br><span class="line">    r = <span class="keyword">await</span> abinary(n - <span class="number">1</span>)</span><br><span class="line">    <span class="keyword">return</span> l + <span class="number">1</span> + r</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">timeit</span><span class="params">(func, depth, repate)</span>:</span></span><br><span class="line">t0 = time.time()</span><br><span class="line"><span class="keyword">for</span> _ <span class="keyword">in</span> range(repeat):</span><br><span class="line">o = func(depth)</span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line"><span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">o.send(<span class="literal">None</span>)</span><br><span class="line"><span class="keyword">except</span> StopIteration:</span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line">t1 = time.time()</span><br><span class="line">print(<span class="string">'&#123;&#125;(&#123;&#125;) * &#123;&#125;: total &#123;:.3f&#125;s'</span>.format(func.__name__, depth, repeat, t1-t0))</span><br></pre></td></tr></table></figure><p>结果是没有观察到明显的性能差异：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">binary(19) * 30: total 53.321s</span><br><span class="line">abinary(19) * 30: total 55.073s</span><br><span class="line"></span><br><span class="line">binary(19) * 30: total 53.361s</span><br><span class="line">abinary(19) * 30: total 51.360s</span><br><span class="line"></span><br><span class="line">binary(19) * 30: total 49.438s</span><br><span class="line">abinary(19) * 30: total 51.047s</span><br></pre></td></tr></table></figure><p>注 depth = 19 以为着 1048575 次调用。</p><h2 id="实施"><a href="#实施" class="headerlink" title="实施"></a>实施</h2><p>可以通过 <a href="http://bugs.python.org/issue24017" target="_blank" rel="external nofollow noopener noreferrer">引用 15</a> 追踪具体实施过程，它在 2015-5-11 提交。</p><h2 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h2><ol><li><a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine" target="_blank" rel="external nofollow noopener noreferrer">https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine</a></li><li><a href="http://wiki.ecmascript.org/doku.php?id=strawman:async_functions" target="_blank" rel="external nofollow noopener noreferrer">http://wiki.ecmascript.org/doku.php?id=strawman:async_functions</a></li><li><a href="https://github.com/1st1/cpython/tree/await" target="_blank" rel="external nofollow noopener noreferrer">https://github.com/1st1/cpython/tree/await</a></li><li><a href="https://hg.python.org/benchmarks" target="_blank" rel="external nofollow noopener noreferrer">https://hg.python.org/benchmarks</a></li><li><a href="https://msdn.microsoft.com/en-us/library/hh191443.aspx" target="_blank" rel="external nofollow noopener noreferrer">https://msdn.microsoft.com/en-us/library/hh191443.aspx</a></li><li><a href="http://docs.hhvm.com/manual/en/hack.async.php" target="_blank" rel="external nofollow noopener noreferrer">http://docs.hhvm.com/manual/en/hack.async.php</a></li><li><a href="https://www.dartlang.org/articles/await-async/" target="_blank" rel="external nofollow noopener noreferrer">https://www.dartlang.org/articles/await-async/</a></li><li><a href="http://docs.scala-lang.org/sips/pending/async.html" target="_blank" rel="external nofollow noopener noreferrer">http://docs.scala-lang.org/sips/pending/async.html</a></li><li><a href="https://github.com/google/traceur-compiler/wiki/LanguageFeatures#async-functions-experimental" target="_blank" rel="external nofollow noopener noreferrer">https://github.com/google/traceur-compiler/wiki/LanguageFeatures#async-functions-experimental</a></li><li><a href="http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3722.pdf" target="_blank" rel="external nofollow noopener noreferrer">http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3722.pdf</a></li><li><a href="https://docs.python.org/3/reference/expressions.html#generator-iterator-methods" target="_blank" rel="external nofollow noopener noreferrer">https://docs.python.org/3/reference/expressions.html#generator-iterator-methods</a></li><li><a href="https://docs.python.org/3/reference/expressions.html#primaries" target="_blank" rel="external nofollow noopener noreferrer">https://docs.python.org/3/reference/expressions.html#primaries</a></li><li><a href="https://mail.python.org/pipermail/python-dev/2015-May/139851.html" target="_blank" rel="external nofollow noopener noreferrer">https://mail.python.org/pipermail/python-dev/2015-May/139851.html</a></li><li><a href="https://mail.python.org/pipermail/python-dev/2015-May/139844.html" target="_blank" rel="external nofollow noopener noreferrer">https://mail.python.org/pipermail/python-dev/2015-May/139844.html</a></li><li><a href="http://bugs.python.org/issue24017" target="_blank" rel="external nofollow noopener noreferrer">http://bugs.python.org/issue24017</a></li><li><a href="https://github.com/python/asyncio/issues/233" target="_blank" rel="external nofollow noopener noreferrer">https://github.com/python/asyncio/issues/233</a></li><li><a href="https://hg.python.org/cpython/rev/7a0a1a4ac639" target="_blank" rel="external nofollow noopener noreferrer">https://hg.python.org/cpython/rev/7a0a1a4ac639</a></li><li><a href="http://bugs.python.org/issue24400" target="_blank" rel="external nofollow noopener noreferrer">http://bugs.python.org/issue24400</a></li><li><a href="http://bugs.python.org/issue27243" target="_blank" rel="external nofollow noopener noreferrer">http://bugs.python.org/issue27243</a></li><li><a href="https://docs.python.org/3/reference/datamodel.html#async-iterators" target="_blank" rel="external nofollow noopener noreferrer">https://docs.python.org/3/reference/datamodel.html#async-iterators</a></li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://peps.python.org/pep-0492/" target="_blank" rel="external nofollow noopener noreferrer">PEP 492 – Coroutines with async and await syntax</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;本文的主体内容大部分来自对 &lt;a href=&quot;https://peps.python.org/pep-0492/&quot; target=&quot;_bla
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Python" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Python/"/>
    
    
      <category term="Python" scheme="https://wiki.blanc.site/tags/Python/"/>
    
      <category term="WeeklyPEP" scheme="https://wiki.blanc.site/tags/WeeklyPEP/"/>
    
  </entry>
  
  <entry>
    <title>WeeklyPEP-3-PEP 318-函数装饰器-overview</title>
    <link href="https://wiki.blanc.site/archives/c29b4f3b.html"/>
    <id>https://wiki.blanc.site/archives/c29b4f3b.html</id>
    <published>2024-02-25T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文的主体内容大部分来自对 <a href="https://peps.python.org/pep-0318/#current-syntax" target="_blank" rel="external nofollow noopener noreferrer">PEP 318</a> 原文的翻译，剩余部分是本人对原文的理解，在整理过程中我没有刻意地区分二者，这两部分被糅杂在一起形成了本文。因此请不要带着「本文的内容是百分百正确」的想法阅读。如果文中的某些内容让你产生疑惑，你可以给我留言与我讨论或者对比 <a href="https://peps.python.org/pep-0318/#current-syntax" target="_blank" rel="external nofollow noopener noreferrer">PEP 318</a> 的原文加以确认。</p><p><strong>注：<a href="https://peps.python.org/pep-0318/#current-syntax" target="_blank" rel="external nofollow noopener noreferrer">PEP 318</a> 创建于 2003-06-05，Python 2.4</strong></p><h2 id="警告和摘要"><a href="#警告和摘要" class="headerlink" title="警告和摘要"></a>警告和摘要</h2><p>本文档的主要目的是描述装饰器语法和做出相关决策过程。它既不试图涵盖全部潜在的替代语法，也不试图详尽地罗列出每种语法的优缺点。</p><p>当前（Python 2.4 之前）转换一个函数或方法（例如将它们定义为一个类方法或静态方法）的方案很笨拙，并且可能会导致降低代码的可读性。理想情况下，这类转换应该与函数或方法的定义同步进行。本 PEP 为函数或方法实现这类转换引入了全新的语法。</p><h2 id="动机"><a href="#动机" class="headerlink" title="动机"></a>动机</h2><p><strong>当前（Python 2.4 之前）实现一个函数或方法转换的方案是将转换定义在函数声明的后面。</strong> 对于一些大型函数来说，这样做会让函数行为的关键部分与函数外部内容形成割裂感，例如：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(self)</span>:</span></span><br><span class="line"><span class="comment"># perform method operation</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line">foo = classmethod(foo)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(cls)</span>:</span></span><br><span class="line">   <span class="keyword">pass</span></span><br><span class="line">bar = synchronized(lock)(foo)</span><br><span class="line">bar = classmethod(bar)</span><br></pre></td></tr></table></figure><p>这种方案不仅使得那些长函数的<strong>可读性变差</strong>，还会使得一个单一的概念存在<strong>多次声明</strong>，很不 <a href="https://peps.python.org/pep-0020/" target="_blank" rel="external nofollow noopener noreferrer">pythonic</a>。一个解决此问题的方案是让函数转换贴近函数自身的声明。新语法的意图就是将装饰器放在函数声明中以替代现有方案：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@classmethod</span></span><br><span class="line"><span class="meta">@synchronized(lock)</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(cls)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>以这种形式修改类是完全可行的，尽管这样做的受益并没有那么明显。当然，任何可以使用类装饰器完成的事情都可以使用元类完成。但是使用元类是一种高阶的方案，所以「能以一种更简洁明了的方式对类进行简单修改」是有吸引力的。Python 2.4 中仅添加了函数/方法装饰器。</p><p><a href="https://peps.python.org/pep-3129/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3129</a> 建议从 Python 2.6 开始添加类装饰器。</p><h3 id="做决定为什么这么难"><a href="#做决定为什么这么难" class="headerlink" title="做决定为什么这么难"></a>做决定为什么这么难</h3><p>在 Python 2.2 之后就有两个装饰器（<code>classmethod()</code> 和 <code>staticmethod()</code>）可以被使用。差不多从这时起，大家便认为 Python 最终会在语言层面为它们添加语法上的支持。也许你会好奇，为什么达成最终的共识如此困难（从 Python 2.2 到 Python 2.4）。函数装饰器最佳实现方案相关的讨论在 comp.lang.python 和 python-dev 邮件列表中一直不断，主要的分歧集中在以下几个问题上：</p><ol><li>声明位置：几乎所有人都同意，在函数主体声明之后进行转换是不理想的，但具体应该放在哪里并没有形成共识。</li><li>语法：Python 是一个语法十分简明的编程语言，为了保持这种简明（无论是体现在直觉上还是具体实现上）它对什么可以做和什么不能做都有相当严格的限制。没有一种特别合适的方法能够让第一次接触这个概念的使用者在看到这种语法时就能快速理解这个语法代表了什么。似乎最好的方案就是能防止初学者对这种语法产生错误的第一印象。</li><li>对概念不熟悉：对于那些对代数（甚至基础算数）有一定了解或至少使用过一种其他编程语言的人来说，Python 中大部分语法和概念都是符合直觉的。但是在使用 Python 装饰器之前，很少有人对「装饰器」这个概念有了解，也没有一个较好的类比对象来帮助人们快速的理解。</li></ol><p>语法往往比其他任何事情都容易引起更多的争论，[PEP 308] 中与三元运算符语法相关的讨论是另一个例子。</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>人们普遍认为，以当前的状态，为装饰器提供语法支持是可取的。Guido 也在第十届 Python 大会的 DevDay 主题演讲中提到了对装饰器的语法支持，尽管 <a href="https://mail.python.org/pipermail/python-dev/2002-February/020017.html" target="_blank" rel="external nofollow noopener noreferrer">他后来说</a> 这只是他在那里半开玩笑地提出的几个拓展之一。在会议结束不久之后，Michael Hudson 在 python-dev 上发布了这个 <a href="https://mail.python.org/pipermail/python-dev/2002-February/020005.html" target="_blank" rel="external nofollow noopener noreferrer">主题</a>，并将最初的方括号语法归因于 Gareth McCaughan 在 comp.lang.python 上的 <a href="https://groups.google.com/g/comp.lang.python/c/cbBrqwHmT6o/m/otVqFn24eUEJ?hl=en" target="_blank" rel="external nofollow noopener noreferrer">早期提案</a>。</p><p>类装饰器似乎会顺理成章的成为下一个目标，因为类的定义和函数的定义在语法上是相似的，但 Guido 任然保持怀疑，因此类装饰器几乎可以确认不会在 Python 2.4 中出现。</p><h2 id="关于装饰器这个名字"><a href="#关于装饰器这个名字" class="headerlink" title="关于装饰器这个名字"></a>关于装饰器这个名字</h2><p>有很多人抱怨为这个特性选择「装饰器」这个名字。其中最主要的原因是这个名字与 <a href="https://web.archive.org/web/20031204182047/http://patterndigest.com/patterns/Decorator.html" target="_blank" rel="external nofollow noopener noreferrer">GoF</a> 书（<a href="https://book.douban.com/subject/1436745/" target="_blank" rel="external nofollow noopener noreferrer">设计模式：可复用面向对象软件的基础</a>）中所阐述的概念并不一致。选择「装饰器」这个名字更多的是由于它在编译器领域的使用——语法树被遍历和注释。很可能会出现一个更好的名字（<em>目前看来并没有</em>）。</p><h2 id="设计目标"><a href="#设计目标" class="headerlink" title="设计目标"></a>设计目标</h2><p><strong>注：译者猜测在设计时还没有明确装饰器这个概念所以原文使用 wrapper 来表示被设计的主体（也就是装饰器）。</strong></p><p>新语法应该：</p><ol><li>能够适应任何使用场景，包括使用者定义的可调用对象和内置的 <code>classmethod()</code> 以及 <code>staticmethod()</code>。这项需求同时意味着必须能够向 wrapper constructor 传递参数；</li><li>允许在一个定义中使用多个 wrappers；</li><li>能够明显的表现出它的作用，至少要做到明显，并且初学者在编写自己的代码时可以放心的忽略它；</li><li>是一种经过讲解后很容易就记住的语法；</li><li>拥有较好的拓展性；</li><li>容易使用，在需要使用的地方可以频繁的使用；</li><li>不能削弱代码的可读性，让函数的定义保持简明；</li><li>不会不必要地增加辅助工具（例如语言敏感的编辑器或其他解析器工具）的复杂性；</li><li>允许将来的编译器为装饰器进行优化，由于 Python 的 JIT 编译器有希望在某个时间实现，这就需要装饰器的语法出现在函数声明之前；</li><li>从函数声明的尾部移动到头部。</li></ol><p>Andrew Kuchling 在他的博客（已经无法访问）中有一些关于动机和用例的讨论的链接，特别值得注意的是 <a href="https://mail.python.org/pipermail/python-dev/2004-April/044132.html" target="_blank" rel="external nofollow noopener noreferrer">Jim Huginin 的用例列表</a>。</p><h2 id="当前语法"><a href="#当前语法" class="headerlink" title="当前语法"></a>当前语法</h2><p>在 Python 2.4a2 中实现的函数装饰器的语法是：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@dec2</span></span><br><span class="line"><span class="meta">@dec1</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">func</span><span class="params">(arg1, arg2, ...)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>这相当于：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">func</span><span class="params">(arg1, arg2, ...)</span></span></span><br><span class="line"><span class="function"><span class="title">pass</span></span></span><br><span class="line">func = dec2(dec1(func))</span><br></pre></td></tr></table></figure><p>没有对 <code>func</code> 的多次赋值，装饰器就在函数声明的周围，<code>@</code> 符号能够提醒使用者：这里有一些新特性在起作用。</p><p>从上到下逐个起作用的逻辑源自数学中函数应用的通常顺序。在数学中，结构是 <code>(g o f)(x)</code> 的函数会被转换为为 <code>g(f(x))</code>。在 Python 中，<code>@g @f def foo()</code> 会被翻译为 <code>foo=g(f(foo))</code>。</p><p>装饰器语句所能接受的内容是有限的（任何表达式都不起作用）。<a href="https://mail.python.org/pipermail/python-dev/2004-August/046711.html" target="_blank" rel="external nofollow noopener noreferrer">Guido 喜欢这样</a>，因为更符合直觉。</p><p>当前语法还允许装饰器声明调用一个返回装饰器的函数：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@decomaker(argA, argB, ...)</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">func</span><span class="params">(arg1, arg2, ...)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>这相当于：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">func = decomaker(argA, argB, ...)(func)</span><br></pre></td></tr></table></figure><p>这个语法生效的逻辑是将 <code>@</code> 符号后面的内容视作一个表达式（语法上被限制为：只能是一个函数），并且无论该表达式返回什么都会被调用。 </p><h2 id="语法方案讨论"><a href="#语法方案讨论" class="headerlink" title="语法方案讨论"></a>语法方案讨论</h2><p>目前已经提出了大量不同的语法，与其试图逐一讨论这些语法，不如将「语法讨论」分成几个方面。试图对每种可能的语法进行讨论是一种疯狂的行为，并且会产生一个非常臃肿的 PEP。</p><h3 id="装饰器位置"><a href="#装饰器位置" class="headerlink" title="装饰器位置"></a>装饰器位置</h3><p>第一个值得讨论的语法问题是：装饰器的位置。下面的代码示例中会使用 Python 2.4a2 中的最终确定的 <code>@</code> 符号作为装饰器符号。</p><h4 id="def-语句上面"><a href="#def-语句上面" class="headerlink" title="def 语句上面"></a>def 语句上面</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@clasmethod</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(arg1, arg2)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@accepts(int, int)</span></span><br><span class="line"><span class="meta">@returns(float)</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(low, high)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>人们对这种方案有一些反对意见，其中最主要的是：这是（当时） Python 中第一例某行代码会对下一行代码产生影响的案例。在 2.4a3 版本中要求每行一个装饰器（在 2.4a2 版本中，可以在同一行指定多个装饰器），而 2.4final 的最终决定是每行一个装饰器。也有人抱怨说这种语法会是的在使用多个装饰器时变得笨重。不过有人指出，在一个函数上使用大量装饰器的可能性很小，因此这不是一个大问题。</p><p>这种方案的优点是装饰器位于函数声明外部，这使得人们能够直观地理解装饰器会在定义函数时执行。另一个优点是，在函数定义上添加前缀符合在代码本身之前了解代码语义变化的要求。使用者可以正确并快速地理解代码的语义，而不必在阅读代码时反复查看上下文。</p><p>Gudio 也更偏向于将装饰器定义在 def 的上一行，因为长的参数列表意味着装饰器可能被忽略。</p><h4 id="def-和-函数名之间或函数名和参数列表之间"><a href="#def-和-函数名之间或函数名和参数列表之间" class="headerlink" title="def 和 函数名之间或函数名和参数列表之间"></a>def 和 函数名之间或函数名和参数列表之间</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> @<span class="title">classmethod</span> <span class="title">foo</span><span class="params">(arg1, arg2)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">def @accept(int, int),@returns(float) bar(low, high):</span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span> @<span class="title">classmethod</span> <span class="params">(arg1, arg2)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">def bar @accept(int, int),@return(float) (low, high):</span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>这个方案也一些反对意见。首先，它很容易破坏源代码的「可重命名性」，你不再能通过搜索 <code>def foo(</code> 并找到函数定义。第二个更严重的反对意见是，在s使用多个装饰器的情况下语法会显得及其笨重。</p><h4 id="函数声明尾部的-之前"><a href="#函数声明尾部的-之前" class="headerlink" title="函数声明尾部的 : 之前"></a>函数声明尾部的 <code>:</code> 之前</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(arg1, arg2)</span> @<span class="title">classmethod</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">def bar(low, high) @accepts(int, int),@returns(float):</span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>Gudio 总结了反对这个方案的几种论点（其中很多也适用于前一种形式）：</p><ol><li>它在签名之后隐藏了关键信息（例如，它是一个静态方法），这很容易被遗漏；</li><li>很容易遗忘长参数和长装饰器列表之间的过渡；</li><li>使用剪切和粘贴来重用装饰器列表变得很麻烦，因为它在一行的中间开始和结束；</li></ol><h4 id="与-docstring-当前所在位置相同"><a href="#与-docstring-当前所在位置相同" class="headerlink" title="与 docstring 当前所在位置相同"></a>与 docstring 当前所在位置相同</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(arg1, arg2)</span>:</span></span><br><span class="line"><span class="meta">@classmethod</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(low, high)</span>:</span></span><br><span class="line"><span class="meta">@accepts(int, int)</span></span><br><span class="line"><span class="meta">@returns(float)</span></span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>这种形式的主要缺点是，它需要“窥视”函数内部才能确定装饰器。此外这些位于函数内部的内容，在运行时也不会执行。Gudio 认为 docstring 不是一个很好的反例，并且使用 docstring 来放置装饰器很有可能会使得最终不得不把文档字符串移动到函数声明外部。</p><h4 id="创建一个新的代码块"><a href="#创建一个新的代码块" class="headerlink" title="创建一个新的代码块"></a>创建一个新的代码块</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">decorate:</span><br><span class="line">classmethod</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(arg1, arg2)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">decorate:</span><br><span class="line">accepts(int, int)</span><br><span class="line">returns(float)</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(low, high)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>这种形式会导致使用装饰器函数和没使用装饰器的函数的缩进不一致，另外被装饰的函数的声明需要写在第三层缩进。</p><h3 id="语法形式"><a href="#语法形式" class="headerlink" title="语法形式"></a>语法形式</h3><h4 id="decorator"><a href="#decorator" class="headerlink" title="@decorator"></a>@decorator</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@classmethod</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(arg1, arg2)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@accepts(int,int)</span></span><br><span class="line"><span class="meta">@returns(float)</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(low,high)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>反对这种语法的主要理由是 <code>@</code> 符号从未在 Python 中使用过（但是在 IPython 和 Leo 中都有使用），并且 <code>@</code> 符号没有意义。另外一种反对意见是，这种方案浪费了一种从未使用的字符（一个有限的集合），这些字符应该被用在更重要的场合。</p><h4 id="decorator-1"><a href="#decorator-1" class="headerlink" title="|decorator"></a>|decorator</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">|classmethod</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(arg1,arg2)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">|accepts(int,int)</span><br><span class="line">|returns(float)</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(low,high)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>这是 <code>@decorator</code> 的变体，它的优点是不会破坏 IPython 和 Leo，主要缺点是符号 <code>|</code> 看起来既像大写的 <code>I</code> 又像小写的 <code>l</code>。</p><h4 id="列表语法"><a href="#列表语法" class="headerlink" title="列表语法"></a>列表语法</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">[classmethod]</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(arg1,arg2)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">[accepts(int,int), returns(float)]</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(low,high)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>列表语法最重要的缺点是它在 Python 中是有具体含义的，其次它也不能很好地表明该表达式是一个装饰器。</p><h4 id="使用其他符号的列表语法"><a href="#使用其他符号的列表语法" class="headerlink" title="使用其他符号的列表语法"></a>使用其他符号的列表语法</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;classmethod&gt;</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(arg1,arg2)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">&lt;accepts(int,int), returns(float)&gt;</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(low,high)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>这些替代方案都没有获得太多支持。使用双方括号的替代方案只是为了表明这是一个装饰器不是一个列表，并没有使解析变得更容易。尖括号的替代方案也存在解析问题，因为 <code>&lt;</code> 和 <code>&gt;</code> 都有独立的含义，对于装饰器来说 <code>&gt;</code> 可能是一个大于号而不是装饰器定义的关闭符号。</p><h4 id="decorate"><a href="#decorate" class="headerlink" title="decorate()"></a>decorate()</h4><p><code>decorate()</code> 的方案是不实现新的语法，而是实现一个能够使用内省来控制其后面紧跟的函数的内置函数。Jp Calderone 和 Philip Eby 都实现了这样的函数。Gudio 非常坚决地反对这样（不使用新的语法）做，这种方案带来了极大的不确定性。</p><h4 id="新的关键字（和块）"><a href="#新的关键字（和块）" class="headerlink" title="新的关键字（和块）"></a>新的关键字（和块）</h4><p>这个想法是 comp.lang.python 的共识替代方案，在下面的 [社区共识](# 社区共识) 中有更多关于这一点的内容。Robert Brewer 写了一份详细的 J2 提案文件（无法访问），概述了支持这种形式的论点。初始问题是：</p><ol><li>需要一个新的关键字，因此需要一个 <code>from __future__ import decorators</code> 语句。</li><li>关键词的选择是有争议的。然而， <code>using</code> 作为共识选择出现，并在提案和实现中使用。</li><li>关键字/块形式产生的东西看起来像一个正常的代码块，但不是。尝试在此块中使用语句将导致语法错误，这可能会使使用者感到困惑。</li></ol><p>几天后，Guido 基于 <a href="https://mail.python.org/pipermail/python-dev/2004-September/048518.html" target="_blank" rel="external nofollow noopener noreferrer">两个主要理由</a> 拒绝了这项提议。</p><h4 id="其他形式"><a href="#其他形式" class="headerlink" title="其他形式"></a>其他形式</h4><p>在 <a href="https://wiki.python.org/moin/PythonDecoratorProposals" target="_blank" rel="external nofollow noopener noreferrer">维基页面</a> 上还有很多其他的变体和提案。</p><h3 id="为什么使用"><a href="#为什么使用" class="headerlink" title="为什么使用 @"></a>为什么使用 @</h3><p> 在 Java 的历史中，<code>@</code> 最初在 Javadoc comments 中使用被作为标记，后来在 Java 1.5 中用于 annotations，类似于 Python 装饰器。在此之前，<code>@</code> 从未在 Python 中用作标记，这样的代码不能被早期的 Python 版本解析，可能会导致微妙的语义错误。这也意味着什么是装饰器，什么不是的模糊性被消除了。也就是说，<code>@</code> 仍然是一个相当随意的选择。有些人建议使用 <code>|</code>。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>在原文中还有两部分分别描述了最终实施的过程和一些示例，这里我就不展示了，感兴趣的可以自行翻阅原文。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://peps.python.org/pep-0318/#current-syntax" target="_blank" rel="external nofollow noopener noreferrer">PEP 318 – Decorators for Functions and Methods</a></li><li><a href="https://peps.python.org/pep-0020/" target="_blank" rel="external nofollow noopener noreferrer">PEP 20 – The Zen of Python</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;本文的主体内容大部分来自对 &lt;a href=&quot;https://peps.python.org/pep-0318/#current-synta
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Python" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Python/"/>
    
    
      <category term="Python" scheme="https://wiki.blanc.site/tags/Python/"/>
    
      <category term="WeeklyPEP" scheme="https://wiki.blanc.site/tags/WeeklyPEP/"/>
    
  </entry>
  
  <entry>
    <title>Django 笔记-3-文档阅读-关于 Django 文档</title>
    <link href="https://wiki.blanc.site/archives/ab18c3c3.html"/>
    <id>https://wiki.blanc.site/archives/ab18c3c3.html</id>
    <published>2024-02-16T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Django-笔记系列"><a href="#Django-笔记系列" class="headerlink" title="Django 笔记系列"></a>Django 笔记系列</h2><ol><li><a href="https://wiki.blanc.site/archives/953c2552.html">Django 笔记-1-从请求到响应</a></li><li><a href="https://wiki.blanc.site/archives/b641d3ac.html">Django 笔记-2-源码理解-urls 篇</a></li></ol><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近春节假期在家闲着无聊，就顺着之前阅读 Django Url 模块的劲头开始读起了 Django 的源码和文档。在阅读 <a href="https://docs.djangoproject.com/zh-hans/4.2/" target="_blank" rel="external nofollow noopener noreferrer">Django 文档</a> 的过程中，我第一次发现 Django 文档实际的组织方式，于是便有了这篇文章。</p><h2 id="脉络梳理"><a href="#脉络梳理" class="headerlink" title="脉络梳理"></a>脉络梳理</h2><p>我们打开 Django 文档时一般看到的都是 <a href="https://docs.djangoproject.com/zh-hans/4.2/" target="_blank" rel="external nofollow noopener noreferrer">根目录</a>，在根目录下 Django 很贴心地使用副标题分割出了 16 个话题，并在相应的话题下面对话题所包含的内容进行展开和索引。这很好，当我对 Django 中某个部分（模块）产生疑惑的时候我都能够通过这个页面快速定位到对应的页面。我一度认为 Django 的文档就是通过话题进行组织的，但在我上次想要通读 Django 文档时，我从页面右侧我老早就发现但是从未点击的目录按钮进入到了 Django 文档真实的组织架构页面：<a href="https://docs.djangoproject.com/zh-hans/4.2/contents/" target="_blank" rel="external nofollow noopener noreferrer">目录页</a>。</p><p>根据 <a href="https://docs.djangoproject.com/zh-hans/4.2/contents/" target="_blank" rel="external nofollow noopener noreferrer">目录页</a> 可以看出 Django 文档实际被分为了 9 部分（忽略索引和词汇表），分别是：</p><ol><li><a href="https://docs.djangoproject.com/zh-hans/4.2/intro/" target="_blank" rel="external nofollow noopener noreferrer">开始 | intro</a>：通过一个简单的教程帮助初学者快速掌握 Django 开发的能力。</li><li><a href="https://docs.djangoproject.com/zh-hans/4.2/topics/" target="_blank" rel="external nofollow noopener noreferrer">使用 Django | topics</a>：<strong>重点</strong>，将 Django 中较为关键部分的概念以主题的形式进行介绍。</li><li><a href="https://docs.djangoproject.com/zh-hans/4.2/howto/" target="_blank" rel="external nofollow noopener noreferrer">操作指南 | howto</a>：一些常用流程的整理，个人觉得没啥用，这些大都能通过搜索引擎找到更加细致的回答。</li><li><a href="https://docs.djangoproject.com/zh-hans/4.2/faq/" target="_blank" rel="external nofollow noopener noreferrer">Django FAQ | faq</a>：一些常见问题的解答，其中关于如何 <a href="https://docs.djangoproject.com/zh-hans/4.2/faq/help/" target="_blank" rel="external nofollow noopener noreferrer">寻求帮助</a> 和 <a href="https://docs.djangoproject.com/zh-hans/4.2/faq/contributing/" target="_blank" rel="external nofollow noopener noreferrer">如何贡献代码</a> 的部分可以根据需要阅读一下。</li><li><a href="https://docs.djangoproject.com/zh-hans/4.2/ref/" target="_blank" rel="external nofollow noopener noreferrer">API 参考 | ref</a>：<strong>重点</strong>，将 Django 中较为关键部分的接口设计以主题的形式进行介绍。</li><li><a href="https://docs.djangoproject.com/zh-hans/4.2/misc/" target="_blank" rel="external nofollow noopener noreferrer">元文件和杂记 | misc</a>：不要被标题迷惑，里面存放的是 Django 的设计理念，内容不多可以读一下。</li><li><a href="https://docs.djangoproject.com/zh-hans/4.2/glossary/" target="_blank" rel="external nofollow noopener noreferrer">术语 | glossary</a>：没啥用，总结的并不全面。</li><li><a href="https://docs.djangoproject.com/zh-hans/4.2/releases/" target="_blank" rel="external nofollow noopener noreferrer">发行说明 | releases</a>：<strong>重点</strong>，Django 的发版日志，用于了解不同 Django 版本之间的区别。</li><li><a href="https://docs.djangoproject.com/zh-hans/4.2/internals/" target="_blank" rel="external nofollow noopener noreferrer">Django 内部 | internals</a>：<strong>重点</strong>，如果你是期望更加深入的了解、讨论或参与 Django，这里里面的章节就都适合读一读。</li></ol><p>竖线前是章节的中文翻译，竖线后是对应的 url path。</p><p><a href="https://docs.djangoproject.com/zh-hans/4.2/" target="_blank" rel="external nofollow noopener noreferrer">根目录</a> 实际上是将 <a href="https://docs.djangoproject.com/zh-hans/4.2/contents/" target="_blank" rel="external nofollow noopener noreferrer">目录页</a> 中的大部分内容通过话题的形式进行重新组合的产物。</p><h2 id="阅读方式"><a href="#阅读方式" class="headerlink" title="阅读方式"></a>阅读方式</h2><h3 id="首次使用"><a href="#首次使用" class="headerlink" title="首次使用"></a>首次使用</h3><p>适合场景：第一次了解 Django，想要快速掌握 Django 的使用方法。</p><p>这时候可以阅读 <a href="https://docs.djangoproject.com/zh-hans/4.2/intro/" target="_blank" rel="external nofollow noopener noreferrer">开始</a> 部分，里面的教程几乎是每个 Django 入门者的必经之路。</p><h3 id="搜索阅读"><a href="#搜索阅读" class="headerlink" title="搜索阅读"></a>搜索阅读</h3><p>适合场景：遇到一些比较生僻的<strong>概念</strong>，直接丢到搜索引擎没办法快速理解。</p><p>这时候就可以尝试将这个概念丢到 Django 文档右上角的搜索框中，它会为你列出在文档中所有出现过这个概念的地方。</p><p>有两个需要注意的点：</p><ol><li>只建议搜索概念性的问题，搜索词尽可能是一个单词；</li><li>建议先使用英语搜索，找到搜索结果后再切换到中文（如果没有英文阅读方法）。</li></ol><h3 id="概念阅读"><a href="#概念阅读" class="headerlink" title="概念阅读"></a>概念阅读</h3><p>适合场景：希望能够准确掌握 Django 中某个概念 50% 以上的内容。</p><p>这时候可以通过 <a href="https://docs.djangoproject.com/zh-hans/4.2/" target="_blank" rel="external nofollow noopener noreferrer">根目录</a> 配合目录页的  <a href="https://docs.djangoproject.com/zh-hans/4.2/topics/" target="_blank" rel="external nofollow noopener noreferrer">使用 Django</a> 和 <a href="https://docs.djangoproject.com/zh-hans/4.2/ref/" target="_blank" rel="external nofollow noopener noreferrer">API 参考</a> 阅读相关的内容。</p><p>这样做能够使你快速从零开始了解一个 Django 概念，并掌握其 50% 以上的内容（如何使用），剩下的部分（具体代码实现）则需要阅读 Django 源码来补齐。</p><h3 id="通读"><a href="#通读" class="headerlink" title="通读"></a>通读</h3><p>适用场景：希望对 Django 有一个全面了解，进一步掌握 Django。</p><p>这时候可以以根目录中 <a href="https://docs.djangoproject.com/zh-hans/4.2/#the-model-layer" target="_blank" rel="external nofollow noopener noreferrer">模型层</a> 以下的主题为索引，从目录页的  <a href="https://docs.djangoproject.com/zh-hans/4.2/topics/" target="_blank" rel="external nofollow noopener noreferrer">使用 Django</a>  和  <a href="https://docs.djangoproject.com/zh-hans/4.2/ref/" target="_blank" rel="external nofollow noopener noreferrer">API 参考</a>  找到相应的章节，先读  <a href="https://docs.djangoproject.com/zh-hans/4.2/topics/" target="_blank" rel="external nofollow noopener noreferrer">使用 Django</a> 中的概念讲解，再读  <a href="https://docs.djangoproject.com/zh-hans/4.2/ref/" target="_blank" rel="external nofollow noopener noreferrer">API 参考</a> 中的接口设计。</p><h2 id="关于翻译"><a href="#关于翻译" class="headerlink" title="关于翻译"></a>关于翻译</h2><p>Django 文档虽然有较为全面的中文翻译，但是从我阅读的体验来看绝大多数应该是机翻，而且因为文档中有大量的引用格式，导致在汉化的过程中会存在解析失败的情况，所以有能力的最好还是直接阅读英文版。实在不行我也建议中英文混合阅读，可以中文英文各开一个页面，也可以使用能够保留英文原文的翻译插件。</p><h2 id="Django-社区"><a href="#Django-社区" class="headerlink" title="Django 社区"></a>Django 社区</h2><p>在 <a href="https://docs.djangoproject.com/zh-hans/4.2/internals/" target="_blank" rel="external nofollow noopener noreferrer">Django 内部 | internals</a> 中有对 Django 社区的全面介绍，这里只简单介绍几个常用的：</p><ol><li><a href="https://forum.djangoproject.com/" target="_blank" rel="external nofollow noopener noreferrer">Github 源码</a>：没有 Issue；</li><li><a href="https://forum.djangoproject.com/" target="_blank" rel="external nofollow noopener noreferrer">Django 论坛</a>： 可以提问或者宣传自己的 Django 项目；</li><li>Django 邮件列表：Django 有几个官方的邮件列表，感兴趣的话可以去文档里翻找一下；</li><li><a href="https://code.djangoproject.com/query" target="_blank" rel="external nofollow noopener noreferrer">Django Issues</a>：Django 独立的 issue 提交平台，可以在这里提交或搜索你遇到的 bug；</li></ol><p>注：上面的几个社区平台基本全是英文的，所以需要一定的英语阅读方法；</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://docs.djangoproject.com/zh-hans/4.2/" target="_blank" rel="external nofollow noopener noreferrer">Django 文档-4.2-中文</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;Django-笔记系列&quot;&gt;&lt;a href=&quot;#Django-笔记系列&quot; class=&quot;headerlink&quot; title=&quot;Django 笔记系列&quot;&gt;&lt;/a&gt;Django 笔记系列&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.blanc.
      
    
    </summary>
    
    
      <category term="2-软件使用" scheme="https://wiki.blanc.site/categories/2-%E8%BD%AF%E4%BB%B6%E4%BD%BF%E7%94%A8/"/>
    
      <category term="Django" scheme="https://wiki.blanc.site/categories/2-%E8%BD%AF%E4%BB%B6%E4%BD%BF%E7%94%A8/Django/"/>
    
    
      <category term="学习笔记" scheme="https://wiki.blanc.site/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
      <category term="Django" scheme="https://wiki.blanc.site/tags/Django/"/>
    
  </entry>
  
  <entry>
    <title>Django 笔记-2-源码理解-urls 篇</title>
    <link href="https://wiki.blanc.site/archives/b641d3ac.html"/>
    <id>https://wiki.blanc.site/archives/b641d3ac.html</id>
    <published>2024-02-05T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Django-笔记系列"><a href="#Django-笔记系列" class="headerlink" title="Django 笔记系列"></a>Django 笔记系列</h2><ol><li><a href="https://wiki.blanc.site/archives/953c2552.html">Django 笔记-1-从请求到响应</a></li></ol><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>注：本文使用 Django 版本：<code>4.2.x</code></p><p>最近在处理公司接口端（基于 DRF）业务逻辑的时候想要通过 <a href="https://www.django-rest-framework.org/api-guide/routers/#defaultrouter" target="_blank" rel="external nofollow noopener noreferrer">DRF 的 DefaultRouter</a> 定制化一个类似 <a href="https://swagger.io/" target="_blank" rel="external nofollow noopener noreferrer">Swagger</a> 的 API 页面展示，但是在编写路由解析方法的时候却犯了难。之前我能只理解了如何使用 Django urls 模块中的方法生成满足业务需求的路由，但是我还真没研究过怎么收集现有路由，并进行遍历和反向解析，于是便有了此次源码阅读。</p><p><strong>本文以 Django 初始化和请求流程为主线，研究在这个过程中 Django 的 urls 模块做了哪些工作</strong>，并不是详细讲解 urls 模块下的全部方法。</p><h2 id="流程梳理"><a href="#流程梳理" class="headerlink" title="流程梳理"></a>流程梳理</h2><p>本章以最常用的 <code>python manage.py runserve</code> 为例，梳理 Django 初始化和请求流程。这里为了阅读体验简化了步骤，想了解更完整的请求流程可搭配 <a href="https://wiki.blanc.site/archives/953c2552.html">Django 笔记-1-从请求到响应</a> 进行阅读：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">python manage.py runserver</span><br><span class="line">    ↓</span><br><span class="line">django.core.management.commands.runserver.Command.handle</span><br><span class="line">    ↓</span><br><span class="line">runserver.Command.run(**options)</span><br><span class="line">    ↓</span><br><span class="line">runserver.Command.inner_run</span><br><span class="line">    ↓  handler&#x3D;runserver.Command.get_handler()</span><br><span class="line">↓                     ↓</span><br><span class="line">↓  django.core.wsgi.get_wsgi_application() → django.core.handlers.wsgi.WSGIHandler()</span><br><span class="line">django.core.servers.basehttp.run(..., wsgi_handler&#x3D;handler, ...)</span><br><span class="line">    ↓</span><br><span class="line">django.core.servers.basehttp.WSGIServer.serve_forever()</span><br><span class="line">↓ ← request 请求进入</span><br><span class="line">  .....</span><br><span class="line">  ↓</span><br><span class="line">django.core.servers.basehttp.WSGIRequestHandler.handle_one_request()</span><br><span class="line">↓</span><br><span class="line">django.core.servers.basehttp.ServerHandler.run(self.server.get_app())</span><br><span class="line">↓</span><br><span class="line">self.server.get_app()()</span><br><span class="line">↓</span><br><span class="line">django.core.handlers.wsgi.WSGIHandler()()</span><br><span class="line">↓</span><br><span class="line">django.core.handlers.wsgi.WSGIHandler.__call__</span><br><span class="line">    ↓</span><br><span class="line">django.core.handlers.wsgi.WSGIHandler.get_response(request)</span><br><span class="line">    ↓</span><br><span class="line">django.core.handlers.wsgi.WSGIHandler._middleware_chain(request)</span><br><span class="line">    ↓ </span><br><span class="line">django.core.handlers.wsgi.WSGIHandler._get_response</span><br><span class="line">    ↓</span><br><span class="line">django.core.handlers.wsgi.WSGIHandler.resolve_request</span><br><span class="line">    ↓</span><br><span class="line">django.urls.resolvers.URLResolver(settings.ROOT_URLCONF).resolve(request.path_info)</span><br><span class="line">    ↓ return (callback, callback_args, callback_kwargs) from URLResolver.resolve(request.path_info)</span><br><span class="line">   ...</span><br><span class="line">    ↓</span><br><span class="line">response &#x3D; callback(request, ...)</span><br></pre></td></tr></table></figure><p>可以看到最后最关键的部分是调用了 <code>django.urls.resolvers.URLResolver(settings.ROOT_URLCONF).resolve(request.path_info)</code> 这样的一个方法，而这一个链式调用是由 <code>django.core.handlers.wsgi.WSGIHandler.resolve_request</code> 产生的，下面我们就以 <code>resolve_request</code> 方法为入口详细分析整个 urls 模块的调用链。</p><h2 id="resolve-request"><a href="#resolve-request" class="headerlink" title="resolve_request"></a>resolve_request</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.core.handlers.base.BaseHandler（WSGIHandler 的父类）</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">resolve_request</span><span class="params">(self, request)</span>:</span>  </span><br><span class="line"><span class="comment"># 一般来说 request 是没有 urlconf 的所以走的是 else</span></span><br><span class="line">    <span class="keyword">if</span> hasattr(request, <span class="string">'urlconf'</span>):</span><br><span class="line">        urlconf = request.urlconf  </span><br><span class="line">        set_urlconf(urlconf)  </span><br><span class="line">        resolver = get_resolver(urlconf)  </span><br><span class="line">    <span class="keyword">else</span>:  </span><br><span class="line">        resolver = get_resolver()  </span><br><span class="line">    resolver_match = resolver.resolve(request.path_info)  </span><br><span class="line">    request.resolver_match = resolver_match  </span><br><span class="line">    <span class="keyword">return</span> resolver_match</span><br><span class="line"></span><br><span class="line"><span class="comment"># django.urls.get_resolver</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_resolver</span><span class="params">(urlconf=None)</span>:</span>  </span><br><span class="line">    <span class="keyword">if</span> urlconf <span class="keyword">is</span> <span class="literal">None</span>:  </span><br><span class="line">        urlconf = settings.ROOT_URLCONF  </span><br><span class="line">    <span class="keyword">return</span> _get_cached_resolver(urlconf)</span><br><span class="line"></span><br><span class="line"><span class="comment"># django.urls._get_cached_resolver</span></span><br><span class="line"><span class="meta">@functools.lru_cache(maxsize=None)  </span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_get_cached_resolver</span><span class="params">(urlconf=None)</span>:</span>  </span><br><span class="line">    <span class="keyword">return</span> URLResolver(RegexPattern(<span class="string">r'^/'</span>), urlconf)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 通过这三个方法的链式调用我们能够得知最终在 resolve_request 使用的 resolver 是</span></span><br><span class="line"><span class="comment"># URLResolver(RegexPattern(r'^/'), settings.ROOT_URLCONF)</span></span><br><span class="line"><span class="comment"># 从函数调用上我们还可以发现，Django 使用 functools.lru_cache 将整个 resolver 的结果缓存下来了</span></span><br></pre></td></tr></table></figure><h2 id="settings-ROOT-URLCONF"><a href="#settings-ROOT-URLCONF" class="headerlink" title="settings.ROOT_URLCONF"></a>settings.ROOT_URLCONF</h2><p>Django 文档对于 settings.ROOT_URLCONF 的定义是：</p><blockquote><p>ROOT_URLCONF<br>默认：未定义</p><p>一个字符串，代表你的根 URLconf 的完整 Python 导入路径，例如 “mydjangoapps.urls”。可以通过在传入的 HttpRequest 对象上设置属性 urlconf 来覆盖每个请求。详情请参见 Django 如何处理一个请求。</p></blockquote><p>一般情况下就是我们使用 <code>django-admin startproject &lt;projectname&gt;</code> 启动项目后在 <code>&lt;projectname&gt;</code> 目录下的 urls.py 模块，这里为了方便讲解我们模拟这样一个项目：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">testapp\</span><br><span class="line">apps.py</span><br><span class="line">views.py</span><br><span class="line">urls.py</span><br><span class="line">testproject\</span><br><span class="line">__init__.py</span><br><span class="line">asgi.py</span><br><span class="line">settings.py</span><br><span class="line">urls.py</span><br><span class="line">wsgi.py</span><br><span class="line">manage.py</span><br></pre></td></tr></table></figure><p>其中几个重要文件的内容分别为：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#testapp.views</span></span><br><span class="line"><span class="keyword">from</span> django.http.response <span class="keyword">import</span> HttpResponse</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">test</span><span class="params">(request)</span>:</span></span><br><span class="line"><span class="keyword">return</span> HttpResponse(<span class="string">"test"</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#testapp.urls</span></span><br><span class="line"><span class="keyword">from</span> .views <span class="keyword">import</span> test  </span><br><span class="line"><span class="keyword">from</span> django.urls <span class="keyword">import</span> re_path  </span><br><span class="line">  </span><br><span class="line">app_name = <span class="string">"testapp"</span>  </span><br><span class="line">urlpatterns = [  </span><br><span class="line">    re_path(<span class="string">r'^test-api/'</span>, test, name=<span class="string">"testapi"</span>),  </span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="comment">#testproject.urls</span></span><br><span class="line"><span class="keyword">from</span> django.contrib <span class="keyword">import</span> admin  </span><br><span class="line"><span class="keyword">from</span> django.urls <span class="keyword">import</span> path, re_path, include  </span><br><span class="line">  </span><br><span class="line">urlpatterns = [  </span><br><span class="line">    path(<span class="string">'admin/'</span>, admin.site.urls),</span><br><span class="line">    re_path(<span class="string">r'^test2/'</span>, test, name=<span class="string">"test2"</span>),  </span><br><span class="line">    re_path(<span class="string">'test/'</span>, include(<span class="string">"testapp.urls"</span>, namespace=<span class="string">"test"</span>)),</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h2 id="path-re-path-include"><a href="#path-re-path-include" class="headerlink" title="path, re_path, include"></a>path, re_path, include</h2><p>通过观察 <code>testproject.urls</code> 不难看出在 Django 项目下注册路由主要是通过 <code>django.urls</code> 模块下的 path，re_path 和 include 三个方法，我们先观察一下这三个方法的定义：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.urls.conf.py</span></span><br><span class="line"><span class="keyword">from</span> functools <span class="keyword">import</span> partial</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">include</span><span class="params">(arg, namespace=None)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_path</span><span class="params">(route, view, kwargs=None, name=None, Pattern=None)</span>:</span></span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">path = partial(_path, Pattern=RoutePattern)</span><br><span class="line">re_path = partial(_path, Pattern=RegexPattern)</span><br></pre></td></tr></table></figure><p>将定义中使用 partial 的部分替换后可得</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">path = _path(..., Pattern=RoutePattern)</span><br><span class="line">re_path = _path(..., Pattern=RegexPattern)</span><br><span class="line"></span><br><span class="line">include(arg, namespace=<span class="literal">None</span>)</span><br></pre></td></tr></table></figure><h2 id="re-path-‘test-‘-include-“testapp-urls”-namespace-”testapp”"><a href="#re-path-‘test-‘-include-“testapp-urls”-namespace-”testapp”" class="headerlink" title="re_path(‘test/‘, include(“testapp.urls”, namespace=”testapp”))"></a>re_path(‘test/‘, include(“testapp.urls”, namespace=”testapp”))</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.urls.conf</span></span><br><span class="line"><span class="keyword">from</span> importlib <span class="keyword">import</span> import_module</span><br><span class="line"></span><br><span class="line"><span class="comment"># include("testapp.urls", namespace="test")</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">include</span><span class="params">(arg, namespace=None)</span>:</span></span><br><span class="line"><span class="comment"># arg = "testapp.urls"</span></span><br><span class="line"><span class="comment"># namespace = "test"</span></span><br><span class="line">    app_name = <span class="literal">None</span></span><br><span class="line">    <span class="keyword">if</span> isinstance(arg, tuple):</span><br><span class="line">    <span class="comment"># 如果 arg 是元组则解包成 路由模块 和 应用名称</span></span><br><span class="line">        <span class="keyword">try</span>:  </span><br><span class="line">            urlconf_module, app_name = arg  </span><br><span class="line">        <span class="keyword">except</span> ValueError:  </span><br><span class="line">            <span class="keyword">if</span> namespace:  </span><br><span class="line">                <span class="keyword">raise</span> ImproperlyConfigured(...)  </span><br><span class="line">            <span class="keyword">raise</span> ImproperlyConfigured(...)  </span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">    <span class="comment"># 否则 路由模块 就是 arg</span></span><br><span class="line">        urlconf_module = arg  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> isinstance(urlconf_module, str):  </span><br><span class="line">    <span class="comment"># 如果 urlconf_module 是字符串尝试导入</span></span><br><span class="line">        urlconf_module = import_module(urlconf_module)  </span><br><span class="line"></span><br><span class="line">    <span class="comment"># 从 路由模块 中获取 路由模式 和 应用名称</span></span><br><span class="line">    patterns = getattr(urlconf_module, <span class="string">"urlpatterns"</span>, urlconf_module)  </span><br><span class="line">    app_name = getattr(urlconf_module, <span class="string">"app_name"</span>, app_name)  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> namespace <span class="keyword">and</span> <span class="keyword">not</span> app_name:  </span><br><span class="line">        <span class="keyword">raise</span> ImproperlyConfigured(...)  </span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果没有 namespace 那么将 app_name 设置为 namespace</span></span><br><span class="line">    namespace = namespace <span class="keyword">or</span> app_name </span><br><span class="line"></span><br><span class="line"><span class="comment"># [re_path(r'^test-api/', test, name="testapi")]</span></span><br><span class="line"><span class="comment"># 检查 路由模式 中的 匹配项 是否存在问题</span></span><br><span class="line">    <span class="keyword">if</span> isinstance(patterns, (list, tuple)):  </span><br><span class="line">        <span class="keyword">for</span> url_pattern <span class="keyword">in</span> patterns:  </span><br><span class="line">            pattern = getattr(url_pattern, <span class="string">"pattern"</span>, <span class="literal">None</span>)  </span><br><span class="line">            <span class="keyword">if</span> isinstance(pattern, LocalePrefixPattern):  </span><br><span class="line">            <span class="comment"># 这里为什么对 LocalePrefixPattern 报错我不是很理解一下是 AI 给出的回答：</span></span><br><span class="line">            <span class="comment"># 在 include 中不允许使用 i18n_patterns</span></span><br><span class="line">            <span class="comment"># 是因为 Django 的国际化和本地化系统（i18n）的设计限制</span></span><br><span class="line">            <span class="comment"># i18n_patterns 是用于在 URL 中添加语言前缀的便捷方法</span></span><br><span class="line">            <span class="comment"># 但由于其特性，它只能在主 URL 配置中使用</span></span><br><span class="line">            <span class="comment"># 这种限制是为了确保URL配置的一致性和可维护性。</span></span><br><span class="line">            <span class="comment"># 如果允许在包含的URL配置中使用i18n_patterns，</span></span><br><span class="line">            <span class="comment"># 可能会导致混乱和不一致的URL结构，从而增加了维护和调试的复杂性。</span></span><br><span class="line"><span class="comment"># 因此，为了遵循最佳实践并确保代码的清晰性，</span></span><br><span class="line"><span class="comment"># Django限制了i18n_patterns的使用范围，只允许在主URL配置中使用。</span></span><br><span class="line">                <span class="keyword">raise</span> ImproperlyConfigured(...)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 可以看到 include 方法最终是将传入的内容解析成了一个三元元组</span></span><br><span class="line">    <span class="comment"># 分别是 路由模块、应用名称和命名空间</span></span><br><span class="line"><span class="comment"># 返回 (&lt;testapp.urls&gt;, "testapp", "test")</span></span><br><span class="line">    <span class="keyword">return</span> (urlconf_module, app_name, namespace)</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.urls.conf</span></span><br><span class="line"><span class="comment"># admin.site.urls 具体的对应关系可以去 django.contrib.admin.sites.AdminSite 查看</span></span><br><span class="line"><span class="comment"># path('admin/', admin.site.urls) </span></span><br><span class="line"><span class="comment"># _path('admin/', (self.get_urls(), "admin", self.name), Pattern=RoutePattern)</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># re_path('test2/', test)</span></span><br><span class="line"><span class="comment"># _path('test2/', test, Pattern=RegexPattern)</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># re_path('test/', (&lt;testapp.urls&gt;, "testapp", "test"))</span></span><br><span class="line"><span class="comment"># _path('test/', (&lt;testapp.urls&gt;, "testapp", "test"), Pattern=RegexPattern)</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_path</span><span class="params">(route, view, kwargs=None, name=None, Pattern=None)</span>:</span>  </span><br><span class="line">    <span class="keyword">from</span> django.views <span class="keyword">import</span> View</span><br><span class="line">  </span><br><span class="line">    <span class="keyword">if</span> kwargs <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span> <span class="keyword">and</span> <span class="keyword">not</span> isinstance(kwargs, dict):  </span><br><span class="line">        <span class="keyword">raise</span> TypeError(...)</span><br><span class="line">    <span class="keyword">if</span> isinstance(view, (list, tuple)):</span><br><span class="line">    <span class="comment"># 如果 view 是数组或元组，使用 RegexPattern 实例化匹配规则并返回 URLResolver 解析器</span></span><br><span class="line">        pattern = Pattern(route, is_endpoint=<span class="literal">False</span>)  </span><br><span class="line">        urlconf_module, app_name, namespace = view  </span><br><span class="line">        <span class="keyword">return</span> URLResolver(  </span><br><span class="line">            pattern,  </span><br><span class="line">            urlconf_module,  </span><br><span class="line">            kwargs,  </span><br><span class="line">            app_name=app_name,  </span><br><span class="line">            namespace=namespace,  </span><br><span class="line">        )</span><br><span class="line">    <span class="keyword">elif</span> callable(view):  </span><br><span class="line">    <span class="comment"># 如果 view 是可调用对象，使用 RoutePattern 实例化匹配规则并返回 URLPattern 匹配器</span></span><br><span class="line">        pattern = Pattern(route, name=name, is_endpoint=<span class="literal">True</span>)  </span><br><span class="line">        <span class="keyword">return</span> URLPattern(pattern, view, kwargs, name)  </span><br><span class="line">    <span class="keyword">elif</span> isinstance(view, View):  </span><br><span class="line">        view_cls_name = view.__class__.__name__  </span><br><span class="line">        <span class="keyword">raise</span> TypeError(...)  </span><br><span class="line">    <span class="keyword">else</span>:  </span><br><span class="line">        <span class="keyword">raise</span> TypeError(...)</span><br></pre></td></tr></table></figure><h2 id="RoutePattern-与-RegexPattern"><a href="#RoutePattern-与-RegexPattern" class="headerlink" title="RoutePattern 与 RegexPattern"></a>RoutePattern 与 RegexPattern</h2><p>RoutePattern 与 RegexPattern 最后都会被转换为正则匹配，只是 RoutePattern 在定义的时候可以使用特殊的语法定义参数变量，而 RegexPattern 则需要使用正则匹配去表达这些内容，例如 <code>RoutePattern(&#39;foo/&lt;int:pk&gt;&#39;)</code> 会被转换为 <code>RegexPattern(&#39;^foo\\/(?P&lt;pk&gt;[0-9]+)&#39;)</code>。感兴趣的可以看一下 <code>django.urls.resolvers._route_to_regex</code> 方法。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.urls.resolvers.py</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">LocaleRegexDescriptor</span>:</span></span><br><span class="line"><span class="comment"># LocaleRegexDescriptor 是一个描述器，可以查看 参考文章3 进行学习</span></span><br><span class="line"><span class="comment"># 这个描述器的功能还是比较简单的大概理解就是：</span></span><br><span class="line"><span class="comment"># 初始化时设置一个变量名称为 attr 的属性名称</span></span><br><span class="line"><span class="comment"># 将调用实例中名字和变量 attr 相同的属性复制给 pattern</span></span><br><span class="line"><span class="comment"># 将调用实例的 regx 属性设置为调用实例的 _compile(pattern) 的返回值</span></span><br><span class="line"><span class="comment"># 看文字一大串好像很难理解</span></span><br><span class="line"><span class="comment"># **其实就是把传给 RoutePattern 或 RegexPattern 的匹配字符串变成一个正则对象**</span></span><br><span class="line"><span class="comment"># 之所以搞这么多步骤主要是为了在调用 __get__ 方法的时候设置一些国际化相关的内容</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, attr)</span>:</span>  </span><br><span class="line">        self.attr = attr  </span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__get__</span><span class="params">(self, instance, cls=None)</span>:</span>  </span><br><span class="line">    <span class="keyword">if</span> instance <span class="keyword">is</span> <span class="literal">None</span>:  </span><br><span class="line">            <span class="keyword">return</span> self  </span><br><span class="line">        pattern = getattr(instance, self.attr)  </span><br><span class="line">        <span class="keyword">if</span> isinstance(pattern, str):  </span><br><span class="line">            instance.__dict__[<span class="string">"regex"</span>] = instance._compile(pattern)  </span><br><span class="line">            <span class="keyword">return</span> instance.__dict__[<span class="string">"regex"</span>]  </span><br><span class="line">        language_code = get_language()  </span><br><span class="line">        <span class="keyword">if</span> language_code <span class="keyword">not</span> <span class="keyword">in</span> instance._regex_dict:  </span><br><span class="line">            instance._regex_dict[language_code] = instance._compile(str(pattern))  </span><br><span class="line">        <span class="keyword">return</span> instance._regex_dict[language_code]</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RoutePattern</span><span class="params">(CheckURLMixin)</span>:</span>  </span><br><span class="line">    regex = LocaleRegexDescriptor(<span class="string">"_route"</span>)  </span><br><span class="line">  </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, route, name=None, is_endpoint=False)</span>:</span> </span><br><span class="line">    <span class="comment"># 一般来说 route 就是调用 path 或 re_path 时传入的那个用于做路由匹配的字符串</span></span><br><span class="line">    <span class="comment"># path('admin/', admin.site.urls)  中的 admin/</span></span><br><span class="line">        self._route = route  </span><br><span class="line">        self._regex_dict = &#123;&#125;  </span><br><span class="line">        self._is_endpoint = is_endpoint  </span><br><span class="line">        self.name = name</span><br><span class="line">        self.converters = _route_to_regex(str(route), is_endpoint)[<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">match</span><span class="params">(self, path)</span>:</span></span><br><span class="line"><span class="comment"># 在解析是被调用的匹配方法，很重要但是没什么难懂的地方</span></span><br><span class="line">    match = self.regex.search(path)  </span><br><span class="line">    <span class="keyword">if</span> match:  </span><br><span class="line">        kwargs = match.groupdict()  </span><br><span class="line">        <span class="keyword">for</span> key, value <span class="keyword">in</span> kwargs.items():  </span><br><span class="line">            converter = self.converters[key]  </span><br><span class="line">            <span class="keyword">try</span>:  </span><br><span class="line">                kwargs[key] = converter.to_python(value)  </span><br><span class="line">            <span class="keyword">except</span> ValueError:  </span><br><span class="line">                <span class="keyword">return</span> <span class="literal">None</span>  </span><br><span class="line">        <span class="keyword">return</span> path[match.end() :], (), kwargs  </span><br><span class="line">    <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_compile</span><span class="params">(self, route)</span>:</span>  </span><br><span class="line">    <span class="keyword">return</span> re.compile(_route_to_regex(route, self._is_endpoint)[<span class="number">0</span>])</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RegexPattern</span><span class="params">(CheckURLMixin)</span>:</span>  </span><br><span class="line">    regex = LocaleRegexDescriptor(<span class="string">"_regex"</span>)  </span><br><span class="line">  </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, regex, name=None, is_endpoint=False)</span>:</span></span><br><span class="line">    <span class="comment"># 同上，只是不叫 route 叫 regx</span></span><br><span class="line">    <span class="comment"># re_path('test/', (&lt;testapp.urls&gt;, "testapp", "test")) 中的 test/</span></span><br><span class="line">        self._regex = regex  </span><br><span class="line">        self._regex_dict = &#123;&#125;  </span><br><span class="line">        self._is_endpoint = is_endpoint  </span><br><span class="line">        self.name = name  </span><br><span class="line">        self.converters = &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">match</span><span class="params">(self, path)</span>:</span></span><br><span class="line"><span class="comment"># 在解析是被调用的匹配方法，很重要但是没什么难懂的地方</span></span><br><span class="line">    match = (  </span><br><span class="line">        self.regex.fullmatch(path)  </span><br><span class="line">        <span class="keyword">if</span> self._is_endpoint <span class="keyword">and</span> self.regex.pattern.endswith(<span class="string">"$"</span>)  </span><br><span class="line">        <span class="keyword">else</span> self.regex.search(path)  </span><br><span class="line">    )  </span><br><span class="line">    <span class="keyword">if</span> match:  </span><br><span class="line">        kwargs = match.groupdict()  </span><br><span class="line">        args = () <span class="keyword">if</span> kwargs <span class="keyword">else</span> match.groups()  </span><br><span class="line">        kwargs = &#123;k: v <span class="keyword">for</span> k, v <span class="keyword">in</span> kwargs.items() <span class="keyword">if</span> v <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>&#125;  </span><br><span class="line">        <span class="keyword">return</span> path[match.end() :], args, kwargs  </span><br><span class="line">    <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_compile</span><span class="params">(self, regex)</span>:</span>  </span><br><span class="line"><span class="keyword">try</span>:  </span><br><span class="line"><span class="keyword">return</span> re.compile(regex)  </span><br><span class="line"><span class="keyword">except</span> re.error <span class="keyword">as</span> e:  </span><br><span class="line"><span class="keyword">raise</span> ImproperlyConfigured(...) <span class="keyword">from</span> e</span><br></pre></td></tr></table></figure><h2 id="URLPattern-与-URLResolver"><a href="#URLPattern-与-URLResolver" class="headerlink" title="URLPattern 与 URLResolver"></a>URLPattern 与 URLResolver</h2><p>URLPattern 与 URLResolver 是不同模式路由匹配方案，URLPattern 用于定义简单路由基本上可以理解为一个萝卜一个坑，一个 URLPattern 只负责一个视图的匹配，而 URLResolver 则是通过命名空间和应用名称将一组路由（这一组路由中也可能只有一个路由）汇集到一起用于匹配。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.urls.resolvers.py</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ResolverMatch</span>:</span></span><br><span class="line"><span class="comment"># 用于路由匹配解析结果的类，主要的方法是 __getitem__ 方法</span></span><br><span class="line"><span class="comment"># 最后会使用 __gititem__ 进行解包操作</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None, route=None, ...)</span>:</span></span><br><span class="line">self.func = func</span><br><span class="line">self.args = args</span><br><span class="line">self.kwargs = kwargs</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__getitem__</span><span class="params">(self, index)</span>:</span>  </span><br><span class="line">    <span class="keyword">return</span> (self.func, self.args, self.kwargs)[index]</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">URLPattern</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, pattern, callback, default_args=None, name=None)</span>:</span>  </span><br><span class="line">        self.pattern = pattern  </span><br><span class="line">        self.callback = callback  <span class="comment"># the view  </span></span><br><span class="line">        self.default_args = default_args <span class="keyword">or</span> &#123;&#125;  </span><br><span class="line">        self.name = name</span><br><span class="line">        ...</span><br><span class="line">        </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">resolve</span><span class="params">(self, path)</span>:</span>  </span><br><span class="line">    match = self.pattern.match(path)  </span><br><span class="line">    <span class="keyword">if</span> match:</span><br><span class="line">        new_path, args, captured_kwargs = match  </span><br><span class="line">        kwargs = &#123;**captured_kwargs, **self.default_args&#125;  </span><br><span class="line">        <span class="keyword">return</span> ResolverMatch(  </span><br><span class="line">            self.callback,  </span><br><span class="line">            args,  </span><br><span class="line">            kwargs,  </span><br><span class="line">            self.pattern.name,  </span><br><span class="line">            route=str(self.pattern),  </span><br><span class="line">            captured_kwargs=captured_kwargs,  </span><br><span class="line">            extra_kwargs=self.default_args,  </span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">URLResolver</span>:</span>  </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(  </span></span></span><br><span class="line"><span class="function"><span class="params">        self, pattern, urlconf_name, default_kwargs=None, app_name=None, namespace=None  </span></span></span><br><span class="line"><span class="function"><span class="params">    )</span>:</span>  </span><br><span class="line">        self.pattern = pattern  </span><br><span class="line">        self.urlconf_name = urlconf_name  </span><br><span class="line">        self.callback = <span class="literal">None</span>  </span><br><span class="line">        self.default_kwargs = default_kwargs <span class="keyword">or</span> &#123;&#125;  </span><br><span class="line">        self.namespace = namespace  </span><br><span class="line">        self.app_name = app_name  </span><br><span class="line">        self._reverse_dict = &#123;&#125;  </span><br><span class="line">        self._namespace_dict = &#123;&#125;  </span><br><span class="line">        self._app_dict = &#123;&#125;  </span><br><span class="line">        self._callback_strs = set()  </span><br><span class="line">        self._populated = <span class="literal">False</span>  </span><br><span class="line">        self._local = Local()</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">resolve</span><span class="params">(self, path)</span>:</span>  </span><br><span class="line"><span class="comment"># URLResolver.resolve 方法是一个比较绕的执行流程</span></span><br><span class="line"><span class="comment"># 感兴趣的话可以将前面得到的结果拿过来然后自己模拟输入一些路径来尝试匹配</span></span><br><span class="line"><span class="comment"># 例如 _path('test/', (&lt;testapp.urls&gt;, "testapp", "test"), Pattern=RegexPattern).resolve(&lt;request_path&gt;)</span></span><br><span class="line">    path = str(path)</span><br><span class="line">    tried = []  </span><br><span class="line">    match = self.pattern.match(path)  </span><br><span class="line">    <span class="keyword">if</span> match:  </span><br><span class="line">        new_path, args, kwargs = match  </span><br><span class="line">        <span class="keyword">for</span> pattern <span class="keyword">in</span> self.url_patterns:  </span><br><span class="line">            <span class="keyword">try</span>:  </span><br><span class="line">                sub_match = pattern.resolve(new_path)</span><br><span class="line">            <span class="keyword">except</span> Resolver404 <span class="keyword">as</span> e:  </span><br><span class="line">                self._extend_tried(tried, pattern, e.args[<span class="number">0</span>].get(<span class="string">"tried"</span>))  </span><br><span class="line">            <span class="keyword">else</span>:  </span><br><span class="line">                <span class="keyword">if</span> sub_match:  </span><br><span class="line">                    sub_match_dict = &#123;**kwargs, **self.default_kwargs&#125;  </span><br><span class="line">                    sub_match_dict.update(sub_match.kwargs)  </span><br><span class="line">                    sub_match_args = sub_match.args  </span><br><span class="line">                    <span class="keyword">if</span> <span class="keyword">not</span> sub_match_dict:  </span><br><span class="line">                        sub_match_args = args + sub_match.args  </span><br><span class="line">                    current_route = (  </span><br><span class="line">                        <span class="string">""</span>  </span><br><span class="line">                        <span class="keyword">if</span> isinstance(pattern, URLPattern)  </span><br><span class="line">                        <span class="keyword">else</span> str(pattern.pattern)  </span><br><span class="line">                    )  </span><br><span class="line">                    self._extend_tried(tried, pattern, sub_match.tried)  </span><br><span class="line">                    <span class="keyword">return</span> ResolverMatch(</span><br><span class="line">                        sub_match.func,  </span><br><span class="line">                        sub_match_args,  </span><br><span class="line">                        sub_match_dict,  </span><br><span class="line">                        sub_match.url_name,  </span><br><span class="line">                        [self.app_name] + sub_match.app_names,  </span><br><span class="line">                        [self.namespace] + sub_match.namespaces,  </span><br><span class="line">                        self._join_route(current_route, sub_match.route),  </span><br><span class="line">                        tried,  </span><br><span class="line">                        captured_kwargs=sub_match.captured_kwargs,  </span><br><span class="line">                        extra_kwargs=&#123;  </span><br><span class="line">                            **self.default_kwargs,  </span><br><span class="line">                            **sub_match.extra_kwargs,  </span><br><span class="line">                        &#125;,  </span><br><span class="line">                    )  </span><br><span class="line">                tried.append([pattern])  </span><br><span class="line">        <span class="keyword">raise</span> Resolver404(&#123;<span class="string">"tried"</span>: tried, <span class="string">"path"</span>: new_path&#125;)  </span><br><span class="line">    <span class="keyword">raise</span> Resolver404(&#123;<span class="string">"path"</span>: path&#125;)</span><br></pre></td></tr></table></figure><h2 id="调用出栈"><a href="#调用出栈" class="headerlink" title="调用出栈"></a>调用出栈</h2><p>伴随着 URLPattern 与 URLResolver 被理解，我们可以沿着目录开始一层一层的将调用结果出栈，最终返回到 <a href="#resolve_request">resolve_request</a> 的最后三行：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.core.handler.base.BaseHandler.resolve_request</span></span><br><span class="line">resolver_match = resolver.resolve(request.path_info)  </span><br><span class="line">request.resolver_match = resolver_match</span><br><span class="line"><span class="comment"># 这里返回的就是一个 ResolverMatch 对象</span></span><br><span class="line"><span class="keyword">return</span> resolver_match</span><br><span class="line"></span><br><span class="line"><span class="comment"># django.core.handler.base.BaseHandler._get_response</span></span><br><span class="line"><span class="comment"># 这里通过解包调用 ResolverMatch 对象的 __getitem__ 方法</span></span><br><span class="line">callback, callback_args, callback_kwargs = self.resolve_request(request)</span><br><span class="line">...</span><br><span class="line"><span class="comment"># 最终经过一系列过程大概会执行一个类似这样的方法</span></span><br><span class="line"><span class="comment"># 其中 callback 就是根据 request 信息，通过 url 匹配获取到的视图方法</span></span><br><span class="line">response = callback(request, ...)</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">path(&#39;admin&#x2F;&#39;, admin.site.urls),</span><br><span class="line">re_path(r&#39;^test2&#x2F;&#39;, test, name&#x3D;&quot;test2&quot;),  </span><br><span class="line">re_path(&#39;test&#x2F;&#39;, include(&quot;testapp.urls&quot;, namespace&#x3D;&quot;test&quot;)),</span><br><span class="line">               re_path(r&#39;^test-api&#x2F;&#39;, test, name&#x3D;&quot;testapi&quot;),  </span><br><span class="line">我们的 urlpatterns 类似上面这样，这里我们可以想象一下：假设有一个 &#x2F;test2&#x2F; 请求进入会怎么进行：</span><br><span class="line">1. 首先是走到 _get_cached_resolver(urlconf&#x3D;None) 返回的 URLResolver(RegexPattern(r&#39;^&#x2F;&#39;), urlconf)</span><br><span class="line">2. URLResolver(RegexPattern(r&#39;^&#x2F;&#39;)） 将 &#x2F;test2&#x2F; 中的 &#x2F; 拿走，剩余 test2&#x2F; 继续匹配</span><br><span class="line">3. 匹配到 test2&#x2F; 最终执行 test 方法</span><br><span class="line">4. 将匹配结果返回，最终 django.core.handler.base.BaseHandler._get_response 调用 test 方法</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://docs.djangoproject.com/zh-hans/4.2/" target="_blank" rel="external nofollow noopener noreferrer">Django 文档</a></li><li><a href="https://github.com/django/django/tree/stable/4.2.x" target="_blank" rel="external nofollow noopener noreferrer">Django 4.2.x 源码</a></li><li><a href="https://docs.python.org/zh-cn/3/howto/descriptor.html" target="_blank" rel="external nofollow noopener noreferrer">Python 文档-描述器</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;Django-笔记系列&quot;&gt;&lt;a href=&quot;#Django-笔记系列&quot; class=&quot;headerlink&quot; title=&quot;Django 笔记系列&quot;&gt;&lt;/a&gt;Django 笔记系列&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.blanc.
      
    
    </summary>
    
    
      <category term="2-软件使用" scheme="https://wiki.blanc.site/categories/2-%E8%BD%AF%E4%BB%B6%E4%BD%BF%E7%94%A8/"/>
    
      <category term="Django" scheme="https://wiki.blanc.site/categories/2-%E8%BD%AF%E4%BB%B6%E4%BD%BF%E7%94%A8/Django/"/>
    
    
      <category term="学习笔记" scheme="https://wiki.blanc.site/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
      <category term="Django" scheme="https://wiki.blanc.site/tags/Django/"/>
    
  </entry>
  
  <entry>
    <title>「翻译」如何组织大型 Python 项目</title>
    <link href="https://wiki.blanc.site/archives/d644d904.html"/>
    <id>https://wiki.blanc.site/archives/d644d904.html</id>
    <published>2023-08-04T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://blog.europython.eu/kraken-technologies-how-we-organize-our-very-large-pythonmonolith/" target="_blank" rel="external nofollow noopener noreferrer">Kraken Technologies: How we organise our very large Python monolith</a><br>作者：David Seddon from Kraken Technologies<br>翻译：<a href="https://github.com/ryomahan" target="_blank" rel="external nofollow noopener noreferrer">RyomaHan | 小白</a><br>提示：本文是原作者以第一人称书写，翻译时未做更改</p></blockquote><h2 id="TLDR-AI-Claude"><a href="#TLDR-AI-Claude" class="headerlink" title="TLDR(AI-Claude)"></a>TLDR(AI-Claude)</h2><p>本文来自一位 Python 开发者对一个庞大的 Python 项目的代码组织结构的总结。</p><p>该项目包含近 3万个 Python 文件,由全球 400 多名开发者共同维护。为了应对代码日益增长的复杂性,项目采用了分层架构的设计。即将代码库划分为多个层级,并限制不同层级之间的依赖关系,依赖只能从上层流向下层。</p><p>文章详细介绍了该项目的分层结构,以及如何利用 Import Linter 工具来强制执行分层规则。通过追踪被忽略的非法 import 语句数量,可以衡量分层结构实现的进度。</p><p>分层架构确实能够有效降低大型项目的复杂度,方便独立开发。但也存在一些缺点,比如容易在高层产生过多代码,完全实施分层需要花费时间等。总体来说,尽早引入分层架构,能够减少后期的重构工作量,是管理大型 Python 项目的一个有效方式。</p><p>本文通过一个真实的大规模 Python 项目案例,生动地介绍了分层架构的实施过程、优势和不足,对于管理大型项目很有借鉴作用。</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>大家好，我是来自 Kraken Technologies 的 Python 开发者， David。我在的 Kraken 工作是维护一个Python 应用，根据最新统计它拥有 27637 个模块的 。是的，你没看错，这个项目拥有近 28K 独立的 Python 文件（不包括测试代码）。我与全球其他 400 名开发人员一同维护这个庞然大物，不断地为它合并新的代码。任何人只需要在 Github 上获得一位同事的批准，就能修改代码文件，并启动软件的部署，该软件在 17 家不同的能源和公用事业公司运行着，拥有数百万的客户群体。</p><p>看到上面的描述，你大概率会下意识地认为这个项目的代码肯定无比的混乱。坦白讲，我也会这么想。但事实是，至少在我工作的领域，大量的开发人员可以在一个大型的 Python 项目上高效地工作。实现这个目标的要素有很多，其中许多要素来自文化与规则而非技术，在本篇博文中，我想着重讲一下我们是如何通过<strong>优化代码组织结构</strong>来实现这一目标的。</p><h2 id="分层架构"><a href="#分层架构" class="headerlink" title="分层架构"></a>分层架构</h2><p>如果你已经负责维护某个应用的代码仓库一段时间，肯定会感受到随着时间的推移代码复杂度越来越高。在不断开发与维护的过程中，应用中各部分的逻辑代码混合在一起，独立地分析应用中的某个模块变得越来越困难。这也是我们早期维护代码仓库时遇到的问题，经过研究后我们决定采用<strong>分层架构</strong>（即<strong>将代码库划分成多个组件（也就是层级，后面不再注释），并限制各组件间的引用关系</strong>）来应对这一问题。</p><p>分层（Layering）是一种较为常见的软件架构模式，在这种模式下不同的组件（即层级，后面不在重复注释）会被以（概念上）栈的形式组织起来。在这个栈中，下层组件不能依赖（引入）其上层组件。</p><p><img src="https://img.blanc.site/wiki/img/202307302224.png" alt="依赖向下关系流动的分层架构"></p><p>例如，在上图中，C 可以依赖 B 和 A，但不能依赖 D。</p><p>分层架构的应用很宽泛，你可以自由地定义组件。例如：你可以将多个可独立部署的服务视作多个组件，也可以直接将项目中不同部分的源码文件视作不同的组件。</p><p>依赖关系的定义也很宽泛。通常，只要两个组件间存在直接交叉（即使只发生在概念层级上），我们就认为它们之间存在依赖关系。间接交叉（例如通过配置传递）通常不被视为依赖关系。</p><h2 id="如何在-Python-项目中应用分层架构"><a href="#如何在-Python-项目中应用分层架构" class="headerlink" title="如何在 Python 项目中应用分层架构"></a>如何在 Python 项目中应用分层架构</h2><p>分层架构在 Python 项目中的最佳实践是：<strong>将 Python 模块作为分层依据，将导入语句视为依赖依据</strong>。</p><p>以如下项目仓库目录举例：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">myproject</span><br><span class="line">__init__.py</span><br><span class="line">payments&#x2F;</span><br><span class="line">__init__.py</span><br><span class="line">api.py</span><br><span class="line">vendor.py</span><br><span class="line">products.py</span><br><span class="line">shopping_cart.py</span><br></pre></td></tr></table></figure><p>目录中模块之间的嵌套关系是分层的最佳依据。假设，我们决定按照一下顺序进行分层：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 依赖关系向下流动（即上层可以依赖下层）</span><br><span class="line">shopping_cart</span><br><span class="line">payments</span><br><span class="line">products</span><br></pre></td></tr></table></figure><p>为了满足上述架构的要求，我们需要禁止 <code>payments</code> 中的模块从 <code>shopping_cart</code> 模块中引入内容，但可以从 <code>products</code> 模块中引入内容（参考图 1）。</p><p>分层也可以嵌套，因此我们可以在 payments 模块中继续分层，例如：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">api</span><br><span class="line">vendor</span><br></pre></td></tr></table></figure><p>设置多少分层以及以什么顺序进行排列没有唯一正确的答案，需要我们不断的在实践中总结。但是合理的运用分层架构确实能够有效地降低项目结构的复杂度，使其能够更易于理解和修改。</p><h2 id="我们是如何在-Kraken-的项目中实践分层架构的"><a href="#我们是如何在-Kraken-的项目中实践分层架构的" class="headerlink" title="我们是如何在 Kraken 的项目中实践分层架构的"></a>我们是如何在 Kraken 的项目中实践分层架构的</h2><p>在我编写这边文章的时候，已经有 17 家不同的能源和公共事业相关的企业购买了 Kraken 的许可证。我们在内部称呼这些企业为 client，并为每一家企业都运行了一个独立的实例。也正因如此，Kraken 的不同实例间形成了一种「同根不同枝」的特点。通俗地讲就是不同实例间的很多行为其实是共享的，但是每个 client 也都有属于自己的定制代码，以满足他们特定的需求。从地域层面来讲也如此，在英国运行的所有 client 之间存在一定的共性（他们属于同类的能源行业），而日本的 Octopus Energy 则不共享这些的共性。</p><p>随着 Kraken 平台的成长，我们也在不断地优化着我们的分成架构，来帮助我们更好地满足不同客户的需求。目前的分层的顶层结构大致如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"># 依赖关系向下流动（即上层可以依赖下层）</span><br><span class="line">kraken&#x2F;</span><br><span class="line">    __init__.py</span><br><span class="line">    </span><br><span class="line">    client&#x2F;</span><br><span class="line">        __init__.py</span><br><span class="line">        oede&#x2F;</span><br><span class="line">        oegb&#x2F;</span><br><span class="line">        oejp&#x2F;</span><br><span class="line">        ...</span><br><span class="line">    </span><br><span class="line">    territories&#x2F;</span><br><span class="line">        __init__.py</span><br><span class="line">        deu&#x2F;</span><br><span class="line">        gbr&#x2F;</span><br><span class="line">        jpn&#x2F;</span><br><span class="line">        ...</span><br><span class="line">    </span><br><span class="line">    core&#x2F;</span><br></pre></td></tr></table></figure><p>client 组件在结构的顶部。每一个 client 在该层都有一个专属的子包（例如，oede 对应 Octopus Energy Germany）。在此之下的是 territories 组件，用于满足不用国家所需的特定行为，同样为不同地区设置了不同的子包。最底层是 core 组件，包含了所用 client 都会用到的通用代码。我们还制定了一个特别的规则：client 组件下的子包必须是独立的（即不能被其他 client 引用），territories 组件下的子包也是如此。</p><p>将 Kraken 以这种分层结构构建之后，我们可以在有限的区域内（例如一个组件的子包）便捷地进行代码的更新和维护。由于 client 组件位于结构的顶部，因此不会有任何其他组件会直接依赖于它，这样我们就能更方便地更改特定 client 有关的内容，而且不必但因会影响到其他 client 的行为。同样，只更改 territories 组件内的一个子包也不会影响到其他的子包。这样，我们就可以快速、独立地进行跨团队开发，尤其是当我们进行的更改只影响少量 Kraken 实例的时候。</p><h2 id="通过-Import-Linter-确保项目中的分层实现"><a href="#通过-Import-Linter-确保项目中的分层实现" class="headerlink" title="通过 Import Linter 确保项目中的分层实现"></a>通过 Import Linter 确保项目中的分层实现</h2><p>虽然引入了分层结构，但我们很快发现，仅仅在理论上论述分层是不够的。开发人员经常会不小心进行分层间的违规引入。我们需要以某种方式确保分层结构的理论能够在代码结构中被遵循，为了达到此目的我们在项目中引入了第三方库 <a href="https://import-linter.readthedocs.io/" target="_blank" rel="external nofollow noopener noreferrer">Import Linter</a>。</p><p>Import Linter 是一款开源工具，用于检查项目中的引用逻辑是否遵循了指定的结构。首先，我们需要在一个 INI 文件中定义一个描述目标需求的配置，类似这样：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[importlinter:contract:top-level]</span></span><br><span class="line"></span><br><span class="line"><span class="attr">name</span> = Top level layers</span><br><span class="line"><span class="attr">type</span> = layers</span><br><span class="line"><span class="attr">layers</span> =</span><br><span class="line">    kraken.clients</span><br><span class="line">    kraken.territories</span><br><span class="line">    Kraken.core</span><br></pre></td></tr></table></figure><p>我们还可以使用另外两个配置文件强制不同的 clients、territories 之间相互独立。类似这样：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 文件 1</span></span><br><span class="line"><span class="section">[importlinter:contract:client-independence]</span></span><br><span class="line"><span class="attr">name</span> = Client independence</span><br><span class="line"><span class="attr">type</span> = independence</span><br><span class="line"><span class="attr">layers</span> =</span><br><span class="line">    kraken.clients.oede</span><br><span class="line">    kraken.clients.oegb</span><br><span class="line">    kraken.clients.oejp</span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line"><span class="comment"># 文件 2</span></span><br><span class="line"><span class="section">[importlinter:contract:territory-independence]</span></span><br><span class="line"><span class="attr">name</span> = Territory independence</span><br><span class="line"><span class="attr">type</span> = independence</span><br><span class="line"><span class="attr">layers</span> =</span><br><span class="line">    kraken.territories.deu</span><br><span class="line">    kraken.territories.gbr</span><br><span class="line">    kraken.territories.jpn</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><p>然后，你可以在命令行运行 <code>lint-import</code>，它会告诉你项目中是否有任何导入行为违反了我们配置中的要求。我们会在每次拉取代码的时候运行此功能，因此如果有人使用了不合规的导入，检查就会失败，代码也就不会被合并。</p><p>上面展示的并不是我们项目全部的配置文件。团队成员可以在应用程序的更深处添加自己的分层，例如：kranken.ritories.jpn 本身就是分层。我们目前拥有超过 40 个配置文件用于规定我们的分层结构。</p><h2 id="消除技术债"><a href="#消除技术债" class="headerlink" title="消除技术债"></a>消除技术债</h2><p>我们没有办法在确定是由分层架构的第一时间就使整个项目符合架构需求。因此，我们使用了 Import Linter 中的一项特性，该功能允许您在检查非法导入之前忽略对某些导入的检查。</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[importlinter:contract:my-layers-contract]</span></span><br><span class="line"><span class="attr">name</span> = My contract</span><br><span class="line"><span class="attr">type</span> = layers</span><br><span class="line"><span class="attr">layers</span> =</span><br><span class="line">    kraken.clients</span><br><span class="line">    kraken.territories</span><br><span class="line">    kraken.core</span><br><span class="line"><span class="attr">ignore_imports</span> =</span><br><span class="line">    kraken.core.customers -&gt;</span><br><span class="line">    kraken.territories.gbr.customers.views</span><br><span class="line">    kraken.territories.jpn.payments -&gt; kraken.utils.urls</span><br><span class="line">    (and so on...)</span><br></pre></td></tr></table></figure><p>此后，我们使用项目构建时被 Import Linter 忽略的导入语句的数量作为跟踪技术债完成度的指标。这样，我们就能观察到随着时间的推移技术债的情况是否有所改善，以及改善的速度如何。</p><p><img src="https://img.blanc.site/wiki/img/202308040136122.png" alt="Ignored imports since 1 May 2022"></p><p>上图是我们过去一年多的时间里被我们忽略的有问题的引入语句数量的变化。我会定期分享这张图，想大家展示我们最新的工作进度，并鼓励我们的开发者努力做到完全遵守分层结构的约定。我们对其他几个技术债也使用了这种燃尽图的方法去展示。</p><h2 id="没有银弹，谈谈分层架构的缺点"><a href="#没有银弹，谈谈分层架构的缺点" class="headerlink" title="没有银弹，谈谈分层架构的缺点"></a>没有银弹，谈谈分层架构的缺点</h2><h3 id="复杂现实"><a href="#复杂现实" class="headerlink" title="复杂现实"></a>复杂现实</h3><p>现实世界无比的复杂，依赖关系遍布在项目的各个角落。在采用分层架构后，你会经常遇到想要打破现有层级关系的情况，会经常在不经意间从低层级的组件中调用高层级的组件。</p><p>幸运的是，总有办法解决这类问题，那就是所谓的 <a href="https://seddonym.me/2019/04/15/inversion-of-control/" target="_blank" rel="external nofollow noopener noreferrer">控制反转（Ioc）</a>，在 Python 中你可以很容易地做到这一点，只是需要转换一下思维方式。不过使用这个方法会增加「局部复杂性」，但为了让项目整体变得更加简单，这点代价还是值得的。</p><h3 id="结构中高层代码过多"><a href="#结构中高层代码过多" class="headerlink" title="结构中高层代码过多"></a>结构中高层代码过多</h3><p>在分层结构中，层数越高的组件天然地越容易更改。正因如此，我们特地简化了修改特定 clinents 或 territories 的代码流程。另一方面，core 是一切其他代码的基础，修改它就成为了一件高成本、高风险的事情。</p><p>高成本、高风险的底层代码修改行为让我们望而却步，促使我们编写更多针对特定客户或地区的高层级代码。最终的结果就是，高层的代码比我们想象中要多的多的多。我们仍在学习如何解决这个问题。</p><h3 id="目前为止我们仍未完全完成"><a href="#目前为止我们仍未完全完成" class="headerlink" title="目前为止我们仍未完全完成"></a>目前为止我们仍未完全完成</h3><p>还记得之前提到过的被设置在 Import Linter 特殊配置文件中被忽略的 import 吗？多年过去了，它仍未被全部解决，根据统计还有最少 15 个。最后的这几个 import 也是最顽固、最难以被优化的。</p><p>我们需要付出很多的时间才能重构完一个现有项目，所以，越早分层需要面对的麻烦就越少。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Kraken 的分层结构使我们在如此庞大的代码体量下仍旧保持着健康的开发和维护，而且操作难度相对较小，特别是在考虑到它的规模的情况下。如果不对数以万计的模块之间的依赖关系加以限制，我们的项目仓库很可能会像揉乱的线团一样复杂。但是我们选择的代码架构顺利的帮助我们在单一的 Python 代码库中进行大量工作。看似不可能，但这就是事实。</p><p>如果你正在开发一个大型的 Python 项目，或者哪怕是一个相对较小的项目，不发试试分层结构，还是那句话：越早分层需要面对的麻烦就越少。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&quot;https://blog.europython.eu/kraken-technologies-how-we-organize-our-very-large-pythonmonolith/&quot; target=&quot;_blank&quot; r
      
    
    </summary>
    
    
      <category term="1-网络笔记" scheme="https://wiki.blanc.site/categories/1-%E7%BD%91%E7%BB%9C%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="翻译" scheme="https://wiki.blanc.site/tags/%E7%BF%BB%E8%AF%91/"/>
    
  </entry>
  
  <entry>
    <title>记一次失败的 AI 辅助编程全历程</title>
    <link href="https://wiki.blanc.site/archives/c57afff6.html"/>
    <id>https://wiki.blanc.site/archives/c57afff6.html</id>
    <published>2023-05-20T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近公司的一个新项目上马，我被安排来做项目初始化，前端初始化的时候使用了 Vue3 + Tailwind CSS + NaiveUI，在搞基础布局的主题变化时出现了本次插曲。</p><h2 id="基础背景"><a href="#基础背景" class="headerlink" title="基础背景"></a>基础背景</h2><ul><li>我使用的 AI 工具是安装在自己 Slack 频道的 Claude App；</li><li>我对 Tailwind CSS 的实现原理一无所知，我只知道简单的使用方法。</li></ul><h2 id="具体问题"><a href="#具体问题" class="headerlink" title="具体问题"></a>具体问题</h2><p>按照设计，Web PC 端的左侧边栏有一排导航按钮，这些导航按钮在 light 模式下应该是白底，在 dark 模式下应该是 Naive UI 的默认底色。按照需求描述，只需要使用 Tailwind CSS 来实现「仅在 light 模式下修改指定按钮背景色为白色」就行了。</p><p>可坑爹的是 Tailwind CSS 只提供了 <code>dark:bg-white</code> 的写法而没有类似 <code>light:bg-white</code> 的写法，按照正常写法可以使用 <code>class=&quot;bg-white dark:bg-[#需要的颜色]&quot;</code> 来处理。但是我不希望这样写，因为 dark 模式下 UI 按钮组件默认的底色就是我所需的，所以我没理由再去强调 dark 模式下的底色是什么，我只需要使用类似 <code>light:!bg-white</code> 的写法来强制覆盖 light 模式下 UI 按钮组件的底色就行。</p><h2 id="开始踩坑"><a href="#开始踩坑" class="headerlink" title="开始踩坑"></a>开始踩坑</h2><p>于是我开始了无休止的 AI 辅助编程：</p><h3 id="直接提问"><a href="#直接提问" class="headerlink" title="直接提问"></a>直接提问</h3><p>注：这一步我的诉求是让 AI 帮忙直接生成解决方案。</p><p>我最开始的思路是直接将问题描述清楚，让 AI 帮忙生成解决方案，于是：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">ryomahan  [5:26 PM]</span><br><span class="line"></span><br><span class="line">Tailwind CSS 有没有类似 light:bg-white 的写法</span><br><span class="line"></span><br><span class="line">ClaudeAPP  [5:26 PM]</span><br><span class="line"></span><br><span class="line">在 Tailwind CSS 中,确实有类似 light:bg-white 这样的写法。这叫做 variant。Tailwind CSS 支持几种 variant:- responsive:用于响应式设计,如 sm:bg-white 表示在小屏幕上使用 bg-white 类  </span><br><span class="line"><span class="bullet">- </span>hover/focus/active:用于鼠标交互状态,如 hover:bg-white 表示鼠标悬停时使用 bg-white 类  </span><br><span class="line"><span class="bullet">- </span>dark:用于暗色模式,如 dark:bg-white 表示在暗色模式下使用 bg-white 类  </span><br><span class="line"><span class="bullet">- </span>light:用于亮色模式,如 light:bg-white 表示在亮色模式下使用 bg-white 类所以,如果你想在亮色模式下应用 bg-white 类,可以写成:</span><br><span class="line"></span><br><span class="line">html</span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"light:bg-white bg-gray-800"</span>&gt;</span></span>...<span class="xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line">这个 div 在亮色模式下会有 bg-white 类,在暗色模式下会有 bg-gray-800 类。除此之外,Tailwind CSS 还有很多其他的 variant,可以在官方文档中找到:[<span class="string">https://tailwindcss.com/docs/variants</span>](<span class="link">https://tailwindcss.com/docs/variants</span>)希望这个解释可以帮助你!有任何其他问题 feel free 问我。</span><br></pre></td></tr></table></figure><p>马后炮：其实这里已经将问题的核心点出来了，即 Tailwind CSS 实现类似 <code>dark:</code> 的写法是通过一种叫做 <code>variant</code> 的概念来实现的，Tailwind CSS 本身提供了一些基础写法，并且给出了文档。</p><p>我当时着急直接得到解决方案，便以为 <code>&lt;div class=&quot;light:bg-white bg-gray-800&quot;&gt;...&lt;/div&gt;</code> 就是我想要的答案，但是拿到项目代码中测试发现不管用。于是便开始了下一次问答，而无视了这条答案中最重要的那句话：<code>在 Tailwind CSS 中,确实有类似 light:bg-white 这样的写法。这叫做 variant。</code></p><p>在直接提问这个思路下我又尝试问过如下几个问题：</p><ul><li>Tailwind CSS 中与 dark:bg-whtie 对立的表达是什么</li><li>Tailwind CSS 怎样才能使得 bg-white 仅对 light theme 有效对 dark mode 无效</li><li>tailwind css how to make style invalid in drak mode</li></ul><p>得到的答案都不是我想要的东西，于是我开始转换思路。</p><h3 id="从原理提问"><a href="#从原理提问" class="headerlink" title="从原理提问"></a>从原理提问</h3><p>注：这一步我的诉求是让 AI 帮忙直接生成解决方案。</p><p>经过直接提问发现无法得到我想要的答案，于是我开始尝试从实现原理层面进行引导，让 AI 帮忙生成解决方案，我尝试过如下提问：</p><ul><li>如何创建类似 dark:* 的自定义形式</li><li>Tailwind CSS 如何自定义实现类似 dark:* 的效果</li><li>Tailwind CSS 如何自定义实现类似 dark:* 的功能，例如 custom:*</li><li>Tailwind CSS 如何自定义实现类似 dark:* 的功能实现 light:* 使得其后面跟随的 style 仅在 light mode 生效</li><li>Tailwind CSS 中如何实现类似 dark:bg-white 的效果使得 bg-white 仅在 light mode 生效</li></ul><p>经过这几次提问之后我发现其中多个答案都在配置文件中提到了 variants 这项配置，于是：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">ryomahan  [7:09 PM]</span><br><span class="line"></span><br><span class="line">variants 是什么</span><br><span class="line"></span><br><span class="line">ClaudeAPP  [7:09 PM]</span><br><span class="line"></span><br><span class="line">variants 是 Tailwind CSS 一个很强大的功能。它允许你为某些类定义不同的变体(variants),然后在使用这些类时,可以通过如 dark: 前缀来选择使用哪个变体。比如说,对于背景颜色 bg-blue 这个类,你可以定义两个变体:</span><br><span class="line"></span><br><span class="line">js</span><br><span class="line">variants: &#123;</span><br><span class="line">  backgroundColor: ['responsive', 'dark']</span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line">这定义了 responsive 和 dark 两个变体。然后在使用时,可以这样写:</span><br><span class="line"><span class="bullet">- </span>bg-blue - 选择默认的变体  </span><br><span class="line"><span class="bullet">- </span>dark:bg-blue - 选择 dark 变体  </span><br><span class="line"><span class="bullet">- </span>responsive:bg-blue - 选择 responsive 变体所以通过 variants,一个类可以有多种样式,你可以根据需要选择使用哪个变体。variants 常见的使用场景有:- 定义 light/dark 模式下的样式  </span><br><span class="line"><span class="bullet">- </span>定义不同 breakpoint 下的响应式样式  </span><br><span class="line"><span class="bullet">- </span>定义不同互动状态(hover/focus/active)下的样式  </span><br><span class="line"><span class="bullet">- </span>...可以说,variants 是 Tailwind CSS 实现高度可定制和条件化样式的基石功能之一。掌握好了 variants,可以极大提高在 Tailwind 中写 CSS 的能力。总之,variants 是一个非常强大而又重要的 Tailwind CSS 功能,值得深入学习和使用。</span><br></pre></td></tr></table></figure><p>马后炮：这个回答其实已经讲了很多有用的信息了。</p><p>我还是执着于让 AI 直接给我生成方案，所以我粗略看了一下之后又进行了如下提问：</p><ul><li>如何为默认变体提供 alias 到 light</li><li>在 Tailwind  CSS 中如何使用 variants 使得 light:bg-white</li><li>在 Tailwind CSS 中如何使用 variants 使得 light:bg-white 等同于 bg-white</li><li>在 Tailwind CSS 中如何使用 variants 使得 <code>&lt;div class=&quot;bg-white&quot;&gt;</code> 等同于 <code>&lt;div class=&quot;light:bg-white&quot;&gt;</code></li><li>如何应用到全部 style 上（因为上一个回答我感觉有点靠谱所以又追问这个问题）</li><li>没用（因为我发现它的上个回答并没有生效所以我希望它能够自动修改，结果新生成的内容依旧是没用的）</li><li>Tailwind CSS variants 的作用是什么（这里我觉得还需要再理解一下实现原理）</li><li>dark class mode 是如何生效的</li><li>如何自定义一套 .light 类使得样式仅在 light 模式下生效（不死心，还想再尝试一下）</li><li>lightMode 对应的 class 类是什么（这是针对上个问题的追问）</li><li>没有效果（哈哈，生成的内容依旧是无效的）</li><li>必须叫 lightMode 吗（针对上个问题的追问）</li><li>Tailwind css 如何自定义 dark mode class name（因为上面的回答都无效所以我又开了一个新的会话继续提问）</li><li>没用（依旧没用）</li><li>按照你的描述修改了配置文件后并不能生效（还是没用）</li><li>使用上面的配置文件，在 <code>.light</code> class 下 dark:bg-white 没有生效（这里我把我使用的配置文件贴上去了，因为内容太长就不复制过来了）</li><li>使用 Tailwind CSS 时如何使得 <code>bg-white</code> 可以仅在 light mode 生效在 dark mode 不生效（又开了一个新的会话，这已经是第二天了，昨天晚上感觉这是个坑所以就去忙别的了）</li></ul><p>其实这一步的所有提问基本都是在做无用功，因为我太执着于直接获取解决方案而忽略了解决问题的正确流程，我把 AI 想象的过于强大了。</p><h3 id="从原理理解"><a href="#从原理理解" class="headerlink" title="从原理理解"></a>从原理理解</h3><p>注：这一步我的诉求是让 AI 帮忙解释这一类功能的实现原理，在从原理引导 AI 帮忙生成解决方案。</p><p>经过了漫长的无效提问后我开始反思，我在处理问题的过程中太执着于让 AI 直接帮忙生成解决方案了，于是我又转换思路：先让 AI 帮忙阐述实现原理，再从原理入手生成解决方案。</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">ryomahan  [9:40 AM]</span><br><span class="line">Tailwind CSS dark mode 实现原理</span><br><span class="line"></span><br><span class="line">ClaudeAPP  [9:40 AM]</span><br><span class="line"></span><br><span class="line">Tailwind CSS 的 dark mode 实现原理主要有以下几点:1. 定义 dark 模式的颜色变量。Tailwind 在 colors.js 文件中定义了 dark 模式下的颜色变量,例如:</span><br><span class="line"></span><br><span class="line">js</span><br><span class="line">colors: &#123;</span><br><span class="line">  // ...</span><br><span class="line">  gray: &#123;</span><br><span class="line"><span class="code">    900: '#202225',</span></span><br><span class="line"><span class="code">    800: '#2f303d',</span></span><br><span class="line"><span class="code">    700: '#353646',</span></span><br><span class="line"><span class="code">    // ...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line">这些颜色变量会在 build 过程中生成对应的 utility。2. 定义 prefers-color-scheme 相关的 variants。在 variants.js 中定义了:</span><br><span class="line"></span><br><span class="line">js</span><br><span class="line">variants: &#123;</span><br><span class="line">  // ...</span><br><span class="line">  prefersColorScheme: ['dark']</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">这表示 dark mode 下会生成 .dark 等 class。3. 在 build 过程中生成 dark mode 的样式。在 build 过程的 variants 阶段,会根据 prefers-color-scheme 的 variants 生成 .dark 等 class,并使用 dark mode 的颜色变量填充这些 class。4. 在 HTML 中使用 prefers-color-scheme media query 加载相应的样式。例如:</span><br><span class="line"></span><br><span class="line">html</span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">"stylesheet"</span> <span class="attr">href</span>=<span class="string">"tailwind.css"</span>&gt;</span></span></span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">"stylesheet"</span> <span class="attr">media</span>=<span class="string">"prefers-color-scheme: dark"</span> <span class="attr">href</span>=<span class="string">"tailwind.dark.css"</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line">tailwind.dark.css 是 dark mode 的样式,会在 prefers-color-scheme: dark 条件下被加载。5. 可选:使用 JavaScript 切换模式。可以使用 matchMedia 检测 prefers-color-scheme 的变化,并动态加载样式来实现模式切换。所以总体来说,Tailwind 通过定义 variants、颜色变量和生成不同模式的样式文件来实现 dark mode。hope this helps!</span><br></pre></td></tr></table></figure><p>马后炮：回过头来看，这个回答完全没有参考价值，甚至不如前面提到 variants 的那几个回答有价值，所以说 AI 的生成式回答的随机性还是比较坑的。</p><p>然后我问了：</p><ul><li>Tailwind CSS 如何表达 .abc .bg-white</li><li>Tailwind CSS 如何匹配父 class</li><li>Tailwind CSS 如何实现父 class 选择器</li></ul><p>这里我是想：在当前项目中所谓的主题模式是通过控制 Layout 根 DOM 的 class name 来实现的，当 class 是 dark 时为 dark mode，是 light 时为 light mode。所以只需匹配父类就能够实现我想要的东西。事实证明这个思路确实是没问题的，但是并不是通过简单的 class 实现的。</p><p>最终在 Tailwind CSS 的官方文档的引导下我发出了如下提问：</p><ul><li>Tailwind CSS plugin addVariant 如何使用</li><li>我希望使用 addVariant 实现 <code>class=&quot;light:bg-white&quot;</code> 能够转换为如下 CSS  <code>.light .light:bg-white { background: #fff }</code></li><li>如何让它更加泛化，使得无论 light: 后面跟什么都能转换为对应的 Tailwind CSS</li><li>我希望最终实现的效果与 dark:* 类似，当有 .light 的父类 css class 时触发 light:*</li><li>仅保留 .light 就可以了，其他的删掉</li></ul><p>经过这四连问之后，我以为找到了解决这个问题的正确途径，因为这几个问题最终得到的结果虽然不能生效但是看上去像是那么回事，于是我整理了之前的问题继续发问：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">ryomahan  [9:58 AM]</span><br><span class="line"></span><br><span class="line">在 Tailwind CSS 中使用 addVariant 实现如下效果使得 <span class="code">`class="light:*"`</span> 能够与 <span class="code">`class="dark:*"`</span> 类似在有 .light 父 css class 时生效</span><br><span class="line"></span><br><span class="line">ClaudeAPP  [9:58 AM]</span><br><span class="line"></span><br><span class="line">你可以这样使用 Tailwind 的 addVariant 功能来实现这个效果:</span><br><span class="line"></span><br><span class="line">js</span><br><span class="line">addVariant('light', (&#123; modifySelectors, separator &#125;) =&gt; &#123;</span><br><span class="line">  modifySelectors((&#123; className &#125;) =&gt; &#123;</span><br><span class="line"><span class="code">    return `.light $&#123;className&#125;`</span></span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">这段代码会添加一个 light 变体,使得任何使用 light: 作为前缀的类都会在 .light 父元素下生效。例如:</span><br><span class="line"></span><br><span class="line">html</span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"light"</span>&gt;</span></span></span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">"light:bg-blue-500"</span>&gt;</span></span>...<span class="xml"><span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line">这个 p 元素会具有 bg-blue-500 这个类,因为 light:bg-blue-500 在 .light 元素下生效了。如果没有 .light 父元素,light: 类就不会生效,和 Tailwind 的 dark: 变体一致。所以这给你实现了和 dark: 类似的机制,来根据父元素开启某些样式。Hope this helps! Let me know if you have any other questions. (edited)</span><br></pre></td></tr></table></figure><p>这是 AI 给我的方案中距离正确答案最近的一次 ，但是仍旧无效，我又在这个问题的基础上尝试了十几轮的询问，得到的答案都是无效的。</p><h3 id="自我救赎"><a href="#自我救赎" class="headerlink" title="自我救赎"></a>自我救赎</h3><p>在经历昨天下午两个小时外加今天上午两个小时的 AI 问询无果后，我悟了：就当从来没有 AI，早些时候我怎么解决问题现在还怎么解决，于是：</p><ol><li>我在一些前端群里发求助信息，与人交流；</li><li>同时我开始下载 Tailwind CSS 源码开始审查源码看源码是如何实现 dark mode 的。</li></ol><p>整个流程大概持续了十分钟左右的时间，我解决了这个问题，其实如果不去群里发求助信息一上来就看源码的话应该三两分钟就能解决问题。</p><p>完整搜索路径：先在 Tailwind CSS 中全局搜索 dark，看了一些结果发现都不是，好多都是测试用例或者其他无关文件中的。于是将搜索范围调整到 src 目录，大概视察了一下后定位在了一个叫 darkVariants 的变量上，跳转到指定文件后果然就是 dark mode 的定义原文，原文如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">darkVariants: <span class="function">(<span class="params">&#123; config, addVariant &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">let</span> [mode, className = <span class="string">'.dark'</span>] = [].concat(config(<span class="string">'darkMode'</span>, <span class="string">'media'</span>))</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (mode === <span class="literal">false</span>) &#123;</span><br><span class="line">    mode = <span class="string">'media'</span></span><br><span class="line">    log.warn(<span class="string">'darkmode-false'</span>, [</span><br><span class="line">      <span class="string">'The `darkMode` option in your Tailwind CSS configuration is set to `false`, which now behaves the same as `media`.'</span>,</span><br><span class="line">      <span class="string">'Change `darkMode` to `media` or remove it entirely.'</span>,</span><br><span class="line">      <span class="string">'https://tailwindcss.com/docs/upgrade-guide#remove-dark-mode-configuration'</span>,</span><br><span class="line">    ])</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (mode === <span class="string">'class'</span>) &#123;</span><br><span class="line">    addVariant(<span class="string">'dark'</span>, <span class="string">`:is(<span class="subst">$&#123;className&#125;</span> &amp;)`</span>)</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (mode === <span class="string">'media'</span>) &#123;</span><br><span class="line">    addVariant(<span class="string">'dark'</span>, <span class="string">'@media (prefers-color-scheme: dark)'</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它被放在一个 variantPlugins 变量中，然后我通过引用查看发现使用该变量的地方是项目初始化内部 plugin，然后我参考这个思路定义了一个自定义插件：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/** <span class="doctag">@type <span class="type">&#123;import('tailwindcss').Config&#125;</span> </span>*/</span></span><br><span class="line"><span class="keyword">import</span> plugin <span class="keyword">from</span> <span class="string">"tailwindcss/plugin"</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  darkMode: <span class="string">"class"</span>,</span><br><span class="line">  content: [</span><br><span class="line">    <span class="string">"./index.html"</span>,</span><br><span class="line">    <span class="string">"./src/**/*.&#123;vue,js,ts,jsx,tsx&#125;"</span>,</span><br><span class="line">  ],</span><br><span class="line">  theme: &#123;&#125;,</span><br><span class="line">  plugins: [</span><br><span class="line">    plugin(<span class="function"><span class="keyword">function</span> (<span class="params">&#123; addVariant &#125;</span>) </span>&#123;</span><br><span class="line">      addVariant(<span class="string">"light"</span>, <span class="string">":is(.light &amp;)"</span>)</span><br><span class="line">    &#125;)</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>问题解决。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>经过这一次折磨人的使用经历后我觉得我需要重新审视一下 AI 在我日常编程时的定位，或者说我在编程时应该如何使用 AI。一下是基于本次经历我的几点感悟：</p><ol><li>解决问题的第一步是动脑子而不是动键盘；</li><li>解决问题的最好思路永远在源码中而不在 AI 的回答里；</li><li>在与 AI 对话时不要期望重复问题或者微调问题就能得到正确的答案；</li><li>在不熟悉的领域内提问时尽量不要期望一开始就能生成完整的解决方案。</li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;最近公司的一个新项目上马，我被安排来做项目初始化，前端初始化的时候使用了 Vue3 + Tailwind CSS + NaiveUI，在搞基
      
    
    </summary>
    
    
      <category term="1-网络笔记" scheme="https://wiki.blanc.site/categories/1-%E7%BD%91%E7%BB%9C%E7%AC%94%E8%AE%B0/"/>
    
    
  </entry>
  
  <entry>
    <title>解剖 Python 类</title>
    <link href="https://wiki.blanc.site/archives/291be77c.html"/>
    <id>https://wiki.blanc.site/archives/291be77c.html</id>
    <published>2022-04-01T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近想为 SQLAlchemy 封装一套类似 Django ORM 的 Model Manager，于是捡起了「流畅的 Python」开始看被我遗留的「元编程」部分。在阅读的过程中，我慢慢发现自己并没有像想象的那样对 Python 类了如指掌，在很多概念的划分上我都是模棱两可的。因此特地总结这样一篇文章，希望能够由浅至深对 Python 类进行一次全面解剖手术。</p><p><strong>注：本文所讨论的内容仅适用于 Python3。</strong><br><strong>注：本文针对源码的讨论仅适用于 CPython。</strong><br><strong>注：本文写作时参考 CPython 代码版本：3.9。</strong><br><strong>注：本文写作时参考 Python 官方文档版本：3.10.4。</strong></p><h2 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h2><p>在开始解剖 Python 类之前需要先正确的理解一些概念。</p><p>在 Python 中有一句影响深远的话：一切皆对象。请不要被误导，此处的「对象」指的是一个抽象的概念，而 Python 内部为了能够清楚的表达「对象」这个概念将其分为了两个部分：object and type，即对象和类型。由这两部分组成的能够表达某类抽象「对象」的东西在 Python 中被称为 class，即类。</p><p><strong>注：后文中凡是有括号的「对象」表示的都是一个抽象概念，而无括号的对象表示的则是 Python 中的 object。</strong></p><p>另外还有一组概念要提前理解：</p><ul><li>当在 IDE 或文本编辑器中通过 <code>class XX</code> 的语法定义好某个类后，我们得到的只是一个<strong>类</strong>；</li><li>一旦这个模块被导入后，Python 解释器就会生成相应的<strong>类对象</strong>；</li><li>一旦这个类在导入时或运行时被实例化了，Python 解释器就会生成相应的<strong>实例对象</strong>。</li></ul><p><strong>注：关于这几个概念在本章的后续会有更详细的解释，而更深入的解析会在对应名字的章节上。</strong></p><p>我们在使用 Python 语法编写程序时之所以能够一上来就定义一些复杂的类、生成复杂的类对象和实例对象，是因为 Python 在出厂时为我们包装好了各种基于类型和对象生成的类和相应类对象与实例对象（有些是内置的工具使用 C 实现的，拥有更好的执行效率）。</p><h3 id="对象-object"><a href="#对象-object" class="headerlink" title="对象 | object"></a>对象 | object</h3><p><a href="https://docs.python.org/3/library/functions.html#object" target="_blank" rel="external nofollow noopener noreferrer">object</a>。</p><h3 id="类型-type"><a href="#类型-type" class="headerlink" title="类型 | type"></a>类型 | type</h3><p><a href="https://docs.python.org/3/library/functions.html#type" target="_blank" rel="external nofollow noopener noreferrer">type</a>。</p><h3 id="类-class"><a href="#类-class" class="headerlink" title="类 | class"></a>类 | class</h3><blockquote><p>Classes provide a means of bundling data and functionality together. Creating a new class creates a new type of object, allowing new instances of that type to be made. Each class instance can have attributes attached to it for maintaining its state. Class instances can also have methods (defined by its class) for modifying its state.</p><p>类把数据与功能绑定在一起。创建新类就是创建新的对象类型，从而创建该类型的新实例。类实例支持维持自身状态的属性，还支持（由类定义的）修改自身状态的方法。</p><p>——<a href="https://docs.python.org/zh-cn/3/tutorial/classes.html#tut-private" target="_blank" rel="external nofollow noopener noreferrer">《Python 官方文档 - Python 教程 - 9. 类》</a></p></blockquote><p><strong>注：可以仔细品读一下官方文档的这段话，这段话已经将类的绝大多数秘密展示出来了。</strong></p><h3 id="导入时和运行时-import-amp-run"><a href="#导入时和运行时-import-amp-run" class="headerlink" title="导入时和运行时 | import &amp; run"></a>导入时和运行时 | import &amp; run</h3><p>导入时：import 某个模块时此模块所处的状态；<br>运行时：调用某个模块时模块所处的状态；</p><p><strong>注：在《流畅的 Python》这本书的 21.3 和 21.4 两个章节里有两个实例能够帮助更好地理解这两个概念。</strong></p><h3 id="类对象-class-object"><a href="#类对象-class-object" class="headerlink" title="类对象 | class object"></a>类对象 | class object</h3><p>类对象用于维护类的基本信息。</p><blockquote><p>类对象支持两种操作：属性引用和实例化。</p><p>属性引用使用 Python 中所有属性引用所使用的标准语法：obj.name。有效的属性名称是类对象被创建时存在于类<a href="https://docs.python.org/zh-cn/3/tutorial/classes.html#python-scopes-and-namespaces" target="_blank" rel="external nofollow noopener noreferrer">命名空间</a>中的所有名称。</p><p>类的实例化使用函数表达法。可以把类对象视为是返回该类的一个新实例的不带参数的函数。<br>实例化操作（“调用”类对象）会创建一个空对象。 许多类喜欢创建带有特定初始状态的自定义实例。 为此类定义可能包含一个名为 __init__() 的特殊方法。<br>当一个类定义了 __init__() 方法时，类的实例化操作会自动为新创建的类实例发起调用 __init__()。<br>当然，__init__() 方法还可以有额外参数以实现更高灵活性。 在这种情况下，提供给类实例化运算符的参数将被传递给 __init__()。</p><p>——<a href="https://docs.python.org/zh-cn/3/tutorial/classes.html#tut-private" target="_blank" rel="external nofollow noopener noreferrer">《Python 官方文档 - Python 教程 - 9. 类》</a></p></blockquote><h3 id="实例对象-instance-object"><a href="#实例对象-instance-object" class="headerlink" title="实例对象 | instance object"></a>实例对象 | instance object</h3><p>实例对象用于维护实例的基本信息。</p><blockquote><p>实例对象所能理解的唯一操作是属性引用。 有两种有效的属性名称：数据属性和方法。</p><p>数据属性 对应于 Smalltalk 中的“实例变量”，以及 C++ 中的“数据成员”。 数据属性不需要声明；像局部变量一样，它们将在第一次被赋值时产生。</p><p>另一类实例属性引用称为 方法。 方法是“从属于”对象的函数。在 Python 中，方法这个术语并不是类实例所特有的：其他对象也可以有方法。 例如，列表对象具有 append, insert, remove, sort 等方法。</p><p>——<a href="https://docs.python.org/zh-cn/3/tutorial/classes.html#tut-private" target="_blank" rel="external nofollow noopener noreferrer">《Python 官方文档 - Python 教程 - 9. 类》</a></p></blockquote><p><strong>注：请从此刻开始有意识的思考「实例对象的属性和类对象的属性之间的优先级关系」</strong><br><strong>注：有关类对象和实例对象的对比会在后文详细展开。</strong><br><strong>注：关于类对象和实例对象部分 <a href="https://mooc.study.163.com/course/2001361005?tid=2403008003&_trace_c_p_k2_=cebe971c79de49d793ef1bf8d63db625#/info" target="_blank" rel="external nofollow noopener noreferrer">网易云课堂-Python 微专业</a> 的面向对象基础里有比较详细的讲解。</strong><br><strong>注：针对上一条，如果你搞不到资源的话可以看后面我自己的总结，内容大差不差。</strong></p><h3 id="实例-instance"><a href="#实例-instance" class="headerlink" title="实例 | instance"></a>实例 | instance</h3><p>实例，代表的是类对象与其实例对象或类型与对应类之间的关系。</p><p>type，即类型是实例关系的顶点，type 的类型还是 type，object 的类型也是 type。</p><h3 id="继承-subclass"><a href="#继承-subclass" class="headerlink" title="继承 | subclass"></a>继承 | subclass</h3><p>继承，代表的是父对象与子对象或父类型与子类型之间的关系。</p><p>object，即对象是继承关系的顶点，object 没有更上一层的对象了，而 type 的父对象是 object。</p><p><strong>注：关于实例和继承的详细过程也会在后文详展开。</strong></p><h3 id="变量和属性-variable-amp-attributes"><a href="#变量和属性-variable-amp-attributes" class="headerlink" title="变量和属性 | variable &amp; attributes"></a>变量和属性 | variable &amp; attributes</h3><p>变量，指的是在进行 Python 编码的过程中为某个具体对象赋予的名称。</p><p>属性也是变量，但属性不会单独出现。我们在称呼一个变量为属性的时候一般会称其为某某对象的属性。当然在一些常见场景中，为了方便称呼会省略定语「某某对象的」，但省略不代表没有。</p><h3 id="函数和方法-function-amp-method"><a href="#函数和方法-function-amp-method" class="headerlink" title="函数和方法 | function &amp; method"></a>函数和方法 | function &amp; method</h3><p>在 <a href="https://docs.python.org/3/library/types.html" target="_blank" rel="external nofollow noopener noreferrer">Python 官方文档的 types 文档</a> 中有这么几个类型：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">types.FunctionType</span><br><span class="line">types.LambdaType</span><br><span class="line"></span><br><span class="line">The type of user-defined functions and functions created by lambda expressions.</span><br><span class="line"></span><br><span class="line"># ---</span><br><span class="line"></span><br><span class="line">types.MethodType</span><br><span class="line"></span><br><span class="line">The type of methods of user-defined class instances.</span><br><span class="line"></span><br><span class="line"># ---</span><br><span class="line"></span><br><span class="line">types.BuiltinFunctionType</span><br><span class="line">types.BuiltinMethodType</span><br><span class="line"></span><br><span class="line">The type of built-in functions like len() or sys.exit(), and methods of built-in classes. (Here, the term “built-in” means “written in C”.)</span><br></pre></td></tr></table></figure><p>大概可以理解为：<br>用户直接定义的函数或通过 lambda 生成的函数称为函数，是 FunctionType 类型，也就是函数；<br>用户通过实例引用的函数称为方法，是 MethodType 类型，也就是方法；<br>所有内建的函数（方法）称为内建函数（方法），是 BuiltinFunctionType，也就是内建函数（方法）；</p><p>其中 FunctionType 和 LambdaType 是等效的，BuiltinFunctionType 和 BuiltinMethodType 是等效的。</p><p><strong>注：关于函数和方法的详细区别，以及类中函数转化为方法的过程会在后文展开。</strong></p><h3 id="可调用对象-callable"><a href="#可调用对象-callable" class="headerlink" title="可调用对象 | callable"></a>可调用对象 | callable</h3><p>可以通过函数调用的方式（例如 <code>foo()</code>）进行调用的对象。</p><h2 id="对象：万物的起源"><a href="#对象：万物的起源" class="headerlink" title="对象：万物的起源"></a>对象：万物的起源</h2><p>我刚开始接触 Python 时看的各种学习资料都在强调一点：在 Python 中，一切皆是对象。直到我整理这篇文章之前我对这句话的理解都仅限于字面意思。最近，正好借着工作需要的由头我收集了一些相关的资料（比较重要的我都放在了 <a href="#参考">参考</a> 中）横向地了解了一下。下面就正式开始，好好地扒一扒 Python 中的「一切接对象」到底是什么意思以及隐藏在这句话背后的 Python 类究竟是如何构造的。</p><h3 id="官方文档"><a href="#官方文档" class="headerlink" title="官方文档"></a>官方文档</h3><p>从 Python 官方文档来看，object 关键字对应的是一个可调用对象，下面是官方文档对 object 的描述：</p><blockquote><p>Return a new featureless object. object is a base for all classes. It has methods that are common to all instances of Python classes. This function does not accept any arguments.<br>object does not have a __dict__, so you can’t assign arbitrary attributes to an instance of the object class.</p><p>返回一个不带特征的新对象。object 是所有类的基类。它带有所有 Python 类实例均通用的方法。本函数不接受任何参数。<br>由于 object 没有 __dict__，因此无法将任意属性赋给 object 的实例。</p><p>——<a href="https://docs.python.org/3/library/functions.html#object" target="_blank" rel="external nofollow noopener noreferrer">《Python 官方文档 - 内置函数》</a></p></blockquote><p><strong>注：这里有个细节可以关注一下，虽然 object 被放在内置函数章节，但是却被标记为了 class。</strong></p><h3 id="Cpython"><a href="#Cpython" class="headerlink" title="Cpython"></a>Cpython</h3><h4 id="PyObject-amp-PyVarObject-对象基石"><a href="#PyObject-amp-PyVarObject-对象基石" class="headerlink" title="PyObject &amp; PyVarObject | 对象基石"></a>PyObject &amp; PyVarObject | 对象基石</h4><p>Cpython 中 Python 所有类型都由结构体 <code>PyObject</code> 扩展而来，而那些指向可变大小 Python 对象的指针都可以被转换为 <code>PyVarObject</code>，具体如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 注：为了阅读体验，在书写格式上有所调整</span></span><br><span class="line"><span class="comment">// cpython-root/Include/object.h</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> Py_TRACE_REFS</span></span><br><span class="line"><span class="comment">/* Define pointers to support a doubly-linked list of all live heap objects. */</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> _PyObject_HEAD_EXTRA            \</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">object</span> *_<span class="title">ob_next</span>;</span>           \</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">object</span> *_<span class="title">ob_prev</span>;</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> _PyObject_EXTRA_INIT 0, 0,</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">else</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> _PyObject_HEAD_EXTRA</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> _PyObject_EXTRA_INIT</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Nothing is actually declared to be a PyObject, but every pointer to</span></span><br><span class="line"><span class="comment"> * a Python object can be cast to a PyObject*.  This is inheritance built</span></span><br><span class="line"><span class="comment"> * by hand.  Similarly every pointer to a variable-size Python object can,</span></span><br><span class="line"><span class="comment"> * in addition, be cast to PyVarObject*.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * 实际上没有任何东西被声明为 PyObject，</span></span><br><span class="line"><span class="comment"> * 但是每一个指向 Python 对象的指针都可以被转换为 PyObject。 </span></span><br><span class="line"><span class="comment"> * 这是通过手动强制建立的继承关系。 </span></span><br><span class="line"><span class="comment"> * 同样地，每个指向可变大小的 Python 对象的指针都可以被转换为 PyVarObject</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> _<span class="title">object</span> &#123;</span></span><br><span class="line">    <span class="comment">// 代表了两个 PyObject* 双向链表的指针，用于把堆上的所有对象链接起来，</span></span><br><span class="line">    <span class="comment">// 只会在开启了 Py_TRACE_REFS 宏的时候进行构造，方便调试；</span></span><br><span class="line">    _PyObject_HEAD_EXTRA</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 引用计数，用于垃圾回收；</span></span><br><span class="line">    Py_ssize_t ob_refcnt;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 指针，指向对象的类型对象，用来标识对象属于的类型，并存储类型的元数据；</span></span><br><span class="line">    PyTypeObject *ob_type;</span><br><span class="line">&#125; PyObject;</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    PyObject ob_base;</span><br><span class="line">    Py_ssize_t ob_size; <span class="comment">/* Number of items in variable part */</span></span><br><span class="line">&#125; PyVarObject;</span><br></pre></td></tr></table></figure><p>PyObject 和 PyVarObject 一般是作为头部被包含在一个变量结构体中的（可以近似理解为继承），根据该变量大小是否固定来选择使用哪一种。PyObject 和 PyVarObject 是 Cpython 的基石，Python 中的 object 和 type 都是由这两个结构体拓展而来。</p><h4 id="PyBaseObject-Type-object-的实现"><a href="#PyBaseObject-Type-object-的实现" class="headerlink" title="PyBaseObject_Type | object 的实现"></a>PyBaseObject_Type | object 的实现</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 注：为了阅读体验，在书写格式上有所调整</span></span><br><span class="line"><span class="comment">// cpython-root/Object/typeobject.c</span></span><br><span class="line"></span><br><span class="line">PyTypeObject PyBaseObject_Type = &#123;</span><br><span class="line">    PyVarObject_HEAD_INIT(&amp;PyType_Type, <span class="number">0</span>)</span><br><span class="line">    <span class="string">"object"</span>,                                   <span class="comment">/* tp_name */</span></span><br><span class="line">    <span class="keyword">sizeof</span>(PyObject),                           <span class="comment">/* tp_basicsize */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_itemsize */</span></span><br><span class="line">    object_dealloc,                             <span class="comment">/* tp_dealloc */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_vectorcall_offset */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_getattr */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_setattr */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_async */</span></span><br><span class="line">    object_repr,                                <span class="comment">/* tp_repr */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_number */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_sequence */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_mapping */</span></span><br><span class="line">    (hashfunc)_Py_HashPointer,                  <span class="comment">/* tp_hash */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_call */</span></span><br><span class="line">    object_str,                                 <span class="comment">/* tp_str */</span></span><br><span class="line">    PyObject_GenericGetAttr,                    <span class="comment">/* tp_getattro */</span></span><br><span class="line">    PyObject_GenericSetAttr,                    <span class="comment">/* tp_setattro */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_buffer */</span></span><br><span class="line">    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   <span class="comment">/* tp_flags */</span></span><br><span class="line">    object_doc,                                 <span class="comment">/* tp_doc */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_traverse */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_clear */</span></span><br><span class="line">    object_richcompare,                         <span class="comment">/* tp_richcompare */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_weaklistoffset */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_iter */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_iternext */</span></span><br><span class="line">    object_methods,                             <span class="comment">/* tp_methods */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_members */</span></span><br><span class="line">    object_getsets,                             <span class="comment">/* tp_getset */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_base */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_dict */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_descr_get */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_descr_set */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_dictoffset */</span></span><br><span class="line">    object_init,                                <span class="comment">/* tp_init */</span></span><br><span class="line">    PyType_GenericAlloc,                        <span class="comment">/* tp_alloc */</span></span><br><span class="line">    object_new,                                 <span class="comment">/* tp_new */</span></span><br><span class="line">    PyObject_Del,                               <span class="comment">/* tp_free */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>注：本人没有系统性的学习过 C/C++ 所以在后文的表述中难免有些外行。</strong></p><p>仅从当前展示的代码还比较难理解 CPython 中 Python 类的实现思路，需配合后文中类型的 CPython 源码分析才能更全面的理解 Python 中那些底层的特性是为何表现出来的。</p><h3 id="object-分析"><a href="#object-分析" class="headerlink" title="object 分析"></a>object 分析</h3><h4 id="type-object-type"><a href="#type-object-type" class="headerlink" title="type(object) == type"></a>type(object) == type</h4><p>从代码中不难发现 PyBaseObject_Type 是结构体 PyTypeObject 的一个实现。而且结构体的第一行 <code>PyVarObject_HEAD_INIT(&amp;PyType_Type, 0)</code> 也印证了 <code>type(object) == type</code>（PyType_Type 是 type 的实现）。</p><h4 id="object-base-None"><a href="#object-base-None" class="headerlink" title="object.__base__ == None"></a>object.__base__ == None</h4><p><s>从源码中可以找到，tp_base 对应的值是 0。</s></p><p>最开始我是这么认为的，但后面当我想用同样的道理证明 <code>tpye.__base__ == object</code> 的时候却发现 PyType_Type 的 tp_base 也是 0。后面我在 <a href="https://mp.weixin.qq.com/s/ivZk_ba3qRS94o65n8a2Yw" target="_blank" rel="external nofollow noopener noreferrer">古明地觉：《源码探秘 CPython》3. type 和 object 的恩怨纠葛</a> 一文中看到了半句解释：</p><blockquote><p>因为 Python 的动态性，显然不可能在定义的时候就将所有成员属性都设置好、然后解释器一启动就会得到我们平时使用的类型对象。<br>目前看到的类型对象是一个半成品，有一部分成员属性是在解释器启动之后再进行动态完善的。</p></blockquote><p>我简单的翻看了一下后续的文章没有找到实际完善的地方，后续找到解释再进行补充吧。</p><h4 id="object-里有什么"><a href="#object-里有什么" class="headerlink" title="object 里有什么"></a>object 里有什么</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">dir(object)</span><br><span class="line">[<span class="string">'__class__'</span>,</span><br><span class="line"> <span class="string">'__delattr__'</span>,</span><br><span class="line"> <span class="string">'__dir__'</span>,</span><br><span class="line"> <span class="string">'__doc__'</span>,</span><br><span class="line"> <span class="string">'__eq__'</span>,</span><br><span class="line"> <span class="string">'__format__'</span>,</span><br><span class="line"> <span class="string">'__ge__'</span>,</span><br><span class="line"> <span class="string">'__getattribute__'</span>,</span><br><span class="line"> <span class="string">'__gt__'</span>,</span><br><span class="line"> <span class="string">'__hash__'</span>,</span><br><span class="line"> <span class="string">'__init__'</span>,</span><br><span class="line"> <span class="string">'__init_subclass__'</span>,</span><br><span class="line"> <span class="string">'__le__'</span>,</span><br><span class="line"> <span class="string">'__lt__'</span>,</span><br><span class="line"> <span class="string">'__ne__'</span>,</span><br><span class="line"> <span class="string">'__new__'</span>,</span><br><span class="line"> <span class="string">'__reduce__'</span>,</span><br><span class="line"> <span class="string">'__reduce_ex__'</span>,</span><br><span class="line"> <span class="string">'__repr__'</span>,</span><br><span class="line"> <span class="string">'__setattr__'</span>,</span><br><span class="line"> <span class="string">'__sizeof__'</span>,</span><br><span class="line"> <span class="string">'__str__'</span>,</span><br><span class="line"> <span class="string">'__subclasshook__'</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 除此之外，还有：</span></span><br><span class="line">object.__bases__        <span class="comment"># ()</span></span><br><span class="line">object.__qualname__     <span class="comment"># 'object'</span></span><br><span class="line">object.__subclasses__() <span class="comment"># 太多了...</span></span><br><span class="line">object.mro()            <span class="comment"># [object]</span></span><br></pre></td></tr></table></figure><h4 id="object-a-1"><a href="#object-a-1" class="headerlink" title="object.a = 1"></a>object.a = 1</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">object.a = <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># TypeError: can't set attributes of built-in/extension type 'object'</span></span><br><span class="line"></span><br><span class="line">a = object()</span><br><span class="line">a.a = <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># AttributeError: 'object' object has no attribute 'a'</span></span><br><span class="line"></span><br><span class="line">a.__init__ = <span class="number">1</span></span><br><span class="line">AttributeError: <span class="string">'object'</span> object attribute <span class="string">'__init__'</span> <span class="keyword">is</span> read-only</span><br></pre></td></tr></table></figure><h2 id="类型：类的定义（描述）"><a href="#类型：类的定义（描述）" class="headerlink" title="类型：类的定义（描述）"></a>类型：类的定义（描述）</h2><h3 id="官方文档-1"><a href="#官方文档-1" class="headerlink" title="官方文档"></a>官方文档</h3><p>从 Python 官方文档来看，type 关键字和 object 关键字一样对应的是一个可调用对象，下面是官方文档对 type 的描述：</p><blockquote><p>With one argument, return the type of an object. The return value is a type object and generally the same object as returned by object.<strong>class</strong>.</p><p>The isinstance() built-in function is recommended for testing the type of an object, because it takes subclasses into account.</p><p>With three arguments, return a new type object. This is essentially a dynamic form of the class statement. The name string is the class name and becomes the <strong>name</strong> attribute. The bases tuple contains the base classes and becomes the <strong>bases</strong> attribute; if empty, object, the ultimate base of all classes, is added. The dict dictionary contains attribute and method definitions for the class body; it may be copied or wrapped before becoming the <strong>dict</strong> attribute.</p><p>传入一个参数时，返回 object 的类型。 返回值是一个 type 对象，通常与 object.<strong>class</strong> 所返回的对象相同。</p><p>推荐使用 isinstance() 内置函数来检测对象的类型，因为它会考虑子类的情况。</p><p>传入三个参数时，返回一个新的 type 对象。 这在本质上是 class 语句的一种动态形式，name 字符串即类名并会成为 <strong>name</strong> 属性；bases 元组包含基类并会成为 <strong>bases</strong> 属性；如果为空则会添加所有类的终极基类 object。 dict 字典包含类主体的属性和方法定义；它在成为 <strong>dict</strong> 属性之前可能会被拷贝或包装。</p><p>——<a href="https://docs.python.org/3/library/functions.html#object" target="_blank" rel="external nofollow noopener noreferrer">《Python 官方文档 - 内置函数》</a></p></blockquote><h3 id="CPython"><a href="#CPython" class="headerlink" title="CPython"></a>CPython</h3><h4 id="PyTypeObject-类型基石"><a href="#PyTypeObject-类型基石" class="headerlink" title="PyTypeObject | 类型基石"></a>PyTypeObject | 类型基石</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 注：为了阅读体验，在书写格式上有所调整</span></span><br><span class="line"><span class="comment">// cpython-root/Include/object.h</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> PyObject_VAR_HEAD      PyVarObject ob_base;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* PyTypeObject structure is defined in cpython/object.h.</span></span><br><span class="line"><span class="comment">   In Py_LIMITED_API, PyTypeObject is an opaque structure. */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> _<span class="title">typeobject</span> <span class="title">PyTypeObject</span>;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// cpython-root/Include/cpython/object.h</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    lenfunc sq_length;</span><br><span class="line">    binaryfunc sq_concat;</span><br><span class="line">    ssizeargfunc sq_repeat;</span><br><span class="line">    ssizeargfunc sq_item;</span><br><span class="line">    <span class="keyword">void</span> *was_sq_slice;</span><br><span class="line">    ssizeobjargproc sq_ass_item;</span><br><span class="line">    <span class="keyword">void</span> *was_sq_ass_slice;</span><br><span class="line">    objobjproc sq_contains;</span><br><span class="line"></span><br><span class="line">    binaryfunc sq_inplace_concat;</span><br><span class="line">    ssizeargfunc sq_inplace_repeat;</span><br><span class="line">&#125; PySequenceMethods;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> _<span class="title">typeobject</span> &#123;</span></span><br><span class="line">    PyObject_VAR_HEAD</span><br><span class="line">    <span class="keyword">const</span> <span class="keyword">char</span> *tp_name; <span class="comment">/* For printing, in format "&lt;module&gt;.&lt;name&gt;" */</span></span><br><span class="line">    Py_ssize_t tp_basicsize, tp_itemsize; <span class="comment">/* For allocation 创建实例时分配内存的大小 */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Methods to implement standard operations */</span></span><br><span class="line"></span><br><span class="line">    destructor tp_dealloc; <span class="comment">// 析构</span></span><br><span class="line">    Py_ssize_t tp_vectorcall_offset;</span><br><span class="line">    getattrfunc tp_getattr; <span class="comment">// 弃用 </span></span><br><span class="line">    setattrfunc tp_setattr; <span class="comment">// 弃用 </span></span><br><span class="line">    PyAsyncMethods *tp_as_async; <span class="comment">/* formerly known as tp_compare (Python 2)</span></span><br><span class="line"><span class="comment">                                    or tp_reserved (Python 3) */</span></span><br><span class="line">    reprfunc tp_repr; <span class="comment">// __repr__</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Method suites for standard classes */</span></span><br><span class="line"></span><br><span class="line">    PyNumberMethods *tp_as_number; <span class="comment">// 函数簇，封装了对应类型的操作，可以参考前文给出的 PySequenceMethods</span></span><br><span class="line">    PySequenceMethods *tp_as_sequence;</span><br><span class="line">    PyMappingMethods *tp_as_mapping;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* More standard operations (here for binary compatibility) */</span></span><br><span class="line"></span><br><span class="line">    hashfunc tp_hash; <span class="comment">// __hash__</span></span><br><span class="line">    ternaryfunc tp_call; <span class="comment">// __call__</span></span><br><span class="line">    reprfunc tp_str; <span class="comment">// __str__</span></span><br><span class="line">    getattrofunc tp_getattro; <span class="comment">// __getattr__</span></span><br><span class="line">    setattrofunc tp_setattro; <span class="comment">// __setattr__</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Functions to access object as input/output buffer */</span></span><br><span class="line">    PyBufferProcs *tp_as_buffer; <span class="comment">// 类似前文的 PyNumberMethods</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Flags to define presence of optional/expanded features */</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> tp_flags; <span class="comment">// 由各种标志组成的位掩码</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> <span class="keyword">char</span> *tp_doc; <span class="comment">/* Documentation string | __doc__ */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Assigned meaning in release 2.0 */</span></span><br><span class="line">    <span class="comment">/* call function for all accessible objects */</span></span><br><span class="line">    traverseproc tp_traverse; <span class="comment">// 垃圾回收相关</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* delete references to contained objects */</span></span><br><span class="line">    inquiry tp_clear; <span class="comment">// 垃圾回收相关</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Assigned meaning in release 2.1 */</span></span><br><span class="line">    <span class="comment">/* rich comparisons */</span></span><br><span class="line">    richcmpfunc tp_richcompare;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* weak reference enabler */</span></span><br><span class="line">    Py_ssize_t tp_weaklistoffset; <span class="comment">// 弱引用相关</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Iterators | 迭代器相关 */</span></span><br><span class="line">    getiterfunc tp_iter;</span><br><span class="line">    iternextfunc tp_iternext;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/* Attribute descriptor and subclassing stuff */</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">PyMethodDef</span> *<span class="title">tp_methods</span>;</span> <span class="comment">// regular methods</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">PyMemberDef</span> *<span class="title">tp_members</span>;</span> <span class="comment">// regular data members</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">PyGetSetDef</span> *<span class="title">tp_getset</span>;</span>  <span class="comment">// computed attribute</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">typeobject</span> *<span class="title">tp_base</span>;</span>    <span class="comment">// 基类（父类）</span></span><br><span class="line">    PyObject *tp_dict; <span class="comment">// __dict__</span></span><br><span class="line">    descrgetfunc tp_descr_get; <span class="comment">// 描述符相关</span></span><br><span class="line">    descrsetfunc tp_descr_set; <span class="comment">// 描述符相关</span></span><br><span class="line">    Py_ssize_t tp_dictoffset;  <span class="comment">// 与 __slots__ 有关</span></span><br><span class="line">    initproc tp_init;</span><br><span class="line">    allocfunc tp_alloc;</span><br><span class="line">    newfunc tp_new;</span><br><span class="line">    freefunc tp_free; <span class="comment">/* Low-level free-memory routine */</span></span><br><span class="line">    inquiry tp_is_gc; <span class="comment">/* For PyObject_IS_GC | 垃圾回收相关 */</span></span><br><span class="line">    PyObject *tp_bases;</span><br><span class="line">    PyObject *tp_mro; <span class="comment">/* method resolution order */</span></span><br><span class="line">    PyObject *tp_cache;</span><br><span class="line">    PyObject *tp_subclasses;</span><br><span class="line">    PyObject *tp_weaklist;</span><br><span class="line">    destructor tp_del;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Type attribute cache version tag. Added in version 2.6 */</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">int</span> tp_version_tag;</span><br><span class="line"></span><br><span class="line">    destructor tp_finalize;</span><br><span class="line">    vectorcallfunc tp_vectorcall;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>通过分析 PyTypeObject 我们可以发现一些 Python 底层有趣的实现：</p><p>在 PyTypeObject 中有这样几个值</p><p>PyAsyncMethods *tp_as_async<br>PyNumberMethods *tp_as_number;<br>PySequenceMethods *tp_as_sequence;<br>PyMappingMethods *tp_as_mapping;<br>PyBufferProcs *tp_as_buffer;</p><p>这些值的类型分别对应到了同名的结构体上，而这些结构体中定义了相关类型的操作方法，正是由于这样的结构使得 Python 可以支持鸭子类型。</p><p><strong>注：这里描述的并非鸭子类型的原理，仅仅是通过源码进行一些延伸。</strong></p><p>除此之外，如果想了解更多 PyTypeObject 内部变量的具体意义可以访问 <a href="https://docs.python.org/3/c-api/typeobj.html" target="_blank" rel="external nofollow noopener noreferrer">Python 官方文档-C API-Type Objects</a> 进行查看。</p><h4 id="PyType-Type-type-实现"><a href="#PyType-Type-type-实现" class="headerlink" title="PyType_Type | type 实现"></a>PyType_Type | type 实现</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line">PyTypeObject PyType_Type = &#123;</span><br><span class="line">    PyVarObject_HEAD_INIT(&amp;PyType_Type, <span class="number">0</span>)</span><br><span class="line">    <span class="string">"type"</span>,                                     <span class="comment">/* tp_name */</span></span><br><span class="line">    <span class="keyword">sizeof</span>(PyHeapTypeObject),                   <span class="comment">/* tp_basicsize */</span></span><br><span class="line">    <span class="keyword">sizeof</span>(PyMemberDef),                        <span class="comment">/* tp_itemsize */</span></span><br><span class="line">    (destructor)type_dealloc,                   <span class="comment">/* tp_dealloc */</span></span><br><span class="line">    offsetof(PyTypeObject, tp_vectorcall),      <span class="comment">/* tp_vectorcall_offset */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_getattr */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_setattr */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_async */</span></span><br><span class="line">    (reprfunc)type_repr,                        <span class="comment">/* tp_repr */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_number */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_sequence */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_mapping */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_hash */</span></span><br><span class="line">    (ternaryfunc)type_call,                     <span class="comment">/* tp_call */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_str */</span></span><br><span class="line">    (getattrofunc)type_getattro,                <span class="comment">/* tp_getattro */</span></span><br><span class="line">    (setattrofunc)type_setattro,                <span class="comment">/* tp_setattro */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_as_buffer */</span></span><br><span class="line">    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |</span><br><span class="line">    Py_TPFLAGS_BASETYPE | Py_TPFLAGS_TYPE_SUBCLASS |</span><br><span class="line">    Py_TPFLAGS_HAVE_VECTORCALL,                 <span class="comment">/* tp_flags */</span></span><br><span class="line">    type_doc,                                   <span class="comment">/* tp_doc */</span></span><br><span class="line">    (traverseproc)type_traverse,                <span class="comment">/* tp_traverse */</span></span><br><span class="line">    (inquiry)type_clear,                        <span class="comment">/* tp_clear */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_richcompare */</span></span><br><span class="line">    offsetof(PyTypeObject, tp_weaklist),        <span class="comment">/* tp_weaklistoffset */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_iter */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_iternext */</span></span><br><span class="line">    type_methods,                               <span class="comment">/* tp_methods */</span></span><br><span class="line">    type_members,                               <span class="comment">/* tp_members */</span></span><br><span class="line">    type_getsets,                               <span class="comment">/* tp_getset */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_base */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_dict */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_descr_get */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_descr_set */</span></span><br><span class="line">    offsetof(PyTypeObject, tp_dict),            <span class="comment">/* tp_dictoffset */</span></span><br><span class="line">    type_init,                                  <span class="comment">/* tp_init */</span></span><br><span class="line">    <span class="number">0</span>,                                          <span class="comment">/* tp_alloc */</span></span><br><span class="line">    type_new,                                   <span class="comment">/* tp_new */</span></span><br><span class="line">    PyObject_GC_Del,                            <span class="comment">/* tp_free */</span></span><br><span class="line">    (inquiry)type_is_gc,                        <span class="comment">/* tp_is_gc */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="type-分析"><a href="#type-分析" class="headerlink" title="type 分析"></a>type 分析</h3><h4 id="type-type-type"><a href="#type-type-type" class="headerlink" title="type(type) == type"></a>type(type) == type</h4><p>从源码中可以发现 PyType_Type 也是结构体 PyTypeObject 的实现。同样结构体的第一行 <code>PyVarObject_HEAD_INIT(&amp;PyType_Type, 0)</code> 印证了 <code>type(type) == type</code>。</p><p>甚至：type(type(type)) == type，无论嵌套多少层都是成立的。</p><h4 id="type-base-object"><a href="#type-base-object" class="headerlink" title="type.__base__ == object"></a>type.__base__ == object</h4><p>原因待补充。</p><h4 id="type-里有什么"><a href="#type-里有什么" class="headerlink" title="type 里有什么"></a>type 里有什么</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line">dir(type)</span><br><span class="line">[<span class="string">'__abstractmethods__'</span>,</span><br><span class="line"> <span class="string">'__base__'</span>,</span><br><span class="line"> <span class="string">'__bases__'</span>,</span><br><span class="line"> <span class="string">'__basicsize__'</span>,</span><br><span class="line"> <span class="string">'__call__'</span>,</span><br><span class="line"> <span class="string">'__class__'</span>,</span><br><span class="line"> <span class="string">'__delattr__'</span>,</span><br><span class="line"> <span class="string">'__dict__'</span>,</span><br><span class="line"> <span class="string">'__dictoffset__'</span>,</span><br><span class="line"> <span class="string">'__dir__'</span>,</span><br><span class="line"> <span class="string">'__doc__'</span>,</span><br><span class="line"> <span class="string">'__eq__'</span>,</span><br><span class="line"> <span class="string">'__flags__'</span>,</span><br><span class="line"> <span class="string">'__format__'</span>,</span><br><span class="line"> <span class="string">'__ge__'</span>,</span><br><span class="line"> <span class="string">'__getattribute__'</span>,</span><br><span class="line"> <span class="string">'__gt__'</span>,</span><br><span class="line"> <span class="string">'__hash__'</span>,</span><br><span class="line"> <span class="string">'__init__'</span>,</span><br><span class="line"> <span class="string">'__init_subclass__'</span>,</span><br><span class="line"> <span class="string">'__instancecheck__'</span>,</span><br><span class="line"> <span class="string">'__itemsize__'</span>,</span><br><span class="line"> <span class="string">'__le__'</span>,</span><br><span class="line"> <span class="string">'__lt__'</span>,</span><br><span class="line"> <span class="string">'__module__'</span>,</span><br><span class="line"> <span class="string">'__mro__'</span>,</span><br><span class="line"> <span class="string">'__name__'</span>,</span><br><span class="line"> <span class="string">'__ne__'</span>,</span><br><span class="line"> <span class="string">'__new__'</span>,</span><br><span class="line"> <span class="string">'__prepare__'</span>,</span><br><span class="line"> <span class="string">'__qualname__'</span>,</span><br><span class="line"> <span class="string">'__reduce__'</span>,</span><br><span class="line"> <span class="string">'__reduce_ex__'</span>,</span><br><span class="line"> <span class="string">'__repr__'</span>,</span><br><span class="line"> <span class="string">'__setattr__'</span>,</span><br><span class="line"> <span class="string">'__sizeof__'</span>,</span><br><span class="line"> <span class="string">'__str__'</span>,</span><br><span class="line"> <span class="string">'__subclasscheck__'</span>,</span><br><span class="line"> <span class="string">'__subclasses__'</span>,</span><br><span class="line"> <span class="string">'__subclasshook__'</span>,</span><br><span class="line"> <span class="string">'__text_signature__'</span>,</span><br><span class="line"> <span class="string">'__weakrefoffset__'</span>,</span><br><span class="line"> <span class="string">'mro'</span>]</span><br></pre></td></tr></table></figure><h2 id="小结：对象与类型"><a href="#小结：对象与类型" class="headerlink" title="小结：对象与类型"></a>小结：对象与类型</h2><p>至此，CPython 中用于实现 Python 类的几个比较重要的结构体和实现都被展示出来了：</p><p><img src="https://img.blanc.site//wiki/img202204011031943.png" alt="CPython 结构关系"></p><p>后文基本上都是基于 Python 的内容了，不会再设计 CPython 源码的列举。</p><h2 id="类型实例（类对象）：类的生成"><a href="#类型实例（类对象）：类的生成" class="headerlink" title="类型实例（类对象）：类的生成"></a>类型实例（类对象）：类的生成</h2><p>根据前文的内容我们已然得知：类是由类型实例化得到的。</p><h3 id="手写类的生成"><a href="#手写类的生成" class="headerlink" title="手写类的生成"></a>手写类的生成</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, name)</span>:</span></span><br><span class="line">        self.name = name</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">custom_init</span><span class="params">(self, name)</span>:</span></span><br><span class="line">    self.name = name</span><br><span class="line"></span><br><span class="line">Bar = type(<span class="string">"Bar"</span>, (object, ), &#123;<span class="string">"__init__"</span>: custom_init&#125;)</span><br><span class="line"></span><br><span class="line">a = Foo(<span class="string">"foo"</span>)</span><br><span class="line">b = Bar(<span class="string">"bar"</span>)</span><br></pre></td></tr></table></figure><p>通过上面的代码我们能得到两个类对象：Foo 和 Bar，其中 Foo 的定义方式是在 Python 编码过程中常用的形式，而 Bar 的定义方式则更能体现「类是有类型实例化得到的」。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span><span class="params">(metaclass=type)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, name)</span>:</span></span><br><span class="line">        self.name = name</span><br></pre></td></tr></table></figure><p>当有很多属性和方法需要定义时，使用 Bar 的定义方式会显得很不方便。但在通过对 Foo 的定义方式进行改造后，同样能够帮助我们清楚地看清类对象生成过程。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FooType</span><span class="params">(type)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__new__</span><span class="params">(cls, name, bases, attrs)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> type.__new__(cls, name, bases, attrs)</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span><span class="params">(metaclass=FooTyoe)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, name)</span>:</span></span><br><span class="line">        self.name = name</span><br></pre></td></tr></table></figure><p>为了更清楚的理解类型（元类）的工作过程，我封装了一个 FooType 类型，并让 Foo 类对象使用这个类型。</p><h3 id="类对象"><a href="#类对象" class="headerlink" title="类对象"></a>类对象</h3><p>当我们使用编辑器在文件中定义好类的主体后，就可以使用 Python 解释器加载相关文件（模块）了，在 Python 解释器加载了相关文件（模块）后，定义好的类主体会被用来生成相应的类对象。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># test.py</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span>:</span></span><br><span class="line">    a = <span class="number">1</span></span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, b=None)</span>:</span></span><br><span class="line">        self.b = b</span><br><span class="line">    </span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">static_foo</span><span class="params">()</span>:</span></span><br><span class="line">        print(Foo.a)</span><br><span class="line">    </span><br><span class="line"><span class="meta">    @classmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">class_foo</span><span class="params">(cls)</span>:</span></span><br><span class="line">        print(cls.a)</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(self)</span>:</span></span><br><span class="line">        print(self.a)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Foo.__dict__</span></span><br><span class="line"><span class="comment"># mappingproxy(&#123;'__module__': '__main__',</span></span><br><span class="line"><span class="comment">#              'a': 1,</span></span><br><span class="line"><span class="comment">#              '__init__': &lt;function __main__.Foo.__init__(self, b=None)&gt;,</span></span><br><span class="line"><span class="comment">#              'static_foo': &lt;staticmethod at 0x108992160&gt;,</span></span><br><span class="line"><span class="comment">#              'class_foo': &lt;classmethod at 0x108992e50&gt;,</span></span><br><span class="line"><span class="comment">#              'foo': &lt;function __main__.Foo.foo(self)&gt;,</span></span><br><span class="line"><span class="comment">#              '__dict__': &lt;attribute '__dict__' of 'Foo' objects&gt;,</span></span><br><span class="line"><span class="comment">#              '__weakref__': &lt;attribute '__weakref__' of 'Foo' objects&gt;,</span></span><br><span class="line"><span class="comment">#              '__doc__': None&#125;)</span></span><br></pre></td></tr></table></figure><h3 id="类的继承"><a href="#类的继承" class="headerlink" title="类的继承"></a>类的继承</h3><p>在 Python3 中，类的继承解析使用的是 C3 算法，可以参考我的另一篇文章：<a href="https://wiki.blanc.site/archives/ffe3a081.html">Python MRO</a>。</p><h2 id="实例：类的实例"><a href="#实例：类的实例" class="headerlink" title="实例：类的实例"></a>实例：类的实例</h2><h3 id="手写类的实例化"><a href="#手写类的实例化" class="headerlink" title="手写类的实例化"></a>手写类的实例化</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, name)</span>:</span></span><br><span class="line">        self.name = name</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">instance_maker</span><span class="params">(the_class, *args, **kwargs)</span>:</span></span><br><span class="line">    new_inscance = the_class.__new__(the_class, *args, **kwargs)</span><br><span class="line">    <span class="keyword">if</span> isinstance(new_inscance, the_class):</span><br><span class="line">        the_class.__init__(new_inscance, *args, **kwargs)</span><br><span class="line">    <span class="keyword">return</span> new_inscance</span><br><span class="line"></span><br><span class="line"><span class="comment"># 以下两条语句效果一致</span></span><br><span class="line">x = Foo(<span class="string">"foo"</span>)</span><br><span class="line">z = instance_maker(Foo, <span class="string">"foo"</span>)</span><br></pre></td></tr></table></figure><h3 id="实例对象"><a href="#实例对象" class="headerlink" title="实例对象"></a>实例对象</h3><p>当我们在编码的过程中定义了类实例化的内容，并在导入相关文件（模块）后执行相关代码，Python 解释器会在执行到类实例化内容的时候生成实例对象。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">class Foo:</span><br><span class="line">    def __init__(self, name):</span><br><span class="line">        self.name &#x3D; name</span><br><span class="line"></span><br><span class="line">foo &#x3D; Foo(&quot;foo&quot;)</span><br></pre></td></tr></table></figure><h3 id="类对象和实例对象的简单对比"><a href="#类对象和实例对象的简单对比" class="headerlink" title="类对象和实例对象的简单对比"></a>类对象和实例对象的简单对比</h3><ul><li>类对象是全局唯一的；</li><li>实例对象在每次执行到实例化内容时都会生成；</li><li>可以利用各种技巧实现单例模式，使得类对象在实例化的过程中只返回唯一的实例；</li><li>类对象可以调用类属性、类方法以及静态方法；</li><li>实例对象可以调用类属性、类方法、实例属性、实例方法以及静态方法；</li></ul><p><strong>注：类对象也是可以调用实例方法的，只是需要传入一个存在的实例对象作为 self；</strong></p><h3 id="函数是如何变成方法的-self-是在何时被赋值的"><a href="#函数是如何变成方法的-self-是在何时被赋值的" class="headerlink" title="函数是如何变成方法的 | self 是在何时被赋值的"></a>函数是如何变成方法的 | self 是在何时被赋值的</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line">class Foo:</span><br><span class="line">    def __init__(self, name):</span><br><span class="line">        self.name &#x3D; name</span><br><span class="line">    </span><br><span class="line">    def foo(self):</span><br><span class="line">        print(self.name)</span><br><span class="line"></span><br><span class="line">foo &#x3D; Foo(&quot;foo&quot;)</span><br><span class="line">foo.foo()</span><br><span class="line"># foo</span><br><span class="line"></span><br><span class="line">Foo.foo()</span><br><span class="line"># TypeError: foo() missing 1 required positional argument: &#39;self&#39;</span><br><span class="line"></span><br><span class="line">type(Foo.foo) # function | 函数</span><br><span class="line">type(foo.foo) # method   | 方法</span><br><span class="line"></span><br><span class="line">Foo.foo # &lt;function __main__.Foo.foo(self)&gt;</span><br><span class="line">foo.foo # &lt;bound method Foo.foo of &lt;__main__.Foo object at 0x10bcc01c0&gt;&gt;</span><br><span class="line"></span><br><span class="line">dir(Foo.foo)</span><br><span class="line"># ...</span><br><span class="line"># __call__          # 调用时执行的方法</span><br><span class="line"># __code__          # 包含已编译函数的代码对象 bytecode</span><br><span class="line"># __defaults__      # 所有位置或关键字参数的默认值的元组</span><br><span class="line"># __kwdefaults__    # 所有关键字参数默认值的映射</span><br><span class="line"># ...</span><br><span class="line"></span><br><span class="line">dir(foo.foo)</span><br><span class="line"># ...</span><br><span class="line"># __call__          # 调用时执行的方法</span><br><span class="line"># __func__          # 实现该方法的函数对象  </span><br><span class="line"># __self__          # 该方法被绑定的实例，若没有绑定则为 None</span><br><span class="line"># ...</span><br><span class="line"></span><br><span class="line"># 实例对象下方法的 __func__ 指向的是类对象中的函数</span><br><span class="line">id(foo.foo)          # 4504426880</span><br><span class="line">id(foo.foo.__func__) # 4505398432</span><br><span class="line">id(Foo.foo)          # 4505398432</span><br><span class="line"></span><br><span class="line"># 实例对象下的方法绑定的 self 是实例对象本身</span><br><span class="line">id(foo)              # 4493634912</span><br><span class="line">id(foo.foo.__self__) # 4493634912</span><br></pre></td></tr></table></figure><ul><li>定义在类中的函数会在类对象实例化后化作实例对象中绑定方法的 __func__ 属性；</li><li>方法会将自己绑定的实例对象存放在 __slef__ 属性中；</li><li>绑定方法在执行时会将 __self__ 属性当做 __func__ 的第一个参数传入。</li></ul><p>以上，方法在无形中将 self 传入到了函数中。</p><h3 id="各种优先级对比"><a href="#各种优先级对比" class="headerlink" title="各种优先级对比"></a>各种优先级对比</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span><span class="params">(Bar)</span>:</span></span><br><span class="line">    a = <span class="number">1</span></span><br><span class="line">    b = <span class="number">2</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, b=None)</span>:</span></span><br><span class="line">        <span class="keyword">if</span> b:</span><br><span class="line">            self.b = b</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">echo</span><span class="params">(self)</span>:</span></span><br><span class="line">        print(self.a, self.b)</span><br><span class="line">    </span><br><span class="line"><span class="meta">    @property</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">name</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">"foo"</span></span><br><span class="line">    </span><br><span class="line"><span class="meta">    @property</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">"property"</span></span><br><span class="line">    </span><br><span class="line"></span><br><span class="line">foo = Foo(<span class="string">"foo"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 实例属性 &gt; 类属性</span></span><br><span class="line">foo.a = <span class="number">3</span></span><br><span class="line">foo.echo()           <span class="comment"># 3 foo</span></span><br><span class="line">print(Foo.a, Foo.b)  <span class="comment"># 1 2</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># property 特性 &gt; 实例属性</span></span><br><span class="line">foo.foo         <span class="comment"># property</span></span><br><span class="line">foo.foo = <span class="string">"foo"</span> <span class="comment"># AttributeError: can't set attribute</span></span><br><span class="line">foo.__dict__[<span class="string">"foo"</span>] = <span class="string">"foo"</span></span><br><span class="line">foo.foo         <span class="comment"># property</span></span><br><span class="line">foo.b           <span class="comment"># foo</span></span><br><span class="line">Foo.b = property(<span class="keyword">lambda</span> self: <span class="string">"property"</span>)</span><br><span class="line">foo.b           <span class="comment"># property</span></span><br></pre></td></tr></table></figure><h2 id="小结：类型、类、实例"><a href="#小结：类型、类、实例" class="headerlink" title="小结：类型、类、实例"></a>小结：类型、类、实例</h2><p><img src="https://img.blanc.site//wiki/img202204011254343.png" alt="类型、类和实例的关系"></p><h2 id="概念的抽象"><a href="#概念的抽象" class="headerlink" title="概念的抽象"></a>概念的抽象</h2><p>Python 之所以要设计这么复杂的一套逻辑来构建「类」体系，其核心目的就是为了帮助使用者在使用的过程中能够更加方便的进行概念抽象化，并且能够趁手的使用被抽象化后的类。</p><p>关于抽象，我最早产生好奇的时候搜到了 CSDN 杏仁技术栈转载的章烨明的 <a href="https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/78863467" target="_blank" rel="external nofollow noopener noreferrer">谈谈到底什么是抽象，以及软件设计的抽象原则</a> 以及 optman 的 <a href="https://blog.csdn.net/optman/article/details/1383325" target="_blank" rel="external nofollow noopener noreferrer">抽象的层次</a>，此外应该还看过其他的一些文章但是我没有保存下来所以也没法考究了。在那之后，我大致能在编程的过程中理解抽象以及那些被抽象的东西，并且能够沿着抽象层级一点一点的去拨开各类开源库的核心，从中学习好的编码思想。</p><p>现在，我对「抽象」这个概念又产生了新的困惑，我开始好奇原本抽象的上一层抽象是什么，我还好奇数学是被发现的还是被发明的。通过最近这段时间的阅读我已经有了笼统的概念，下面是我的一些浅显的理解，希望能够帮助同样好奇的你。</p><h3 id="Python-中的抽象"><a href="#Python-中的抽象" class="headerlink" title="Python 中的抽象"></a>Python 中的抽象</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"># 在编程过程中，下面的概念自左向右抽象等级依次提高</span><br><span class="line">variable → collections → method → class → module → package </span><br><span class="line"># 但从编程语言的实现角度来看所有的概念又可以抽象为 object</span><br><span class="line">                        ↓↓↓</span><br><span class="line">                       object</span><br><span class="line">                      ↓       ↓</span><br><span class="line">                    object   type</span><br><span class="line"># 而在 Python 的最终实现上，object 这个抽象概念被实现成了 object 和 type 两个 object</span><br><span class="line"># 通过 object 和 type 就可以生产所有最上面提到的概念</span><br></pre></td></tr></table></figure><h3 id="通过道德经歪解抽象"><a href="#通过道德经歪解抽象" class="headerlink" title="通过道德经歪解抽象"></a>通过道德经歪解抽象</h3><p><strong>注：以下歪解大都是以「更好的理解 Python」为出发点的。</strong></p><blockquote><p>道生一，一生二，二生三，三生万物。</p><p>——《道德经》</p></blockquote><p>人们将自己对于现实世界的观察（道）进行抽象产生了对象（一）这个概念，再以对象为基础创建了一门编程语言，这门编程语言中万事万物都是对象，并通过对象和类型两个概念（二）去描述所有的对象（万物）。</p><p>当然，人们对世界的观察并非真正的「道」而是人们对「道」的「名」。但放到一门编程语言中，这些「名」组成了这门语言的「道」。</p><blockquote><p>“一”可以是阴，也可以是阳。但是只能是一个面。“一”有它的对立面，这就有了阴阳。“二”就是阴阳。阴阳是有力量的， 他们互不相容，必定要相互作用，这种相互作用的趋势，就是“三”，就是冲气。道生一，一生二，二生三，把“生”字换成“有”更好理解。道有一，也有零；一有它的对立面，这就构成了二；二有三，因为二中的两个一相互对立，所以要相互作用，这个趋势就是三。“三生万物”的“生”理解为“产生”更合适。</p><p>——《道德经》 富强 译注</p></blockquote><p>参照注释的讲解，可以对我上面的描述进行更进一步的理解：在开始创建一门面向对象的编程语言的时候，这门语言中还没有任何概念，因此我们从现实生活中进行抽象，得到了「对象」。仅有「对象」这个概念是无法开展下一步的工作的，因此我们尝试对「对象」进行描述得到了「类型」，在有了「类型」后「对象」和「类型」就有了一种相互作用的能力：「（反）实例化」。</p><p>在有了这样的一门编程语言之后，我们就可以利用面向对象的特性和抽象思想将现实生活中的业务场景使用编程语言表达出来。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>本来是想好好的剖析一下 Python 中的类，结果写出这么一篇四不像来。在写作的过程中总是想涵盖更多的东西，结果发现自己好像 hold 不住，于是开始删减，只保留了当前的这些内容，希望能够帮助你更好的理解 Python 中的类。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://docs.python.org/3/index.html" target="_blank" rel="external nofollow noopener noreferrer">Python 官方文档</a></li><li><a href="https://www.zhihu.com/question/38791962/answer/78159157" target="_blank" rel="external nofollow noopener noreferrer">灼弦-知乎回答：Python 的 type 和 object 之间是怎么一种关系？</a></li><li><a href="https://www.zhihu.com/question/38791962/answer/78218158" target="_blank" rel="external nofollow noopener noreferrer">高山流水-知乎回答：Python 的 type 和 object 之间是怎么一种关系？</a></li><li><a href="https://mooc.study.163.com/course/2001361005?tid=2403008003&_trace_c_p_k2_=cebe971c79de49d793ef1bf8d63db625#/info" target="_blank" rel="external nofollow noopener noreferrer">网易云课堂-Python 微专业（目前不开了已经）</a></li><li><a href="https://zhuanlan.zhihu.com/p/357021677" target="_blank" rel="external nofollow noopener noreferrer">Providence：Python 源码学习（1）：类型和对象</a></li><li><a href="https://book.douban.com/subject/27028517/" target="_blank" rel="external nofollow noopener noreferrer">Luciano Ramalho：流畅的 Python（安道 吴珂译）</a></li><li><a href="https://he11olx.com/2018/07/14/1.CPython3.6%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/1.0.PyObject/" target="_blank" rel="external nofollow noopener noreferrer">Lx’s Blog：【CPython3.6源码分析】PyObject/PyObjectType</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;最近想为 SQLAlchemy 封装一套类似 Django ORM 的 Model Manager，于是捡起了「流畅的 Python」开始看
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Python" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Python/"/>
    
    
      <category term="Python" scheme="https://wiki.blanc.site/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>WeeklyPEP-2-PEP 343-with 语句-overview</title>
    <link href="https://wiki.blanc.site/archives/5823d5df.html"/>
    <id>https://wiki.blanc.site/archives/5823d5df.html</id>
    <published>2022-02-01T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文的主体内容大部分来自对 <a href="https://www.python.org/dev/peps/pep-0343/" target="_blank" rel="external nofollow noopener noreferrer">PEP 343</a> 原文的翻译，其余部分为本人对原文的理解，在整理过程中我没有刻意地区分翻译的部分和我个人理解的部分，这两部分内容被糅杂在一起形成了本文。因此，请不要带着「本文的内容是百分之百正确」的想法阅读。如果文中的某些内容让你产生疑惑，你可以给我留言与我讨论或者对比 <a href="https://www.python.org/dev/peps/pep-0343/" target="_blank" rel="external nofollow noopener noreferrer">PEP 343</a> 的原文加以确认。</p><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>本 PEP 为 Python 增加了一个新的语法：with，它能够更加简便的代替标准的 try/finally 语法。</p><p>在本 PEP 中，<a href="https://docs.python.org/3/glossary.html#term-context-manager" target="_blank" rel="external nofollow noopener noreferrer">上下文管理器</a> 提供了 __enter__() 和 __exit__() 方法，分别在进入和退出 with 语句时被调用。</p><h2 id="历史背景"><a href="#历史背景" class="headerlink" title="历史背景"></a>历史背景</h2><p>本 PEP 最初是由 <a href="https://gvanrossum.github.io/" target="_blank" rel="external nofollow noopener noreferrer">Guido</a> 以第一人称撰写，随后由 <a href="http://www.curiousefficiency.org/" target="_blank" rel="external nofollow noopener noreferrer">Nick Coghlan</a> 更新那些后来在 <a href="https://mail.python.org/pipermail/python-dev/" target="_blank" rel="external nofollow noopener noreferrer">python-dev</a> 上的讨论。</p><p>在对 <a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a> 及其代替方案进行大量讨论后，<a href="https://gvanrossum.github.io/" target="_blank" rel="external nofollow noopener noreferrer">Guido</a> 决定撤销 <a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a> 并在 <a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 的基础上提出一个新的版本。此版本增加了一个 throw() 方法用于在 <a href="#生成器状态">暂停的生成器</a> 中引发异常以及一个 close() 方法用来抛出 GeneratorExit 异常，并将语法关键字修改为 with（在 <a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a> 中定义的是 block）。</p><p>在 <a href="https://www.python.org/dev/peps/pep-0343/" target="_blank" rel="external nofollow noopener noreferrer">PEP 343</a> 被正式通过后，以下 PEP 由于内容重叠被驳回或撤销：</p><ol><li><a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310：最初的 with 语法的提案</a></li><li><a href="https://www.python.org/dev/peps/pep-0319/" target="_blank" rel="external nofollow noopener noreferrer">PEP 319：它所描述的场景可以通过提供合适的 with 语法实现</a></li><li><a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a></li><li><a href="https://www.python.org/dev/peps/pep-0346/" target="_blank" rel="external nofollow noopener noreferrer">PEP 346</a></li></ol><p>在 <a href="https://wiki.python.org/moin/WithStatement" target="_blank" rel="external nofollow noopener noreferrer">Python Wiki</a> 上对本 PEP 的早期版本进行过一些讨论。</p><h2 id="提案动机"><a href="#提案动机" class="headerlink" title="提案动机"></a>提案动机</h2><p><a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a> 中阐述了大量优秀的想法，例如：使用生成器作为 block 语法的模板，添加异常处理和 finalization（个人感觉可以理解为析构方法） 给生成器等等。除了赞同，它也因为底层是循环结构的事实遭到了很多人的反对。使用循环结构意味着 block 语法体内的 break 和 continue 会扰乱正常的代码逻辑带来不确定性，即使它被用来作为一个非循环资源的管理工具。</p><p><a href="https://twitter.com/chencravat" target="_blank" rel="external nofollow noopener noreferrer">Raymond Chen</a> 的 <a href="https://devblogs.microsoft.com/oldnewthing/20050106-00/?p=36783" target="_blank" rel="external nofollow noopener noreferrer">一篇对流程控制宏提出反对的文章</a> 让 Guido 做出了最终决定——放弃 <a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a>。Raymond 的文章中有一个观点：将流程控制隐藏在宏中会让代码变得更难让人读懂也更难维护，Guido 发现这个观点在 Python 和 C 都适用，并由此意识到 <a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a> 提出的 templates 会隐藏几乎所有控制流程。例如，<a href="https://www.python.org/dev/peps/pep-0340/#id15" target="_blank" rel="external nofollow noopener noreferrer">PEP 340 的 examples 4</a> 中的 auto_retry 函数只能捕获异常并且重复 block 最多三次（而这一点被隐藏在了语法的内部）。</p><p>对比来看 <a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 的 with 语法并没有隐藏流程控制，虽然 finally 会暂时地中止控制流，但在最后（finally 内的内容执行完毕后）控制流会继续执行就好像 finally 不存在一样。</p><p><a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 中粗略地提出了这样的语法（其中 VAR 是可选的）:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> VAR = EXPR:</span><br><span class="line">    BLOCK</span><br></pre></td></tr></table></figure><p>上面的语法大致上可以翻译成这样：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">VAR = EXPR</span><br><span class="line">VAR.__enter__()</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    BLOCK</span><br><span class="line"><span class="keyword">finally</span>:</span><br><span class="line">    VAR.__exit__()</span><br></pre></td></tr></table></figure><p>但当在 <a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 的语法基础上实现如下代码时：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> f = open(<span class="string">"/ect/passwd"</span>):</span><br><span class="line">    BLOCK1</span><br><span class="line">BLOCK2</span><br></pre></td></tr></table></figure><p>按照前面给的示例这段代码大致可以被翻译成这样：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">f = open(<span class="string">"/etc/passwd"</span>)</span><br><span class="line">f.__enter__()</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    BLOCK1</span><br><span class="line"><span class="keyword">finally</span>:</span><br><span class="line">    f.__exit__()</span><br><span class="line"></span><br><span class="line">BLOCK2</span><br></pre></td></tr></table></figure><p>上面的代码好像默认了 BLOCK1 的部分会没有异常地顺利的执行，然后 BLOCK2 部分会被紧接着调用。但如果在 BOLCK1 中有异常抛出或执行了一个 non-local goto（例如：break, continue, return），BLOCK2 就无法被顺利执行。with 语法在尾部增加的魔法（__exit__()）并不能解决这一问题。</p><p>你也许会问：如果在 __exit__() 方法中抛出了一个异常会怎样？<br>如果真的发生了，那一切就都完了，但这并不会比其他情况下引发的异常更糟糕。</p><p>异常的本质就是它可以在代码的任意位置被抛出，而你只能忍受这一点。即使你写的代码不会抛出任何异常，一个 KeyboardInterrupt 异常仍然会导致它在任意两个虚拟机器操作码之间退出（个人理解：哪怕你的程序没有任何问题，正在正常执行的程序也可能因为你的强制退出行为而退出，例如常见的  <code>Ctrl + c</code>）。</p><p>上面的这些讨论以及 <a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 表现出来的特性让 Guido 开始更倾向于使用 <a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 的语法，但是他还是希望能够实现 <a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a> 中提出的：使用生成器作为获取和释放锁或打开和关闭文件等抽象概念的「模板」。这是一个很有用的功能，通过 <a href="https://www.python.org/dev/peps/pep-0340/#id15" target="_blank" rel="external nofollow noopener noreferrer">PEP 340 的示例</a> 就能够有所了解。</p><p>受 Phillip Eby 对 <a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a> 一个反对建议的启发，Guido 尝试创建一个装饰器，将一个合适的生成器变成一个具有必要的 __enter__() 和 __exit__() 方法的对象。但在实现过程中他遇到一个障碍：处理 locking example 并不困难，但是处理 opening example 几乎是不可能的。他是这样定义语法模板的：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">opening</span><span class="params">(filename)</span>:</span></span><br><span class="line">    f = open(filename)</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">yield</span> f</span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        f.close()</span><br></pre></td></tr></table></figure><p>并且可以被这样使用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> f = opening(filename):</span><br><span class="line">    ...read data <span class="keyword">from</span> f...</span><br></pre></td></tr></table></figure><p>问题是在 <a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 中，EXPR 表达式的结果会被直接分配给 VAR，然后在退出 BLOCK1 时调用 VAR 的 __exit__() 方法。但在这里，VAR 显然需要得到被打开的文件对象，这就意味着 __exit__() 必须是文件对象上的一个方法。</p><p>虽然这个问题可以通过代理类来解决，但这种解决方案并不优雅。在思考之后，Guido 意识到：只需要稍微改动语法模板就可以轻松地编写出所需的装饰器。这个改动就是：<strong>将 VAR 设置为 __enter__() 方法的调用结果，并且保存 EXPR 的值以便后面调用它的 __exit__() 方法</strong>。此时装饰器就能返回一个 wrapper 类的实例，实例的 __enter__() 方法调用生成器的 next() 方法返回 next() 所返回的内容。wrapper 类实例的 __exit__() 方法会再次调用生成器的 next() 方法并（期望）抛出一个 Stoplteration 异常。详情可以看下面的 <a href="#生成器装饰器">生成器装饰器一节</a>。</p><p>所以现在最后的障碍是 <a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 的语法：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">with VAR &#x3D; EXPR:</span><br><span class="line">    BLOCK1</span><br></pre></td></tr></table></figure><p>不应该使用赋值操作而应该使用隐式操作，因为本质上并不是将 EXPR 赋值给 VAR。参考 <a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340</a> 的实现，可以修改为：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">with EXPR as VAR:</span><br><span class="line">    BLOCK1</span><br></pre></td></tr></table></figure><p>在提案的讨论中还能够看出，开发者们普遍希望能够在生成器中捕获到异常，即使仅是拿来做日志。但生成器是不允许 yield 其他值的，因为 with 语法不应该被设计成一个循环（抛出一个不同的异常是稍微能够被接受的）。</p><p>为了实现这个需求，一个新的生成器方法 throw() 被提出，它将接收一到三个代表异常的可选参数（type, value, traceback）并且在 <a href="#生成器状态">生成器暂停时</a> 抛出它。</p><p>在提出 throw() 方法后另一个生成器方法 close() 也自然而然地被提出了。close() 方法能够通过一个名为 GeneratorExit 的特殊异常调用 throw() 方法。这个行为相当于告诉生成器准备退出，并且在此基础上还有一个小小的改进，即在生成器被垃圾回收机制回收时会自动触发 close() 方法。</p><p>自此以后，我们就可以在 try-finally 语法中插入 yield 语法了，因为我们现在能够保证 finally 语句最后一定会被执行。但还有一些关于 finalization apply 的常见警告：进程可能在没有析构任何对象的情况下突然终止，而且对象可能会因为应用中的循环或内存泄漏而永远存在（与被 GC 控制 的 Python 的循环或内存泄漏相反）。</p><p>注意，我们并不能保证 finally 部分会在生成器对象无引用后被立即执行，尽管 CPython 中是这样实现的。这与自动关闭文件类似：虽然在 CPython 中对象的最后一个引用消失后会立即删除该对象，但其他的 GC 算法未必做了同样的实现。</p><p>可以去 <a href="https://www.python.org/dev/peps/pep-0342/" target="_blank" rel="external nofollow noopener noreferrer">PEP 342</a> 中找那些生成器被修改的细节。</p><h2 id="语法规范"><a href="#语法规范" class="headerlink" title="语法规范"></a>语法规范</h2><p>一个新的声明被提出，它的语法是：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> EXPR <span class="keyword">as</span> VAR:</span><br><span class="line">    BLOCK</span><br></pre></td></tr></table></figure><p>其中 with 和 as 是新的关键字。EXPR 是一个任意的表达式（但不能是一个表达式列表），VAR 是一个单一的分配目标，它不能是逗号分割的变量序列，但可以被括号包裹的逗号分割的变量序列（这个限制使得未来的语法有可能拓展为多个逗号分割的资源，每个资源都有对应的 as 语句）。</p><p><code>as VAR</code> 部分是可选的，上述声明被翻译为：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">mgr = (EXPR)</span><br><span class="line">exit = type(mgr).__exit__ <span class="comment"># Not calling it yet</span></span><br><span class="line">value = type(mgr).__enter__(mgr)</span><br><span class="line">exc = <span class="literal">True</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        VAR = value <span class="comment"># Only if "as VAR" is present</span></span><br><span class="line">        BLOCK</span><br><span class="line">    <span class="keyword">except</span>:</span><br><span class="line">        <span class="comment"># The exceptional case is handled here</span></span><br><span class="line">        <span class="comment"># 如果 BLCOK 部分出现异常</span></span><br><span class="line">        exc = <span class="literal">False</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> exit(mgr, *sys.exc_info()):</span><br><span class="line">            <span class="keyword">raise</span></span><br><span class="line">        <span class="comment"># The exception is swallowed if exit() returns true</span></span><br><span class="line">        <span class="comment"># 如果 exit() 返回 true，则异常会被忽略</span></span><br><span class="line"><span class="keyword">finally</span>:</span><br><span class="line">    <span class="comment"># The normal and non-local goto case are handled here</span></span><br><span class="line">    <span class="comment"># 如果 BLOCK 没有异常或者执行了 non-local goto</span></span><br><span class="line">    <span class="keyword">if</span> exc:</span><br><span class="line">        exit(mgr, <span class="literal">None</span>, <span class="literal">None</span>, <span class="literal">None</span>)</span><br></pre></td></tr></table></figure><p>其中小写的变量（mgr, exit, value, exc）都是内部变量，用户不能访问，它们大概率会被实现为特殊的寄存器或 stack positions。sys.exc_info 的详细信息可以参考 <a href="#sys.exc_info">sys.exc_info 章节</a></p><p>上述代码翻译细节是为了规定准确的语义。<strong>如果相关方法没有像预期那样被找到，解释器将会抛出 AttributeError 异常</strong>。同样，如果任何一个调用抛出了一个异常，其效果与上述代码一致。最后，<strong>如果 BLOCK 包含 non-local goto 语句，那么 __exit__() 方法会像 BLOCK 被正常执行完一样被调用，并带有三个 None 参数</strong>。也就是说，这些「伪异常」不会被 __exit__() 视为异常。</p><p>如果 <code>as VAR</code> 部分的语法被省略，翻译中 <code>VAR =</code> 的部分也会被省略，但 <code>mgr.__enter()</code> 仍被调用。</p><p>mgr.__exit__() 的调用惯例如下：</p><ul><li>如果 BLOCK 中的内容正常执行完毕或某个 non-local goto 被触发，执行到 finally 语法块时 mgr.__exit__() 将被调用，并带有三个 None 参数。</li><li>如果 BLOCK 中的内容引发了异常，执行到 finally 语法块时，mgr.__exit__() 将被调用，并被传递三个分别代表异常 type, value 和 traceback 的参数。</li></ul><p>重要的是：<strong>如果 mgr.__exit__() 返回的是 True，异常就会被忽略</strong>。也就是说，如果 mgr.__exit__() 返回 True，那么在 with 语句之后的下一个语句仍会被执行，即使 with 语法内部发生了异常。</p><p>然而，如果 with 语法被某个 non-local goto 中断，当 mgr.__exit__() 返回时，这个 non-local return 将在不考虑返回值的情况下被恢复。这样做的目的是使 mgr.__exit__() 能够在不被频繁误触发的情况下实现忽略异常抛出（因为 mgr.__exit__() 的默认返回值是 None 相当于 Flase，能够使异常被重新抛出）。</p><p>实现忽略异常抛出功能的主要目的是使编写 @contextmanger 装饰器成为可能。在能够忽略异常抛出的情况下实现的 @contextmanger 装饰器能够使一个被装饰的生成器内部的 try/except 块的行为就像生成器的主体在 with 语法的位置上被在线拓展一样。</p><p>将异常细节传递给 __exit__() 的动机，与 <a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310</a> 中无参数的 __exit__() 一致，具体可以参考示例章节的 <a href="#transaction：数据库事务管理">transaction：数据库事务管理</a>。在本示例中，生成器函数必须根据是否发生异常来决定是否进行事物回滚。在这种情况下，传递完整异常信息要比使用一个布尔值来标记是否发生异常更具有可拓展性，例如为异常记录工具提供接口。</p><p>依靠 sys.exc_info() 来获取异常信息的做法被否定了，因为 sys.exc_info() 的语义非常复杂，它完全有可能返回一个很久以前就被捕获的异常信息。还有人提议增加一个布尔值来区分顺利执行完 BLOCK 块和 BLOCK 块被 non-local goto 中断。这个提议也被否定了，因为它太过复杂且没有必要。对于数据库事务回滚的决定来说，non-local goto 应该被视为异常。</p><p><strong>为了使直接操作上下文管理器的 Python 代码的上线文变得简单，__exit__() 方法不应该重新抛出传递给它们的异常。应该总是由 __exit__() 方法的调用者负责决定何时重新引发异常。</strong></p><p>因此，调用者可以通过是否抛出异常来区分 __exit__() 是否执行失败：</p><ul><li>如果 __exit__() 未抛出异常，代表方法本身执行成功，无论被传入的异常是否被忽略；</li><li>如果 __exit__() 抛出异常，代表方法本身执行失败。</li></ul><p>因此，在实现 __exit__() 方法时应该避免抛出异常，除非真的存在异常（不需要避免抛出那些被传入的异常）。</p><h2 id="生成器装饰器"><a href="#生成器装饰器" class="headerlink" title="生成器装饰器"></a>生成器装饰器</h2><p>在 <a href="https://www.python.org/dev/peps/pep-0342/" target="_blank" rel="external nofollow noopener noreferrer">PEP 342</a> 通过后，我们就可以通过编写一个装饰器，让被此装饰器装饰的生成器可能被 with 语法使用，且此生成器刚好 yeild 一次。下面是完成此类装饰器的代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">GeneratorContextManager</span><span class="params">(object)</span>:</span></span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, gen)</span>:</span></span><br><span class="line">        self.gen = gen</span><br><span class="line">        </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__enter__</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="keyword">return</span> self.gen.next()</span><br><span class="line">        <span class="keyword">except</span> StopIteration:</span><br><span class="line">            <span class="keyword">raise</span> RuntimeError(<span class="string">"generator didn't yield"</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__exit__</span><span class="params">(self, type, value, traceback)</span>:</span></span><br><span class="line">        <span class="keyword">if</span> type <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">try</span>:</span><br><span class="line">                self.gen.next()</span><br><span class="line">            <span class="keyword">except</span> StopIteration:</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                <span class="keyword">raise</span> RuntimeError(<span class="string">"generator didn't stop"</span>)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="keyword">try</span>:</span><br><span class="line">                self.gen.throw(type, value, traceback)</span><br><span class="line">                <span class="keyword">raise</span> RuntimeError(<span class="string">"generator didn't stop after throw()"</span>)</span><br><span class="line">            <span class="keyword">except</span> StopIteration:</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line">            <span class="keyword">except</span>:</span><br><span class="line">                <span class="comment"># only re-raise if it's not the exception that was</span></span><br><span class="line">                <span class="comment"># passed to throw(), because __exit__() must not raise</span></span><br><span class="line">                <span class="comment"># an exception unless __exit__() itself failed. But</span></span><br><span class="line">                <span class="comment"># throw() has to raise the exception to signal</span></span><br><span class="line">                <span class="comment"># propagation, so thi fixes the impedance mismatch</span></span><br><span class="line">                <span class="comment"># between the throw() protocol and the __exit__()</span></span><br><span class="line">                <span class="comment"># protocol .</span></span><br><span class="line">                <span class="comment"># 只有当它不是传递给 throw() 的异常时才会重新被抛出，</span></span><br><span class="line">                <span class="comment"># 因为 __exit__() 只能在方法本身执行失败的时候抛出异常</span></span><br><span class="line">                <span class="comment"># 但是 throw() 必须引发异常来传递信号</span></span><br><span class="line">                <span class="comment"># 所以 thi 修复了 throw() 协议和 __exit__() 协议之间的不协调</span></span><br><span class="line">                </span><br><span class="line">                <span class="keyword">if</span> sys.exc_info()[<span class="number">1</span>] <span class="keyword">is</span> <span class="keyword">not</span> value:</span><br><span class="line">                    <span class="keyword">raise</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">contextmanager</span><span class="params">(func)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">helper</span><span class="params">(*args, **kwds)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> GeneratorContextManager(func(*args, **kwds))</span><br><span class="line">    <span class="keyword">return</span> helper</span><br></pre></td></tr></table></figure><p>这个装饰器可以被这样使用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">opening</span><span class="params">(filename)</span>:</span></span><br><span class="line">    f = open(filename) <span class="comment"># IOError is untouched by GeneratorContext</span></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">yield</span> f</span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        f.close() <span class="comment"># Ditto for errors here(howere unlikely)</span></span><br></pre></td></tr></table></figure><p>对此装饰器更加完善的实现已经成为 <a href="https://docs.python.org/zh-cn/3/library/contextlib.html#module-contextlib" target="_blank" rel="external nofollow noopener noreferrer">标准库的一部分</a>。</p><h2 id="标准库中存在的上下文管理器"><a href="#标准库中存在的上下文管理器" class="headerlink" title="标准库中存在的上下文管理器"></a>标准库中存在的上下文管理器</h2><p>可以给文件、套接字和锁等等对象添加 __enter__() 和 __exit__() 方法，这样我们就可以像下面这样操作这些对象：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> locking(myLock):</span><br><span class="line">    BLOCK</span><br></pre></td></tr></table></figure><p>也可以简写成</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> myLock:</span><br><span class="line">    BLOCK</span><br></pre></td></tr></table></figure><p>但是使用过程中应该谨慎，它可能会导致类似的错误：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">f = open(filename)</span><br><span class="line"><span class="keyword">with</span> f:</span><br><span class="line">    BLOCK1</span><br><span class="line"><span class="keyword">with</span> f:</span><br><span class="line">    BLOCK2</span><br></pre></td></tr></table></figure><p>上面的代码并不会像人们想象的那样顺利的执行，因为 f 在进入 BLCOK2 之前被就已经被关闭了。</p><p>还有很多类似上面示例中的错误，例如：</p><ul><li>使用上面提到的 @contextmanager 装饰器装饰的生成器，会在第二次被 with 语法调用 f.__enter__() 时抛出 RuntimeError 异常；</li><li>如果 __enter__ 在一个 closed 文件对象上被调用，也会引发类似的错误。</li></ul><p>对于 Python 2.5，以下类型已经被确定为上下文管理器：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">- file</span><br><span class="line">- thread.LockType</span><br><span class="line">- threading.Lock</span><br><span class="line">- threading.RLock</span><br><span class="line">- threading.Condition</span><br><span class="line">- threading.Semaphore</span><br><span class="line">- threading.BoundedSemaphore</span><br></pre></td></tr></table></figure><p><a href="https://docs.python.org/3/library/decimal.html#context-objects" target="_blank" rel="external nofollow noopener noreferrer">一个上下文管理器也将被添加到 decimal 模块中</a>，以支持在 with 语句中使用本地 decimal arithmetic 上下文，当 with 语句退出时自动回复原始上下文。</p><h2 id="标准术语"><a href="#标准术语" class="headerlink" title="标准术语"></a>标准术语</h2><p>本 PEP 提议将由 __enter__() 和 __exit__() 方法组成的协议称为「上下文管理协议」，而实现该协议的对象被称为「上下文管理器」。</p><p>语句中紧跟 with 关键字的表达式是一个「上下文表达式」。</p><p>with 语句主体中的代码和 as 关键字后面的变量名（或名称）没有特殊的术语。可以使用一般的术语「statement body」和「target list」，如果这些术语不清楚的话，可以用 with 或 with statement 作为前缀。</p><p>鉴于 decimal 模块的算术上下文等对象的存在，使得单独使用「上下文」这个术语可能存在歧义。如果有必要，可以用「上下文管理器」来表示由上下文表达式创建的具体对象，用「运行时上下文」或（最好是）「运行时环境」来表示由上下文管理器进行的实际状态修改，从而使描述更加具体。</p><p>当简单地讨论 with 语句的使用时，由于上下文表达式已经全面描述了对运行时环境所做的修改，因此此时这种模糊性不应该太重要。但在讨论 with 语句本身的机制以及如何实际实现上下文管理器时，这些名词之间的区别就尤为重要。</p><h2 id="如何缓存上下文管理器"><a href="#如何缓存上下文管理器" class="headerlink" title="如何缓存上下文管理器"></a>如何缓存上下文管理器</h2><p>许多上下文管理器（如文件和基于生成器的上下文）都是一次性使用的对象。一旦 __exit__() 方法被调用，上下文管理器将不可复用 (例如，文件已被关闭，或者底层生成器已执行完毕)。</p><p>为每个 with 语句创建一个新的上下文管理器对象是避免多线程代码或嵌套的 with 语句试图使用同一个上下文管理器问题的最简单的解决方案。标准库中所有可复用的上下文管理器都来自 threading 模块，它们都针对线程和嵌套使用所产生的问题进行过相应设计。</p><p>这意味着，为了在多个 with 语句中重复使用一个带有特定初始化参数的上下文管理器，通常需要将其存储在一个零参数的可调用对象中，然后在每个语句的上下文表达式中调用，而不是直接缓存上下文管理器。</p><p>当这种限制不适用时，受影响的上下文管理器的文档应该明确说明这一点。</p><h2 id="被拒绝的选项"><a href="#被拒绝的选项" class="headerlink" title="被拒绝的选项"></a>被拒绝的选项</h2><p>几个月来，PEP 都为了避免隐藏控制流程而禁止忽略异常抛出，但在实施过程中发现这是一个很难避免的麻烦，因此 Guido 重启了这个功能。</p><p>本 PEP 的另一个核心是提出了一个 __context__() 方法，类似于 iterable’s __iter__() 方法。这引起了无休止的问题和术语讨论。不断解释 __content__() 方法相关问题的过程中产生的新问题让 Guido 最终彻底删除了这个概念。</p><p><a href="https://mail.python.org/pipermail/python-dev/2005-October/056969.html" target="_blank" rel="external nofollow noopener noreferrer">直接使用 PEP 342 的生成器 API 来定义 with 语法</a> 也被简短地讨论过，但很快就被否定了，因为如果这么做会使得编写非生成器的上下文管理器程序变得异常困难。</p><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><p>基于生成器的例子依赖于 <a href="https://www.python.org/dev/peps/pep-0342/" target="_blank" rel="external nofollow noopener noreferrer">PEP 342</a>。另外，有些例子在实践中是不必要的，因为现有的对象，如 threading.RLock，能够直接用于 with 语句中。</p><p>示例中上下文的名称所使用的时态不是任意的：</p><ul><li>过去时态（ed）表示一个在 __enter__ 方法中完成，在 __eixt__ 方法中撤销的动作；</li><li>进行时态（ing）表示一个在 __exit__ 方法中完成的动作。</li></ul><h3 id="locked：锁管理"><a href="#locked：锁管理" class="headerlink" title="locked：锁管理"></a>locked：锁管理</h3><p>在语句开始时获得锁，离开时释放锁：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">locked</span><span class="params">(lock)</span>:</span></span><br><span class="line">    lock.acquire()</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">yield</span></span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        lock.release()</span><br></pre></td></tr></table></figure><p>可以这样使用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> locked(myLock):</span><br><span class="line">    <span class="comment"># Code here executes with myLock held.  The lock is</span></span><br><span class="line">    <span class="comment"># guaranteed to be released when the block is left (even</span></span><br><span class="line">    <span class="comment"># if via return or by an uncaught exception).</span></span><br><span class="line">    <span class="comment"># 这里的代码是在持有 myLock 的情况下被执行的。</span></span><br><span class="line">    <span class="comment"># 这个锁被确保在离开 with 语句之后被释放，即使在执行过程中有中断或异常</span></span><br></pre></td></tr></table></figure><h3 id="opened：文件管理"><a href="#opened：文件管理" class="headerlink" title="opened：文件管理"></a>opened：文件管理</h3><p>在语句开始时通过特定模式打开文件，离开时关闭文件：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">opened</span><span class="params">(filename, mode=<span class="string">"r"</span>)</span>:</span></span><br><span class="line">    f = open(filename, mode)</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">yield</span> f</span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        f.close()</span><br></pre></td></tr></table></figure><p>可以这样使用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> opened(<span class="string">"/etc/passwd"</span>) <span class="keyword">as</span> f:</span><br><span class="line">    <span class="keyword">for</span> line <span class="keyword">in</span> f:</span><br><span class="line">        <span class="keyword">print</span> line.rstrip()</span><br></pre></td></tr></table></figure><h3 id="transaction：数据库事务管理"><a href="#transaction：数据库事务管理" class="headerlink" title="transaction：数据库事务管理"></a>transaction：数据库事务管理</h3><p>提交或回滚数据库事务：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">transaction</span><span class="params">(db)</span>:</span></span><br><span class="line">    db.begin()</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">yield</span> <span class="literal">None</span></span><br><span class="line">    <span class="keyword">except</span>:</span><br><span class="line">        db.rollback()</span><br><span class="line">        <span class="keyword">raise</span></span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        db.commit()</span><br></pre></td></tr></table></figure><h3 id="class-locked：不使用生成器实现的锁管理"><a href="#class-locked：不使用生成器实现的锁管理" class="headerlink" title="class locked：不使用生成器实现的锁管理"></a>class locked：不使用生成器实现的锁管理</h3><p>在不使用生成器情况下重写 <a href="#locked：锁管理">例 1</a>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">locked</span>:</span></span><br><span class="line">   <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, lock)</span>:</span></span><br><span class="line">       self.lock = lock</span><br><span class="line"></span><br><span class="line">   <span class="function"><span class="keyword">def</span> <span class="title">__enter__</span><span class="params">(self)</span>:</span></span><br><span class="line">       self.lock.acquire()</span><br><span class="line"></span><br><span class="line">   <span class="function"><span class="keyword">def</span> <span class="title">__exit__</span><span class="params">(self, type, value, tb)</span>:</span></span><br><span class="line">       self.lock.release()</span><br></pre></td></tr></table></figure><p>这个例子很容易修改成其他相对无状态的例子，这表明，如果不需要保留特殊状态，很容易避免对生成器的依赖。</p><h3 id="stdout-redirected：输出重定向管理"><a href="#stdout-redirected：输出重定向管理" class="headerlink" title="stdout_redirected：输出重定向管理"></a>stdout_redirected：输出重定向管理</h3><p>暂时重定向 stdout：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">stdout_redirected</span><span class="params">(new_stdout)</span>:</span></span><br><span class="line">    save_stdout = sys.stdout</span><br><span class="line">    sys.stdout = new_stdout</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">yield</span> <span class="literal">None</span></span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        sys.stdout = save_stdout</span><br></pre></td></tr></table></figure><p>可以这样使用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> opened(filename, <span class="string">"w"</span>) <span class="keyword">as</span> f:</span><br><span class="line">    <span class="keyword">with</span> stdout_redirected(f):</span><br><span class="line">        <span class="keyword">print</span> <span class="string">"Hello world"</span></span><br></pre></td></tr></table></figure><p>这样实现不能保证线程安全，但如果手动实现相同的动作也无法保证线程安全。在单线程程序中（例如，在脚本中），这是一种流行的处理方案。</p><h3 id="opened-w-error：文件（-enter-同时返回两个值）"><a href="#opened-w-error：文件（-enter-同时返回两个值）" class="headerlink" title="opened_w_error：文件（__enter__ 同时返回两个值）"></a>opened_w_error：文件（__enter__ 同时返回两个值）</h3><p>opened() 的一个变体，能够同时返回文件句柄和异常内容：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">opened_w_error</span><span class="params">(filename, mode=<span class="string">"r"</span>)</span>:</span></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        f = open(filename, mode)</span><br><span class="line">    <span class="keyword">except</span> IOError, err:</span><br><span class="line">        <span class="comment"># 使用元组同时产生两个返回值</span></span><br><span class="line">        <span class="keyword">yield</span> <span class="literal">None</span>, err</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="comment"># 使用元组同时产生两个返回值</span></span><br><span class="line">            <span class="keyword">yield</span> f, <span class="literal">None</span></span><br><span class="line">        <span class="keyword">finally</span>:</span><br><span class="line">            f.close()</span><br></pre></td></tr></table></figure><p>可以这样使用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在 as 后使用元组接收返回值</span></span><br><span class="line"><span class="keyword">with</span> opened_w_error(<span class="string">"/etc/passwd"</span>, <span class="string">"a"</span>) <span class="keyword">as</span> (f, err):</span><br><span class="line">    <span class="keyword">if</span> err:</span><br><span class="line">        <span class="keyword">print</span> <span class="string">"IOError:"</span>, err</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        f.write(<span class="string">"guido::0:0::/:/bin/sh\n"</span>)</span><br></pre></td></tr></table></figure><h3 id="signal：信号拦截"><a href="#signal：信号拦截" class="headerlink" title="signal：信号拦截"></a>signal：信号拦截</h3><p>另一个有用的例子是阻断信号的操作，实现代码如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> signal</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> signal.blocked():</span><br><span class="line">    <span class="comment"># code executed without worrying about signals</span></span><br><span class="line">    <span class="comment"># 执行的代码不用担心信号问题</span></span><br></pre></td></tr></table></figure><p>可以传入一个要屏蔽的信号列表，默认情况下，会屏蔽所有的信号。</p><h3 id="decimal"><a href="#decimal" class="headerlink" title="decimal"></a>decimal</h3><p>decimal 上下文，这里有一个简单的例子：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> decimal</span><br><span class="line"></span><br><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">extra_precision</span><span class="params">(places=<span class="number">2</span>)</span>:</span></span><br><span class="line">    c = decimal.getcontext()</span><br><span class="line">    saved_prec = c.prec</span><br><span class="line">    c.prec += places</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">yield</span> <span class="literal">None</span></span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        c.prec = saved_prec</span><br></pre></td></tr></table></figure><p>使用示例（改编自 Python Library Reference）:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">sin</span><span class="params">(x)</span>:</span></span><br><span class="line">    <span class="comment"># Return the sine of x as measured in radians.</span></span><br><span class="line">    <span class="comment"># 返回以弧度为单位的x的正弦。</span></span><br><span class="line">    <span class="keyword">with</span> extra_precision():</span><br><span class="line">        i, lasts, s, fact, num, sign = <span class="number">1</span>, <span class="number">0</span>, x, <span class="number">1</span>, x, <span class="number">1</span></span><br><span class="line">        <span class="keyword">while</span> s != lasts:</span><br><span class="line">            lasts = s</span><br><span class="line">            i += <span class="number">2</span></span><br><span class="line">            fact *= i * (i<span class="number">-1</span>)</span><br><span class="line">            num *= x * x</span><br><span class="line">            sign *= <span class="number">-1</span></span><br><span class="line">            s += num / fact * sign</span><br><span class="line">    <span class="comment"># The "+s" rounds back to the original precision,</span></span><br><span class="line">    <span class="comment"># so this must be outside the with-statement:</span></span><br><span class="line">    <span class="keyword">return</span> +s</span><br></pre></td></tr></table></figure><h3 id="another-decimal"><a href="#another-decimal" class="headerlink" title="another decimal"></a>another decimal</h3><p>一个简单的 decimal 模块的上下文管理器：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">localcontext</span><span class="params">(ctx=None)</span>:</span></span><br><span class="line">    <span class="string">"""Set a new local decimal context for the block"""</span></span><br><span class="line">    <span class="comment"># Default to using the current context</span></span><br><span class="line">    <span class="keyword">if</span> ctx <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">        ctx = getcontext()</span><br><span class="line">    <span class="comment"># We set the thread context to a copy of this context</span></span><br><span class="line">    <span class="comment"># to ensure that changes within the block are kept</span></span><br><span class="line">    <span class="comment"># local to the block.</span></span><br><span class="line">    newctx = ctx.copy()</span><br><span class="line">    oldctx = decimal.getcontext()</span><br><span class="line">    decimal.setcontext(newctx)</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">yield</span> newctx</span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        <span class="comment"># Always restore the original context</span></span><br><span class="line">        decimal.setcontext(oldctx)</span><br></pre></td></tr></table></figure><p>使用案例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> decimal <span class="keyword">import</span> localcontext, ExtendedContext</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">sin</span><span class="params">(x)</span>:</span></span><br><span class="line">    <span class="keyword">with</span> localcontext() <span class="keyword">as</span> ctx:</span><br><span class="line">        ctx.prec += <span class="number">2</span></span><br><span class="line">        <span class="comment"># Rest of sin calculation algorithm</span></span><br><span class="line">        <span class="comment"># uses a precision 2 greater than normal</span></span><br><span class="line">    <span class="keyword">return</span> +s <span class="comment"># Convert result to normal precision</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">sin</span><span class="params">(x)</span>:</span></span><br><span class="line">    <span class="keyword">with</span> localcontext(ExtendedContext):</span><br><span class="line">        <span class="comment"># Rest of sin calculation algorithm</span></span><br><span class="line">        <span class="comment"># uses the Extended Context from the</span></span><br><span class="line">        <span class="comment"># General Decimal Arithmetic Specification</span></span><br><span class="line">    <span class="keyword">return</span> +s <span class="comment"># Convert result to normal context</span></span><br></pre></td></tr></table></figure><h3 id="closing：object-closing-上下文管理器"><a href="#closing：object-closing-上下文管理器" class="headerlink" title="closing：object-closing 上下文管理器"></a>closing：object-closing 上下文管理器</h3><p>通用「object-closing」上下文管理器：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">closing</span><span class="params">(object)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, obj)</span>:</span></span><br><span class="line">        self.obj = obj</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__enter__</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> self.obj</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__exit__</span><span class="params">(self, *exc_info)</span>:</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            close_it = self.obj.close</span><br><span class="line">        <span class="keyword">except</span> AttributeError:</span><br><span class="line">            <span class="keyword">pass</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            close_it()</span><br></pre></td></tr></table></figure><p>可以用来确切地关闭任何有 close 方法的对象，无论是文件、生成器还是其他东西。它也可以在不确定对象是否需要关闭时使用（例如，一个接受任意 iterable 的函数）：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># emulate opening():</span></span><br><span class="line"><span class="keyword">with</span> closing(open(<span class="string">"argument.txt"</span>)) <span class="keyword">as</span> contradiction:</span><br><span class="line">   <span class="keyword">for</span> line <span class="keyword">in</span> contradiction:</span><br><span class="line">       <span class="keyword">print</span> line</span><br><span class="line"></span><br><span class="line"><span class="comment"># deterministically finalize an iterator:</span></span><br><span class="line"><span class="keyword">with</span> closing(iter(data_source)) <span class="keyword">as</span> data:</span><br><span class="line">   <span class="keyword">for</span> datum <span class="keyword">in</span> data:</span><br><span class="line">       process(datum)</span><br></pre></td></tr></table></figure><p>Python 2.5 的 contextlib 模块包含 <a href="https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.closing" target="_blank" rel="external nofollow noopener noreferrer">这个上下文管理器的实现（3.x 版本也保留了）</a>。</p><h3 id="released：锁管理"><a href="#released：锁管理" class="headerlink" title="released：锁管理"></a>released：锁管理</h3><p><a href="https://www.python.org/dev/peps/pep-0319/" target="_blank" rel="external nofollow noopener noreferrer">PEP 319</a> 给出了一个用例，即使用 release 上下文来暂时释放先前获得的锁。</p><p>可以通过交换 acquisition() 和 release() 的调用顺序，实现上面的 <a href="#locked：锁管理">locked：锁管理</a> 。</p><p>实现代码如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">released</span>:</span></span><br><span class="line">  <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, lock)</span>:</span></span><br><span class="line">      self.lock = lock</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">def</span> <span class="title">__enter__</span><span class="params">(self)</span>:</span></span><br><span class="line">      self.lock.release()</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">def</span> <span class="title">__exit__</span><span class="params">(self, type, value, tb)</span>:</span></span><br><span class="line">      self.lock.acquire()</span><br></pre></td></tr></table></figure><p>使用案例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> my_lock:</span><br><span class="line">    <span class="comment"># Operations with the lock held</span></span><br><span class="line">    <span class="keyword">with</span> released(my_lock):</span><br><span class="line">        <span class="comment"># Operations without the lock</span></span><br><span class="line">        <span class="comment"># e.g. blocking I/O</span></span><br><span class="line">    <span class="comment"># Lock is held again here</span></span><br></pre></td></tr></table></figure><h3 id="nested：嵌套使用"><a href="#nested：嵌套使用" class="headerlink" title="nested：嵌套使用"></a>nested：嵌套使用</h3><p>一个「嵌套」的上下文管理器，它能自动地将提供的上下文从左到右嵌套，以避免过度缩进：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@contextmanager</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">nested</span><span class="params">(*contexts)</span>:</span></span><br><span class="line">    exits = []</span><br><span class="line">    vars = []</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="keyword">for</span> context <span class="keyword">in</span> contexts:</span><br><span class="line">                exit = context.__exit__</span><br><span class="line">                enter = context.__enter__</span><br><span class="line">                vars.append(enter())</span><br><span class="line">                exits.append(exit)</span><br><span class="line">            <span class="keyword">yield</span> vars</span><br><span class="line">        <span class="keyword">except</span>:</span><br><span class="line">            exc = sys.exc_info()</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            exc = (<span class="literal">None</span>, <span class="literal">None</span>, <span class="literal">None</span>)</span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        <span class="keyword">while</span> exits:</span><br><span class="line">            exit = exits.pop()</span><br><span class="line">            <span class="keyword">try</span>:</span><br><span class="line">                exit(*exc)</span><br><span class="line">            <span class="keyword">except</span>:</span><br><span class="line">                exc = sys.exc_info()</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                exc = (<span class="literal">None</span>, <span class="literal">None</span>, <span class="literal">None</span>)</span><br><span class="line">        <span class="keyword">if</span> exc != (<span class="literal">None</span>, <span class="literal">None</span>, <span class="literal">None</span>):</span><br><span class="line">            <span class="comment"># sys.exc_info() may have been</span></span><br><span class="line">            <span class="comment"># changed by one of the exit methods</span></span><br><span class="line">            <span class="comment"># so provide explicit exception info</span></span><br><span class="line">            <span class="keyword">raise</span> exc[<span class="number">0</span>], exc[<span class="number">1</span>], exc[<span class="number">2</span>]</span><br></pre></td></tr></table></figure><p>使用案例</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> nested(a, b, c) <span class="keyword">as</span> (x, y, z):</span><br><span class="line">    <span class="comment"># Perform operation</span></span><br></pre></td></tr></table></figure><p>等同于：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> a <span class="keyword">as</span> x:</span><br><span class="line">    <span class="keyword">with</span> b <span class="keyword">as</span> y:</span><br><span class="line">        <span class="keyword">with</span> c <span class="keyword">as</span> z:</span><br><span class="line">            <span class="comment"># Perform operation</span></span><br></pre></td></tr></table></figure><p>Python 2.5的 contextlib 模块包含这个上下文管理器的实现（没有在 3.x 的文档中找到同名方法）。</p><h2 id="实现过程"><a href="#实现过程" class="headerlink" title="实现过程"></a>实现过程</h2><p>本 PEP 最初是由 Guido 在 2005.06.27 的 EuroPython 主题演讲中接受的。后来它被再次接受，并加入了 __context__ 方法。</p><p>本 PEP 是在 Python 2.5a1 的 Subversion 中实现的，在 Python 2.5b1 中删除了 __context__()方法。</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><h3 id="生成器状态"><a href="#生成器状态" class="headerlink" title="生成器状态"></a>生成器状态</h3><p>可以通过 <code>inspect.getgeneratorstate(generator)</code> 查看生成器状态：</p><p>GEN_CREATED: Waiting to start execution.<br>GEN_RUNNING: Currently being executed by the interpreter.<br>GEN_SUSPENDED: Currently suspended at a yield expression.<br>GEN_CLOSED: Execution has completed.</p><h3 id="sys-exc-info"><a href="#sys-exc-info" class="headerlink" title="sys.exc_info"></a>sys.exc_info</h3><p>本函数返回的元组包含三个值，它们给出当前正在处理的异常的信息。返回的信息仅限于当前线程和当前堆栈帧。如果当前堆栈帧没有正在处理的异常，则信息将从下级被调用的堆栈帧或上级调用者等位置获取，依此类推，直到找到正在处理异常的堆栈帧为止。此处的「处理异常」指的是「执行 except 子句」。任何堆栈帧都只能访问当前正在处理的异常的信息。</p><p>如果整个堆栈都没有正在处理的异常，则返回包含三个 None 值的元组。否则返回值为 (type, value, traceback)。它们的含义是：</p><ul><li>type：正在处理的异常类型（它是 BaseException 的子类）；</li><li>value：异常实例（异常类型的实例）；</li><li>traceback：一个 回溯对象，该对象封装了最初发生异常时的调用堆栈。</li></ul><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://www.python.org/dev/peps/pep-0343/" target="_blank" rel="external nofollow noopener noreferrer">PEP 343 原文</a></li><li><a href="https://www.python.org/dev/peps/pep-0310/" target="_blank" rel="external nofollow noopener noreferrer">PEP 310 原文</a></li><li><a href="https://www.python.org/dev/peps/pep-0340/" target="_blank" rel="external nofollow noopener noreferrer">PEP 340 原文</a></li><li><a href="https://devblogs.microsoft.com/oldnewthing/20050106-00/?p=36783" target="_blank" rel="external nofollow noopener noreferrer">Raymond: A rant against flow control macros</a></li><li><a href="https://snarky.ca/unravelling-the-with-statement/" target="_blank" rel="external nofollow noopener noreferrer">Brett Cannon: Unravelling the `with` statement</a></li><li><a href="https://docs.python.org/zh-cn/3/library/sys.html#module-sys" target="_blank" rel="external nofollow noopener noreferrer">Python 官方文档-Python标准库-sys 库</a></li><li><a href="https://docs.python.org/zh-cn/3/reference/datamodel.html#traceback-objects" target="_blank" rel="external nofollow noopener noreferrer">Python 官方文档-数据模型-回溯对象</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;本文的主体内容大部分来自对 &lt;a href=&quot;https://www.python.org/dev/peps/pep-0343/&quot; targ
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Python" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Python/"/>
    
    
      <category term="Python" scheme="https://wiki.blanc.site/tags/Python/"/>
    
      <category term="WeeklyPEP" scheme="https://wiki.blanc.site/tags/WeeklyPEP/"/>
    
  </entry>
  
  <entry>
    <title>所谓 ASGI</title>
    <link href="https://wiki.blanc.site/archives/f5b4f56.html"/>
    <id>https://wiki.blanc.site/archives/f5b4f56.html</id>
    <published>2022-01-31T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文的主体内容大部分来自对 <a href="https://asgi.readthedocs.io/en/latest/" target="_blank" rel="external nofollow noopener noreferrer">ASGI Documentation</a> 原文的翻译，其余部分为本人对原文的理解，在整理过程中我没有刻意地区分翻译的部分和我个人理解的部分，这两部分内容被糅杂在一起形成了本文。因此，请不要带着「本文的内容是百分之百正确」的想法阅读。如果文中的某些内容让你产生疑惑，你可以给我留言与我讨论或者对比 <a href="https://asgi.readthedocs.io/en/latest/" target="_blank" rel="external nofollow noopener noreferrer">ASGI Documentation</a> 的原文加以确认。</p><p>本人在之前整理过一篇 <a href="https://wiki.blanc.site/archives/7a294715.html">何为 WSGI</a> 是对 <a href="https://www.python.org/dev/peps/pep-333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 333</a> 和 <a href="https://www.python.org/dev/peps/pep-3333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 的翻译和整理，感兴趣的话可以结合起来一起阅读。</p><h2 id="字典参照"><a href="#字典参照" class="headerlink" title="字典参照"></a>字典参照</h2><p>注：仅代表本人理解，在涉及到这些单词的地方均使用英文单词。</p><table><thead><tr><th>英文</th><th>中文</th><th>解释</th></tr></thead><tbody><tr><td>ASGI</td><td>异步服务器网关接口</td><td></td></tr><tr><td>WSGI</td><td>Web 服务器网关接口</td><td></td></tr><tr><td>Server</td><td>服务器</td><td>Web 软件中面向 Client 提供具体服务的部分</td></tr><tr><td>Application</td><td>应用（应用框架）</td><td>Web 软件中面向 Server 提供具体应用服务的部分</td></tr><tr><td>Connection</td><td>连接</td><td>一个满足某种协议的 Socket 连接</td></tr><tr><td>Event</td><td>事件</td><td>连接中发生的事件的抽象</td></tr><tr><td>scope</td><td>\</td><td>存放连接细节的容器</td></tr><tr><td>send</td><td>发送器</td><td>应用发送事件消息的工具</td></tr><tr><td>receive</td><td>接收器</td><td>应用接受事件消息的工具</td></tr></tbody></table><h2 id="何为-ASGI"><a href="#何为-ASGI" class="headerlink" title="何为 ASGI"></a>何为 ASGI</h2><blockquote><p>ASGI (Asynchronous Server Gateway Interface) is a spiritual successor to WSGI, intended to provide a standard interface between async-capable Python web servers, frameworks, and applications.</p><p>ASGI（异步服务器网关接口）是 WSGI 的精神续作，目的是为具有异步功能的 Python Web 服务器、框架和应用之间提供一个标准接口。</p><p>from <a href="https://asgi.readthedocs.io/en/latest/" target="_blank" rel="external nofollow noopener noreferrer">ASGI Documentation</a></p></blockquote><h2 id="ASGI-vs-WSGI：宏观"><a href="#ASGI-vs-WSGI：宏观" class="headerlink" title="ASGI vs WSGI：宏观"></a>ASGI vs WSGI：宏观</h2><ol><li>ASGI 并非由某个具体 PEP 提出，而是由 Django 开源团队提出并维护的一套对 WSGI 向下兼容的精神续作；</li><li>在异步、长连接盛行的今天，WSGI 所描述的由单一同步可调用对象组成的 Web Application 已经无法平滑地满足现在的开发需求。</li></ol><h2 id="ASGI-简介"><a href="#ASGI-简介" class="headerlink" title="ASGI 简介"></a>ASGI 简介</h2><p>一个 ASGI Application 是一个单一异步可调用对象，它包含三个参数：</p><ol><li>scope：一个存放着 Connection 细节的 Python 字典；</li><li>send：一个异步可调用对象，使得 Appliction 可以发送 Event 消息到 Client；</li><li>receive：一个异步可调用对象，能够让 Application 从 Client 接收 Event 消息。</li></ol><p>在这种结构下，ASGI 下的每个 Application 不仅能同时处理多个 incoming Event 和 outcoming Event，在协程的加持下 Application 还可以做更多的事情，例如监听像 Redis 队列一样的外部的 trigger Event。</p><p>下面是一个最简单的 ASGI Application 模型：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">application</span><span class="params">(scope, receive, send)</span>:</span></span><br><span class="line">    event = <span class="keyword">await</span> receive()</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">await</span> send(&#123;<span class="string">"tpye"</span>: <span class="string">"websocket.send"</span>, ...&#125;)</span><br></pre></td></tr></table></figure><p>所有的 Event 都是通过一个特定格式的 Python 字典被 send 和 receive 进行处理，正是这些 Event formats 构成了 ASGI 标准的基础，使得 Application 能够适应不同的 Servers。</p><p>所有的 Event formats 都有一个用来决定自身结构的 type 键值对，下面就是一个 ASGI 中 HTTP 协议发送请求的 Event format 示例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="string">"type"</span>: <span class="string">"http.request"</span>,</span><br><span class="line">    <span class="string">"body"</span>: <span class="string">b"Hello World"</span>,</span><br><span class="line">    <span class="string">"more_body"</span>: <span class="literal">False</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>下面则是一个 ASGI 中通过 WebSocket 协议发送消息的 Event format：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="string">"type"</span>: <span class="string">"websocket.send"</span>,</span><br><span class="line">    <span class="string">"text"</span>: <span class="string">"Hello world!"</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="与-WSGI-的兼容性"><a href="#与-WSGI-的兼容性" class="headerlink" title="与 WSGI 的兼容性"></a>与 WSGI 的兼容性</h2><p>ASGI 被设计成 WSGI 的超集，并且给出了一个两者之间的转换方式，允许 WSGI Application 通过一个 <a href="https://github.com/django/asgiref/blob/main/asgiref/wsgi.py" target="_blank" rel="external nofollow noopener noreferrer">translation wrapper</a> 在 ASGI Server 中运行。</p><p>一个线程池可以在 async event loop 之外运行同步的 WSGI Applications。</p><p><strong>注：这里的原话是 <code>A threadpool can be used to run the synchronous WSGI applications away from the async event loop.</code>，不太清楚是想表示一种并列还是异或关系。</strong></p><h2 id="ASGI-规范（V-3-0）"><a href="#ASGI-规范（V-3-0）" class="headerlink" title="ASGI 规范（V 3.0）"></a>ASGI 规范（V 3.0）</h2><h3 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h3><p>本规范提出了一种 Network Protocol Servers（尤其是 Web Servers） 和 Python Applications 之间通讯的标准接口，旨在允许通过一套协议同时处理多种常见协议，例如：HTTP、HTTP/2、和 WebSocket。</p><p>本规范期望通过固定一套 API，满足 Server 与 Server 之间和 Server 与 Application 之间的交互需要。每一个被支持的协议都有一个子规范，子规范描述了如何将该协议编码或解码为 Event 消息。</p><h3 id="提出理由"><a href="#提出理由" class="headerlink" title="提出理由"></a>提出理由</h3><p>WSGI 规范自推出以来一直运行良好，它的出现使得 Python Framework 和 Web Server 之间的对接变得更加灵活和简便。然而，它的设计过分依赖于 HTTP 风格的 Request/Response 循环，但现在越来越多不遵循这种交互模式的协议正在网络编程时被普遍使用，其中最明显的就是 WebSocket。</p><p>ASGI 便在这种情况下应运而生，它试图维护一个简单 Application API，同时提供一套抽象方法，允许来自不同 Appliction 线程或进程的数据在任何时刻被发送和接受。</p><p>基于「将不同协议转换为 Python 兼容且异步友好的消息集」的原则，ASGI 可以概括为两部分：</p><ul><li>用于构建 Servers 的标准化通信接口；</li><li>一套支持不同协议的标准消息格式。</li></ul><p>ASGI 的核心目的是提供一种方法在能够处理 HTTP/2 和 WebSocket 协议的同时正常处理 HTTP 协议。然而要实现这个功能就必然意味着需要一个简单快捷的方式来支持现有的 WSGI Servers 和 Applications。因为目前大部分的 Python Web 服务都依赖于 WSGI。这部分内容被放在了 <a href>HTTP 子协议</a>。</p><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>ASGI 由两个不同的组件组成：</p><ul><li>Protocol Server：用于终止 sockets 并将其转换为 connections 和相关 event messages；</li><li>Application：寄生在 Protocol Server 中，会在每个 connection 中被调用一次并在调用时处理相关 event messages，在必要时将处理好的相关 event messages 返回。</li></ul><p>与 WSGI 相同的是，Server 会在内部处理 Application 并以标准化的格式向其分派请求。不同的是，ASGI 中的 Application 是异步的可调用对象，而不是简单的可调用对象，它通过接收和发送异步事件与 Server 进行通信，而不是接收单一的输入流并返回单一的可迭代对象。</p><p>ASGI Applications 必须以 async/await 兼容的协程程序运行，及兼容 asyncio。如果需要使用同步代码可以在主线程自由的使用线程或其他进程。</p><p>与 WSGI 的另一个不同之处是，ASGI 的 connection 有两个独立的部分：</p><ul><li>Connection Scope：代表与用户进行的 Protocol Connection，它会一直存在直到 Connection 关闭；</li><li>Events：它表示当 Connection 有事情发生时发送到 Application 中的信息，以及由 Application 发送给 Server 的消息（其中包括传送给 Client 的的数据）。</li></ul><p>Application 通过一个 Connection Spoce 和两个异步的可调用对象被调用的阻塞（await）来接收和返回事件消息。所有的这些都发生在异步事件循环（async event loop）中。</p><p>每一次对 Application 可调用对象的调用都会映射到一个传入的 socket 或 Connection，并会延续该 Connection 的寿命，如果有 cleanup 则 Connection 会持续更长时间。</p><p>一些协议可能不使用传统的 sockets，ASGI 在规定这些协议时会定义 Scope 合适过期以及合适关闭。</p><h3 id="规范细节"><a href="#规范细节" class="headerlink" title="规范细节"></a>规范细节</h3><h4 id="Connection-Scope-连接范围"><a href="#Connection-Scope-连接范围" class="headerlink" title="Connection Scope | 连接范围"></a>Connection Scope | 连接范围</h4><p>用户对 ASGI Application 的每个请求都会对应一个 Connection 并引发对 Application 可调用对象的调用，来完整地处理此 Connection。对特定 Connection 信息的描述和生命周期的记录被称为 Connection Scope。</p><p>第一个被传入 Application 可调用对象的参数就是一个存放特定 Connection 信息的 Scope 字典。</p><p>例如，在 HTTP 协议下，Connection Scope 仅持续一个请求，但是这个被传递的 Scope 包含了大多数钱请求数据（除了 HTTP Request Body，因为这部分内容是通过事件流传输的）。</p><p>但是，在 WebSocket 协议下，只要 socket 被接通 Connection Scope 就会维持下去。并且 Scope 会传递包含 WebSocket path 在内的信息，不过像消息等细节内容则是作为 Events 传递的。</p><p>一些协议可能会给定一个信息非常有限的 Scope，因为它们封装了类似握手之类的内容。但是每个协议的定义都必须包含 Connection Spoce 的持续时间和你将在这个 Scope 参数中获取那些信息。</p><p>根据协议细节不同，Applications 在与 Client 通讯之前可能需要等待一个初始的启动信息。</p><h4 id="Events-事件"><a href="#Events-事件" class="headerlink" title="Events | 事件"></a>Events | 事件</h4><p>ASGI 将协议分解为一系列 Application 必须接收和反应的的 Events，以及 Application 可能在响应中发送的 Events。对于 HTTP 来说，就是简单的按顺序接收两个 Events：<code>http.request</code> 和 <code>http.disconnect</code> 并且发送相应的 Event 消息。而对于像 WebSocket 之类的协议，它可能更多的会是：先接收一个 <code>websocket.connect</code>，再发送一个 <code>websocket.send</code>，在接收一个 <code>websocket.receive</code> 最后接收一个 <code>websocket.disconnect</code>。</p><p>每个 Event 都是一个带有 top-level type key 的字典，它包含一个关于消息类型的 Unicode 字符串。用户可以自由的创造属于他们自己的消息类型并且在高级 Events Applications 实例之间发送它们，例如：一个了解 Application 可能会通过一个 <code>mychat.message</code> 的 user tpye 发送聊天信息。Applications 应该能够处理一个关于 Events 的混合集合，它们一些来自 Incoming Client Connection，一些来自 Application 的其他部分。</p><p>Events 中的信息可以通过网络发送，因此，它们需要被序列化，所以只能以以下类型进行传播：</p><ul><li>Byte strings | 二进制字符串</li><li>Unicode strings | Unicode 字符串</li><li>Integers (within the signed 64-bit range) | 整数</li><li>Floating point numbers (within the IEEE 754 double precision range; no Nan or infinities) | 浮点数</li><li>Lists (tuples should be encoded as lists) | 列表（元组应该被转换为列表）</li><li>Dicts (keys must be Unicode strings) | 字典（键必须是 Unicode 字符串）</li><li>Booleans | 布尔值</li><li>None</li></ul><h4 id="Applications-应用"><a href="#Applications-应用" class="headerlink" title="Applications | 应用"></a>Applications | 应用</h4><p>在 3.0 版本中，Application 格式改变为使用一个单一的可调用对象，而不是之前的双可调用对象。双可调用的写法在后面的 [Legacy Application](#Legacy Application) 中有所记载。服务器可以使用 <code>asgiref.compatibility</code> 库轻松地实现对它的支持，并且应该尽可能支持它。</p><p>ASGI Application 应该是一个单一的异步可调用对象：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">coroutine application(scope, receive, send)</span><br></pre></td></tr></table></figure><ul><li>scope：存放 Connection Scope 信息，是一个至少包含指定传入协议的的字典；</li><li>receive：一个能够在 Event 字典 available 时产出一个新 Event 字典的异步可调用对象；</li><li>send：一个异步可调用对象，接收一个 Event 字典作为位置参数，并在发送完成或 Connection 关闭时 return。</li></ul><p>Application 会在每个 Connection 中被调用一次。Conection 的定义以及其生命周期由协议规范决定。例如，对于 HTTP 来说一个 Connection 就是一次请求，而对于 WebSocket 来说一个 Connection 是一个 WebSocket 连接。</p><p>你发送和接收的 Scope 和 Event 消息的格式都是由 Application 协议之一定义的。Scope 必须是一个字典。<code>scope[&quot;type&quot;]</code> 必然存在，可以用它来判断那个协议被传入。<code>scope[&quot;asgi&quot;]</code> 也会以字典的形式存在，其中 <code>scope[&quot;asgi&quot;][&quot;version&quot;]</code> 代表了 Server 支持的 ASGI 的版本。如果这个值不存在，则默认为 <code>&quot;2.0&quot;</code>。</p><p>也可能有一些特殊的版本信息存放在 <code>scope[&quot;asgi&quot;][&quot;spec_version&quot;]</code>。这样做能够允许各个协议规范进行增强而不影响整个 ASGI 版本。</p><p>在本规范的具体协议子规范中给出了详细的 Scope 和 Event 消息的格式，它们就类似于 <a href="https://www.python.org/dev/peps/pep-3333/#environ-variables" target="_blank" rel="external nofollow noopener noreferrer">WSGI 的 environ 字典中的 keys 的规范</a>。</p><h4 id="Legacy-Application-Application-的历史版本"><a href="#Legacy-Application-Application-的历史版本" class="headerlink" title="Legacy Application | Application 的历史版本"></a>Legacy Application | Application 的历史版本</h4><p>ASGI v2.0 Application 被定义成一个可调用对象：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">application(scope)</span><br></pre></td></tr></table></figure><p>它能够返回另一个异步的可调用对象：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">coroutine application_instance(receive, send)</span><br></pre></td></tr></table></figure><p>其中 scope, receive 和 send 的含义与新版本一致，但注意：第一个可调用对象是同步的。</p><p>第一个可调用对象会在 Connection 开始时被调用，然后第二个可调用对象紧接着会被调用或阻塞。</p><p>这种书写风格在 v3.0 中已经被淘汰了，使用两个可调用对象的布局方案被认为是没必要的。现在它们已经成为旧时代的遗物被用来支持一些仍旧以这种风格编写的程序。</p><p><a href="https://github.com/django/asgiref/blob/main/asgiref/compatibility.py" target="_blank" rel="external nofollow noopener noreferrer">asgiref.compatibility</a> 模块中有一个兼容性套件，你可以用它来检测旧版风格的应用程序，并将其无缝切换为新版但可调用对象的风格。虽然现在这种遗留的风格仍被支持，但它终究会随着时间的推移而被放弃，所以请尽可能使用新版的风格进行代码开发。</p><h4 id="Protocol-Specifications-协议规范"><a href="#Protocol-Specifications-协议规范" class="headerlink" title="Protocol Specifications | 协议规范"></a>Protocol Specifications | 协议规范</h4><p>具体的协议规范描述了在指定协议下 Scope 和 Event 消息格式的标准化规范。</p><p>所有协议中的 Scope 和 Event 消息中都存在的一个键就是 type，它的值代表了 Scope 或 Event 消息的类型（协议类型）。</p><p>在 Scope 中，type 的值必须是一个 Unicode 字符串，例如 <code>&quot;http&quot;</code> 或 <code>&quot;webscoket</code>，具体参考相关协议子规范的规定。</p><p>在消息中，type 应该被命名为 <code>protocol.message_type</code> 其中 protocol 与 Scope 中的 type 相匹配，message_type 由协议子规范定义。消息类型值的示例包括：<code>http.request</code>，<code>websocket.send</code>。</p><p>注意：Application 应该主动拒绝任何未被定义（无法理解）的协议，并给出一个任意类型的异常。如果不这么做，可能会导致服务器认为你支持一个你并不支持的协议，这在于 Lifespan 协议一起使用时可能会产生混淆，因为 Server 会等你主动启动它。</p><p>当前支持的协议子规范有：</p><ul><li><a href="#HTTPandWebScoketprotocol">HTTP and WebSocket</a></li><li><a href="#Lifespan">Lifespan</a></li></ul><h4 id="Middleware-中间件"><a href="#Middleware-中间件" class="headerlink" title="Middleware | 中间件"></a>Middleware | 中间件</h4><p>ASGI 同样拥有中间件这个概念，中间件同时拥有 Server 和 Application 的功能，能够接收一个 Scope，也能发送或接收异步可调用对象，可以对其内部进行修改并执行内部的 Application。</p><p>当中间件修改 Scope 的时候，它应该在改变 Scope 并将其传递给 Application 之前制作一个备份，以防止 Scope 的改变向上层泄漏。我们在构建中间件时不能想当然的认为中间件发送的 Scope 就是最终版本，因为这中间可能有其他的中间件阻挡。正因如此，不要在中间件中保持对 Scope（包括副本）的引用，更不要尝试在 ASGI app 的外围去改变它。修改 Scope 最好的时机就是将控制权交给子 Application（中间件中的 Application） 之前。</p><h4 id="Error-Handling-异常处理"><a href="#Error-Handling-异常处理" class="headerlink" title="Error Handling | 异常处理"></a>Error Handling | 异常处理</h4><p>如果 Server 接收到一个错误的 Event 字典，例如：包含一个未知类型的、缺少 Event type 必要键的或者对象有错误 Python 类型（例如 HTTP 头信息的 Unicode 字符串），这种情况下应该引发一个异常，从异步可调用对象到 Application。</p><p>如果一个 Application 从 receive 接收到一个无效的事件字典，也应该有引发一个异常。</p><p>但如果是字典中存在额外的键，则不应该引发异常。这就允许在后续过程中对协议规范进行非破坏性的升级或定制。</p><p>Server 可以自由地处理运行在其中的 Application 抛出的异常——记录到控制台，发送到 syslog 或其他自定义操作——但一旦发生异常 Server 就必须终止 Application 实例和其中的相关 Connection。</p><p>注意在 Connection 关闭后 Server 接收到的 message 不被视为错误，在这种情况下 send 异步可调用对象应该充当一个 no-op。</p><h4 id="Extra-Coroutins-额外的协程"><a href="#Extra-Coroutins-额外的协程" class="headerlink" title="Extra Coroutins | 额外的协程"></a>Extra Coroutins | 额外的协程</h4><p>Frameworks 或 Applications 可能会希望在为每个 Application 启动协同程序外运行额外的协同程序。但由于在 Python 3.7 中无法将额外的程序设置为实例的协同程序的父级，Application 应该确保所有在 Application 运行时启动的协程与 Application 对应的协程同时关闭或在此之前关闭。</p><p>任何在 Application 对应协程关闭后仍在运行的协程都不能保证它能够被顺利执行完毕，因为它可能在任意时间被强制退出。</p><h4 id="Extensions-协议拓展"><a href="#Extensions-协议拓展" class="headerlink" title="Extensions | 协议拓展"></a>Extensions | 协议拓展</h4><p>在有些情况下，我们可能想在核心 ASGI 规范之外提供特定的拓展，或在某个规范推出之前对其进行测试。</p><p>为了应对这些场景，ASGI 定义了一种常见的拓展模式——可以对协议规范进行选择性补充，Server 可以利用这个机制使 Application 获得更多的功能。</p><p>这一切都是通过 scope 词典的 extensions 条目实现的，它本身对应的也是一个字典。Extensions 字典中的 Unicode 字符串名称是由 Server 和 Application 共同约定而成的。</p><p>如果 Server 想要支持一个拓展，需要在 extensions 字典中增加一个条目，并且增加的条目的值也应该是一个字典。Server 可以用这个字典中提供任对 scope 的拓展信息，如果 extension 仅用来表明 Server 的 send 可调用对象允许额外 events，只需要给相关 extension 条目附上一个空字典的值就可以了。</p><p>假设一个提供共 HTTP 服务的 Server 希望提供一个允许某个新 event 被返回的拓展，这个事件会导致刷新操作系统级别的网络发送缓冲区，那就可以通过一下格式进行拓展：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">scope = &#123;</span><br><span class="line">    <span class="string">"type"</span>: <span class="string">"http"</span>,</span><br><span class="line">    <span class="string">"method"</span>: <span class="string">"GET"</span>,</span><br><span class="line">    ...</span><br><span class="line">    <span class="string">"extensions"</span>: &#123;</span><br><span class="line">        <span class="string">"fullflush"</span>: &#123;&#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当某个 Application 接收到包含此 scope 的请求后，它就能在调用指定可调用对象时触发相关自定义事件（本例中就是 http.fullflush）。</p><h4 id="Strings-and-Unicode"><a href="#Strings-and-Unicode" class="headerlink" title="Strings and Unicode"></a>Strings and Unicode</h4><p>在本文档以及所有相关子规范中：<br>byte string：<a href="https://docs.python.org/3/library/stdtypes.html#str" target="_blank" rel="external nofollow noopener noreferrer">Python3 中的 bytes 类型</a>；<br>Unicode string：<a href="https://docs.python.org/3/library/stdtypes.html#bytes" target="_blank" rel="external nofollow noopener noreferrer">Python3 中的 str 类型</a>。</p><p>本文档永远不会使用 string 来模糊两者之间的表达，在涉及这两个概念的地方都使用了 str 和 bytes 作为区分。</p><p>本文档中所有字典的键（包括 scopes 和 events 中的字典）都是 Unicode string。</p><h2 id="HTTP-Protocol"><a href="#HTTP-Protocol" class="headerlink" title="HTTP Protocol"></a>HTTP Protocol</h2><p>HTTP 格式涵盖了 HTTP/1.0 HTTP/1.1 HTTP/2，HTTP/2 的变动主要集中在传输层。支持 HTTP/2 的 ASGI Server 应该为同一个 HTTP/2 上的不同请求生成不同的 scopes，并且能够将来自同一个流的响应正确地复用。HTTP 版本会在 scope 中以字符串的形式存在。</p><p>处理HTTP 协议中具有相同名字的 header 字段是很复杂的。RFC 7230 规定：在处理任何可重复出现的 header 字段时，都视作只发送一次该 header 字段并将所有的值使用逗号连接。</p><p>但同时 RFC 7239 和 RFC 6265 也明确指出，这一规则不适用于 HTTP Cookie 相关的 header 字段（Cookie 和 Set-Cookie）。Cookie header 字段只能由 user-agent 发送一次，但是 Set-Cookie header 字段可以重复出现并且不能使用逗号连接。</p><p>ASGI 协议的抉择是将请求和响应头分为两组 <code>[name, value]</code> 列表并且不对传入的值做任何其他处理。</p><p>ASGI Server 需要在向 ASGI Application 发出请求时通过一个值为 http 的 tpye 字段表明使用的协议。</p><h3 id="HTTP-Connection-Scope"><a href="#HTTP-Connection-Scope" class="headerlink" title="HTTP Connection Scope"></a>HTTP Connection Scope</h3><p>HTTP Connection 拥有一个唯一的 Request Connection Scope，也就是说，ASGI Application 会在请求开始时被调用，并持续到特定的请求结束后，即使底层的 Socket 仍然处于开启状态并且持续有请求进入。</p><p>Connection Scope Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode string</td><td>“http”</td></tr><tr><td>asgi[“version”]</td><td>Unicode string</td><td>使用的 ASGI 规范版本</td></tr><tr><td>asgi[“spec_version”]</td><td>Unicode string</td><td>Server 支持的 ASGI HTTP 规范版本，可选值：”2.0”，”2.1”，”2.2” 或 “2.3”，默认”2.0”</td></tr><tr><td>http_version</td><td>Unicode string</td><td>可选值：”1.0”，”1.1” 或 “2”</td></tr><tr><td>method</td><td>Unicode string</td><td>HTTP 方法名称，大写</td></tr><tr><td>scheme</td><td>Unicode string</td><td>URL 协议，可选值：”http” 或 “https”，默认为 “http”</td></tr><tr><td>path</td><td>Unicode string</td><td>HTTP 请求目标，不包括任何查询字符串，由百分号编码和 UTF-8 字节序解码成字符</td></tr><tr><td>raw_path</td><td>byte string</td><td>原始的未被修改的 HTTP path，来自 Web Server，可能缺省，默认为 None</td></tr><tr><td>query_string</td><td>byte string</td><td>url 中 ? 后的部分，百分号编码</td></tr><tr><td>root_path</td><td>Unicode string</td><td>Application 被绑定到的根路径，和 WSGI 中的 SCRIIPT_PATH 一致，默认为 “”</td></tr><tr><td>headers</td><td>Iterable[[byte string, byte string]]</td><td>一个由 [name, value] 两个子项组成的可迭代对象，name 应尽可能是小写</td></tr><tr><td>client</td><td>Iterable[Unicode string, int]</td><td>一个 [host, port] 可迭代对象，默认为 None</td></tr><tr><td>server</td><td>Iterable[Unicode string, Optional[int]]</td><td>可以是 [host, port] 可迭代对象，也可以是 [path, None]，其中 path 是 unix 套接字路径，缺省为 None</td></tr></tbody></table><p>ASGI Server 应该负责处理所有入站和出站的分块传输编码。当一个带有 chunked encoded body 的请求通过 ASGI Server 时，它应该自动去掉请求的分块以 plain body bytes 的形式提供给 ASGI Application。当一个没有 Content-Length 的响应被提供给 ASGI Server 时，它可以按照合适的方式进行 chunked。</p><h3 id="Request-receive-event"><a href="#Request-receive-event" class="headerlink" title="Request - receive event"></a>Request - receive event</h3><p>由 ASGI Server 发送给 ASGI Application 以标识一个入站请求。关于这个请求的大部分信息都在对应的  Connection Scope 内。</p><p>Receive 中的 body message 是一种传输大量大的入站 HTTP body 块的方式，并且是判断何时执行「实际处理请求的代码」的触发器（因此不应该在仅有一个 Connection Scope 打开时就触发「实际处理请求的代码」）。</p><p>注意：如果请求发送时附带了 <code>Transfer-Encoding: chunked</code> 头，ASGI Server 需要负责处理这种编码。http.request message 应该只包含每个 chunk 的解码信息。</p><p>Request Receive Event Message 包含：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“http.request”</td></tr><tr><td>body</td><td>Byte String</td><td>请求主体，默认为 b””，如果设置了 more_body = True，则将其视为 body chunk 链的一部分，并与后续的 chunks 进行关联</td></tr><tr><td>more_body</td><td>Bool</td><td>标志着是否还有额外的 body 内容，如果是 True 表示还有 body 内容，ASGI Application 需要等待，知道有一个为 False 的 chunk 到达</td></tr></tbody></table><h3 id="Response-Start-send-event"><a href="#Response-Start-send-event" class="headerlink" title="Response Start - send event"></a>Response Start - send event</h3><p>由 ASGI Application 发送给 ASGI Server，用于标识开始向 Web Client 发送响应。在此之后需要紧跟至少一个 response content message。ASGI Server 在接收到至少一个 Response Body 之前不得向 Web Client 发送响应。</p><p>ASGI Application 可能会在消息中发送一个 Transfer-Encoding header，但是 ASGI Server 必须忽略它。ASGI Server 需要自己处理 Transfer-Encoding，如果应用程序呈现的响应没有设置 Content-Length，可以选择使用 <code>Transfer-Encoding: chunked</code>。</p><p>Response Start Send Event Message 包含：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“http.response.start”</td></tr><tr><td>status</td><td>int</td><td>HTTP 状态码</td></tr><tr><td>headers</td><td>Iterable[[byte string, byte string]]</td><td>一个 [name, vlaue] 迭代器，必须与 HTTP Response 中的顺序一致且 Header names 都必须是小写的。默认是 []。不能存在 Pseudo headers（HTTP/2 和 HTTP/3 中的）</td></tr></tbody></table><h3 id="Response-Body-send-event"><a href="#Response-Body-send-event" class="headerlink" title="Response Body - send event"></a>Response Body - send event</h3><p>由 ASGI Application 发送给 ASGI Server，用于继续向 Web Client 发送响应。ASGI Server 必须在 send 返回前将传递给它的全部数据传输到发送缓冲区。如果 more_body 被设置为 False，这个 Connection 将被关闭。</p><p>Response Body Send Event Message 包含：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“http.response.body”</td></tr><tr><td>body</td><td>byte string</td><td></td></tr><tr><td>more_body</td><td>bool</td><td></td></tr></tbody></table><h3 id="Disconnect-receive-event"><a href="#Disconnect-receive-event" class="headerlink" title="Disconnect - receive event"></a>Disconnect - receive event</h3><p>由 ASGI Server 发送给 ASIG Application，在 HTTP Connection 关闭或在响应被发送后调用。主要适用于长轮询（long-polling），如果连接被提前关闭，希望触发某些清理代码时。</p><p>Disconnect Receive Event Message 包含：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“http.disconnect”</td></tr></tbody></table><h2 id="WebSocket-Protocol"><a href="#WebSocket-Protocol" class="headerlink" title="WebSocket Protocol"></a>WebSocket Protocol</h2><p>WebSockets 与 HTTP 存在某些一致的细节：它们都有 path 和 headers，但是也有一些独特的状态。同样，大部分状态都在 Scope 中，只要 socket 存在它们就会一直存在。</p><p>WebSocket 协议服务器（后简称：ASGI Server）应该自行处理 PING/PONG 消息，并在必要时发送 PING 消息以确保 Connection 是有活性的。</p><p>ASGI Server 应该自行处理 message fragmentation，并且将完整的消息传递给 ASGI Application。</p><p>ASGI Server 需要在向 ASGI Application 发出请求时通过一个值为 websocket 的 tpye 字段表明使用的协议。</p><h3 id="WebSocket-Connection-Scope"><a href="#WebSocket-Connection-Scope" class="headerlink" title="WebSocket Connection Scope"></a>WebSocket Connection Scope</h3><p>WebSocket Connection Scope 应该与 socket 共存，如果连接中断，socket 应该被同时关闭，反之亦然。</p><p>WebSocket Connection Scope Message 包含：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“websocket”</td></tr><tr><td>asgi[“version”]</td><td>Unicode String</td><td>使用的 ASGI 规范版本</td></tr><tr><td>asgi[“spec_version”]</td><td>Unicode String</td><td>Server 支持的 ASGI HTTP 规范版本，可选值：”2.0”，”2.1”，”2.2” 或 “2.3”，默认”2.0”</td></tr><tr><td>http_version</td><td>Unicode String</td><td>“1.1” 或 “2” 默认是 “1.1”</td></tr><tr><td>scheme</td><td>Unicode String</td><td>URL 协议，可选值：”ws” 或 “wss”，默认为 “ws”</td></tr><tr><td>path</td><td>Unicode string</td><td>HTTP 请求目标，不包括任何查询字符串，由百分号编码和 UTF-8 字节序解码成字符</td></tr><tr><td>raw_path</td><td>byte string</td><td>原始的未被修改的 HTTP path，来自 Web Server，可能缺省，默认为 None</td></tr><tr><td>query_string</td><td>byte string</td><td>url 中 ? 后的部分，百分号编码</td></tr><tr><td>root_path</td><td>Unicode string</td><td>Application 被绑定到的根路径，和 WSGI 中的 SCRIIPT_PATH 一致，默认为 “”</td></tr><tr><td>headers</td><td>Iterable[[byte string, byte string]]</td><td>一个由 [name, value] 两个子项组成的可迭代对象，name 应尽可能是小写</td></tr><tr><td>client</td><td>Iterable[Unicode string, int]</td><td>一个 [host, port] 可迭代对象，默认为 None</td></tr><tr><td>server</td><td>Iterable[Unicode string, Optional[int]]</td><td>可以是 [host, port] 可迭代对象，也可以是 [path, None]，其中 path 是 unix 套接字路径，缺省为 None</td></tr><tr><td>subprotocols</td><td>Iterable[Unicode string]</td><td>客户端公布的子协议，默认是 []</td></tr></tbody></table><h3 id="Connect-receive-event"><a href="#Connect-receive-event" class="headerlink" title="Connect - receive event"></a>Connect - receive event</h3><p>此事件是在 Web Client 打开一个 connection 并即将完成 WebSocket 握手的时候，由 ASGI Server 发送给 ASGI Application 的。</p><p>本事件的消息体必须被一个 Accept 事件消息或一个 Close 事件消息响应，在对应 socket 将要传递 websocket.receive 事件消息之前。ASGI Server 必须在 WebSocket 握手阶段发送本事件消息，并且在得到回复之前不能完成握手，如果 connection 被拒绝，则返回 HTTP 403。</p><p>Connect Receive Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“websocket.connect”</td></tr></tbody></table><h3 id="Accept-send-event"><a href="#Accept-send-event" class="headerlink" title="Accept - send event"></a>Accept - send event</h3><p>此事件是在 ASGI Application 期望接受一个 incoming connection 时由 ASGI Application 向 ASGI Server 发送的。</p><p>Accept Send Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“websocket.accept”</td></tr><tr><td>subprotocol</td><td>Unicode String</td><td>ASGI Server 期望接受的子协议，可选值，默认为 None</td></tr><tr><td>headers</td><td>Iterable[[byte string, byte string]]</td><td>一个 [name, value] 的可迭代对象。更多描述请看原文。</td></tr></tbody></table><h3 id="Receive-receive-event"><a href="#Receive-receive-event" class="headerlink" title="Receive - receive event"></a>Receive - receive event</h3><p>此事件是在收到来自 Web Client 的数据消息时由 ASGI Server 发送给 ASGI Application 的。</p><p>Receive Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“websocket.receive”</td></tr><tr><td>bytes</td><td>Byte String</td><td>message content，binary 模式，可选项，默认为 None</td></tr><tr><td>text</td><td>Unicode String</td><td>message content， text 模式，可选项，默认为 None</td></tr></tbody></table><p>bytes 或 text 至少要存在一个，也可以两个都存在。</p><h3 id="Send-send-event"><a href="#Send-send-event" class="headerlink" title="Send - send event"></a>Send - send event</h3><p>由 ASGI Application 发送给 ASGI Server，为 Web Client 发送一条数据信息。</p><p>Send Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“websocket.send”</td></tr><tr><td>bytes</td><td>Byte String</td><td>同上</td></tr><tr><td>text</td><td>Unicode String</td><td>同上</td></tr></tbody></table><p>bytes 或 text 至少要存在一个，也可以两个都存在。</p><h3 id="Disconnect-reveive-event"><a href="#Disconnect-reveive-event" class="headerlink" title="Disconnect - reveive event"></a>Disconnect - reveive event</h3><p>此事件是在任何一个与 Web Client 的链接断开时（包括 Web Client 关闭连接、ASGI Server 关闭连接或 socket 丢失）由 ASGI Server 发送给 ASGI Application 的。</p><p>Disconnect Reveive Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“websocket.disconnect”</td></tr><tr><td>code</td><td>int</td><td>websocket close code</td></tr></tbody></table><h3 id="Close-send-event"><a href="#Close-send-event" class="headerlink" title="Close - send event"></a>Close - send event</h3><p>由 ASGI Application 发送给 ASGI Server 告知 connection 关闭。</p><p>如果在 socket 被接受之前发送，ASGI Server 必须以 HTTP 403 错误代码关闭 connection，并且不完成 WebSocket 握手，这在某些浏览器上可能表现为不同的 WebSocket 错误代码（例如 1006，异常关闭）。</p><p>如果在 socket 被接受后发送，ASGI Server 必须通过传递 message 关闭 socket（默认 code 是 1000）。</p><p>Close Send Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“websocket.close”</td></tr><tr><td>code</td><td>int</td><td>websocket close code，可选项，默认为 1000</td></tr><tr><td>reason</td><td>Unicode String</td><td>close 原因，可选项，默认为空字符串</td></tr></tbody></table><h2 id="WSGI-Compatibility-WSGI-兼容性"><a href="#WSGI-Compatibility-WSGI-兼容性" class="headerlink" title="WSGI Compatibility | WSGI 兼容性"></a>WSGI Compatibility | WSGI 兼容性</h2><p>HTTP 子协议的设计有一部分是为了确保它能够与 WSGI 规范保持一致，以降低兼容两种规范的难度，并且使得 ASGI Server 搭配 WSGI Application 成为可能。</p><p>WSGI Application 是同步的，必须运行在线程池中才能被驱动，否则它的 runtime 就会映射到 HTTP Connection Scope 的 lifetime 上。</p><p>WSGI 的 environ 变量中的各种特殊 key 几乎都可以直接映射到 HTTP Connection Scope 上：</p><table><thead><tr><th>WSGI environ</th><th>ASGI HTTP Scope</th></tr></thead><tbody><tr><td>REQUEST_METHOD</td><td>method</td></tr><tr><td>SCRIPT_NAME</td><td>root_path</td></tr><tr><td>PATH_INFO</td><td>从 root 中剥离 root_path 获得</td></tr><tr><td>QUERY_STRING</td><td>query_string</td></tr><tr><td>CONTENT_TYPE</td><td>从 headers 中剥离</td></tr><tr><td>CONTENT_LENGTH</td><td>从 headers 中剥离</td></tr><tr><td>SERVER_NAME and SERVER_PORT</td><td>server</td></tr><tr><td>REMOTE_HOST/REMOTE_ADDR and REMOTE_PORT</td><td>client</td></tr><tr><td>SERVER_PROTOCOL</td><td>http_version</td></tr><tr><td>wsgi.url_scheme</td><td>scheme</td></tr><tr><td>wsgi.input</td><td>一个基于 http.request 的 StringIO</td></tr><tr><td>wsgi.errors</td><td>directed by the wrapper as needed</td></tr></tbody></table><p>WSGI 中的 start_response 可调用对象与 http.response.start 类似：</p><ul><li>status argument 变成了 status，并且去掉了 reason phrase。</li><li>response_headers 映射到了 headers 上。</li></ul><p><img src="https://img.blanc.site//wiki/img202208041805019.png" alt="WSGI start_response"></p><p>从 WSGI Application 中产生的内容映射到了 http.response.body 的 message 中。</p><h2 id="WSGI-encoding-differences-WSGI-编码异同"><a href="#WSGI-encoding-differences-WSGI-编码异同" class="headerlink" title="WSGI encoding differences | WSGI 编码异同"></a>WSGI encoding differences | WSGI 编码异同</h2><p>WSGI 规范（如 <a href="https://www.python.org/dev/peps/pep-3333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 所定义）规定：所有发送或来自 WSGI Server 的 strings 必须是 str 类型，但值包括 ISO-8859-1(“lantin-1”) 范围内的 codepoints。这是因为它最迟是为 Python2 以及不同的 set of string types 设计的。</p><p>ASGI 的 HTTP 和 WebSocket 子规范将 Scope 字典的每个条目指定为 byte string 或 Unicode String 的其中一种。HTTP 作为一个早期协议，在编码制定上存在一些不完善的地方，所以在何处使用 Unicode 何处使用 byte 并没有明确的说明。</p><ul><li>path：URL 可以同时拥有 percent-encoded 或 UTF-8 编码。这部分的解码行为通常是在底层服务器或传播过程中的服务器进行的，因此这个字段被设置成了 Unicode String，由 UTF-8 和 percent 编码同时构成。</li><li>headers：这些是由 Web Client 或 Server 发送的确切字节序的字节字符串。虽然现在的 HTTP 标准规定 headers 应该是 ASCII 码，但是旧的标准并没有规定，而是允许尽可能多的编码选择。Framework 或 Application 应该按照他们认为合适的方式进行 headers 的解码。</li><li>query_string：不像 path，它不会收到 Server 的干扰，因此使用 raw byte sring 呈现，使用 percent-encode 编码。</li><li>root_path：为了与 path 匹配同样使用 Unicode String。</li></ul><h2 id="Lifespan-Protocol"><a href="#Lifespan-Protocol" class="headerlink" title="Lifespan Protocol"></a>Lifespan Protocol</h2><p>Lifesapn ASGI 子规范概括了如何在 ASGI Application 中传递像 startup 或 shutdown 之类的 lifespan events。</p><p>Lifespan message 允许 ASGI Application 在一个运行中的事件循环的上下文中初始化或停止。这方面的一个例子是创建一个连接池，随后关闭连接池释放连接。</p><p>Lifespan 应该在处理请求的每个事件循环中执行一次。在多进程环境中，每个进程都会有  Lifespan event。重要的是，lifespan 和 request 是在同一个事件循环中运行的，以确保像数据库连接池这样的对象不会在循环中被移除或共享。</p><p>一个 lifespan 实现举例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">app</span><span class="params">(scope, receive, send)</span>:</span></span><br><span class="line">    <span class="keyword">if</span> scope[<span class="string">"type"</span>] == <span class="string">"lifespan"</span>:</span><br><span class="line">        <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">            message = <span class="keyword">await</span> receive()</span><br><span class="line">            <span class="keyword">if</span> message[<span class="string">"type"</span>] == <span class="string">"lifespan.startup"</span>:</span><br><span class="line">                <span class="comment"># Do some startup here</span></span><br><span class="line">                <span class="keyword">await</span> send(&#123;<span class="string">"type"</span>: <span class="string">"lifespan.startup.complete"</span>&#125;)</span><br><span class="line">            <span class="keyword">elif</span> message[<span class="string">"type"</span>] == <span class="string">"lifespan.shutdown"</span>:</span><br><span class="line">                <span class="comment"># Do some shutdown here</span></span><br><span class="line">                <span class="keyword">await</span> send(&#123;<span class="string">"type"</span>: <span class="string">"lifespan.shutdown.complete"</span>&#125;)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="comment"># Handle other</span></span><br><span class="line">        <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><h3 id="Scope"><a href="#Scope" class="headerlink" title="Scope"></a>Scope</h3><p>Lifespan Scope 会持续存在直到事件循环结束。</p><p>Lifespan Scope Message 包含：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“lifespan”</td></tr><tr><td>asgi[“version”]</td><td>Unicode String</td><td>ASGI 协议版本</td></tr><tr><td>asgi[“spec_version”]</td><td>Unicode String</td><td>子协议版本，默认 “1.0”</td></tr></tbody></table><p>如果在调用带有 lifespan.startup 消息的 Application 或处理 type 是 lifespan 的 Scope 时抛出了异常，ASGI Server 需要继续执行但不 send any lifespan events。</p><p>这允许兼容不支持 lifespan 的 ASGI Application。如果需要记录在 lifespan 启动过程中发生的错误并阻止 ASGI Server 的启动过程，可以通过发送 lifespan.startup.filed 来实现。</p><h3 id="Startup-reveive-event"><a href="#Startup-reveive-event" class="headerlink" title="Startup - reveive event"></a>Startup - reveive event</h3><p>此事件是在 ASGI Server 准备好处理 startup 和 receive connection 但还没开始处理的时候，由 ASGI Server 发送给 ASGI Application 的。</p><p>Startup Receive Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“lifespan.startup”</td></tr></tbody></table><h3 id="Startup-Complete-send-event"><a href="#Startup-Complete-send-event" class="headerlink" title="Startup Complete - send event"></a>Startup Complete - send event</h3><p>此事件是在 ASGI Application 处理完 startup 后，由 ASGI Application 发送给 ASGI Server 的。<strong>ASGI Server 在开始处理 connection 之前必须等待这个事件</strong>。</p><p>Startup Complete Send Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“lifespan.startup.complete”</td></tr></tbody></table><h3 id="Startup-Failed-send-event"><a href="#Startup-Failed-send-event" class="headerlink" title="Startup Failed - send event"></a>Startup Failed - send event</h3><p>此事件是在 ASGI Application 未能完成 startup 时，由 ASGI Application 发送给 ASGI Server 的。ASGI Server 应该在接收到事件消息后记录或打印消息所提供的内容，然后退出。</p><p>Startup Failed Send Event Message 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“lifespan.startup.failed”</td></tr><tr><td>message</td><td>Unicode String</td><td>默认为 “”</td></tr></tbody></table><h3 id="Shutdown-reveive-event"><a href="#Shutdown-reveive-event" class="headerlink" title="Shutdown - reveive event"></a>Shutdown - reveive event</h3><p>此事件是在 ASGI Server 停止接受 connection 并关闭所有正在处理的 connection 后，由 ASGI Server 发送给 ASGI Application 的。</p><p>Shutdown Reveive Event 包括：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“lifespan.shutdown”</td></tr></tbody></table><h3 id="Shutdown-Complete-send-event"><a href="#Shutdown-Complete-send-event" class="headerlink" title="Shutdown Complete - send event"></a>Shutdown Complete - send event</h3><p>此事件是在 ASGI Application 完成 cleanup 之后，由 ASGI Application 发送给 ASGI Server 的。ASGI Server 在终止前必须等待此事件消息。</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“lifespan.shutdown.complete”</td></tr></tbody></table><h3 id="Shutdown-Failed-send-event"><a href="#Shutdown-Failed-send-event" class="headerlink" title="Shutdown Failed - send event"></a>Shutdown Failed - send event</h3><p>此事件是在 ASGI Application 未能正常处理 cleanup 之后，由 ASGI Application  发送给 ASGI Server 的。ASGI Server 应该在接收到事件消息后记录或打印消息所提供的内容，然后退出。</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>type</td><td>Unicode String</td><td>“lifespan.shutdown.failed”</td></tr><tr><td>message</td><td>Unicode String</td><td>默认 “”</td></tr></tbody></table><h2 id="ASGI-TLS-Extension"><a href="#ASGI-TLS-Extension" class="headerlink" title="ASGI TLS Extension"></a>ASGI TLS Extension</h2><p>本规范概述了如何在 ASGI connection scope 对象中传递 TLS（或 SSL）connection 信息。</p><h3 id="The-Base-Protocol-基础协议"><a href="#The-Base-Protocol-基础协议" class="headerlink" title="The Base Protocol | 基础协议"></a>The Base Protocol | 基础协议</h3><p>TSL 是无法单独使用的，它总是包裹着另一个协议。因此，此规范并非用来规定如何单独使用 TSL 的，它必须作为一个其他 ASGI 子协议的拓展来使用。与 TSL 搭配的其他 ASGI 子协议被称为基础协议。</p><p>对于 HTTP-over-TLS(HTTPS) 来说，需要联合使用 TLS 子规范和 ASGI HTTP 子规范，其中基础协议是 ASGI HTTP 子规范。</p><p>对于 WebSockets-over-TLS(wss:// protocol) 来说，需要联合使用 TLS 子规范和 ASGI WebSocket 子规范，其中基础协议是 ASGI WebSocket 子规范。</p><p>在搭配基础协议使用此拓展协议时需要注意：基础协议中必须定义 Connection Scope 以确保它最多包含一个 TLS 连接，否则，就不能使用此拓展协议。</p><h3 id="When-to-use-this-extension"><a href="#When-to-use-this-extension" class="headerlink" title="When to use this extension"></a>When to use this extension</h3><p>此拓展仅限于在 TLS Connection 中使用。</p><p>对于非 TLS Connection，ASGI Server 禁止提供此拓展。</p><p>ASGI Application 可以在 Connection Scope 的 extensions 字典中检查是否存在 tls 拓展。如果存在，说明 ASGI Server 支持此拓展，并且 Connection 是建立在 TLS 上的。如果不存在，说明 ASGI Server 不支持这个拓展或 Connection 不是建立在 TLS 上的。</p><h3 id="TLS-Connection-Scope"><a href="#TLS-Connection-Scope" class="headerlink" title="TLS Connection Scope"></a>TLS Connection Scope</h3><p>基础协议的 Connection Scope 中包含一个 extensions 键值对，它的值是一个字典。在该字典中的 tls 键值即是 TLS Connection Scope：</p><table><thead><tr><th>名称</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>server_cert</td><td>Unicode String or None</td><td>ASGI Server 在建立 TLS 连接时发送的 x509 证书的 PEM 编码版本。一些 ASGI Server 的实现可能无法提供这一点（例如，如果 TLS 是由一个单独的代理或负载平衡服务器终止的），在这种情况下应该是 None。必须存在的。</td></tr><tr><td>client_cert_chain</td><td>Iterable[Unicode string]</td><td>每个字符串都是一个 PEM 编码的 x509 证书。第一个证书是客户端证书。任何后续证书都是客户端发送的证书链的一部分，每个证书都会签署前面的证书。如果客户端没有提供证书，那么它就是一个空的可迭代对象。可选的，如果缺失默认为空的可迭代对象。</td></tr><tr><td>client_cert_name</td><td>Unicode String or None</td><td>客户端证书主题的 x509 标识名（DN），按照 RFC 4514 中定义进行编码的单一字符串。如果客户端没有提供证书，则为 None。如果 client_cert_chain 被提供且不是空可迭代对象，那么这个字段必须要被提供，并且必须包含与 client_cert_chain[0] 一致的信息。可选的，如缺失默认为 None。</td></tr><tr><td>client_cert_error</td><td>Unicode String or None</td><td>如果提供了客户端证书并验证成功，或没有提供客户端证书都为 None。如果提供了客户端证书但验证失败，则是一个非空字符串，包含一个错误信息或错误代码，表明验证失败的原因。大多数 Web Server 会在客户端证书验证失败后直接拒绝而非设置这个值。可选的，默认为 None。</td></tr><tr><td>tls_version</td><td>Integer or None</td><td>正在使用的 TLS 版本。这是在 TLS 规范中定义的版本号之一，是一个无符号整数。常见值包括：0x0303 for TLS 1.2 或 0x0304 for TLS 1.3，如果 TLS 没有被使用则设置为 None。必须存在。</td></tr><tr><td>cipher_suite</td><td>Integer or None</td><td>正在使用的 TLS cipher suite。一个 16  位无符号证书，按照网络字节序对相关 RFC 中规定的一对 8 位证书进行编码。一些 Web Server 无法提供这个功能，这种情况下，设置为 None。必须存在。</td></tr></tbody></table><h3 id="Events"><a href="#Events" class="headerlink" title="Events"></a>Events</h3><p>所有的 Event 都是基于基础协议的。</p><h3 id="Rational-Informative"><a href="#Rational-Informative" class="headerlink" title="Rational(Informative)"></a>Rational(Informative)</h3><p>略</p><h2 id="已有实现"><a href="#已有实现" class="headerlink" title="已有实现"></a>已有实现</h2><h3 id="Server"><a href="#Server" class="headerlink" title="Server"></a>Server</h3><h4 id="Daphne"><a href="#Daphne" class="headerlink" title="Daphne"></a>Daphne</h4><p><a href="http://github.com/django/daphne" target="_blank" rel="external nofollow noopener noreferrer">Stable / http://github.com/django/daphne</a></p><p>当前 ASGI 的参考 Server 实现（亲儿子），使用 <a href="https://twistedmatrix.com/trac/" target="_blank" rel="external nofollow noopener noreferrer">Twisted</a> 编写，作为 <a href="https://channels.readthedocs.io/en/stable/" target="_blank" rel="external nofollow noopener noreferrer">Django Channel</a> 项目组的一部分。支持 HTTP/1, HTTP/2 和 WebSockets。</p><h4 id="Uvicorn"><a href="#Uvicorn" class="headerlink" title="Uvicorn"></a>Uvicorn</h4><p><a href="https://www.uvicorn.org/" target="_blank" rel="external nofollow noopener noreferrer">Stable / https://www.uvicorn.org/</a></p><p>一个基于 <a href="https://github.com/MagicStack/uvloop" target="_blank" rel="external nofollow noopener noreferrer">uvloop</a> 和 <a href="https://github.com/MagicStack/httptools" target="_blank" rel="external nofollow noopener noreferrer">httptools</a> 的 ASGI Server。支持 HTTP/1 和 WebSockets。</p><h4 id="Hypercorn"><a href="#Hypercorn" class="headerlink" title="Hypercorn"></a>Hypercorn</h4><p><a href="https://pgjones.gitlab.io/hypercorn/index.html" target="_blank" rel="external nofollow noopener noreferrer">Beta / https://pgjones.gitlab.io/hypercorn/index.html</a></p><p>一个基于 <a href="https://sans-io.readthedocs.io/" target="_blank" rel="external nofollow noopener noreferrer">sans-io hyper</a>, <a href="https://github.com/python-hyper/h11" target="_blank" rel="external nofollow noopener noreferrer">h11</a>, <a href="https://github.com/python-hyper/h2" target="_blank" rel="external nofollow noopener noreferrer">h2</a> 和 <a href="https://python-hyper.org/projects/wsproto/en/stable/" target="_blank" rel="external nofollow noopener noreferrer">wsproto</a> 库的 ASGI Server。支持 HTTP/1, HTTP/2 和 WebSockets。</p><h3 id="Application-Frameworks"><a href="#Application-Frameworks" class="headerlink" title="Application Frameworks"></a>Application Frameworks</h3><h4 id="Django-Channels"><a href="#Django-Channels" class="headerlink" title="Django/Channels"></a>Django/Channels</h4><p><a href="http://channels.readthedocs.io" target="_blank" rel="external nofollow noopener noreferrer">Stable / http://channels.readthedocs.io</a></p><p>Channels 是 Django 项目的一部分，旨在为 Django 提供异步支持能力，是 ASGI 项目的发起者。为 Django 整合了处理 HTTP，WebSocket 以及任何满足 ASGI-native 代码实现协议的能力。</p><h4 id="FastAPI"><a href="#FastAPI" class="headerlink" title="FastAPI"></a>FastAPI</h4><p><a href="https://github.com/tiangolo/fastapi" target="_blank" rel="external nofollow noopener noreferrer">Beta / https://github.com/tiangolo/fastapi</a></p><p>FastPI 是一个在 Starlette 框架基础上进一步封装的 ASGI Web 框架，它整合了标准 Python 类型注释、OpenAPI、JSON Schema、OAuth 等特性。支持 HTTP 和 WebSocket 协议。</p><h4 id="Quart"><a href="#Quart" class="headerlink" title="Quart"></a>Quart</h4><p><a href="https://github.com/pgjones/quart" target="_blank" rel="external nofollow noopener noreferrer">Beta / https://github.com/pgjones/quart</a></p><p>Quart 是一个 Python ASGI 微框架。它专注于使用最简单的 asyncio 特性为 Web 应用提供异步能力，常被用在 Flask apps 中。支持 HTTP 协议。</p><h4 id="Sanic"><a href="#Sanic" class="headerlink" title="Sanic"></a>Sanic</h4><p><a href="https://sanicframework.org" target="_blank" rel="external nofollow noopener noreferrer">Beta / https://sanicframework.org</a></p><p>Sanic 是一个灵活的框架，它既能够作为 ASGI Server 使用也能够作为 ASGI Application 使用。支持 HTTP 和 WebSockets 协议。</p><h4 id="Starlette"><a href="#Starlette" class="headerlink" title="Starlette"></a>Starlette</h4><p><a href="https://github.com/encode/starlette" target="_blank" rel="external nofollow noopener noreferrer">Beta / https://github.com/encode/starlette</a></p><p>Starlette 是一个提供了编写基础但强大请求或响应对象的极简的 ASGI 库。支持 HTTP 和 WebSockets 协议。</p><h4 id="rpc-py"><a href="#rpc-py" class="headerlink" title="rpc.py"></a>rpc.py</h4><p><a href="https://github.com/abersheeran/rpc.py" target="_blank" rel="external nofollow noopener noreferrer">Beta / https://github.com/abersheeran/rpc.py</a></p><p>一个易于使用的强大的 RPC 框架。RPC Server 基于 WSGI 和 ASGI，Client 基于 <a href="https://www.python-httpx.org/" target="_blank" rel="external nofollow noopener noreferrer">httpx</a>。支持同步、异步、同步生成器和异步生成器，并提供可选的类型注释和 OpenAPI 文档生成特性。</p><h3 id="Tools"><a href="#Tools" class="headerlink" title="Tools"></a>Tools</h3><h4 id="a2wsgi"><a href="#a2wsgi" class="headerlink" title="a2wsgi"></a>a2wsgi</h4><p><a href="https://github.com/abersheeran/a2wsgi" target="_blank" rel="external nofollow noopener noreferrer">Stable / https://github.com/abersheeran/a2wsgi</a></p><p>将一个 WSGI Application 转换为一个 ASGI Application 的工具。纯 Python 实现，仅依赖于原生库。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>我在整理 <a href="#已有实现">已有实现章节</a> 的时候发现，<a href="https://github.com/encode" target="_blank" rel="external nofollow noopener noreferrer">encode</a> 这个 Github Group 有点强，Django-REST-Framework, Starlette, Uvicorn, httpx 都是出自这一个组织。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://asgi.readthedocs.io/en/latest/" target="_blank" rel="external nofollow noopener noreferrer">ASGI Documentation</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;本文的主体内容大部分来自对 &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/&quot; target=
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Python" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Python/"/>
    
    
      <category term="Python" scheme="https://wiki.blanc.site/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>WeeklyPEP-0-overview</title>
    <link href="https://wiki.blanc.site/archives/2accc063.html"/>
    <id>https://wiki.blanc.site/archives/2accc063.html</id>
    <published>2021-11-04T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在学习 Python 的过程中阅读 PEP 是绕不过去的一件事情，所有新鲜的 Python 特性都是经过 PEP 讨论和公示后才加入到新版本中的。因此无论是想要了解某个现有 Python 特性的详细说明，还是想要了解某个 Python 新特性的形成过程，阅读 PEP 都是一个不二的选择。</p><p>WeeklyPEP 系列文章的目的是：</p><ol><li>督促本人周期性的学习和了解现有的 PEP；</li><li>将本人阅读并整理后的 PEP 总结成文帮助有需要的人；</li><li>希望自己能够在深入了解 Pyhton 语言后出现在 <a href="https://www.python.org/dev/peps/#authors-owners" target="_blank" rel="external nofollow noopener noreferrer">PEP 0 #Authors/Owners</a> 中。</li></ol><h2 id="规划"><a href="#规划" class="headerlink" title="规划"></a>规划</h2><h3 id="更新周期"><a href="#更新周期" class="headerlink" title="更新周期"></a>更新周期</h3><p>每月至少围绕一个 PEP 进行更新，并将至少一个 PEP 的核心内容理解和整理成文。</p><h3 id="文章命名"><a href="#文章命名" class="headerlink" title="文章命名"></a>文章命名</h3><p>常规：<code>WeeklyPEP-[number]-[PEP number]-[PEP name]</code></p><p>对于 Python 重要特性的 PEP 可能会拆分成多篇文章进行更新：</p><ol><li>概览：<code>WeeklyPEP-[number]-[PEP number]-[PEP name]-overview</code></li><li>详解：<code>WeeklyPEP-[number]-[PEP number]-[PEP name]-feature</code></li></ol><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><ul><li><a href="https://wiki.blanc.site/archives/5823d5df.html">WeeklyPEP-2-PEP 343-with 语句-overview</a></li><li><a href="https://wiki.blanc.site/archives/c29b4f3b.html">WeeklyPEP-3-PEP 318-函数装饰器-overview</a></li><li><a href="https://wiki.blanc.site/archives/7eaf0f5f.html">WeeklyPEP-8-PEP 492-使用 async 和 await 语法的协程-overview</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;在学习 Python 的过程中阅读 PEP 是绕不过去的一件事情，所有新鲜的 Python 特性都是经过 PEP 讨论和公示后才加入到新版本
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Python" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Python/"/>
    
    
      <category term="Python" scheme="https://wiki.blanc.site/tags/Python/"/>
    
      <category term="WeeklyPEP" scheme="https://wiki.blanc.site/tags/WeeklyPEP/"/>
    
  </entry>
  
  <entry>
    <title>SSH overview</title>
    <link href="https://wiki.blanc.site/archives/5c113e93.html"/>
    <id>https://wiki.blanc.site/archives/5c113e93.html</id>
    <published>2021-10-22T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><h2 id="SSH-是什么"><a href="#SSH-是什么" class="headerlink" title="SSH 是什么"></a>SSH 是什么</h2><blockquote><p>Secure Shell（安全外壳协议，简称 SSH）是一种加密的网络传输协议，可以再不安全的网络中为网络服务提供安全的传输环境。</p><p>——<a href="https://zh.wikipedia.org/wiki/Secure_Shell" target="_blank" rel="external nofollow noopener noreferrer">维基百科</a></p></blockquote><p>通过维基百科的说明可以看出 SSH 实际上指的是一种加密的网络传输协议，而我们经常用来登录远程主机的 ssh 命令实际上是某个软件对 SSH 这种协议的包装实现，其中最常见的开源实现方案是 <a href="https://zh.wikipedia.org/wiki/OpenSSH" target="_blank" rel="external nofollow noopener noreferrer">OpenSSH（OpenBSD Secure Shell）</a>。</p><p>SSH 目前还是比较可靠的，利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。通过 SSH 可以对所有传输的数据进行加密，也能够防止 DNS 欺骗和 IP 欺骗。SSH 的另一项优点是其传输的数据可以是经过压缩的，所以可以加快传输速度。</p><p>SSH 当下有两个版本，分别是 SSHv1 和 SSHv2，v2 是主流版本，v1 版本存在中间人攻击的安全风险。</p><h2 id="基本流程"><a href="#基本流程" class="headerlink" title="基本流程"></a>基本流程</h2><p>SSH 协议规定的通讯流程可以分解成几个主要阶段：</p><ol><li>SSH 协议版本协商阶段</li><li>密钥和算法协商阶段</li><li>客户端认证阶段</li><li>会话请求阶段</li><li>交互会话阶段</li></ol><h3 id="SSH-协议版本协商阶段"><a href="#SSH-协议版本协商阶段" class="headerlink" title="SSH 协议版本协商阶段"></a>SSH 协议版本协商阶段</h3><ol><li>服务端打开服务端口（默认为 22），等待客户端连接；</li><li>客户端向服务端发起 TCP 连接，双方完成握手并建立连接；</li><li>服务端通过 TCP 连接向客户端发送一个包含 SSH 版本信息的报文；</li><li>客户端收到报文后对比报文给出的版本信息和自身版本信息：<ol><li>报文版本低于自身版本但自身能够兼容，使用报文版本进行通信；</li><li>其他情况下使用自身版本进行通信；</li></ol></li><li>客户端将确定好的版本号通过 TCP 连接发送给服务端，服务端判断是否支持：<ol><li>支持，进入密钥和算法协商阶段；</li><li>不支持，无法完成 SSH 连接，中断此次请求；</li></ol></li></ol><h3 id="密钥和算法协商阶段"><a href="#密钥和算法协商阶段" class="headerlink" title="密钥和算法协商阶段"></a>密钥和算法协商阶段</h3><ol><li>服务端和客户端通过 TCP 连接分别发送算法协商报文给对端，报文中包含自己支持的公钥算法列表，加密算法列表，MAC 算法列表，压缩算法列表等；</li><li>与协议版本协商阶段类似，服务端和客户端根据自己和对端支持的算法来决定最终使用的算法；</li><li>服务端和客户端利用 Diffie-Hellman 密钥交换使用算法，主机密钥对等参数，生成共享密钥和会话 ID；</li><li>至此加密的 SSH 通信通道建立完成，在后续的通信过程中共享密钥用于两端对传输数据的加密和解密，会话 ID 用于认证过程。</li></ol><h4 id="Diffie-Hellman-算法"><a href="#Diffie-Hellman-算法" class="headerlink" title="Diffie-Hellman 算法"></a>Diffie-Hellman 算法</h4><p>DH 算法可以在一个不安全的信道上建立安全连接，从而解决不安全信道上信息安全交互的问题。</p><p>假设 A 与 B 要在不安全信道上使用 DH 算法安全地交换信息，大致流程如下：</p><ol><li>A 与 B 经过协商选定一个质数 p 及其本原根 g；</li><li>A 生成随机数 $a \in [1, p-1]$，计算 $Y_{A} = g^{a} mod p$</li><li>B 生成随机数 $b \in [1, p-1]$，计算 $Y_{B} = g^{b} mod p$</li><li>双方交换各自生成的 $Y$；</li><li>A 计算 $K = Y_{B}^{a} mod p$；</li><li>B 计算 $K = Y_{A}^{b} mod p$；</li><li>经过上述过程，A 和 B 都得到了安全的共享密钥 $K$。</li></ol><p>注 1：流程中的数理知识这里不做普及，因为博主本身也不是很懂；<br>注 2：这里给出的仅仅是一个粗略的原理解释，并非最佳实现过程；</p><p>算法成立的原因：再此方法中公开数据有 $p，g，Y_{A}，Y_{B}$，若想要通过公开数据计算 $K$，则需要求取 $Y_{A} = g^{a} mod p \mid Y_{B} = g^{b} mod p$ 中的 a 或 b，求解此类问题一般使用穷举法，时间复杂度为 $O(p)$，只要 p 足够大就能够保证此方法目前可以达到计算机安全的要求。</p><h4 id="SSH-如何使用-Diffie-Hellman-算法"><a href="#SSH-如何使用-Diffie-Hellman-算法" class="headerlink" title="SSH 如何使用 Diffie-Hellman 算法"></a>SSH 如何使用 Diffie-Hellman 算法</h4><p>博主技术有限，没筛选出这一流程的 TCP 包，因此参考 <a href="https://blog.csdn.net/wchrt/article/details/45752933" target="_blank" rel="external nofollow noopener noreferrer">第三篇参考文章</a> 给出 diffie-hellman-group-exchange-sha256 的大致实现流程：</p><ol><li>客户端通过 TCP 连接发送报文通知服务端开始 DH 交换流程；</li><li>服务端将选择好的 p 和 g 发送给客户端（含义参考上一小节）；</li><li>客户端接收到 p 和 g 后生成自己的 Y-客户端，并返回给服务端；</li><li>服务端接收到 Y-客户端后：<ol><li>通过计算得到密钥 K；</li><li>使用 sha256 算法将一些已知信息加密为 H-服务端并用 rsa 为其签名；</li><li>将 rsa 的公钥，Y-服务端，rsa 签名后的 H-服务端发送给客户端；</li></ol></li><li>客户端接收到服务端的返回值：<ol><li>计算出相同的密钥 K；</li><li>同样使用 sha256 算法将相同信息加密为 H-客户端；</li><li>利用 rsa 服务端公钥得到 H-客户端签名与 H-服务端签名进行对比；</li><li>校验无误，返回特定报文表示密钥交换完成，以后数据都由此密钥进行加密。</li></ol></li></ol><p>H 的计算方法：$H = hash(VC \parallel VS \parallel IC \parallel IS \parallel KS \parallel YC \parallel YS \parallel K)$</p><table><thead><tr><th>类型</th><th>名称</th><th>意义</th></tr></thead><tbody><tr><td>string</td><td>VC</td><td>客户端的初始报文</td></tr><tr><td>string</td><td>VS</td><td>服务端的初始报文</td></tr><tr><td>string</td><td>IC</td><td>客户端 SSH_MSG_KEX_INIY 的有效载荷</td></tr><tr><td>string</td><td>IS</td><td>服务端 SSH_MSG_KEX_INIT 的有效载荷</td></tr><tr><td>string</td><td>KS</td><td>服务端主机密钥（host key 一般是 RSA 公钥）</td></tr><tr><td>string</td><td>YC</td><td>Y-客户端</td></tr><tr><td>string</td><td>YS</td><td>Y-服务端</td></tr><tr><td>string</td><td>K</td><td>通过 DH 产生的共享密钥</td></tr></tbody></table><p>以上内容按顺序进行拼接，不夹杂或尾随多余字符，拼接后的字符串进行 sha256 计算出结果就是 H 即 session_id。只有会话第一次密钥交换生成的 H 是 session_id，后面再进行密钥交换时，session_id 不会改变。</p><p>后续通信一般是采用 AES 算法进行加密，密钥计算方法：$hash(K \parallel H \parallel W \parallel session-id)$，其中 W 代指单个大写的 ASCII 字母，不同的加密秘钥使用不同的字符来计算。</p><table><thead><tr><th>字母</th><th>类型</th></tr></thead><tbody><tr><td>A</td><td>客户端到服务端的初始 IV</td></tr><tr><td>B</td><td>服务端到客户端的初始 IV</td></tr><tr><td>C</td><td>客户端到服务端的加密秘钥</td></tr><tr><td>D</td><td>服务端到客户端的加密秘钥</td></tr><tr><td>E</td><td>客户端到服务端的完整性秘钥</td></tr><tr><td>F</td><td>服务端到客户端的完整性秘钥</td></tr></tbody></table><p>经过计算得到字符串 RE，如果我们想要的秘钥长度比 RE 长，则在 RE 后面继续加上一个值：$hash(K \parallel H \parallel RE)$ 成为一个加长的 RE。如果还不够，则继续使用同样方法进行累加。</p><p>注 1：关于秘钥计算公式中 H 和 session-id 同时出现博主表示存疑，但是没找到更多资料所以暂时先这么写了，如果具体实现和给出内容有出入的话希望您能不吝赐教，第一时间联系博主进行修改。</p><h4 id="延伸：SSH-为什么要使用-Diffie-Hellman-算法"><a href="#延伸：SSH-为什么要使用-Diffie-Hellman-算法" class="headerlink" title="延伸：SSH 为什么要使用 Diffie-Hellman 算法"></a>延伸：SSH 为什么要使用 Diffie-Hellman 算法</h4><blockquote><p>我对 SSH 协商过程的理解：</p><ol><li>A 利用 RSA 算法生成公钥 A 和私钥 A，B 同上生成公钥 B 和私钥 B；</li><li>A 把公钥 A 发给 B；</li><li>B 把公钥 B 发给 A；</li><li>后续通讯过程 A 用公钥 B 加密内容发给 B；B 用公钥 A 加密内容发给 A。</li></ol></blockquote><blockquote><p>我的疑惑是：<br>看很多资料在解释Linux下两台主机ssh通信协商时会提到DH（diffie-hellman），我知道DH是密钥交换算法，可以使通信双方安全地产生一个公共密钥（对称密钥）。但是通过上述&gt; 上述协商过程，A 和 B 不是已经可以利用 RSA 算法产生的公钥和私钥进行加密通信了吗，那为什么还需要 DH 算法呢？<br>难道上述过程之后还要用 DH 算法再生成一个公共密钥？</p><p>——<a href="https://www.zhihu.com/question/282877403" target="_blank" rel="external nofollow noopener noreferrer">知乎问题：SSH为什么要用到DH（Diffie-Hellman Exchange）？</a></p></blockquote><p>首先要指出的是问题提出者所理解的 SSH 协商过程是错误的。</p><p>至于为什么不直接用 RSA 算法进行加密通信其实和 HTTS 差不多一个道理：RSA 算法是非对称加密算法，消耗资源多，运行效率低，不适合用于长连接下的数据交换加密。</p><p>如果想看此问题下的更多展开内容推荐 <a href="https://www.zhihu.com/question/282877403/answer/433845936" target="_blank" rel="external nofollow noopener noreferrer">点击此链接查看第六篇参考文章</a> 给出的回答。</p><h3 id="客户端认证阶段"><a href="#客户端认证阶段" class="headerlink" title="客户端认证阶段"></a>客户端认证阶段</h3><ol><li>客户端向服务端发送认证请求；</li><li>服务端对客户端进行认证，如果认证失败则向客户端发送失败消息；</li><li>客户端可以选择再次认证知道达到认证次数上线（如果有设置的话）或认证成功位置。</li></ol><p>常见的客户端认证方式有两种</p><ol><li>密码认证：密码认证所用的账户密码一般与系统用户密码相同；</li><li>密钥认证：可接受公钥一般存放在用户目录下的 <code>~/.ssh/authorized_keys</code> 中，注意需要 SSH 服务端拥有此文件的访问权限。</li></ol><h3 id="会话请求阶段"><a href="#会话请求阶段" class="headerlink" title="会话请求阶段"></a>会话请求阶段</h3><ol><li>服务端等待客户端请求；</li><li>认证完成后，客户端向服务端发送会话请求；</li><li>服务端处理客户端请求：<ol><li>完成后向客户端 SSH_SMSG_SUCCESS 报文，双方进入交互会话阶段；</li><li>如果请求未被成功处理，返回 SSH_SMSG_FAILURE 报文，表示请求处理失败或者不能识别客户端请求。</li></ol></li></ol><h3 id="交互会话阶段"><a href="#交互会话阶段" class="headerlink" title="交互会话阶段"></a>交互会话阶段</h3><ol><li>客户端将要执行的命令加密发送给服务端。</li><li>服务端收到后解密命令，执行后将结果加密返回客户端。</li><li>客户端将返回结果解密后显示到终端上。</li></ol><h2 id="OpenSSH"><a href="#OpenSSH" class="headerlink" title="OpenSSH"></a>OpenSSH</h2><blockquote><p>OpenSSH 是在 1999年 10月第一次在 OpenBSD 2.6 里出现，当初的项目是取代由 SSH Communications Security 所提供的 SSH 软件。</p><p>——<a href="https://zh.wikipedia.org/wiki/OpenSSH" target="_blank" rel="external nofollow noopener noreferrer">维基百科</a></p></blockquote><p>程序主要包括了几个部分：</p><ol><li>ssh：SSH 客户端实现</li><li>scp, sftp：rcp的替代方案，将文件复制到其他电脑上</li><li>sshd：SSH 服务端实现</li><li>ssh-keygen：产生 RSA 或 ECDSA 密钥，用来认证</li><li>ssh-agent, ssh-add：帮助用户不需要每次输入密钥或密码的工具</li><li>ssh-keysacn：收集大量主机的 ssh 主机公钥</li></ol><h3 id="ssh-keygen"><a href="#ssh-keygen" class="headerlink" title="ssh-keygen"></a>ssh-keygen</h3><p>常用选项：</p><table><thead><tr><th>选项</th><th>含义</th><th>作用</th></tr></thead><tbody><tr><td>-t</td><td>type</td><td>指定要生成的密钥类型</td></tr><tr><td>-C</td><td>comment</td><td>提供一个注释</td></tr><tr><td>-b</td><td>bits</td><td>指定要生成的密钥长度（单位：bit）</td></tr><tr><td>-f</td><td>filename</td><td>指定生成的密钥文件名</td></tr></tbody></table><h3 id="ssh-agent-与-ssh-add"><a href="#ssh-agent-与-ssh-add" class="headerlink" title="ssh-agent 与 ssh-add"></a>ssh-agent 与 ssh-add</h3><p>ssh-agent 是 OpenSSH 开发的用户提供 ssh 代理的工具，它可以为其他需要使用 ssh key 的程序提供代理。</p><p>ssh-add 是用来配合 ssh-agent 的，使用此工具可以向 ssh-agent 中添加私钥。</p><p>可以用过 <code>ssh-add -l</code> 查看已经添加的私钥列表。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://mp.weixin.qq.com/s?src=11&timestamp=1634861787&ver=3389&signature=G8macIgCA48p5IgAfrfsER*RFodNvg2idSyOyBqAP4Rzo4f6dXtBSdcFVsiy-fosKE4P3R9prcfI6IEnrIB6CHpoOJ0LWHInXXAP9*9Yev82BRwT5TRBNIGwRdwKiTxm&new=1" target="_blank" rel="external nofollow noopener noreferrer">松鼠尚学堂：SSH 工作原理</a></li><li><a href="https://blog.csdn.net/qwertyupoiuytr/article/details/71213463" target="_blank" rel="external nofollow noopener noreferrer">运行的风：Linux SSH建立连接过程分析</a></li><li><a href="https://blog.csdn.net/wchrt/article/details/45752933" target="_blank" rel="external nofollow noopener noreferrer">wchrt：ssh秘钥交换详解与实现…</a></li><li><a href="http://yueban.github.io/2020/11/01/Diffie%E2%80%93Hellman%20%E5%AF%86%E9%92%A5%E5%8D%8F%E5%95%86%E7%AE%97%E6%B3%95%E8%AF%A6%E8%A7%A3/" target="_blank" rel="external nofollow noopener noreferrer">月半兄：Diffie–Hellman 密钥协商算法详解</a></li><li><a href="https://soulike.tech/article?id=29" target="_blank" rel="external nofollow noopener noreferrer">Soulike：Diffie-Hellman 密钥交换算法及其安全性</a></li><li><a href="https://www.zhihu.com/question/282877403/answer/433845936" target="_blank" rel="external nofollow noopener noreferrer">知乎车小胖回答：SSH 为什么要用到 DH（Diffie-Hellman Exchange）？</a></li><li><a href="https://blog.csdn.net/liujiakunit/article/details/45849095" target="_blank" rel="external nofollow noopener noreferrer">坤哥玩csdn：关于ssh-keygen命令的介绍与用法</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;h2 id=&quot;SSH-是什么&quot;&gt;&lt;a href=&quot;#SSH-是什么&quot; class=&quot;headerlink&quot; title=&quot;SSH 是什么&quot;&gt;&lt;/
      
    
    </summary>
    
    
      <category term="1-网络笔记" scheme="https://wiki.blanc.site/categories/1-%E7%BD%91%E7%BB%9C%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="ssh" scheme="https://wiki.blanc.site/tags/ssh/"/>
    
  </entry>
  
  <entry>
    <title>Python MRO</title>
    <link href="https://wiki.blanc.site/archives/ffe3a081.html"/>
    <id>https://wiki.blanc.site/archives/ffe3a081.html</id>
    <published>2021-07-25T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p><strong>注：本文中给出的所有案例结果都经过实际代码验证可放心食用。</strong></p><h2 id="什么是-MRO"><a href="#什么是-MRO" class="headerlink" title="什么是 MRO"></a>什么是 MRO</h2><p>方法解析顺序（Method Resolution Order MRO），指的是在多继承编程语言中查找类的某个方法来自哪个基类的搜索顺序。</p><h2 id="Python-MRO-历史"><a href="#Python-MRO-历史" class="headerlink" title="Python MRO 历史"></a>Python MRO 历史</h2><table><thead><tr><th align="center">周期</th><th align="center">类存在形式和对应算法</th></tr></thead><tbody><tr><td align="center">Python 2.1</td><td align="center">经典类 -&gt; DFS</td></tr><tr><td align="center">Python 2.2</td><td align="center">经典类 -&gt; DFS | 新式类 -&gt; BFS</td></tr><tr><td align="center">Python 2.3-2.7</td><td align="center">经典类 -&gt; DFS | 新式类 -&gt; C3</td></tr><tr><td align="center">Python 3</td><td align="center">新式类 -&gt; C3</td></tr></tbody></table><h3 id="经典类和新式类"><a href="#经典类和新式类" class="headerlink" title="经典类和新式类"></a>经典类和新式类</h3><p>在 Python 2.1 及以前，定义一个类的形式如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span>:</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>在 Python 2.2 中引入了一种新的类的定义方式：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span><span class="params">(object)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>为了保持向上兼容，Python 2.2 及以后的版本同时保留这两种定义方式而两种方式产生的类及其实例具有不同的特性，为了区分两种定义方式将 Python 2.1 及以前版本的书写方式产生的类称为经典类（Old-style Class）将 Python 2.2 及以后版本的书写方式产生的类称为新式类（New-style Class）。</p><p>伴随着 Python3 的推出经典类已经被完全废弃，因为在 Python3 中无论以何种方式定义产生的都是新式类。</p><h2 id="DFS-深度优先搜索"><a href="#DFS-深度优先搜索" class="headerlink" title="DFS 深度优先搜索"></a>DFS 深度优先搜索</h2><h3 id="DFS-搜索流程"><a href="#DFS-搜索流程" class="headerlink" title="DFS 搜索流程"></a>DFS 搜索流程</h3><ol><li>检查当前类是否有目标函数，如果有则直接调用，如果没有则进入下一步；</li><li>按照从左至右的方向将当前类的第一个父类赋值为当前类并重复步骤一，若前类无父类则进入下一步；</li><li>向上回溯一层并按照步骤二规定的方向将当前类的下一个父类赋值为当前类并重复步骤一，直至无法继续回溯；</li></ol><h3 id="案例分析"><a href="#案例分析" class="headerlink" title="案例分析"></a>案例分析</h3><p><img src="https://img.blanc.site//wiki/img/20210726105026.png" alt="DFS 案例分析"></p><p>参考 [DFS 搜索流程](#DFS 搜索流程)，搜索顺序为：A -&gt; B -&gt; D -&gt; H -&gt; E -&gt; C -&gt; F -&gt; G</p><h2 id="BFS-广度优先搜索"><a href="#BFS-广度优先搜索" class="headerlink" title="BFS 广度优先搜索"></a>BFS 广度优先搜索</h2><h3 id="BFS-搜索流程"><a href="#BFS-搜索流程" class="headerlink" title="BFS 搜索流程"></a>BFS 搜索流程</h3><ol><li>检查当前类是否有目标函数，如果有则直接调用，如果没有则进入下一步；</li><li>按照从左至右的方向依次检查当前类的一级父类是否有目标函数，如果有则直接调用，否则依旧按照当前方向检查当前类的下一级父类是否有目标函数直至检查完当前类的最高级父类；</li></ol><h3 id="BFS-案例分析"><a href="#BFS-案例分析" class="headerlink" title="BFS 案例分析"></a>BFS 案例分析</h3><p>由于未能在本地下载 Python 2.2 因此无法验证复杂的案例，故这里给出网上反复论证过的案例。</p><p><img src="https://img.blanc.site//wiki/img/20210726112350.png" alt="BFS 案例分析"></p><p>参考 [BFS 搜索流程](#BFS 广度优先搜索)，搜索顺序为：A -&gt; B -&gt; C -&gt; D</p><h2 id="为什么放弃-DFS-和-BFS"><a href="#为什么放弃-DFS-和-BFS" class="headerlink" title="为什么放弃 DFS 和 BFS"></a>为什么放弃 DFS 和 BFS</h2><p>从 [Python MRO 历史](#Python MRO 历史) 可以看出无论是 DFS 还是 BFS 最终都被 C3 算法代替了，原因是 DFS 和 BFS 在处理复杂继承关系时会出现无法满足局部优先或单调性的问题。</p><h3 id="局部优先"><a href="#局部优先" class="headerlink" title="局部优先"></a>局部优先</h3><p>查找过程应该按照子类声明时给定的父类顺序进行查找（从左向右）。</p><h3 id="单调性"><a href="#单调性" class="headerlink" title="单调性"></a>单调性</h3><p>在多继承关系中，假设类 C 的 MRO 结果给出类 A 排在类 B 前面，则在类 C 的所有子类中也需要满足同样的先后顺序；</p><h3 id="参考案例"><a href="#参考案例" class="headerlink" title="参考案例"></a>参考案例</h3><h4 id="DFS-不满足单调性案例"><a href="#DFS-不满足单调性案例" class="headerlink" title="DFS 不满足单调性案例"></a>DFS 不满足单调性案例</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> inspect</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">D</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line">    </span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span><span class="params">(D)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line">    </span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">C</span><span class="params">(D)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span><span class="params">(B, C)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">print(inspect.getmro(A))</span><br><span class="line"><span class="comment"># 以下是结果（被我按行分割了）</span></span><br><span class="line"><span class="comment"># &lt;class __main__.A at 0x10719d3d0&gt;</span></span><br><span class="line"><span class="comment"># &lt;class __main__.B at 0x10717fd00&gt;</span></span><br><span class="line"><span class="comment"># &lt;class __main__.D at 0x10717fc20&gt;</span></span><br><span class="line"><span class="comment"># &lt;class __main__.C at 0x10719d130&gt;</span></span><br><span class="line"></span><br><span class="line">print(inspect.getmro(B))</span><br><span class="line"><span class="comment"># &lt;class __main__.B at 0x10717fd00&gt;</span></span><br><span class="line"><span class="comment"># &lt;class __main__.D at 0x10717fc20&gt;</span></span><br><span class="line"></span><br><span class="line">print(inspect.getmro(C))</span><br><span class="line"><span class="comment"># &lt;class __main__.C at 0x10719d130&gt;</span></span><br><span class="line"><span class="comment"># &lt;class __main__.D at 0x10717fc20&gt;</span></span><br></pre></td></tr></table></figure><p>从结果来看，在类 C 的 MRO 中类 D 在类 C 之后，而在类 A 的 MRO 中类 D 在类 C 之前。</p><h4 id="BFS-不满足局部优先性案例"><a href="#BFS-不满足局部优先性案例" class="headerlink" title="BFS 不满足局部优先性案例"></a>BFS 不满足局部优先性案例</h4><p>由于未能在本地部署 Python 2.2 环境，本案例取自 <a href="https://xubiubiu.com/python-mro/" target="_blank" rel="external nofollow noopener noreferrer">参考文章-1</a>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span><span class="params">(object)</span>:</span> <span class="keyword">pass</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span><span class="params">(A)</span>:</span> <span class="keyword">pass</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">C</span><span class="params">(A, B)</span>:</span> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">print</span> A.__mro__</span><br><span class="line"><span class="comment"># &lt;class '__main__.A'&gt;, &lt;type 'object'&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">print</span> B.__mro__</span><br><span class="line"><span class="comment"># &lt;class '__main__.B'&gt; </span></span><br><span class="line"><span class="comment"># &lt;class '__main__.A'&gt;, &lt;type 'object'&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">print</span> C.__mro__</span><br><span class="line"><span class="comment"># &lt;class '__main__.C'&gt;</span></span><br><span class="line"><span class="comment"># &lt;class '__main__.B'&gt;</span></span><br><span class="line"><span class="comment"># &lt;class '__main__.A'&gt;</span></span><br><span class="line"><span class="comment"># &lt;type 'object'&gt;</span></span><br></pre></td></tr></table></figure><p>从结果来看，类 C 的 MRO 类 B 在类 A 之前，而在类 C 的声明中类 B 在类 A 之后。</p><h3 id="疑惑"><a href="#疑惑" class="headerlink" title="疑惑"></a>疑惑</h3><p>在了解了单调性和局部优先原则以及两个算法的反例之后我又产生了一个疑惑，为什么不满足单调性和局部优先的原则就无法使用，不满足这两个原则所带来的弊端有哪些？</p><p>经过简单的思考和搜索我并没有得出答案，以后如果有机会能够实际解除此类问题我会再回来补充。</p><h2 id="C3-线性化算法"><a href="#C3-线性化算法" class="headerlink" title="C3 线性化算法"></a>C3 线性化算法</h2><h3 id="概念梳理"><a href="#概念梳理" class="headerlink" title="概念梳理"></a>概念梳理</h3><p>假设有类 C 继承自类 B1 到 类 BN，则记类 C 的 MRO 为 L[C]（L 代表 linearization，线性化），假设 L[C] 最终的结果是一个特殊的 Python list；</p><p>对于 L[C] = [B1, B2, B3, … BN]，设 [B1] 为 L[C] 的头部，[B2, B3, …, BN] 为 L[C] 的尾部；</p><p>所有类都会继承 object，且 L[object] = object, 以下 object 简写为 o；</p><h3 id="搜索流程"><a href="#搜索流程" class="headerlink" title="搜索流程"></a>搜索流程</h3><p>定义 L[C(B1, B2, …, BN)] = [C] + merge(L[B1], L[B2], …, L[BN], L[o], [B1, B2, …, BN, o])</p><p>通过定义可知，整个搜索流程即是 merge 方法运算的流程（假设被搜索的被是类 C）：</p><ol><li>merge 会从左至右依次解析参数中的 list：<ol><li>选取 list 中的头部，检查头部是否在 merge 参数中其他列表的尾部中里出现：<ol><li>如果未出现，则将头部代表的类加入到类 C 的 MRO 中并将此头部代表的类从所有列表中删除，此后重复步骤 1.1；</li><li>如果出现，跳出对本 list 的检查重复步骤 1，直至 merge 中所有参数 list 都被检查完毕；</li></ol></li></ol></li><li>如果 merge 的所有参数列表都被清空，则输出 MRO，否则抛出异常。</li></ol><p>注意几个点：</p><ol><li>在步骤 1.1 中检查是否在其他列表的尾部中出现时也会检查公式中 [B1, B2, …, BN, o] 这一部分；</li><li>MRO 也可以看做是一个 list，插入的时候采用的是尾部插入；</li></ol><h3 id="案例分析-1"><a href="#案例分析-1" class="headerlink" title="案例分析"></a>案例分析</h3><h4 id="BFS-和-DFS-失败案例演示"><a href="#BFS-和-DFS-失败案例演示" class="headerlink" title="BFS 和 DFS 失败案例演示"></a>BFS 和 DFS 失败案例演示</h4><p>先来看一下前面给出的两个失败案例在 C3 算法下的输出结果：</p><h5 id="DFS-失败案例在-C3-算法下的输出结果"><a href="#DFS-失败案例在-C3-算法下的输出结果" class="headerlink" title="DFS 失败案例在 C3 算法下的输出结果"></a>DFS 失败案例在 C3 算法下的输出结果</h5><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">D</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line">    </span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span><span class="params">(D)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line">    </span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">C</span><span class="params">(D)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span><span class="params">(B, C)</span>:</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">print(A.__mro__)</span><br><span class="line"><span class="comment"># &lt;class '__main__.A'&gt;</span></span><br><span class="line"><span class="comment"># &lt;class '__main__.B'&gt;</span></span><br><span class="line"><span class="comment"># &lt;class '__main__.C'&gt;</span></span><br><span class="line"><span class="comment"># &lt;class '__main__.D'&gt; &lt;class 'object'&gt;</span></span><br><span class="line"></span><br><span class="line">print(B.__mro__)</span><br><span class="line"><span class="comment"># &lt;class '__main__.B'&gt;</span></span><br><span class="line"><span class="comment"># &lt;class '__main__.D'&gt;, &lt;class 'object'&gt;</span></span><br><span class="line"></span><br><span class="line">print(C.__mro__)</span><br><span class="line"><span class="comment"># &lt;class '__main__.C'&gt;</span></span><br><span class="line"><span class="comment"># &lt;class '__main__.D'&gt;, &lt;class 'object'&gt;</span></span><br></pre></td></tr></table></figure><p>C3 执行结果：<br>A: A -&gt; B -&gt; C -&gt; D -&gt; o<br>B: B -&gt; D -&gt; o<br>C: C -&gt; D -&gt; o</p><p>DFS 执行结果：<br>A: A -&gt; B -&gt; D -&gt; C<br>B: B -&gt; D<br>C: C -&gt; D</p><p>从结果对比来看 C3 算法输出的结果解决了 DFS 算法在此案例中无法满足单调性原则的问题。</p><p>下面我们来看一下 C3 算法是如何输出这样的结果的，重点看类 A 的 MRO 生成过程：</p><p>注：在展示 merge 方法执行流程时使用加粗的 <strong>[]</strong> 代表当前列表，使用被 <code>代码块</code> 包裹的类代表待检测类</p><ol><li>根据公式：L[A] = [A] + merge(L[B], L[C], L[o], [B, C, o])</li><li>L[B] = [B] + merge(L[D], L[o], [D, o])</li><li>L[C] = [C] + merge(L[D], L[o], [D, o])</li><li>L[D] = [D] + merge(L[o], [o]) = [D, o]</li><li>将 L[D] 结果反向带入后:<ol><li>L[B] = [B, D, o]</li><li>L[C] = [C, D, o]</li><li>L[A] = [A] + merge([B, D, o], [C, D, o], [o], [B, C, o])，根据 merge 方法执行流程：<ol><li>L[A] = [A] + merge(<strong>[<code>B</code>, D, o]</strong>, [C, D, o], [o], [B, C, o])</li><li>L[A] = [A, B] + merge(<strong>[<code>D</code>, o]</strong>, [C, D, o], [o], [C, o])</li><li>L[A] = [A, B] + merge([D, o], <strong>[<code>C</code>, D, o]</strong>, [o], [C, o])</li><li>L[A] = [A, B, C] + merge([D, o], <strong>[<code>D</code>, o]</strong>, [o], [o])</li><li>L[A] = [A, B, C, D] + merge([o], <strong>[<code>o</code>]</strong>, [o], [o])</li><li>L[A] = [A, B, C, D, o] + merge([], <strong>[]</strong>, [], [])</li></ol></li></ol></li></ol><h5 id="BFS-失败案例在-C3-算法下的输出结果"><a href="#BFS-失败案例在-C3-算法下的输出结果" class="headerlink" title="BFS 失败案例在 C3 算法下的输出结果"></a>BFS 失败案例在 C3 算法下的输出结果</h5><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span>:</span> <span class="keyword">pass</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span><span class="params">(A)</span>:</span> <span class="keyword">pass</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">C</span><span class="params">(A, B)</span>:</span> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">print(A.__mro__)</span><br><span class="line">print(B.__mro__)</span><br><span class="line">print(C.__mro__)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Traceback (most recent call last):</span></span><br><span class="line"><span class="comment">#   File "test.py", line 3, in &lt;module&gt;</span></span><br><span class="line"><span class="comment">#     class C(A, B): pass</span></span><br><span class="line"><span class="comment"># TypeError: Cannot create a consistent method resolution</span></span><br><span class="line"><span class="comment"># order (MRO) for bases A, B</span></span><br></pre></td></tr></table></figure><p>从结果来看，C3 算法最终会<strong>通过引起异常来维护局部优先原则</strong>。</p><p>下面我们来看一下 C3 算法是如何输出这样的结果的，重点看类 C 的 MRO 生成过程：</p><p>注：在展示 merge 方法执行流程时使用加粗的 <strong>[]</strong> 代表当前列表，使用被 <code>代码块</code> 包裹的类代表待检测类</p><ol><li>根据公式：L[C] = [C] + merge(L[A], L[B], L[o], [A, B, o])</li><li>L[A] = [A] + merge(L[o], [o]) = [A, o]</li><li>L[B] = [B] + merge(L[A], L[o], [A, o])</li><li>将 L[A] 结果反向带入后:<ol><li>L[B] = [B, A, o]</li><li>L[C] = [C] + merge([A, o], [B, A, o], L[o], [A, B, o])，根据 merge 方法执行流程：<ol><li>L[C] = [C] + merge(<strong>[<code>A</code>, o]</strong>, [B, A, o], [o], [A, B, o])</li><li>L[C] = [C] + merge([A, o], <strong>[<code>B</code>, A, o]</strong>, [o], [A, B, o])</li><li>L[C] = [C] + merge([A, o], [B, A, o], <strong>[<code>o</code>]</strong>, [A, B, o])</li><li>L[C] = [C] + merge([A, o], [B, A, o], [o], <strong>[<code>A</code>, B, o]</strong>)</li><li>L[C] = [C] + merge([A, o], [B, A, o], L[o], [A, B, o])</li></ol></li></ol></li></ol><p>直至 merge 检查完所有参数 list，仍然存在非空参数 list，因此 merge 抛出异常。</p><h4 id="复杂案例演示"><a href="#复杂案例演示" class="headerlink" title="复杂案例演示"></a>复杂案例演示</h4><p>这里我们通过 <a href="https://zh.wikipedia.org/wiki/C3%E7%BA%BF%E6%80%A7%E5%8C%96#%E4%BE%8B%E5%AD%90" target="_blank" rel="external nofollow noopener noreferrer">维基百科</a> 给出的复杂案例来进行展示：</p><p><strong>注：由于案例过于复杂，这里就不展示源码了，只展示案例依赖图、最终结果和执行过程；</strong></p><p><img src="https://img.blanc.site//wiki/img/20210726232113.png" alt="复杂案例展示"></p><ol><li>根据公式：L[Z] = [Z] + merge(L[K1], L[K3], L[K2], L[o], [K1, K3, K2, o])</li><li>L[K1] = [K1] + merge(L[C], L[A], L[B], L[o], [C, A, B, o]) = [K1, C, A, B, o]</li><li>L[K2] = [A] + merge(L[B], L[D], L[E], L[o], [B, D, E, o]) = [K2, B, D, E, o]</li><li>L[K3] = [A] + merge(L[A], L[D], L[o], [A, D, o]) = [K3, A, D, o]</li><li>L[A] = [A] + merge(L[o], [o]) = [A, o]</li><li>L[B] = [B] + merge(L[o], [o]) = [B, o]</li><li>L[C] = [C] + merge(L[o], [o]) = [C, o]</li><li>L[D] = [D] + merge(L[o], [o]) = [D, o]</li><li>L[E] = [E] + merge(L[o], [o]) = [E, o]</li><li>将结果反向带入得到 L[Z] = [Z] + merge([K1, C, A, B, o], [K3, A, D, o], [K2, B, D, E, o], [o], [K1, K3, K2, o])，根据 merge 方法执行流程：<ol><li>L[Z] = [Z] + merge(<strong>[<code>K1</code>, C, A, B, o]</strong>, [K3, A, D, o], [K2, B, D, E, o], [o], [K1, K3, K2, o])</li><li>L[Z] = [Z, K1] + merge(<strong>[<code>C</code>, A, B, o]</strong>, [K3, A, D, o], [K2, B, D, E, o], [o], [K3, K2, o])</li><li>L[Z] = [Z, K1, C] + merge(<strong>[<code>A</code>, B, o]</strong>, [K3, A, D, o], [K2, B, D, E, o], [o], [K3, K2, o])</li><li>L[Z] = [Z, K1, C] + merge([A, B, o], <strong>[<code>K3</code>, A, D, o]</strong>, [K2, B, D, E, o], [o], [K3, K2, o])</li><li>L[Z] = [Z, K1, C, K3] + merge([A, B, o], <strong>[<code>A</code>, D, o]</strong>, [K2, B, D, E, o], [o], [K2, o])</li><li>L[Z] = [Z, K1, C, K3, A] + merge([B, o], <strong>[<code>D</code>, o]</strong>, [K2, B, D, E, o], [o], [K2, o])</li><li>L[Z] = [Z, K1, C, K3, A] + merge([B, o], [D, o], <strong>[<code>K2</code>, B, D, E, o]</strong>, [o], [K2, o])</li><li>L[Z] = [Z, K1, C, K3, A, K2] + merge([B, o], [D, o], <strong>[<code>B</code>, D, E, o]</strong>, [o], [o])</li><li>L[Z] = [Z, K1, C, K3, A, K2, B] + merge([o], [D, o], <strong>[<code>D</code>, E, o]</strong>, [o], [o])</li><li>L[Z] = [Z, K1, C, K3, A, K2, B, D] + merge([o], [o], <strong>[<code>E</code>, o]</strong>, [o], [o])</li><li>L[Z] = [Z, K1, C, K3, A, K2, B, D, E] + merge([o], [o], <strong>[<code>o</code>]</strong>, [o], [o])</li><li>L[Z] = [Z, K1, C, K3, A, K2, B, D, E, o] + merge([], [], <strong>[]</strong>, [], [])</li></ol></li></ol><p>最终得到结果为：Z -&gt; K1 -&gt; C -&gt; K3 -&gt; A -&gt; K2 -&gt; B -&gt; D -&gt; E -&gt; o</p><h3 id="小技巧"><a href="#小技巧" class="headerlink" title="小技巧"></a>小技巧</h3><p><img src="https://img.blanc.site//wiki/img/20210726235106.jpeg" alt="小技巧展示"></p><ol><li>在多层级多继承关系中最高能够在第二层通过肉眼快速确定 MRO（本类 + 继承类顺序排列 + object）；</li><li>想要快速确定复杂多继承类的 MRO，应该从第二层开始向上层查找；</li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://xubiubiu.com/python-mro/" target="_blank" rel="external nofollow noopener noreferrer">艽野尘梦：Python 方法解析顺序 MRO</a></li><li><a href="https://www.cnblogs.com/panlq/p/13168195.html" target="_blank" rel="external nofollow noopener noreferrer">JonPan：Python的多继承问题-MRO和C3算法</a></li><li><a href="http://kaiyuan.me/2016/04/27/C3_linearization/" target="_blank" rel="external nofollow noopener noreferrer">YuanBao：C3 线性化算法与 MRO</a></li><li><a href="https://lotabout.me/2020/C3-Algorithm/" target="_blank" rel="external nofollow noopener noreferrer">三点水：C3 算法-Python 多继承的内部原理</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;注：本文中给出的所有案例结果都经过实际代码验证可放心食用。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;什么是-MRO&quot;&gt;&lt;a
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Python" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Python/"/>
    
    
      <category term="Python" scheme="https://wiki.blanc.site/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>Shell 基础语法</title>
    <link href="https://wiki.blanc.site/archives/3beb222a.html"/>
    <id>https://wiki.blanc.site/archives/3beb222a.html</id>
    <published>2021-05-09T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文只是对 shell 脚本语言中一些常用的基础语法进行汇总整理，如果你真的想系统的学习 shell 脚本编程，这里推荐两本电子读物：</p><ol><li><a href="http://billie66.github.io/TLCL/index.html" target="_blank" rel="external nofollow noopener noreferrer">The Linux Command Line-中文版</a></li><li><a href="https://linuxstory.gitbook.io/advanced-bash-scripting-guide-in-chinese/" target="_blank" rel="external nofollow noopener noreferrer">Advanced Bash-Scripting Guide-中文版</a></li></ol><p>第一本相对来说更加平滑，适合零基础的人进行自学；<br>第二本相对来说更加全面，适合有一点基础的人进行自学；</p><p>另外，强烈建议你在学习了一定 shell 语法基础之后去找一个比较完善的编码规范进行阅读并严格按照规范进行脚本编辑，这里我个人推荐 <a href="https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/contents/" target="_blank" rel="external nofollow noopener noreferrer">Google Shell 风格指南</a>。</p><p>无论是大的项目脚本还是小的工具脚本，严格的按照一个成熟的编码规范进行编辑能够帮助我们（在前期）更好的规划脚本以及（在后期）更快的 DEBUG。</p><p><strong>注：本文中所有测试代码均为 zsh 输出结果</strong><br><strong>注2：本文中所有测试代码均以 <a href="https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/contents/" target="_blank" rel="external nofollow noopener noreferrer">Google Shell 风格指南</a> 作为编码规范</strong></p><h2 id="变量相关"><a href="#变量相关" class="headerlink" title="变量相关"></a>变量相关</h2><h3 id="变量命名"><a href="#变量命名" class="headerlink" title="变量命名"></a>变量命名</h3><ol><li>定义变量时不需要加美元符号（$）；</li><li>变量名和等号之间不能有空格（分隔符）；</li><li>命名只能使用英文、数字和下划线且首个字符不能是数字；</li><li>不能使用关键字，可以使用 <code>bash -c help</code> 命令查看关键字；</li></ol><h3 id="使用变量"><a href="#使用变量" class="headerlink" title="使用变量"></a>使用变量</h3><p>使用一个已经定义的变量只需要在变量名前加美元符号（$）即可，变量名两边的花括号（{}）可加可不加。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">test_name="test"</span><br><span class="line">echo $test_name</span><br><span class="line">echo $&#123;test_name&#125;</span><br></pre></td></tr></table></figure><p>注意无论您是否选择在变量名两边加入花括号请保持上下文编码规范的一致性。</p><h3 id="只读变量"><a href="#只读变量" class="headerlink" title="只读变量"></a>只读变量</h3><p>使用 readonly 命令可以将变量定义为只读变量，只读变量的值不能被改变。</p><h3 id="删除变量"><a href="#删除变量" class="headerlink" title="删除变量"></a>删除变量</h3><p>可以使用 unset 命令删除变量，被删除的变量不能再次使用，unset 不能删除只读变量。</p><h3 id="变量类型"><a href="#变量类型" class="headerlink" title="变量类型"></a>变量类型</h3><p>shell 中存在三类变量：</p><ol><li>局部变量：在脚本或命令中定义的变量，仅在当前 shell 实例中有效，其他 shell 启动的程序不能访问局部变量；</li><li>环境变量：所有程序（包括 shell 启动的程序）都能访问的变量，有些程序需要环境变量来保证正常运行；</li><li>shell 变量：由 shell 程序设定的特殊变量，有一部分是环境变量、一部分是局部变量，这些变量保证了 shell 程序的正常运行；</li></ol><p><img src="https://img.blanc.site//wiki/img/20210514012900.jpg" alt="shell 环境变量-引用自知乎@小晶"></p><h2 id="数组相关"><a href="#数组相关" class="headerlink" title="数组相关"></a>数组相关</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 定义格式：array_name=(value1 value2 ... valuen)</span></span><br><span class="line">a=1</span><br><span class="line">my_array=("test" 2 "abc" $&#123;a&#125;)</span><br><span class="line">echo $&#123;my_array[0]&#125; # test</span><br><span class="line">echo $&#123;my_array[1]&#125; # 2</span><br><span class="line">echo $&#123;my_array[2]&#125; # abc</span><br><span class="line">echo $&#123;my_array[3]&#125; # 1</span><br><span class="line">echo $&#123;my_array[*]&#125; # test 2 abc 1 输出数组全部元素</span><br><span class="line">echo $&#123;my_array[@]&#125; # test 2 abc 1 输出数组全部元素</span><br><span class="line">echo $&#123;#my_array&#125;   # 4 输出数组长度</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 遍历数组</span></span><br><span class="line">for i in $&#123;my_array[*]&#125;; do</span><br><span class="line">  echo $&#123;i&#125;</span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">for i in $&#123;my_array[@]&#125;; do</span><br><span class="line">  echo $&#123;i&#125;</span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">for (( i = 0; i &lt; $&#123;#my_array&#125;; i++ )) do</span><br><span class="line">  echo $&#123;my_array[i]&#125;</span><br><span class="line">done</span><br></pre></td></tr></table></figure><p><strong>注：这里仅罗列了集中常见用法，如果想要了解更多数组操作可以查看 <a href="https://note.yuchaoshui.com/blog/post/yuziyue/shell%E6%95%B0%E7%BB%84%E6%80%BB%E7%BB%93" target="_blank" rel="external nofollow noopener noreferrer">余子越：shell数组与字典总结</a>；</strong><br><strong>注2：该博文给出的特性本人并未全部测试，请先测试确定有效后再使用；</strong></p><h2 id="字典相关"><a href="#字典相关" class="headerlink" title="字典相关"></a>字典相关</h2><p><strong>本地测试失败，暂不整理。</strong></p><h2 id="字符串相关"><a href="#字符串相关" class="headerlink" title="字符串相关"></a>字符串相关</h2><h3 id="字符串变量定义时的单引号和双引号"><a href="#字符串变量定义时的单引号和双引号" class="headerlink" title="字符串变量定义时的单引号和双引号"></a>字符串变量定义时的单引号和双引号</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">first_name="test"</span><br><span class="line">last_name1="test1 $&#123;first_name&#125;"</span><br><span class="line">last_name2='test1 $&#123;first_name&#125;'</span><br><span class="line"></span><br><span class="line">echo $&#123;last_name1&#125;  # test1 test</span><br><span class="line">echo $&#123;last_name2&#125;  # test1 $&#123;first_name&#125;</span><br></pre></td></tr></table></figure><p>在定义 shell 字符串变量时如果使用单引号包裹变量内容，则被包裹的部分会原样输出，如果使用双引号包裹变量内容，则在输出变量内容时会先解析变量内的变量、命令和转义字符。</p><h3 id="字符串拼接"><a href="#字符串拼接" class="headerlink" title="字符串拼接"></a>字符串拼接</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">your_name="test"</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 单引号拼接</span></span><br><span class="line">welcome1='hello, $&#123;your_name&#125; !'</span><br><span class="line">welcome2='hello, '$&#123;your_name&#125;' !'</span><br><span class="line">echo $&#123;welcome1&#125; # hello, $&#123;your_name&#125; !</span><br><span class="line">echo $&#123;welcome2&#125; # hello, test !</span><br><span class="line"><span class="meta">#</span><span class="bash"> 双引号拼接</span></span><br><span class="line">welcome3="hello, $&#123;your_name&#125; !"</span><br><span class="line">welcome4="hello, "$&#123;your_name&#125;" !"</span><br><span class="line">echo $&#123;welcome3&#125; # hello, test !</span><br><span class="line">echo $&#123;welcome4&#125; # hello, test !</span><br></pre></td></tr></table></figure><p>以上句式中：<br>welcome1 属于单引号字符串无法解析变量，<br>welcome2 属于字符串拼接，<br>welcome3 属于双引号解析字符串变量，<br>welcome4 属于字符串拼接。</p><h3 id="字符串操作"><a href="#字符串操作" class="headerlink" title="字符串操作"></a>字符串操作</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">string="abcdefghijklmn"</span><br><span class="line">string2="这是一段中文字符串"</span><br><span class="line"><span class="meta">#</span><span class="bash"> 输出字符串长度</span></span><br><span class="line">echo $&#123;#string&#125;     # 14</span><br><span class="line">echo $&#123;#string1&#125;    # 0</span><br><span class="line"><span class="meta">#</span><span class="bash"> 截取字符串，从下表 1 截取到下表 4</span></span><br><span class="line">echo $&#123;string:1:4&#125;  # bcde</span><br><span class="line">echo $&#123;string1:1:4&#125; # 无输出</span><br><span class="line">echo $&#123;string1&#125;     # 无输出</span><br><span class="line"><span class="meta">#</span><span class="bash"> 在我的测试系统环境下无法对中文字符串进行操作</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 中文字符串操作具体逻辑我没有详细查询，感兴趣可以自己查询一下，估计是字符集的问题</span></span><br></pre></td></tr></table></figure><h2 id="基础语法相关"><a href="#基础语法相关" class="headerlink" title="基础语法相关"></a>基础语法相关</h2><h3 id="特殊变量"><a href="#特殊变量" class="headerlink" title="特殊变量"></a>特殊变量</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> <span class="variable">$&#123;n&#125;</span>：<span class="variable">$0</span> 表示命令本身，<span class="variable">$1</span>-<span class="variable">$9</span> 代表第 1 到第 9 个参数，10 以上加花括号，个人建议全部加花括号</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> <span class="variable">$&#123;*&#125;</span>：运行当前脚本时的所有参数，且把所有参数看做一个整体</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> <span class="variable">$&#123;@&#125;</span>：运行当前脚本时的所有参数，且把每个参数区别对待</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> <span class="variable">$&#123;#&#125;</span>：运行当前脚本时的所有参数个数</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> <span class="variable">$&#123;$&#125;</span>：当前进程 PID</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> <span class="variable">$&#123;!&#125;</span>：在当前脚本中后台运行的最后一个进程 PID</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> <span class="variable">$&#123;?&#125;</span>：最后一次执行的命令返回状态，0 为执行正确，非 0 为执行错误</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 测试文件名称：test.sh</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 测试命令 ./test.sh 1 2 3 4 5 6 7 8 9</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 同目录下有一个输出 hello, world 的 hello.sh 文件</span></span><br><span class="line"></span><br><span class="line">echo $(dirname $&#123;0&#125;) # .</span><br><span class="line">echo $&#123;0&#125;            # ./test.sh</span><br><span class="line">echo $&#123;1&#125;            # 1</span><br><span class="line">echo $&#123;2&#125;            # 2</span><br><span class="line">echo $&#123;6&#125;            # 6</span><br><span class="line">echo $&#123;99&#125;           # 无输出</span><br><span class="line">echo $&#123;*&#125;            # 1 2 3 4 5 6 7 8 9</span><br><span class="line">echo $&#123;@&#125;            # 1 2 3 4 5 6 7 8 9</span><br><span class="line">echo $&#123;#&#125;            # 9</span><br><span class="line">echo $&#123;$&#125;            # 70149</span><br><span class="line">./hello.sh           # 注意：这里不会立即输出结果</span><br><span class="line">echo $&#123;!&#125;            # 70151</span><br><span class="line">echo $&#123;?&#125;            # 0</span><br><span class="line">                     # hello.sh 的输出结果 -&gt; hello, world</span><br></pre></td></tr></table></figure><h3 id="特殊字符"><a href="#特殊字符" class="headerlink" title="特殊字符"></a>特殊字符</h3><h4 id="井号"><a href="#井号" class="headerlink" title="# | 井号"></a># | 井号</h4><p>注释符。如果一行脚本的开头是#（除了#!），那么代表这一行是注释，不会被执行。</p><h4 id="分号"><a href="#分号" class="headerlink" title="; | 分号"></a>; | 分号</h4><p>命令分隔符。允许在同一行内放置两条或更多的命令。</p><h4 id="冒号"><a href="#冒号" class="headerlink" title=": | 冒号"></a>: | 冒号</h4><p>空命令。它在 shell 中等价于 “NOP”（即no op，空操作）与 shell 内建命令 true 有同样的效果。它本身也是 Bash 的内建命令之一，返回值是 true（0）。</p><p>如果你学过 Python 的话，它很像 Python 中的 pass（个人感觉）。</p><h4 id="单组小括号"><a href="#单组小括号" class="headerlink" title="() | 单组小括号"></a>() | 单组小括号</h4><p>在<strong>新的子 shell （环境）里</strong>执行使用分号（;）隔开的一组命令，且最后一个命令<strong>可以不用分号</strong>。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 测试文件路径 /root/test.sh</span></span><br><span class="line"></span><br><span class="line">pwd            # /root</span><br><span class="line">(cd /etc;pwd)  # /etc</span><br><span class="line">pwd            # /root</span><br></pre></td></tr></table></figure><h4 id="花括号"><a href="#花括号" class="headerlink" title="{} | 花括号"></a>{} | 花括号</h4><p>在<strong>当前 shell （环境）里</strong>执行使用分号（;）隔开的一组命令，<strong>最后一个命令也需要加分号</strong>，且<strong>左括号和第一个命令之间必须有空格（分隔符）</strong>。</p><blockquote><p>代码块，又被称作内联组（inline group）。<br>它实际上创建了一个匿名函数（anonymous function），即没有名字的函数。<br>但是，不同于那些“标准”函数，代码块内的变量在脚本的其他部分仍旧是可见的。</p><p><a href="https://linuxstory.gitbook.io/advanced-bash-scripting-guide-in-chinese/" target="_blank" rel="external nofollow noopener noreferrer">Advanced Bash-Scripting Guide-中文版</a></p></blockquote><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 测试文件路径 /root/test.sh</span></span><br><span class="line"></span><br><span class="line">pwd                # /root</span><br><span class="line">&#123; cd /etc; pwd; &#125;  # /etc</span><br><span class="line">pwd                # /etc</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 注意 &#123; <span class="built_in">cd</span> /etc; <span class="built_in">pwd</span>; &#125; 命令中，除了 &#123; 和 c 之间 以及 <span class="built_in">cd</span> 和 / 之间的空格是必须的其他空格都可省略</span></span><br><span class="line">``` </span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"><span class="comment">### $() 和 ``</span></span></span><br><span class="line"></span><br><span class="line">二者都可用于「命令替换」，所谓命令替换与前面变量章节提到的变量替换类似，即完成区域内部的命令并将结果替换出来和当前命令行重组。</span><br><span class="line"></span><br><span class="line">```shell</span><br><span class="line">echo "today is $(date +%Y-%m-%d)" # today is 2021-05-15</span><br><span class="line">echo "today is `date +%Y-%m-%d`"  # today is 2021-05-15</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 执行逻辑是：先运行命令 date +%Y-%m-%d 然后将结果和原先命令所在的位置进行替换，程序执行时实际运行的是替换后的语句</span></span><br></pre></td></tr></table></figure><p>比较推荐使用 $() 这种形式，理由有：</p><ol><li>反引号（``）容易与单引号（’’）混淆；</li><li>在复合命令替换中，使用反引号（``）需要进行额外的转义</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">a=`command1 \`command2\` `</span><br><span class="line">b=$(command1 $(command2))</span><br></pre></td></tr></table></figure><p>但是 $() 不能支持全部 shell（但 bash 中是可以用的），而反引号（``）基本上可以在所有 unix shell 中使用。</p><h4 id="和-双组小括号"><a href="#和-双组小括号" class="headerlink" title="(()) 和 $(()) | 双组小括号"></a>(()) 和 $(()) | 双组小括号</h4><p>(()) 的特性：与 let 命令类似，允许对算术表达式的扩展和求值，是 let 命令的简化形式。</p><p>(()) 单独使用时的一些作用：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">a=3</span><br><span class="line">(( b = 10 + a ))  # C 语言风格变量赋值， = 前后都有空格</span><br><span class="line">echo $&#123;b&#125;         # 13</span><br><span class="line">(( b-- ))</span><br><span class="line">echo $&#123;b&#125;         # 12</span><br><span class="line">(( b++ ))</span><br><span class="line">echo $&#123;b&#125;         # 13</span><br><span class="line">(( ++b ))</span><br><span class="line">echo $&#123;b&#125;         # 14</span><br><span class="line">(( --b ))</span><br><span class="line">echo $&#123;b&#125;         # 13</span><br><span class="line"></span><br><span class="line">(( t = b &lt; 45 ? 7 : 11 ))</span><br><span class="line"></span><br><span class="line">echo $&#123;t&#125;         # 7</span><br></pre></td></tr></table></figure><p>(()) 配合其他语句使用时的一些例子：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">for (( i = 0; i &lt; 5; i++)); do</span><br><span class="line">  echo $&#123;i&#125;    # 依次输出 0 1 2 3 4</span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">if (( 1 + 2 )); then</span><br><span class="line">  echo "true"  # true</span><br><span class="line">fi</span><br><span class="line"></span><br><span class="line">if !(( 1 - 1 )); then</span><br><span class="line">  echo "false" # false</span><br><span class="line">fi</span><br><span class="line"></span><br><span class="line">if (( 3 &gt; 2 )); then</span><br><span class="line">  echo "3 &gt; 2" # 3 &gt; 2 在 if 语句下可以使用双圆括号直接进行数值比较</span><br><span class="line">fi</span><br></pre></td></tr></table></figure><p>$(()) 的作用：整数运算（不支持浮点数）</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> $(()) 中支持 + - * / % &amp; | ^ !</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 此外，$(()) 还可用作不同进制的运算，但只能输出十进制结果</span></span><br><span class="line">a=1</span><br><span class="line">b=2</span><br><span class="line">c=3</span><br><span class="line">echo $(( a + b ))         # 3</span><br><span class="line">echo $(( c - a ))         # 2</span><br><span class="line">echo $(( a * c ))         # 3</span><br><span class="line">echo $(( c / a ))         # 3</span><br><span class="line">echo $(( c / b ))         # 1 可见这里的取整方式是直接抛弃小数点后面的数</span><br><span class="line">echo $(( c % a ))         # 0</span><br><span class="line">echo $(( (a + b) * c ))   # 9</span><br><span class="line">echo $(( a &amp; b ))         # 0</span><br><span class="line">echo $(( a | b ))         # 3</span><br><span class="line">echo $(( a ^ b ))         # 3</span><br><span class="line">echo $(( !a ))            # 0</span><br><span class="line">echo $(( 16#2a ))         # 42 这里是将 16 进制的 2a 转化成了十进制的 42</span><br></pre></td></tr></table></figure><h4 id="单组中括号"><a href="#单组中括号" class="headerlink" title="[] | 单组中括号"></a>[] | 单组中括号</h4><p>在 shell 中 <code>[ expr ]</code> <strong>（注意：左右中括号和命令之间都有空格）</strong>等同于命令 <code>test expr</code>，主要用于：数值判断、文件判断和字符串判断。</p><p>test 和 [] 中可用的比较运算符只有 = 和 !=，要比较大小则只能使用 <code>test 3 -eq 4</code> 或 <code>[ 3 -eq 4 ]</code> 这种形式</p><ol><li>数值判断</li></ol><table><thead><tr><th align="center">参数</th><th align="center">功能</th></tr></thead><tbody><tr><td align="center">A -eq B</td><td align="center">判断 A 是否等于 B</td></tr><tr><td align="center">A -ne B</td><td align="center">判断 A 是否不等于 B</td></tr><tr><td align="center">A -gt B</td><td align="center">判断 A 是否大于 B</td></tr><tr><td align="center">A -ge B</td><td align="center">判断 A 是否大于等于 B</td></tr><tr><td align="center">A -lt B</td><td align="center">判断 A 是否小于 B</td></tr><tr><td align="center">A -le B</td><td align="center">判断 A 是否小于等于 B</td></tr></tbody></table><p><strong>注：A 和 B 为任意数值或数值变量</strong></p><ol start="2"><li>文件判断</li></ol><table><thead><tr><th align="center">参数</th><th align="center">功能</th></tr></thead><tbody><tr><td align="center">-e filename</td><td align="center">判断文件是否存在</td></tr><tr><td align="center">-r filename</td><td align="center">判断文件是否可读</td></tr><tr><td align="center">-w filename</td><td align="center">判断文件是否可写</td></tr><tr><td align="center">-x filename</td><td align="center">判断文件是否可执行</td></tr><tr><td align="center">-s filename</td><td align="center">判断文件是否存在且至少有一个字符</td></tr><tr><td align="center">-d filename</td><td align="center">判断文件是否存在且为目录文件</td></tr><tr><td align="center">-f filename</td><td align="center">判断文件是否存在且为普通文件</td></tr><tr><td align="center">-c filename</td><td align="center">判断文件是否存在且为字符型特殊文件</td></tr><tr><td align="center">-b filename</td><td align="center">判断文件是否存在且为块特殊文件</td></tr></tbody></table><p><strong>注：filename 为完整（但可以是相对也可以是绝对）文件路径或文件路径变量</strong></p><ol><li>字符串判断</li></ol><table><thead><tr><th align="center">参数</th><th align="center">功能</th></tr></thead><tbody><tr><td align="center">stringA = stringB</td><td align="center">判断 stringA 是否等于 stringB</td></tr><tr><td align="center">stringA == stringB</td><td align="center">判断 stringA 是否等于 stringB</td></tr><tr><td align="center">stringA != stringB</td><td align="center">判断 stringA 是否不等于 stringB</td></tr><tr><td align="center">-z stringA</td><td align="center">判断 stringA 长度是否为零</td></tr><tr><td align="center">-n stringA</td><td align="center">判断 stringA 长度是否不为零</td></tr></tbody></table><p><strong>注：stringA 和 stringB 代表任意字符串或字符串变量</strong></p><h4 id="双组中括号"><a href="#双组中括号" class="headerlink" title="[[]] | 双组中括号"></a>[[]] | 双组中括号</h4><p>[[]] 并非 shell 通用关键字，但大多数常用 shell 都支持，与 [] 相比，[[]] 更加常用。</p><p>[[]] 支持字符串的模式匹配，使用 == 操作符时还支持 shell 的正则表达式，字符串在比较的时候可以将等号右边的内容当做一个正则表达式的匹配模式，而不仅仅是一个字符串，例如 <code>[[ hello == hell? ]]</code> 的结果为 true，[[]] 中匹配字符串或通配符不需要引号。</p><p>在 if 语句中使用 [[]] 可以直接在判断语句中使用 &amp;&amp; || 等操作符，但在 [] 中会报错，例如 <code>if [[ ${a} == 1 &amp;&amp; ${a} != 2 ]]</code>，如果使用 [] 则需写成 <code>if [ ${a} -ne 1 ] &amp;&amp; [ ${a} != 2 ]</code> 或者 <code>if [ ${a} -ne 1 -a ${a} != 2]</code>。</p><p><strong>注：在 [[]] 中使用 &lt; &gt; 是基于字符串进行判断的，例如 <code>[[ 321 &gt; 1234 ]]</code> 输出为真，因为按照字符串来判断，3 大于 1</strong></p><p>在支持 [[]] 的 shell 中会把它内部的表达式当做一个单独的元素，并返回一个退出状态码（0 为真，1 为假）。 </p><h3 id="用户输入"><a href="#用户输入" class="headerlink" title="用户输入"></a>用户输入</h3><h4 id="基本读取"><a href="#基本读取" class="headerlink" title="基本读取"></a>基本读取</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash">!/bin/bash</span></span><br><span class="line">echo -n "Enter your name: " # -n 的作用是不换行</span><br><span class="line">read name</span><br><span class="line">echo "hello $&#123;name&#125;, welcome to my program"</span><br><span class="line">exit 0</span><br></pre></td></tr></table></figure><h4 id="read-参数"><a href="#read-参数" class="headerlink" title="read 参数"></a>read 参数</h4><table><thead><tr><th align="center">参数</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">-a</td><td align="center">后面跟一个变量，该变量会被认为是一个数组，通过 read 给其赋值，默认以空格为分隔符</td></tr><tr><td align="center">-d</td><td align="center">后面跟一个标志符，作为结束的标志</td></tr><tr><td align="center">-p</td><td align="center">后面根提示信息，在输入前打印提示信息</td></tr><tr><td align="center">-e</td><td align="center">输入的时候打开自动补全功能</td></tr><tr><td align="center">-n</td><td align="center">后面跟一个数字，定义输入文本长度</td></tr><tr><td align="center">-r</td><td align="center">屏蔽 \，如果没有该选项 \ 会被认为是转义符，如果有的话会被认为是正常字符</td></tr><tr><td align="center">-s</td><td align="center">安静模式，输入字符时不在屏幕上显示，常用于密码输入</td></tr><tr><td align="center">-t</td><td align="center">后面跟秒数，定义输入字符的等待时间</td></tr><tr><td align="center">-u</td><td align="center">后面跟 fd，从文件描述符中读入</td></tr></tbody></table><h2 id="函数语法相关"><a href="#函数语法相关" class="headerlink" title="函数语法相关"></a>函数语法相关</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 基础语法格式</span></span><br><span class="line">function function_name() &#123;</span><br><span class="line">    command(s)...</span><br><span class="line">    [ return value ] # 用 [] 框起来表示此语句可选</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 函数返回值，可以显式增加 <span class="built_in">return</span> 语句；</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 如果不加，会将最后一条命令运行结果作为返回值。</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 函数在定义时无需在 function_name() 的 () 中写入具体参数，而是通过 <span class="variable">$&#123;1&#125;</span> <span class="variable">$&#123;2&#125;</span> 的形式对参数进行调用</span></span><br><span class="line"></span><br><span class="line">name param1 param2 param3</span><br></pre></td></tr></table></figure><h2 id="流程控制相关"><a href="#流程控制相关" class="headerlink" title="流程控制相关"></a>流程控制相关</h2><h3 id="if-语句"><a href="#if-语句" class="headerlink" title="if 语句"></a>if 语句</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 基本语法格式</span></span><br><span class="line">if [ condition ]; then   </span><br><span class="line">  command(s)...</span><br><span class="line">elif [ condition ]; then </span><br><span class="line">  command(s)...</span><br><span class="line">else</span><br><span class="line">  command(s)...</span><br><span class="line">fi</span><br></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">if [ $&#123;1&#125; -ge 60 ]; then</span><br><span class="line">  echo "及格"</span><br><span class="line">elif [ $&#123;1&#125; -lt 60 ]; then</span><br><span class="line">  echo "不及格" </span><br><span class="line">fi</span><br></pre></td></tr></table></figure><h3 id="case-语句"><a href="#case-语句" class="headerlink" title="case 语句"></a>case 语句</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 基本语法格式</span></span><br><span class="line">case $&#123;variable&#125; in</span><br><span class="line">  "condition1")</span><br><span class="line">    command...</span><br><span class="line">    ;;</span><br><span class="line">  "condition1")</span><br><span class="line">    command...</span><br><span class="line">    ;;</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">  *)</span><br><span class="line">    command...</span><br><span class="line">    ;;</span><br><span class="line">esac</span><br></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 实例</span></span><br><span class="line">case $&#123;1&#125; in</span><br><span class="line">  "1")</span><br><span class="line">    echo 周一</span><br><span class="line">    ;;</span><br><span class="line">  "2")</span><br><span class="line">    echo 周二</span><br><span class="line">    ;;</span><br><span class="line">  *)</span><br><span class="line">    echo 其它</span><br><span class="line">    ;;</span><br><span class="line">esac</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 简写</span></span><br><span class="line">case $&#123;1&#125; in</span><br><span class="line">  "1") echo 周一 ;;</span><br><span class="line">  "2") echo 周二 ;;</span><br><span class="line">  *) echo 其它 ;;</span><br><span class="line">esac</span><br></pre></td></tr></table></figure><h2 id="循环语法相关"><a href="#循环语法相关" class="headerlink" title="循环语法相关"></a>循环语法相关</h2><p>循环是当循环控制条件为真时，一系列命令迭代执行的代码块。</p><h3 id="for-循环"><a href="#for-循环" class="headerlink" title="for 循环"></a>for 循环</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">my_array=("test" 2 "abc" $&#123;a&#125;)</span><br><span class="line"></span><br><span class="line">for i in $&#123;my_array[*]&#125;; do</span><br><span class="line">  echo $&#123;i&#125;</span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">for (( i = 0; i &lt; $&#123;#my_array&#125;; i++ )) do</span><br><span class="line">  echo $&#123;my_array[i]&#125;</span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">for i in "test" 2 "abc" $&#123;a&#125;; do</span><br><span class="line">  echo $&#123;i&#125;</span><br><span class="line">done</span><br></pre></td></tr></table></figure><h3 id="while-循环"><a href="#while-循环" class="headerlink" title="while 循环"></a>while 循环</h3><p>while 循环结构会在循环顶部检测循环条件，若循环条件为真（退出状态为 0）则循环持续进行。<br>与 for 循环 不同的是，while 循环是在不知道循环次数的情况下使用的。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">sum=0</span><br><span class="line">i=0</span><br><span class="line"></span><br><span class="line">while [[ $&#123;i&#125; -le 100 ]]; do</span><br><span class="line">  sum=$(( sum + i ))</span><br><span class="line">  (( i++ ))</span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">echo $&#123;sum&#125; # 5050</span><br></pre></td></tr></table></figure><h3 id="util-循环"><a href="#util-循环" class="headerlink" title="util 循环"></a>util 循环</h3><p>与 while 循环相反，until 循环测试其顶部的循环条件，直到其中的条件为真时停止。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">sum=0</span><br><span class="line">i=0</span><br><span class="line"></span><br><span class="line">until [[ $&#123;i&#125; -gt 100 ]]; do</span><br><span class="line">  sum=$(( sum + i ))</span><br><span class="line">  (( i++ ))</span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">echo $&#123;sum&#125; # 5050</span><br></pre></td></tr></table></figure><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>本来就是想简单的整理一下 shell 的常用基础语法，没想到越整理接触到的概念越多，导致我要往这篇文章里塞入的东西也越来越多，当你读完以上文章内容，不过是接触到了 shell 语法的冰山一角。它还有很多基础语法是我没有整理到位的，也有很多进阶应用是这篇文章不应涉及但却十分重要的，毕竟 shell 非常接近 Unix 系统内核，因此如果你想要系统的学习 shell 还请根据自身情况参考我在 <a href="#前言">前言</a> 中给出的书籍进行学习。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://www.runoob.com/linux/linux-shell-variable.html" target="_blank" rel="external nofollow noopener noreferrer">菜鸟教程：Shell 变量</a></li><li><a href="https://www.runoob.com/linux/linux-shell-test.html" target="_blank" rel="external nofollow noopener noreferrer">菜鸟教程：Shell test 命令</a></li><li><a href="https://www.runoob.com/linux/linux-comm-read.html" target="_blank" rel="external nofollow noopener noreferrer">菜鸟教程：Linux read 命令</a></li><li><a href="https://wiki.jikexueyuan.com/project/13-questions-of-shell/" target="_blank" rel="external nofollow noopener noreferrer">Zhongbo Wang：Shell 十三问</a></li><li><a href="https://www.cnblogs.com/hkui/p/6423918.html" target="_blank" rel="external nofollow noopener noreferrer">梦想远航：shell中的(),{}几种语法用法</a></li><li><a href="https://blog.csdn.net/yjk13703623757/article/details/79028738" target="_blank" rel="external nofollow noopener noreferrer">Locutus：Shell编程中的用户输入处理…</a></li><li><a href="https://zhuanlan.zhihu.com/p/102176365" target="_blank" rel="external nofollow noopener noreferrer">知乎-小晶：掌握Shell编程，一篇就够了</a></li><li><a href="https://linuxstory.gitbook.io/advanced-bash-scripting-guide-in-chinese/" target="_blank" rel="external nofollow noopener noreferrer">Advanced Bash-Scripting Guide-中文版</a></li><li><a href="https://www.cnblogs.com/chengd/p/7803664.html" target="_blank" rel="external nofollow noopener noreferrer">chengd：Linux—shell中$(( ))、$( )、``与${ }的区别</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;本文只是对 shell 脚本语言中一些常用的基础语法进行汇总整理，如果你真的想系统的学习 shell 脚本编程，这里推荐两本电子读物：&lt;/p
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Shell" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Shell/"/>
    
    
      <category term="Shell" scheme="https://wiki.blanc.site/tags/Shell/"/>
    
  </entry>
  
  <entry>
    <title>8086 处理器寻址方式</title>
    <link href="https://wiki.blanc.site/archives/4940711f.html"/>
    <id>https://wiki.blanc.site/archives/4940711f.html</id>
    <published>2021-02-14T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文主要描述了何为寻址，寻址能力的计算以及 8086 处理器的寻址方式有哪些。</p><h2 id="寻址"><a href="#寻址" class="headerlink" title="寻址"></a>寻址</h2><h3 id="什么是寻址？"><a href="#什么是寻址？" class="headerlink" title="什么是寻址？"></a>什么是寻址？</h3><p>内存中每一个字节（8bit）都有一个对应的内存地址，CPU 去访问某一具体内存地址的过程称为寻址。</p><h3 id="寻址能力与什么有关？"><a href="#寻址能力与什么有关？" class="headerlink" title="寻址能力与什么有关？"></a>寻址能力与什么有关？</h3><p>CPU 的寻址能力一般使用寻址空间来表示，寻址空间的大小决定了 CPU 可支持的最大内存容量，以字节为单位。寻址空间的大小由地址总线的地址寄存器宽度（位数）决定，假设地址总线位数为 N 位，则寻址空间为 2 的 N 次方字节（因为计算机使用的是二进制所以是 2 的 N 次方）。</p><h3 id="特殊的-8086-处理器"><a href="#特殊的-8086-处理器" class="headerlink" title="特殊的 8086 处理器"></a>特殊的 8086 处理器</h3><p>8086 处理器有 20 位地址总线，可传送 20 位的地址，寻址空间为 1M。</p><p>而 8086 处理器是 16 位结构的处理器即 8086 内部的寄存器位数为 16 位，如果按照这个数据处理能力 8086 只能发送出 16 位的地址，表现出的寻址能力只有 64 KB。</p><p>为了解决上述问题，在 8086 处理器内部采用了一种使用两个 16 位地址（段地址：偏移地址）合成一个 20 位物理地址的方案。</p><p>具体计算公式为：物理地址 = 段地址左移四位 + 偏移地址。</p><h2 id="指令"><a href="#指令" class="headerlink" title="指令"></a>指令</h2><p>在汇编语言中，一般的指令格式为：指令代码 目的操作数,源操作数。</p><p>目的操作数和源操作数统称为操作数，而寻址方式的主要表现形式就是体现在两个操作数的表现形式上。</p><h2 id="8086-处理器的寻址方式"><a href="#8086-处理器的寻址方式" class="headerlink" title="8086 处理器的寻址方式"></a>8086 处理器的寻址方式</h2><p>8086 处理器有七种基本寻址方式：</p><h3 id="寄存器寻址"><a href="#寄存器寻址" class="headerlink" title="寄存器寻址"></a>寄存器寻址</h3><p>指令执行时，操作数位于寄存器中，可以直接从寄存器中获取。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mov ax,cx     ; 此条指令中目的操作数和源操作数使用的都是寄存器寻址</span><br><span class="line">add bx,0xf000 ; 此条指令中目的操作数使用的是寄存器寻址，源操作数使用的是后面要介绍的立即寻址</span><br><span class="line">inc dx        ; 这是一条比较特殊的指令，它只有目的操作数，并且使用的是寄存器寻址</span><br></pre></td></tr></table></figure><h3 id="立即数寻址"><a href="#立即数寻址" class="headerlink" title="立即数寻址"></a>立即数寻址</h3><p>立即数寻址又称为立即寻址，指的是操作数为立即数的寻址方式。</p><p>所谓立即数指的是直接包含在指令中且紧跟在操作码后可以立即从指令中获取的操作数。</p><p>在立即寻址中立即数可以是 8 位的，也可以是 16 位的（<strong>注意我们的大前提是在 8086 处理器下</strong>）。这种寻址方式主要用于给寄存器或存储单元赋初始值，立即寻址是这七种基本寻址方式中速度最快的寻址方式。</p><p><strong>注：在实例代码中会有一个特殊的立即寻址例子。</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mov ax,oxf000   ; 此条指令中目的操作数使用的是寄存器寻址，源操作数使用的是立即寻址</span><br><span class="line">mov ax,label_a  ; 注意此条指令的源操作数，它采用的也是立即寻址，这里的 &#96;label_a&#96; 虽然是一个标号但是标号是数值的等价形式，它代表了所在位置的汇编地址，在编译阶段被转换为一个立即数</span><br></pre></td></tr></table></figure><h3 id="阶段小结"><a href="#阶段小结" class="headerlink" title="阶段小结"></a>阶段小结</h3><p>此前介绍的两种寻址方式：寄存器寻址和立即数寻址<strong>均未涉及到内存地址</strong>，都是在指令层面或更高级的存储空间（寄存器）层面的数据提取和存储。</p><h3 id="内存寻址"><a href="#内存寻址" class="headerlink" title="内存寻址"></a>内存寻址</h3><p>寄存器寻址的操作数位于寄存器中，立即寻址的操作数位于指令中，是指令的一部分。这是两种速度比较快的寻址方式，但它们也有局限性：一方面，我们不可能总是知道要操作的数是多少，因此也就不可能总是在指令上使用立即数；另一方面，寄存器的数量有限，不可能总指望在寄存器之间来回传递数据。</p><p>而内存恰巧拥有较大的容量，所以在指令中使用内存地址来表明要操作的内存中的数据是比较理想的方案。下面我们要介绍的五中寻址方式才是真正的在内存汇总寻找所需数据的寻址方式，它们统称为内存寻址。</p><p>在正式介绍内存寻址之前需要先理解一个概念：<strong>有效地址，即偏移地址</strong>。有效地址是一个 16 位的无符号数，表示操作数所在内存单元到段首的距离。</p><p>有效地址（Effective Address，EA）= 偏移量（disp）+ 基址（base）+ 变址（index）。</p><p>偏移量：存放在指令中的数，但它不是一个立即数，而是一个地址，可以用变量或标号表示。<br>基址：存放在基址寄存器（BX，BP）中，有效地址的基址部分。<br>变址：存放在变址寄存器（SI，DI）中，有效地址的变址部分。<br>三者都是可选的，但必须存在一个。</p><h4 id="基本规则"><a href="#基本规则" class="headerlink" title="基本规则"></a>基本规则</h4><ol><li>为了防止指令过长，双操作数指令中只能有一个使用存储器寻址方式；</li><li>所有表示内存地址的操作数都需要使用 [] 包裹起来；</li><li>数据默认存放在 DS 段中且默认隐藏前缀，也可以通过显示标注段前缀来覆盖默认值；</li><li>串指令中的目的操作数必须使用 ES 段作为前缀，默认为 [ES:DI]，且不能修改；</li><li>push 和 pop 指令必须使用 [SS:SP]，且不可以修改；</li><li>所有指令内容必须存放在 CS 段中；</li></ol><h4 id="直接寻址"><a href="#直接寻址" class="headerlink" title="直接寻址"></a>直接寻址</h4><p>操作数所表示的有效地址仅包含偏移量一种成分，即有效地址 = 偏移量。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mov ax, [0x5c0f]            ; 目的操作数使用的是寄存器寻址，源操作数使用的是直接寻址，地址为：ds:0x5c0f</span><br><span class="line">mov word [0x0230], 0x5000   ; 目的操作数使用的是直接寻址，地址为：ds:0x0230，源操作数使用的是立即数寻址</span><br><span class="line">xor byte [es:label_b], 0x05 ; 目的操作数使用的是直接寻址，地址为：es:label_b，这里的 label_b 是一个标号，但在编译时会被编译为标号对应的偏移量，源操作数使用的是立即数寻址</span><br></pre></td></tr></table></figure><h4 id="寄存器间接寻址"><a href="#寄存器间接寻址" class="headerlink" title="寄存器间接寻址"></a>寄存器间接寻址</h4><p>操作数所表示的有效地址存放在 BX BP SI DI 中的某一个寄存器中。此种寻址方式与寄存器寻址的区别在于：在寄存器寻址方式下寄存器存储的是待操作数据本身，而在本寻址方式下寄存器存储的是待操作数据所在的内存地址段内偏移量。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mov ax, [bx]    ; 目的操作数使用的是寄存器寻址，源操作数使用的是寄存器间接寻址，地址为：ds:bx</span><br><span class="line">mov ax, [es:bx] ; 目的操作数使用的是寄存器寻址，源操作数使用的是寄存器间接寻址，地址为：es:bx</span><br></pre></td></tr></table></figure><h4 id="寄存器相对寻址"><a href="#寄存器相对寻址" class="headerlink" title="寄存器相对寻址"></a>寄存器相对寻址</h4><p>操作数所表示的有效地址是 BX BP SI DI 中的某一个寄存器的内容和给出的偏移量之和（差）。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mov ax, [bx + 0x0030] ; 目的操作数使用的是寄存器寻址，源操作数使用的是寄存器相对寻址，地址为：es:bx+0x0030</span><br></pre></td></tr></table></figure><h4 id="基址变址寻址"><a href="#基址变址寻址" class="headerlink" title="基址变址寻址"></a>基址变址寻址</h4><p>操作数所表示的有效地址是基址寄存器（BX，BP）和变址寄存器（SI，DI）所表示的内容之和。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mov ax, [bx+si] ; 目的操作数使用的是寄存器寻址，源操作数使用的是基址变址寻址，地址为：es:bx+si</span><br></pre></td></tr></table></figure><h4 id="相对基址变址寻址"><a href="#相对基址变址寻址" class="headerlink" title="相对基址变址寻址"></a>相对基址变址寻址</h4><p>相对基址变址寻址是在基址变址寻址的基础上又多增加了一个偏移量的值。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mov ax, [bx+si+0x0030] ; 目的操作数使用的是寄存器寻址，源操作数使用的是基址变址寻址，地址为：es:bx+si+0x0030</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="http://www.lizhongc.com/index.php/archives/10/" target="_blank" rel="external nofollow noopener noreferrer">李忠：x86汇编语言-7.7</a></li><li><a href="https://evandoz.github.io/Evandoz/posts/2SDDXZX.htm#%E7%AB%8B%E5%8D%B3%E5%AF%BB%E5%9D%80" target="_blank" rel="external nofollow noopener noreferrer">EXP：8086 指令系统寻址</a></li><li><a href="https://www.cnblogs.com/printN/p/5966310.html" target="_blank" rel="external nofollow noopener noreferrer">Faron：8086中的七种寻址方式</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;本文主要描述了何为寻址，寻址能力的计算以及 8086 处理器的寻址方式有哪些。&lt;/p&gt;
&lt;h2 id=&quot;寻址&quot;&gt;&lt;a href=&quot;#寻址&quot; 
      
    
    </summary>
    
    
      <category term="1-网络笔记" scheme="https://wiki.blanc.site/categories/1-%E7%BD%91%E7%BB%9C%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="汇编语言" scheme="https://wiki.blanc.site/tags/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>NASM Overview</title>
    <link href="https://wiki.blanc.site/archives/90a7f914.html"/>
    <id>https://wiki.blanc.site/archives/90a7f914.html</id>
    <published>2021-02-05T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.940Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>机器指令是用二进制代码表示的 CPU 能够直接识别和执行的一种指令，不同的 CPU 架构有不同的机器指令集。汇编指令是将机器指令对应到便于记忆和书写的字符串（<strong>注意并非一一对应，同一汇编器可能存在多个汇编指令对应一个机器指令的情况</strong>），汇编指令编写完成后通过<strong>汇编器</strong>将其翻译成机器指令供 CPU 执行。</p><p><strong>不同汇编器针对同一机器指令可以有不同的汇编指令表达方式，只要汇编器最终能够正确无误地翻译就可以。</strong> 不同的汇编器对应不同的汇编指令格式，不同的汇编指令格式衍生出不同的汇编指令语法。没有一种汇编器可以将所有的汇编语法都正确地翻译成机器指令，因此，随着计算机的发展，不同厂家形成了自家的汇编语言体系并拥有自己的汇编器。</p><p>常见的汇编器有：GNU Assembler(GAS) | Microsoft Macro Assembler(MASM) | Netwide Assembler(NASM) | Flat Assembler(FASM) 等。GAS 使用 AT&amp;T 汇编语法，MASM 使用 Intel 汇编语法，NASM 使用的汇编语法和 Intel 汇编语法类似但要更简单一些。</p><p><strong>注：本文以 NASM 使用的汇编语法为例</strong></p><h2 id="句型句式"><a href="#句型句式" class="headerlink" title="句型句式"></a>句型句式</h2><p>NASM 的基本句型可以由四部分组成：<code>label: instruction operand(s) ; comment</code>。</p><p>理论上来说上面的四个部分都是可选的，但至少存在其中一个部分，一个语句可以没有指令而只存在一个标签。</p><p>而标签后的冒号也是可以省略的。</p><h3 id="空格"><a href="#空格" class="headerlink" title="空格"></a>空格</h3><p>NASM 语法对空格数量没有要求和限制，可以在任何两个部分的间隙添加任意数量的空格（至少一个用来区分两个部分）。</p><h3 id="行连接"><a href="#行连接" class="headerlink" title="行连接"></a>行连接</h3><p>在 NASM 中使用反斜杠（\）作为行的延续符，如果一行以反斜杠结束，则当前行的下一行被认为是当前行的延续。</p><h3 id="标签（label）"><a href="#标签（label）" class="headerlink" title="标签（label）"></a>标签（label）</h3><p>标签可以使用字母、数字、下划线（_）、美元符（$）、井号（#）、艾特（@）、波浪线（~）、英文句号（.）、英文问号（?），其中字母、下划线（_）、英文句号（.）和英文问号（?）。其中以英文句号（.）开头有特殊的含义（详情见下文）。</p><h4 id="局部标签（loacal-label）"><a href="#局部标签（loacal-label）" class="headerlink" title="局部标签（loacal label）"></a>局部标签（loacal label）</h4><p>在 NASM 中所以英文句号（.）开头的的标签会被视为局部标签，所有局部标签会被认为与上一个非局部标签有关联。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">label1:</span><br><span class="line">    some code</span><br><span class="line"></span><br><span class="line">.loop:</span><br><span class="line">    some code</span><br><span class="line">    jne .loop</span><br><span class="line">    ret</span><br><span class="line"></span><br><span class="line">label2:</span><br><span class="line">    some code</span><br><span class="line"></span><br><span class="line">.loop:</span><br><span class="line">    some code</span><br><span class="line">    jne .loop</span><br><span class="line">    ret</span><br><span class="line"></span><br><span class="line">label3:</span><br><span class="line">    some code</span><br><span class="line">    jmp label1.loop</span><br></pre></td></tr></table></figure><p>在上述代码片段中，所有的 jne 指令都会跳转到上面与之相邻的 .loop 标签，因为 .loop 标签的定义形式是一种局部标签定义形式，因此两个 .loop 标签分别会与该标签上面最近的全局标签产生关联。而在为特别指定的情况下局部标签只能在与其相关的全局标签下生效，但也可以通过「全局标签.局部标签」的形式进行调用。</p><h2 id="特殊指令（符号）"><a href="#特殊指令（符号）" class="headerlink" title="特殊指令（符号）"></a>特殊指令（符号）</h2><h3 id="：转义操作符"><a href="#：转义操作符" class="headerlink" title="``：转义操作符"></a>``：转义操作符</h3><p>NASM 使用 C 风格的转义字符，在反斜杠后跟转义码，转义码包括：字符转义码、八进制转义码、十六进制转义码，且转移字符需要使用反引号引用：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">db &#96;\x61&#96; ; 等同于 db a</span><br></pre></td></tr></table></figure><p><strong>注：反引号也可以用来定义普通字符串</strong></p><h3 id="：索引操作符"><a href="#：索引操作符" class="headerlink" title="[]：索引操作符"></a>[]：索引操作符</h3><p>有的地方称为「索引操作符」，表示一种间接取操作数的方式，即取括号内内存地址对应的操作数，类似 C 语言的指针概念。</p><p>括号中一般存放的是一个内存地址，可以是使用寄存器表示的内存地址，可以是使用标记表示的内存地址，也可以是直接用操作数表示的内存地址。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mov ax, [var]</span><br><span class="line">mov byte [es:0x00], &#39;L&#39;</span><br></pre></td></tr></table></figure><h3 id="和"><a href="#和" class="headerlink" title="\$ 和 $$"></a>\$ 和 $$</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$   表示经过 NASM 编译后当前指令位置；</span><br><span class="line">$$  表示经过 NASM 编译后当前 section 起始位置；</span><br></pre></td></tr></table></figure><h3 id="ptr"><a href="#ptr" class="headerlink" title="ptr"></a>ptr</h3><p>ptr -&gt; pointer 即指针的缩写，用来临时指定类型，可以类比为 C 语言中的强制类型转换。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mov ax, bx            ; 由于寄存器 ax 和 bx 都是 word 型，所以没有必要加 word</span><br><span class="line">mov ax, word ptr [bx] ; 取出内存地址是 ds:bx 的值转换为 word 型存放到 ax 处</span><br></pre></td></tr></table></figure><h2 id="伪指令"><a href="#伪指令" class="headerlink" title="伪指令"></a>伪指令</h2><p>伪指令不是真正的指令，而是为了方便 NASM 汇编器而存在，但是它们的地位与真正的指令相同。</p><h3 id="表明指令操作数据大小"><a href="#表明指令操作数据大小" class="headerlink" title="表明指令操作数据大小"></a>表明指令操作数据大小</h3><p>通常存在于操作指令和操作数之间，用来表明操作指令使用的操作数单位大小。</p><table><thead><tr><th align="center">指令</th><th align="center">代表数据大小（位）</th></tr></thead><tbody><tr><td align="center">byte</td><td align="center">8</td></tr><tr><td align="center">word</td><td align="center">16</td></tr><tr><td align="center">dword</td><td align="center">32</td></tr><tr><td align="center">qword</td><td align="center">64</td></tr><tr><td align="center">tword</td><td align="center">80</td></tr><tr><td align="center">oword</td><td align="center">128</td></tr><tr><td align="center">yword</td><td align="center">256</td></tr></tbody></table><h3 id="db-家族：定义初始化数据"><a href="#db-家族：定义初始化数据" class="headerlink" title="db 家族：定义初始化数据"></a>db 家族：定义初始化数据</h3><p>一系列用来声明并初始化数据的伪指令：</p><table><thead><tr><th align="center">指令</th><th align="center">功能</th><th align="center">备注</th></tr></thead><tbody><tr><td align="center">db</td><td align="center">定义字节数据</td><td align="center"></td></tr><tr><td align="center">dw</td><td align="center">定义字数据</td><td align="center"></td></tr><tr><td align="center">dd</td><td align="center">定义双字数据</td><td align="center">可以定义单精度浮点数</td></tr><tr><td align="center">dq</td><td align="center">定义四字数据</td><td align="center">可以定义双精度浮点数</td></tr><tr><td align="center">dt</td><td align="center">定义十字数据</td><td align="center">可以定义扩展精度浮点数</td></tr><tr><td align="center">do</td><td align="center">定义 oword</td><td align="center">可以定义四精度浮点数</td></tr><tr><td align="center">dy</td><td align="center">定义 yword</td><td align="center">可以定义 ymm 数据</td></tr></tbody></table><p><strong>注：dt do dy 不接受整型数值。</strong></p><h3 id="resb-家族：定义非初始化数据"><a href="#resb-家族：定义非初始化数据" class="headerlink" title="resb 家族：定义非初始化数据"></a>resb 家族：定义非初始化数据</h3><p>相比于 db 家族 resb 家族的指令只会在编译阶段声明一个未初始化的出处空间但并不会为其设置初始值。</p><p>resb: reserve byte</p><table><thead><tr><th align="center">指令</th><th align="center">功能</th></tr></thead><tbody><tr><td align="center">resb</td><td align="center">以字节为单位声明一段未初始化数据</td></tr><tr><td align="center">resw</td><td align="center">以字为单位声明一段未初始化数据</td></tr><tr><td align="center">resd</td><td align="center">以双字节为单位声明一段未初始化数据</td></tr><tr><td align="center">resq</td><td align="center">以四字为单位声明一段未初始化数据</td></tr><tr><td align="center">rest</td><td align="center">以十字为单位声明一段未初始化数据</td></tr><tr><td align="center">reso</td><td align="center">以 oword 为单位声明一段未初始化数据</td></tr><tr><td align="center">resy</td><td align="center">以 yword 为单位声明一段未初始化数据</td></tr></tbody></table><h3 id="incbin：包含二进制文件"><a href="#incbin：包含二进制文件" class="headerlink" title="incbin：包含二进制文件"></a>incbin：包含二进制文件</h3><p>NASM 提供了一种包含二进制文件的方法，即使用 incbin 伪指令，此伪指令的作用是包含 graphics 以及 sound 这类数据文件。</p><h3 id="equ：定义常量"><a href="#equ：定义常量" class="headerlink" title="equ：定义常量"></a>equ：定义常量</h3><p>equ 伪指令用来为某个标识符赋值一个整型常量，作用类似于 C 语言的 #define：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">a equ 0             ; 正确</span><br><span class="line">b equ &#39;abcd&#39;        ; 可执行，b &#x3D; 0x64636261</span><br><span class="line">c equ &#39;abcdefghi&#39;   ; 可执行会提示 warning，c &#x3D; 0x64636261</span><br><span class="line">d equ 1.2           ; 错误</span><br></pre></td></tr></table></figure><p>在例子中，b 和 c 存储的是字符串对应的 ASCII 码，而因为整型常量最大是 quadword(8 bytes)，因此 c 对应的字符串会被自动截断为 ‘abcd’。而 d 存储的是非整型值，因此会报错。</p><h3 id="times：重复执行"><a href="#times：重复执行" class="headerlink" title="times：重复执行"></a>times：重复执行</h3><p>用来重复指令（或伪指令），下面是一个比较经典的例子：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">; 用于填充引导代码</span><br><span class="line">times 510-($-$$) db 0</span><br><span class="line">dw 0xaa55</span><br></pre></td></tr></table></figure><h3 id="Unicode-字符串"><a href="#Unicode-字符串" class="headerlink" title="Unicode 字符串"></a>Unicode 字符串</h3><p>NASM 顶一个两个操作数符来定义 Unicode 字符串：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">dw __utf16__(&#39;你好世界&#39;)</span><br><span class="line">dd __utf32__(&#39;你好世界&#39;)</span><br></pre></td></tr></table></figure><h3 id="SECTION-SEGMENT：自定义段"><a href="#SECTION-SEGMENT：自定义段" class="headerlink" title="SECTION | SEGMENT：自定义段"></a>SECTION | SEGMENT：自定义段</h3><p>在 NASM 中 SECTION 和 SEGMENT 指令是相同的同义词，可以改变所写代码被分配到哪一个 section 中。在一些 object file 中，section 的数量是固定的；在其他格式中，用户可以根据自己的需求来自定义 section。</p><p><strong>本章节以 NASM 的 bin output formats 为例讲解多 section 用法</strong></p><p>NASM 支持标准的 .data .text .bss，编译后程序文件中内存地址的顺序是 .text .data 用户 section，同名 section 编译后会放在同一块连续的内存上。</p><p>section 特性：</p><ul><li>section 可以被指定为 progbits 或 nobits，默认为 probits（.bss 默认为 nobits）。</li><li>section 可以在定义时使用 <code>align=</code> 或 <code>start=</code> 字句在指定对齐字节，区别是 align 只接受 2 的 N 次幂，而 start 可以接受任意整数值；</li><li>section 可以在定义时使用 <code>vstart=</code> 字句定义一个虚拟起始地址，它将被用于计算该 section 内的所有内存引用；</li><li>section 可以在定义时使用 <code>follows=&lt;section&gt;</code> 或 <code>vfollows=&lt;section&gt;</code> 字句来进行排序；</li><li>显示定义的 section 之前的所有代码默认会存储在 .text section 中；</li><li>如果没有给出 ORG 语句，则 ORG 默认为零；</li><li>除非显示指定了 start= vstart= follows 或 vfollow，否则 .bss section 将被放在最后一个 progbits section 之后；</li><li>除非显示指定了对齐方式，否则所有 sectioin 都以 dword(4 bytes) 为边界对齐；</li><li>section 不能出现重叠；</li><li>NASM 为每个 section 提供了一个隐藏标签 <code>section.&lt;secname&gt;.start</code> 用来获取该 section 起始地址；</li></ul><p>拓展：<br>progbits：程序内容，包含代码、数据、调试相关信息；<br>nobits：和PROGBITS类似，唯一不同的是在文件中不占空间，对应的进行内存空间是加载的时候申请的；</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">db &#39;11&#39;</span><br><span class="line">section test1 align&#x3D;16</span><br><span class="line">db &#39;12&#39;</span><br><span class="line">section test2 start&#x3D;12</span><br><span class="line">db &#39;13&#39;</span><br><span class="line">section test3 vstart&#x3D;0x7c00</span><br><span class="line">db &#39;14&#39;</span><br><span class="line">section test4 follows&#x3D;test1</span><br><span class="line">db &#39;15&#39;</span><br><span class="line">section .text start&#x3D;100</span><br><span class="line">db &#39;16&#39;</span><br></pre></td></tr></table></figure><p><img src="https://img.blanc.site//wiki/img/20210812231023.png" alt="编译结果"></p><h2 id="数据传送指令"><a href="#数据传送指令" class="headerlink" title="数据传送指令"></a>数据传送指令</h2><h3 id="通用数据传送指令"><a href="#通用数据传送指令" class="headerlink" title="通用数据传送指令"></a>通用数据传送指令</h3><h4 id="mov"><a href="#mov" class="headerlink" title="mov"></a>mov</h4><h4 id="movsx"><a href="#movsx" class="headerlink" title="movsx"></a>movsx</h4><h4 id="movzx"><a href="#movzx" class="headerlink" title="movzx"></a>movzx</h4><h4 id="push"><a href="#push" class="headerlink" title="push"></a>push</h4><h4 id="pop"><a href="#pop" class="headerlink" title="pop"></a>pop</h4><h4 id="pusha"><a href="#pusha" class="headerlink" title="pusha"></a>pusha</h4><h4 id="popa"><a href="#popa" class="headerlink" title="popa"></a>popa</h4><h4 id="pushad"><a href="#pushad" class="headerlink" title="pushad"></a>pushad</h4><h4 id="popad"><a href="#popad" class="headerlink" title="popad"></a>popad</h4><h4 id="bswap"><a href="#bswap" class="headerlink" title="bswap"></a>bswap</h4><h4 id="xchg"><a href="#xchg" class="headerlink" title="xchg"></a>xchg</h4><h4 id="cmpxchg"><a href="#cmpxchg" class="headerlink" title="cmpxchg"></a>cmpxchg</h4><h4 id="xadd"><a href="#xadd" class="headerlink" title="xadd"></a>xadd</h4><h4 id="xlat"><a href="#xlat" class="headerlink" title="xlat"></a>xlat</h4><h3 id="输入输出端口传送指令"><a href="#输入输出端口传送指令" class="headerlink" title="输入输出端口传送指令"></a>输入输出端口传送指令</h3><h3 id="目的地之传送指令"><a href="#目的地之传送指令" class="headerlink" title="目的地之传送指令"></a>目的地之传送指令</h3><h3 id="标志传送指令"><a href="#标志传送指令" class="headerlink" title="标志传送指令"></a>标志传送指令</h3><h2 id="算数运算指令"><a href="#算数运算指令" class="headerlink" title="算数运算指令"></a>算数运算指令</h2><p>算数运算指令主要包括二进制的定点、浮点的加减乘除运算指令；求反、求补、加一、减一、比较指令；十进制加减运算指令等；不同计算机对算数运算指令的支持有很大的差别。</p><h3 id="add"><a href="#add" class="headerlink" title="add"></a>add</h3><p><code>add &lt;目的操作数&gt;, &lt;源操作数&gt;</code></p><p>影响标志位：OF，SF，ZF，AF，PF，CF。<br>将源操作数加到目的操作数上（结果存储在目的操作数上）。<br>源操作数和目的操作数类型必须一致，且两者不能同时使用存储器操作数。</p><h3 id="adc"><a href="#adc" class="headerlink" title="adc"></a>adc</h3><p><code>adc &lt;目的操作数&gt;, &lt;源操作数&gt;</code></p><p>带进位加法指令，与 ADD 基本相同，区别在于执行指令前会将标志位 CF 的值加到目的操作数上，多用于多字节加法运算。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">mov DX, 0x2FFF</span><br><span class="line">mov AX, 0xFF00</span><br><span class="line">add AX, 0x5678</span><br><span class="line">adc dx, 0x1234</span><br><span class="line">; 以上指令实现了多字节数 0x2FFFFF00 与 0x12345678 相加</span><br><span class="line">; 其中 DX 存放 0x2FFF + 0x1234 AX 存放 0xFF00 + 0x5678</span><br><span class="line">; 执行第三步时，低 8 位进行相加，0xFF00 + 0x5678 &#x3D; 0x5578，此时进位标志位 CF&#x3D;1</span><br><span class="line">; 执行第四步时，高 8 位进行相加，0x2FFF + 0x1234 &#x3D; 0x4233，之后加上进位标志位的进位数， DX &#x3D; 0x4234</span><br><span class="line">; 最终得到 0x2FFFFF00 + 0x12345678 &#x3D; 0x42345578</span><br></pre></td></tr></table></figure><h3 id="inc"><a href="#inc" class="headerlink" title="inc"></a>inc</h3><p><code>inc &lt;目的操作数&gt;</code></p><p>加一指令，将目的操作数加一。</p><h3 id="sub"><a href="#sub" class="headerlink" title="sub"></a>sub</h3><p><code>sub &lt;目的操作数&gt;, &lt;源操作数&gt;</code></p><p>用目的操作数减去源操作数（结果存储在目的操作数上）。<br>源操作数和目的操作数类型必须一致，且两者不能同时使用存储器操作数。</p><h3 id="sbb"><a href="#sbb" class="headerlink" title="sbb"></a>sbb</h3><h3 id="dec"><a href="#dec" class="headerlink" title="dec"></a>dec</h3><p><code>dec &lt;目的操作数&gt;</code></p><p>减一指令，将目的操作数减一。</p><h3 id="neg"><a href="#neg" class="headerlink" title="neg"></a>neg</h3><p><code>neg &lt;目的操作数&gt;</code></p><p>用于求目的操作数的补码（取反再加一）。<br>可以通过寄存器或内存单元向 neg 指令传送目的操作数。</p><h3 id="cmp"><a href="#cmp" class="headerlink" title="cmp"></a>cmp</h3><p>用于比较源操作数和目的操作数。</p><p>cmp 指令类似 sub 指令，只是不保存计算结果但对标志寄存器产生影响，其他指令可以通过识别这些被影响的标志寄存器位来得知比较结果。</p><p>在指令中，目的操作数是被测量的对象，源操作数则作为测量的基准。指令使用目的操作数减去源操作数并在不保存结果的情况下对标志寄存器产生影响。</p><p>会被产生影响的标志位有：溢出 符号 零 进位 辅助进位 奇偶 | OF SF ZF CF AF PF</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">mov ax, 8</span><br><span class="line">mov bx, 3</span><br><span class="line">cmp ax, bx</span><br><span class="line">; 执行结果 ax &#x3D; 8 | ZF &#x3D; 0 | PF &#x3D; 1 | SF &#x3D; 0 | CF &#x3D; 0 | OF &#x3D; 0</span><br></pre></td></tr></table></figure><h3 id="aas"><a href="#aas" class="headerlink" title="aas"></a>aas</h3><h3 id="ads"><a href="#ads" class="headerlink" title="ads"></a>ads</h3><h3 id="mul"><a href="#mul" class="headerlink" title="mul"></a>mul</h3><p><code>mul &lt;源操作数&gt;</code></p><p>无符号乘法运算，结果为整数。<br>mul 指令可以通过寄存器或内存单元接受一个 8 位或 16 位的乘数：<br>如果乘数是 8 位的：那么源操作数与寄存器 AL 中的 8 位数相乘得到的结果存储在 AX 中；<br>如果乘数是 16 位的：那么源操作数与寄存器 AX 中的 16 位数相乘得到的结果存储在 DX:AX 中；</p><p>mul 执行后，如果结果的高位全是零则 OF 和 CF 清零，否则置一，对 SF ZF AF 和 PF 标志位影响未定义。</p><h3 id="imul"><a href="#imul" class="headerlink" title="imul"></a>imul</h3><h3 id="aam"><a href="#aam" class="headerlink" title="aam"></a>aam</h3><h3 id="div"><a href="#div" class="headerlink" title="div"></a>div</h3><p><code>div &lt;源操作数&gt;</code></p><p>用于进行无符号除法运算，结果为整数。<br>除数作为源操作数传入，存储在寄存器或内存单元中。<br>被除数默认存放在 AX（16 位以内）或 AX 和 DX（32 位，DX 存放高位，AX 存放低位）中。</p><p>div 操作的结果分为商和余数两部分。<br>如果除数是 8 位的，那么结果中的商存储在 AL 中，余数存储在 AH 中。<br>如果除数是 16 位的，那么结果中的商存储在 AX 中，余数存储在 DX 中。</p><p>执行条件：</p><ol><li>被除数的高位必须小于除数（否则商无法存储）；</li><li>若除数为 16 位，则在运行 div 指令前需要清零 DX 寄存器；</li></ol><h3 id="idiv"><a href="#idiv" class="headerlink" title="idiv"></a>idiv</h3><h3 id="aad"><a href="#aad" class="headerlink" title="aad"></a>aad</h3><h3 id="cbw"><a href="#cbw" class="headerlink" title="cbw"></a>cbw</h3><p><code>cbw</code></p><p>将寄存器 AL 中数据的最高位扩展到 AH 中，若 AL 中最高位为 0，则 AH 被设置为 00H，若 AL 中的最高位为 1，则 AH 被设置为 FFH。</p><h3 id="cwd"><a href="#cwd" class="headerlink" title="cwd"></a>cwd</h3><p><code>cwd</code></p><p>将寄存器 AX 中数据的最高位拓展到 DX中，若 AX 中最高位为 0，则 DX 被设置为 0000H，若 AX 中的最高位为 1，则 DX 被设置为 FFFFH。</p><h3 id="cwde"><a href="#cwde" class="headerlink" title="cwde"></a>cwde</h3><h3 id="cdq"><a href="#cdq" class="headerlink" title="cdq"></a>cdq</h3><h2 id="逻辑运算指令"><a href="#逻辑运算指令" class="headerlink" title="逻辑运算指令"></a>逻辑运算指令</h2><h3 id="and"><a href="#and" class="headerlink" title="and"></a>and</h3><p><code>and &lt;目的操作数&gt;, &lt;源操作数&gt;</code></p><p>将目的操作数和源操作数进行按位逻辑与运算，结果存储在目的操作数。</p><h3 id="or"><a href="#or" class="headerlink" title="or"></a>or</h3><p><code>or &lt;目的操作数&gt;, &lt;源操作数&gt;</code></p><p>将目的操作数和源操作数进行按位逻辑或运算，结果存储在目的操作数。</p><h3 id="xor"><a href="#xor" class="headerlink" title="xor"></a>xor</h3><p><code>xor &lt;目的操作数&gt;, &lt;源操作数&gt;</code></p><p>将目的操作数和源操作数进行按位逻辑异或运算，结果存储在目的操作数。</p><h3 id="not"><a href="#not" class="headerlink" title="not"></a>not</h3><p><code>not &lt;目的操作数&gt;</code></p><p>将目的操作数按位取反，结果存储在目的操作数。</p><h3 id="test"><a href="#test" class="headerlink" title="test"></a>test</h3><p><code>test &lt;目的操作数&gt;, &lt;源操作数&gt;</code></p><p>将目的操作数和源操作数进行按位逻辑与运算，不存储结果。</p><h3 id="shl-amp-sal"><a href="#shl-amp-sal" class="headerlink" title="shl &amp; sal"></a>shl &amp; sal</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">shl &lt;目的操作数&gt; &lt;源操作数（移位次数）&gt;</span><br><span class="line">sal &lt;目的操作数&gt; &lt;源操作数（移位次数）&gt;</span><br></pre></td></tr></table></figure><p>shl（逻辑左移）和 sal（算数左移）的实际效果完全相同。<br>两者的作用是：将目的操作数向左移位源操作数个位数，最低位用 0 填充，最高位移入进位标志位（CF）。</p><h3 id="shr-amp-sar"><a href="#shr-amp-sar" class="headerlink" title="shr &amp; sar"></a>shr &amp; sar</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">shr &lt;目的操作数&gt; &lt;源操作数（移位次数）&gt;</span><br><span class="line">sar &lt;目的操作数&gt; &lt;源操作数（移位次数）&gt;</span><br></pre></td></tr></table></figure><p>shr（逻辑右移）和 sar（算数右移）有所不同：<br>shr：高位用 0 填充，低位移入进位标志位（CF）。<br>sar：高位用符号位填充，低位移入进位标志位（CF）。</p><h3 id="rol"><a href="#rol" class="headerlink" title="rol"></a>rol</h3><p><code>rol &lt;目的操作数&gt;, &lt;源操作数（移位次数）&gt;</code></p><p>rol(Rotate Left)：循环左移指令，将目的操作数左移指定次数，最高位送入最低位和进位标志位（CF）。</p><h3 id="ror"><a href="#ror" class="headerlink" title="ror"></a>ror</h3><p><code>ror &lt;目的操作数&gt;, &lt;源操作数（移位次数）&gt;</code></p><p>ror(Rotate Right)：循环右移指令，将目的操作数右移指定次数，最低位送入最高位和进位标志位（CF）。</p><h3 id="rcr"><a href="#rcr" class="headerlink" title="rcr"></a>rcr</h3><h3 id="rcl"><a href="#rcl" class="headerlink" title="rcl"></a>rcl</h3><h2 id="串指令"><a href="#串指令" class="headerlink" title="串指令"></a>串指令</h2><h3 id="movs-movsb-movsw-movsd"><a href="#movs-movsb-movsw-movsd" class="headerlink" title="movs(movsb, movsw, movsd)"></a>movs(movsb, movsw, movsd)</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">movsw     ; 执行一次</span><br><span class="line">rep movsw ; 利用 rep 命令重复执行</span><br></pre></td></tr></table></figure><p>串传送，从源地址向目的地址批量传送数据。<br>16 位模式下源地址是 <code>DS:SI</code>，目的地址是 <code>ES:DI</code>。<br>32 位模式下源地址是 <code>DS:ESI</code>，目的地址是 <code>ES:EDI</code>。<br>根据传送数据大小又分为 movsb, movsw, movsd，分别对应传送一个字节，一个字，一个双字。</p><p>movs 命令可以使用重复执行，方向标志位 DF 决定了 SI 和 DI 在单次操作后是增加（0）还是减少（1）<br>每次变动的大小与具体执行命令有关：movsb -&gt; 1B | movsw -&gt; 2B | movsd -&gt; 4B</p><h3 id="cmps"><a href="#cmps" class="headerlink" title="cmps"></a>cmps</h3><h3 id="scas"><a href="#scas" class="headerlink" title="scas"></a>scas</h3><h3 id="lods"><a href="#lods" class="headerlink" title="lods"></a>lods</h3><h3 id="stos"><a href="#stos" class="headerlink" title="stos"></a>stos</h3><h3 id="rep"><a href="#rep" class="headerlink" title="rep"></a>rep</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rep movsw</span><br></pre></td></tr></table></figure><p>重复前缀指令，不能单独使用，可以用来重复执行跟在后面的指令，重复次数由 CX 控制（每次重复 CX 减一，知道 CX 值为零停止）。</p><h3 id="repe-amp-repz"><a href="#repe-amp-repz" class="headerlink" title="repe &amp; repz"></a>repe &amp; repz</h3><h3 id="repne-amp-repnz"><a href="#repne-amp-repnz" class="headerlink" title="repne &amp; repnz"></a>repne &amp; repnz</h3><h3 id="repc"><a href="#repc" class="headerlink" title="repc"></a>repc</h3><h3 id="repnc"><a href="#repnc" class="headerlink" title="repnc"></a>repnc</h3><h2 id="程序转移指令"><a href="#程序转移指令" class="headerlink" title="程序转移指令"></a>程序转移指令</h2><p>可以修改 IP 或同时修改 CS 和 IP 寄存器内容的指令统称为转移指令。可以通俗理解为：转移指令就是可以控制 CPU 下一步执行内存中哪一处指令的指令。</p><p>在 8086 中按照转移行为可分为：</p><ol><li>段内转移：只修改 IP<ol><li>短转移：IP 修改范围为 -128~127(2^7-1)</li><li>近转移：IP 修改范围为 -32768~32767(2^15-1)</li></ol></li><li>段间转移（远转移）：同时修改 CS 和 IP</li></ol><p>按照功能不同，转移指令又可细分为一下几种：</p><h3 id="无条件转移指令"><a href="#无条件转移指令" class="headerlink" title="无条件转移指令"></a>无条件转移指令</h3><p>描述：无条件转移指令可以控制 CPU 下一步执行代码段（CS）中任意内存地址对应的指令</p><h4 id="offset"><a href="#offset" class="headerlink" title="offset"></a>offset</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">start: mov ax,offset start ; 相当于 mov ax,0</span><br><span class="line">next: mov ax,offset next ; 相当于 mov ax,3 | 第一条指令长度为三个字节，因此 next 的偏移地址为 3</span><br></pre></td></tr></table></figure><p>操作符，由编译器处理，功能是获取标签的偏移地址</p><h4 id="jmp"><a href="#jmp" class="headerlink" title="jmp"></a>jmp</h4><h5 id="描述"><a href="#描述" class="headerlink" title="描述"></a>描述</h5><p>转移地址可以在指令、内存或寄存器中指出。可以只修改 IP，也可以同时修改 CS 和 IP</p><p>使用 jmp 指令时需要提供两种信息：</p><ol><li>转移的目的地址</li><li>转移类型（段间转移（远转移）、段内短转移、段内近转移）</li></ol><h5 id="语法"><a href="#语法" class="headerlink" title="语法"></a>语法</h5><ol><li>转移目的地址在指令中</li></ol><p>语法：<code>jmp short &lt;标签&gt;</code><br>作用：转移到标签处执行指令<br>描述：这种格式的 jmp 指令实现的是段内短转移，short 为短转移标志<br>原理：ip = ip + 8 位位移 | 8 位位移 = 标签地址 - jmp 指令后第一个字节的地址<br>此指令形式是针对当前指令所在位置（即当前 IP）进行跳转的，且 8 位位移范围是 -128~127，由编译程序在编译时计算<br>示例：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">start:</span><br><span class="line">    mov ax,0</span><br><span class="line">    jmp short next</span><br><span class="line">    add ax,1</span><br><span class="line">next:</span><br><span class="line">    inc ax</span><br><span class="line">; 最终结果 ax 内的值为 1</span><br></pre></td></tr></table></figure><p>语法：<code>jmp near ptr &lt;标签&gt;</code><br>作用：转移到标签处执行指令<br>描述：这种格式的 jmp 指令实现的是段内近转移，near 为近转移标志<br>原理：ip = ip + 16 位位移 | 16 位位移 = 标签地址 - jmp 指令后的第一个字节地址<br>此指令也是针对当前指令所在位置（即当前 IP）进行跳转的，且 16 位位移范围是 -32768~32767，由编译程序在编译时计算</p><p>语法：<code>jmp far ptr &lt;标签标签&gt;</code><br>作用：转移到标签处执行命令<br>描述：这种格式的 jmp 指令实现的是段间转移（即远转移），far ptr 为远转移标志<br>原理：cs = 标签所在段的段地址 | ip = 标签所在段中的偏移 | 高位存储段地址，低位存储偏移地址</p><ol start="2"><li>转移目的地址在内存中</li></ol><p>语法：<code>jmp word ptr &lt;[内存单元地址]&gt;</code><br>作用：转移到目标内存地址所存储的地址处执行指令<br>描述：这种格式的 jmp 指令实现的是段内转移，word ptr 是转移标志<br>原理：ip = 内存地址所存储的内容</p><p>语法：<code>jmp dword ptr &lt;[内存单元地址]&gt;</code><br>作用：在内存单元地址处存放两个字，高地址存放转移的目的段地址，低地址存放转移的目的偏移地址<br>描述：这种格式的 jmp 指令实现的是段间转移（即远转移），dword ptr 为远转移标志<br>原理：cs = 内存单元地址 + 2 所存储的内容 | ip = 内存单元地址存储的内容<br>示例：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">mov ax,0123H</span><br><span class="line">mov ds:[0],ax</span><br><span class="line">mov word ptr ds:[2],0</span><br><span class="line">jmp dword ptr ds:[0]</span><br><span class="line">; 执行后 cs &#x3D; 0 | ip &#x3D; 0123H</span><br></pre></td></tr></table></figure><ol start="3"><li>转移目的地址在寄存器中</li></ol><p>语法：<code>jmp &lt;16位寄存器&gt;</code><br>作用：转移到目标寄存器所存储的地址处执行指令<br>描述：这种格式的 jmp 指令实现的是段内转移<br>原理：ip = 16位寄存器内容</p><p>语法：<code>jmp &lt;[段地址:偏移地址]&gt;</code><br>作用：转移到目标地址处执行命令<br>描述：这种格式的 jmp 执行实现的是段间转移<br>原理：cs = 段地址 | ip = 偏移地址</p><h3 id="条件转移指令"><a href="#条件转移指令" class="headerlink" title="条件转移指令"></a>条件转移指令</h3><h4 id="jz-je-amp-jnz-jne-零判断（相等判断）"><a href="#jz-je-amp-jnz-jne-零判断（相等判断）" class="headerlink" title="jz(je) &amp; jnz(jne) | 零判断（相等判断）"></a>jz(je) &amp; jnz(jne) | 零判断（相等判断）</h4><p>jz 和 je 意义相同，只是写法不同。<br>jnz 和 jne 意义相同，只是写法不同。</p><p>jz：如果标志位 ZF = 1，则跳转到指定地址。<br>jnz：如果标志位 ZF = 0，则跳转到指定地址。</p><p>ZF：零标志位，相关指令执行后结果是否为零 | 0 -&gt; 否 | 1 -&gt; 是。</p><h4 id="jc-amp-jnc-进位判断"><a href="#jc-amp-jnc-进位判断" class="headerlink" title="jc &amp; jnc | 进位判断"></a>jc &amp; jnc | 进位判断</h4><p>jc：如果标志位 CF = 1，则跳转到指定地址。<br>jnc：如果标志位 CF = 0，则跳转到指定地址。</p><p>CF：进位标志位，相关指令执行后是否产生了进位或借位 | 0 -&gt; 没产生 | 1 -&gt; 产生了。</p><h4 id="jp-jpe-amp-jnp-jpo-奇偶判断"><a href="#jp-jpe-amp-jnp-jpo-奇偶判断" class="headerlink" title="jp(jpe) &amp; jnp(jpo) | 奇偶判断"></a>jp(jpe) &amp; jnp(jpo) | 奇偶判断</h4><p>jp 和 jpe 意义相同，只是写法不同。<br>jnp 和 jpo 意义相同，只是写法不同。</p><p>jp：如果标志位 PF = 1，则跳转到指定地址。<br>jnp：如果标志位 PF = 0，则跳转到指定地址。</p><p>PF：奇偶标志位，相关指令执行后结果中为 1 的比特的个数是否为偶数 | 0 -&gt; 奇 | 1 -&gt; 偶。</p><h4 id="js-amp-jns-正负判断"><a href="#js-amp-jns-正负判断" class="headerlink" title="js &amp; jns | 正负判断"></a>js &amp; jns | 正负判断</h4><p>js：如果标志位 SF = 1，则跳转到指定地址。<br>jns：如果标志位 SF = 0，则跳转到指定地址。</p><p>SF：符号标志位，相关指令执行后结果是否为负数 | 0 -&gt; 非负数 | 1 -&gt; 负数。</p><h4 id="jo-amp-jno"><a href="#jo-amp-jno" class="headerlink" title="jo &amp; jno"></a>jo &amp; jno</h4><p>jo：如果标志位 OF = 1，则跳转到指定地址。<br>jnp：如果标志位 OF = 0，则跳转到指定地址。</p><p>OF：溢出标志位，有符号运算结果是否产生溢出 | 0 -&gt; 否 | 1 -&gt; 是。</p><h3 id="循环控制指令"><a href="#循环控制指令" class="headerlink" title="循环控制指令"></a>循环控制指令</h3><h3 id="终端指令"><a href="#终端指令" class="headerlink" title="终端指令"></a>终端指令</h3><h2 id="处理器控制指令"><a href="#处理器控制指令" class="headerlink" title="处理器控制指令"></a>处理器控制指令</h2><p>处理器控制指令包括标志操作指令和 CPU 控制指令</p><h3 id="标志操作指令"><a href="#标志操作指令" class="headerlink" title="标志操作指令"></a>标志操作指令</h3><h4 id="stc-amp-clc-amp-cmc"><a href="#stc-amp-clc-amp-cmc" class="headerlink" title="stc &amp; clc &amp; cmc"></a>stc &amp; clc &amp; cmc</h4><p>stc：将 CF 设置为 1<br>clc：将 CF 设置为 0<br>cmc：将 CF 取反<br>CF：进位标志，计算中是否产生了进位或借位</p><h4 id="std-amp-cld"><a href="#std-amp-cld" class="headerlink" title="std &amp; cld"></a>std &amp; cld</h4><p>std：将 DF 设置为 1<br>cld：将 DF 设置为 0<br>DF：串处理指令中，每次操作后 SI 或 DI 自增（0）还是自减（1）</p><h4 id="sti-amp-cli"><a href="#sti-amp-cli" class="headerlink" title="sti &amp; cli"></a>sti &amp; cli</h4><p>sti：将 IF 设置为 1<br>cli：将 IF 设置为 0<br>IF：中断允许标志，CPU 是否能响应外部课评比中断请求</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://innofang.github.io/2017/11/26/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89%EF%BC%9A%E8%BD%AC%E7%A7%BB%E6%8C%87%E4%BB%A4%E7%9A%84%E5%8E%9F%E7%90%86/" target="_blank" rel="external nofollow noopener noreferrer">Inno’s Blog：汇编语言学习笔记（九）：转移指令的原理</a></li><li><a href="https://www.cnblogs.com/mfm11111/archive/2009/03/27/1422828.html" target="_blank" rel="external nofollow noopener noreferrer">jasonM：一步步学习汇编(10)之jmp指令原理分析（破解软件的必修课）</a></li><li><a href="https://zhuanlan.zhihu.com/p/53394807" target="_blank" rel="external nofollow noopener noreferrer">FullSky：8086运算、位移、处理器指令</a></li><li><a href="https://zhuanlan.zhihu.com/p/53394807" target="_blank" rel="external nofollow noopener noreferrer">知乎-愛是等待是细水长流：汇编语言–x86汇编指令集大全</a></li><li><a href="https://blubiu.github.io/2020/01/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E7%AC%94%E8%AE%B0-DIV%E6%8C%87%E4%BB%A4(%E9%99%A4%E6%B3%95%E6%8C%87%E4%BB%A4)/" target="_blank" rel="external nofollow noopener noreferrer">blubiu：汇编语言笔记(七)–DIV指令(除法指令)</a></li><li><a href="https://blog.csdn.net/fireblue1990/article/details/52530248" target="_blank" rel="external nofollow noopener noreferrer">CSDN-YiShiWenYan：汇编中的PTR含义</a></li><li><a href="https://gowa.club/Asm/%E5%85%B3%E4%BA%8E%E6%B1%87%E7%BC%96%E4%B8%AD%E7%9A%84PTR%E6%93%8D%E4%BD%9C%E7%AC%A6.html" target="_blank" rel="external nofollow noopener noreferrer">退思园：关于汇编中的PTR操作符</a></li><li><a href="http://www.511yj.com/asm-x86-81.html" target="_blank" rel="external nofollow noopener noreferrer">511遇见： 汇编语言标志寄存器cmp 指令</a></li><li><a href="http://demon.tw/programming/assembly-sar-shr.html" target="_blank" rel="external nofollow noopener noreferrer">Demon’s Blog：汇编语言中SAR和SHR指令的区别</a></li><li><a href="http://c.biancheng.net/asm/" target="_blank" rel="external nofollow noopener noreferrer">C 语言中文网：汇编语言</a></li><li><a href="https://blog.csdn.net/huang987246510/article/details/90311189" target="_blank" rel="external nofollow noopener noreferrer">享乐主：两类风格汇编语法对比</a></li><li><a href="https://blog.csdn.net/shiwenlei102/article/details/23127287" target="_blank" rel="external nofollow noopener noreferrer">red_rock：nasm指令详解</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;机器指令是用二进制代码表示的 CPU 能够直接识别和执行的一种指令，不同的 CPU 架构有不同的机器指令集。汇编指令是将机器指令对应到便于记
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="汇编语言" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80/"/>
    
    
      <category term="汇编语言" scheme="https://wiki.blanc.site/tags/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>数据结构笔记1-概论</title>
    <link href="https://wiki.blanc.site/archives/f3c5e08e.html"/>
    <id>https://wiki.blanc.site/archives/f3c5e08e.html</id>
    <published>2020-10-08T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><h3 id="数据"><a href="#数据" class="headerlink" title="数据"></a>数据</h3><p>数据是信息的载体，是<strong>描述客观事物的</strong>数、字符以及所有能输入到计算机中并被计算机程序识别和处理的符合的集合。</p><p>数据大致可分为两类，一类是<strong>数值性数据</strong>，包括整数、浮点数、复数、双精度数等，主要用于工程和科学计算，以及商业事务处理；另一类是<strong>非数值性数据</strong>，主要包括字符和字符串，以及文字、图形、图像、语音等数据。</p><h3 id="数据元素"><a href="#数据元素" class="headerlink" title="数据元素"></a>数据元素</h3><p>数据元素是数据的基本单位，通常作为一个整体进行考虑和处理。一个数据元素可以有若干个数据项组成，数据项是数据元素的不可分割的最小单位。例如：学生记录作为一个数据元素，它由学号、姓名、性别等数据项组成。</p><h3 id="数据对象"><a href="#数据对象" class="headerlink" title="数据对象"></a>数据对象</h3><p>数据对象是具有相同性质的数据元素的集合，是数据的一个子集。</p><h3 id="数据类型"><a href="#数据类型" class="headerlink" title="数据类型"></a>数据类型</h3><p>数据类型是一个值的集合和定义在此集合上一组操作的总称。</p><ol><li>原子类型：其值不可在分割的数据类型；</li><li>结构类型：其值可以在分割成若干成分的数据类型；</li><li>抽象数据类型：抽象数据组织和与之相关的操作。</li></ol><h3 id="抽象数据类型（ADT-Abstract-Data-Type）"><a href="#抽象数据类型（ADT-Abstract-Data-Type）" class="headerlink" title="抽象数据类型（ADT: Abstract Data Type）"></a>抽象数据类型（ADT: Abstract Data Type）</h3><p>一个数学模型以及定义在该模型上的一组操作。</p><p>抽象数据类型的定义仅取决于它的一组逻辑特性，而与其在计算机内部如何表示和实现无关，即不论其内部结构如何变化，只要它的数学特性不变，都不影响其外部的使用。通常用数据对象（D)、 数据关系（S)、基本操作集（P)这样的三元组来表示抽象数据类型。</p><h3 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h3><p>数据结构包括三方面的内容：逻辑结构、存储结构和数据的运算。数据的逻辑结构和存储结构是密不可分的两个方面，一个算法的设计取决于所选定的逻辑结构，而算法的实现依赖于所釆用的存储结构。</p><p>数据结构是由某一数据元素的集合和该集合中数据元素之间的关系组成的。</p><p><code>Data_Structure = {D, R}</code></p><p>D 是某一数据元素的集合，R 是该集合中所有数据元素之间的关系的有限集合。</p><p>有关数据结构的讨论主要涉及<strong>数据元素之间的关系，不涉及数据元素本身的内容</strong>。</p><h3 id="数据的逻辑结构"><a href="#数据的逻辑结构" class="headerlink" title="数据的逻辑结构"></a>数据的逻辑结构</h3><p>数据的逻辑结构是指数据元素之间的逻辑关系，即从逻辑关系上描述数据，与数据的存储无关。</p><h4 id="逻辑结构的第一种分类方式"><a href="#逻辑结构的第一种分类方式" class="headerlink" title="逻辑结构的第一种分类方式"></a>逻辑结构的第一种分类方式</h4><p>依据元素之间关系的不同，数据的逻辑结构分为两大类：线性结构和非线性结构。</p><p><img src="https://img.blanc.site//wiki/img/20201019000530.png" alt="逻辑结构分类-1"></p><h4 id="逻辑结构的第二种分类方式"><a href="#逻辑结构的第二种分类方式" class="headerlink" title="逻辑结构的第二种分类方式"></a>逻辑结构的第二种分类方式</h4><p>线性结构：数据元素之间存在一对一的关系<br>树形结构：数据元素之间存在一对多的关系<br>图形结构：数据元素之间存在多对多的关系<br>集合结构：数据元素属于同一个集合</p><p><img src="https://img.blanc.site//wiki/img/20201106203719.jpg" alt="逻辑结构分类-2"></p><h3 id="数据的存储结构"><a href="#数据的存储结构" class="headerlink" title="数据的存储结构"></a>数据的存储结构</h3><p>数据的存储结构是指数据结构在计算机中的具体表示（又称映像），也成物理结构。包括数据元素的表示和数据元素间关系的表示。数据的存储结构是数据的逻辑结构用计算机语言实现的。它依赖于计算机语言。主要有如下四种结构：</p><ol><li>顺序存储结构：把逻辑上相邻的元素存储在物理位置上也相邻的存储单元里，元素之间的关系由存储单元的邻接关系来体现。其优点是可以实现随机存取，每个元素占用最少的存储空间；缺点是只能使用相邻的一整块存储单元，因此可能产生较多的外部碎片。</li><li>链式存储结构：不要求逻辑上相邻的元素在物理位置上也相邻，借助指示元素存储地址的指针表示元素之间的逻辑关系。其优点是不会出现碎片现象，充分利用所有存储单元；缺点是每个元素因存储指针而占用额外的存储空间，并且只能实现顺序存取。</li><li>索引存储结构：在存储元素信息的同时，还建立附加的索引表。索引表中的每一项称为索引项，索引项的一般形式是：（关键字，地址）。其优点是检索速度快；缺点是增加了附加的索引表，会占用较多的存储空间。另外，在增加和删除数据时要修改索引表，因而会花费较多的时间。</li><li>散列存储结构：根据元素的关键字直接计算出该元素的存储地址，又称为Hash存储。其优点是检索、增加和删除结点的操作都很快；缺点是如果散列函数不好可能出现元素存储单元的冲突，而解决冲突会增加时间和空间开销。</li></ol><h3 id="数据的运算"><a href="#数据的运算" class="headerlink" title="数据的运算"></a>数据的运算</h3><p>施加再数据上的运算包括运算的定义和实现。运算的定义时针对逻辑结构的，之处运算的功能；运算的实现是针对存储结构的，之处运算的具体操作步骤。</p><h2 id="算法"><a href="#算法" class="headerlink" title="算法"></a>算法</h2><h3 id="算法的基本概念"><a href="#算法的基本概念" class="headerlink" title="算法的基本概念"></a>算法的基本概念</h3><p>算法（algorithm）是对特定问题求解步骤的一种描述，它是指令的有限序列，其中每一条指令表示一个或多个操作。</p><h3 id="算法的特性"><a href="#算法的特性" class="headerlink" title="算法的特性"></a>算法的特性</h3><ol><li>有穷性：总是（对任何合法的输入值）在执行又穷步之后结束，且每一步都可在有穷时间内完成</li><li>确定性：每一条指令必须有确切的含义，读者理解时不会产生二义性。即对于相同的输入只能得出相同的输出（幂等性？）</li><li>可行性：算法中描述的操作都是已经实现的基本操作执行有限次来实现的</li><li>输入：有零个或多个输入</li><li>输出：有一个或多个输出</li></ol><h3 id="算法的性能标准"><a href="#算法的性能标准" class="headerlink" title="算法的性能标准"></a>算法的性能标准</h3><ol><li>正确性：正确的执行预定的功能和性能要求</li><li>可读性</li><li>健壮性：输入非法数据时也能适当的做出反应进行处理。</li><li>效率和低存储量需求</li></ol><h3 id="算法效率的度量"><a href="#算法效率的度量" class="headerlink" title="算法效率的度量"></a>算法效率的度量</h3><p>算法效率的度量可分为事前估计和后期测试。</p><h4 id="时间复杂度"><a href="#时间复杂度" class="headerlink" title="时间复杂度"></a>时间复杂度</h4><p>一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记作 <code>T(n)</code>，它是该算法问题规模 <code>n</code> 的函数，时间复杂度主要分析 <code>T(n)</code> 的数量级。算法中的基本运算（最深层循环内的语句）的频度与 <code>T(n)</code> 同数量级，所以通常釆用算法中基本运算的频度  <code>f(n)</code> 来分析算法的时间复杂度。因此，算法的时间复杂度也记为：<br><code>T(n)=O(f(n))</code></p><p>上式中 <code>O</code> 的含义是 <code>T(n)</code> 的数量级，其严格的数学定义是：若 <code>T(n)</code> 和 <code>f(n)</code> 是定义在正整数集合上的两个函数，则存在正常数 <code>C</code> 和 <code>n_0</code>，使得当 <code>n &gt;= n_0</code> 时，都满足 <code>0 &lt;= T(n) &lt;= C * f(n)</code>。</p><p>注意：取 <code>f(n)</code> 中随 <code>n</code> 增长最快的项将其系数置为 1 作为时间复杂度的度量。例如，<code>fi(n) = a * n^3 + b * n^2 + c * n</code>，则其时间复杂度为 <code>O(n^3)</code>。</p><p>算法的时间复杂度不仅依赖于问题的规模 <code>n</code>，也取决于待输入数据的性质（如输入数据元素的初始状态）。</p><p>一般总是考虑在最坏情况下的时间复杂度，以保证算法的运行时间不会比它更长。</p><p>在分析一个程序的时间复杂性时，有以下两条规则：</p><ol><li>加法规则：<code>T(n) = T1(n) + T2(n) = O(f(n)) + O(g(n)) = O(max(f(n), g(n)))</code></li><li>乘法规则：<code>T(n) = T1(n) * T2(n) = O(f(n)) * O(g(n)) = O( f(n) * g(n) )</code></li></ol><p>常见的渐近时间复杂度有：<code>O(1)&lt;O(log2n)&lt;O(n)&lt;O(nlog2n)&lt;O(n^2)&lt;O(n^3)&lt;O(2^n)&lt;O(n!)&lt;O(n^n)</code></p><h4 id="空间复杂度"><a href="#空间复杂度" class="headerlink" title="空间复杂度"></a>空间复杂度</h4><p>算法的空间复杂度 <code>S(n)</code>，定义为该算法所耗费的存储空间，它是问题规模 <code>n</code> 的函数。渐近空间复杂度也常简称为空间复杂度，记作 <code>S(n)=O(g(n))</code>。</p><p>一个上机程序除了需要存储空间来存放本身所用指令、常数、变量和输入数据外，也需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间，若输入数据所占空间只取决于问题本身，和算法无关，则只需分析除输入和程序之外的额外空间。</p><p>算法原地工作是指算法所需辅助空间是常量，即 <code>O(1)</code>。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="http://c.biancheng.net/cpp/shujujiegou/" target="_blank" rel="external nofollow noopener noreferrer">数据结构C语言版：经典数据结构与算法分析教程</a></li><li><a href="https://zhuanlan.zhihu.com/p/146430058" target="_blank" rel="external nofollow noopener noreferrer">知乎-码匠：数据结构与算法是什么？</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;基本概念&quot;&gt;&lt;a href=&quot;#基本概念&quot; class=&quot;headerlink&quot; title=&quot;基本概念&quot;&gt;&lt;/a&gt;基本概念&lt;/h2&gt;&lt;h3 id=&quot;数据&quot;&gt;&lt;a href=&quot;#数据&quot; class=&quot;headerlink&quot; title=&quot;数据&quot;&gt;&lt;/a&gt;数据&lt;/h
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-数据结构和算法" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E7%AE%97%E6%B3%95/"/>
    
    
      <category term="数据结构" scheme="https://wiki.blanc.site/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
  </entry>
  
  <entry>
    <title>所谓 WSGI</title>
    <link href="https://wiki.blanc.site/archives/7a294715.html"/>
    <id>https://wiki.blanc.site/archives/7a294715.html</id>
    <published>2020-06-24T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="注意"><a href="#注意" class="headerlink" title="注意"></a>注意</h2><p>如果你也想阅读 WSGI 相关的 PEP 规范，建议直接阅读 <a href="https://www.python.org/dev/peps/pep-3333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a>，因为 <a href="https://www.python.org/dev/peps/pep-3333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 对 <a href="https://www.python.org/dev/peps/pep-333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 333</a> 是向下兼容的，也可以说 <a href="https://www.python.org/dev/peps/pep-3333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 是对 <a href="https://www.python.org/dev/peps/pep-333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 333</a> 的补充。</p><h2 id="何为-WSGI？"><a href="#何为-WSGI？" class="headerlink" title="何为 WSGI？"></a>何为 WSGI？</h2><blockquote><p>This document specifies a proposed standard interface between web servers and Python web applications or frameworks, to promote web application portability across a variety of web servers.</p><p>本文档详细描述了一个建议用在 Web 服务器和 Python Web 应用或框架之间的标准接口，以提升 Web 应用在各类 Web 服务器之间的可移植性。</p><p>from <a href="https://www.python.org/dev/peps/pep-3333/#abstract" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a></p></blockquote><p>从 <a href="https://www.python.org/dev/peps/pep-3333/#abstract" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 的这段总结来看，WSGI 就是一个 Python 官方建议用在 Web 服务器和 Python Web 应用框架之间的标准接口。</p><h3 id="何为-Web-服务器"><a href="#何为-Web-服务器" class="headerlink" title="何为 Web 服务器"></a>何为 Web 服务器</h3><p>首先，什么是服务器（server）？<br>一般来说，server 有两重意思：</p><ol><li>有时 server 表示硬件，也就是一台机器，也可称为「主机」；</li><li>更多的时候 server 表示软件程序，这种程序主要用来对外提供某种服务，比如：邮件服务、FTP 服务、数据库服务、网页服务等等。</li></ol><p>作为开发者，一般提到 server 时指的都是后者，即一个长时间运行的软件程序。</p><p>所以，什么是 Web Server？<br>通俗的来讲 Web Server 就是一个提供 Web 服务的应用程序。<br>常见的符合 WSGI 规范的 Web Server 有 uWSGI、gunicorn 等等。</p><h3 id="何为-Web-应用或框架"><a href="#何为-Web-应用或框架" class="headerlink" title="何为 Web 应用或框架"></a>何为 Web 应用或框架</h3><p>Web 框架在如今是比较常见的，比较知名的 Python Web 框架有：Django、Flask、Pyramid等等。反倒是 Web 应用不太常见，（个人理解）一般情况下只有在本地测试的时候会写一些简单的 Python Web 应用，平时的开发大多还是使用开源（或公司内部）的 Web 框架。</p><h2 id="为什么需要-WSGI"><a href="#为什么需要-WSGI" class="headerlink" title="为什么需要 WSGI"></a>为什么需要 WSGI</h2><p>作为一个近两年刚接触到 Python Web 编程的新手，在日常的编程过程中完全没有见过所谓的 WSGI，但是我依然可以写好一个完整的 Web 应用，这是为什么？WSGI 有存在的必要嘛？</p><p>答案肯定是：有存在的必要。</p><p>首先解释一下为什么我在过去两年的过程中没有见过 WSGI 却依旧可以进行 Web 编程：因为现在的大多数框架都已经帮我们将 WSGI 标准封装在框架底层。甚至，我用的 Django REST Framework 框架连 HTTP Request 和 HTTP Response 都帮我封装好了。所以，就算我完全不了解 WSGI 这种偏底层的协议也能够进行日常的 Web 开发。</p><p>那 WSGI 到底解决了什么问题？这个在 <a href="https://www.python.org/dev/peps/pep-3333/#original-rationale-and-goals-from-pep-333" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 中有详细的解释，简单的说一下我的理解：在 WSGI 诞生之前，就已经存在了大量使用 Python 编写的 Web 应用框架，相应的也存在很多 Web 服务器。但是，各个 Python Web 框架和 Python Web 服务器之间不能互相兼容。夸张一点说，在当时如果想要开发一个 Web 框架说不定还得单独为这个框架开发一个 Web 服务器（而且这个服务器别的框架还不能用）。为了解决这一现象 Python 社区提交了 <a href="https://www.python.org/dev/peps/pep-333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 333</a>，正式提出了 WSGI 这个概念。</p><p>简单的理解：只要是兼容 WSGI 的 Web 服务器和 Web 框架就能配套使用。开发服务器的程序员只需要考虑在兼容 WSGI 的情况下如何更好的提升服务器程序的性能；开发框架的程序员只需要考虑在兼容 WSGI 的情况下如何适应尽可能多业务开发逻辑（以上只是举例并非真的这样）。</p><p>WSGI 解放了 Web 开发者的精力让他们可以专注于自己需要关注的事情。</p><h2 id="WSGI-做了什么事情？"><a href="#WSGI-做了什么事情？" class="headerlink" title="WSGI 做了什么事情？"></a>WSGI 做了什么事情？</h2><p>注：为了简练而写成了 WSGI 做了什么事情，实际上 WSGI 只是一个规范并不是实际的代码，准确的来说应该是「符合 WSGI 规范的 Web 体系做了什么事情？」</p><p>上面已经提到，WSGI 通过规范化 Web 框架和 Web 服务器之间的接口，让兼容了 WSGI 的框架和服务器能够自由组合使用……</p><p>所以，WSGI 究竟做了什么，让一切变得如此简单？</p><p>在 <a href="https://www.python.org/dev/peps/pep-3333/#specification-overview" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 中对 WSGI 进行了一段简单的概述，这里我结合看过的 <a href="https://skyline75489.github.io/post/2014-9-8_python-wsgi-learning.html" target="_blank" rel="external nofollow noopener noreferrer">一篇博文</a> 进行简单的概括：</p><p>（简单来说）WSGI 将 Web 分成了三个部分，从上到下分别是：Application/Framework, Middleware 和 Server/Grageway，各个部分之间高度解耦尽可能的做到不互相依赖。</p><ol><li>（通常情况下）客户端（一般为浏览器）会向 Server 发送 HTTP 请求以获取数据。</li><li>符合 WSGI 规范的 Server 在接收到请求之后会调用指定的符合 WSGI 规范的 Web Application，并传入 environ 和 start_response 两个参数（并不强制命名，只是一般会这么命名）。</li><li>Web Application 在接收到请求后会生成一个打包好的 HTTP Response 传给 start_response。</li><li>Server 会将 HTTP Response 进行汇总待请求处理完且没有错误时将整个 HTTP Response 内容返回给客户端。</li></ol><p>Middleware 属于三个部分中最为特别的一个，对于 Server 他是一个 Application，对于 Application 它是一个 Server。通俗的来说就是 Middleware 面对 Server 时能够展现出 Application 应有的特性，而面对 Application 时能够展现出 Server 应有的特性，由于这一特点 Middleware 在整个协议中起到了承上启下的功能。在现实开发过程中，还可以通过嵌套 Middleware 以实现更强大的功能。</p><h2 id="WSGI-是如何工作的？"><a href="#WSGI-是如何工作的？" class="headerlink" title="WSGI 是如何工作的？"></a>WSGI 是如何工作的？</h2><p>通过上一小节能够大概的了解到 WSGI 在一次完整的请求中究竟做了什么。下面再来介绍一下一个完整的 WSGI Web 体系是如何工作的。</p><h3 id="一个符合-WSGI-规范的-Python-Web-项目实例"><a href="#一个符合-WSGI-规范的-Python-Web-项目实例" class="headerlink" title="一个符合 WSGI 规范的 Python Web 项目实例"></a>一个符合 WSGI 规范的 Python Web 项目实例</h3><p>为了方便展示先来构建一个符合 WSGI 规范的 Python Web 项目示例：</p><h4 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h4><p><strong>注：示例基于 Python3</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 本示例代码改自参考文章 5：</span></span><br><span class="line"><span class="comment"># Huang Huang 的博客-翻译项目系列-让我们一起来构建一个 Web 服务器</span></span><br><span class="line"><span class="comment"># /path_to_code/server.py</span></span><br><span class="line"><span class="comment"># Examples of wsgi server</span></span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> socket</span><br><span class="line"></span><br><span class="line"><span class="comment"># 根据系统导入响应的 StringIO 模块</span></span><br><span class="line"><span class="comment"># StringIO：用于文本 I/O 的内存数据流</span></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    <span class="keyword">from</span> io <span class="keyword">import</span> StringIO</span><br><span class="line"><span class="keyword">except</span> ImportError:</span><br><span class="line">    <span class="keyword">from</span> cStringIO <span class="keyword">import</span> StringIO</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">WSGIServer</span><span class="params">(object)</span>:</span></span><br><span class="line">    request_queue_size = <span class="number">1</span>              <span class="comment"># 请求队列长度</span></span><br><span class="line">    address_family = socket.AF_INET     <span class="comment"># 设置地址簇</span></span><br><span class="line">    socket_type = socket.SOCK_STREAM    <span class="comment"># 设置 socket 类型</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, server_address)</span>:</span></span><br><span class="line">        <span class="comment"># Server 初始化方法（构造函数）</span></span><br><span class="line">        <span class="comment"># Create a listening socket</span></span><br><span class="line">        self.listen_socket = listen_socket = socket.socket(</span><br><span class="line">            self.address_family,</span><br><span class="line">            self.socket_type</span><br><span class="line">        )</span><br><span class="line">        <span class="comment"># 设置 socket 允许重复使用 address</span></span><br><span class="line">        listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, <span class="number">1</span>)</span><br><span class="line">        <span class="comment"># Bind 绑定端口</span></span><br><span class="line">        listen_socket.bind(server_address)</span><br><span class="line">        <span class="comment"># Activate 激活</span></span><br><span class="line">        listen_socket.listen(self.request_queue_size)</span><br><span class="line">        <span class="comment"># 获取并记录 server host 和 port</span></span><br><span class="line">        host, port = self.listen_socket.getsockname()[:<span class="number">2</span>]</span><br><span class="line">        self.server_name = socket.getfqdn(host)</span><br><span class="line">        self.server_port = port</span><br><span class="line">        <span class="comment"># Return headers set by Web framework/application</span></span><br><span class="line">        self.headers_set = []</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">set_app</span><span class="params">(self, application)</span>:</span></span><br><span class="line">        <span class="comment"># 将传入的 application 设置为实例属性</span></span><br><span class="line">        self.application = application</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">server_forever</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="comment"># 开启 server 循环函数</span></span><br><span class="line">        listen_socket = self.listen_socket</span><br><span class="line">        <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">            <span class="comment"># 获取 client socket 参数 | client_connection 是 client socket 实例</span></span><br><span class="line">            <span class="comment"># 这里会创建一个阻塞，直到接受到 client 连接为止</span></span><br><span class="line">            self.client_connection, client_address = listen_socket.accept()</span><br><span class="line">            <span class="comment"># 调用 handle_one_request 方法处理一次请求并关闭 client 连接然后继续等待新的连接进入</span></span><br><span class="line">            self.handle_one_request()</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">handle_one_request</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="comment"># 处理请求的入口方法 | 用来处理一次请求</span></span><br><span class="line">        <span class="comment"># 从 client socket 中获取 request data</span></span><br><span class="line">        self.request_data = request_data = self.client_connection.recv(<span class="number">1024</span>)</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 调用 parse_request 方法， 传入接收到的 request_data 并对其进行解析</span></span><br><span class="line">        self.parse_request(request_data)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 通过已有数据构造环境变量字典</span></span><br><span class="line">        environ = self.get_environ()</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 调用 application，传入已经生成好的 environ 和 start_response，返回一个可迭代的 Response 对象</span></span><br><span class="line">        result = self.application(environ, self.start_response)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 调用 finish_response 方法，构造一个响应并返回给客户端</span></span><br><span class="line">        self.finish_response(result)</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">parse_request</span><span class="params">(self, text)</span>:</span></span><br><span class="line">        <span class="comment"># 取行</span></span><br><span class="line">        request_line = text.splitlines()[<span class="number">0</span>]</span><br><span class="line">        <span class="comment"># 打碎请求行到组件中</span></span><br><span class="line">        (self.request_method,</span><br><span class="line">         self.path,</span><br><span class="line">         self.request_version</span><br><span class="line">        ) = request_line.split()</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">get_environ</span><span class="params">(self)</span>:</span></span><br><span class="line">        env = &#123;&#125;</span><br><span class="line">        env[<span class="string">"wsgi.version"</span>] = (<span class="number">1</span>, <span class="number">0</span>)</span><br><span class="line">        env[<span class="string">"wsgi.url_scheme"</span>] = <span class="string">"http"</span></span><br><span class="line">        env[<span class="string">"wsgi.input"</span>] = StringIO(self.request_data.decode(<span class="string">"utf-8"</span>))</span><br><span class="line">        env[<span class="string">"wsgi.errors"</span>] = sys.stderr</span><br><span class="line">        env[<span class="string">"wsgi.multithread"</span>] = <span class="literal">False</span></span><br><span class="line">        env[<span class="string">"wsgi.multiprocess"</span>] = <span class="literal">False</span></span><br><span class="line">        env[<span class="string">"wsgi.run_once"</span>] = <span class="literal">False</span></span><br><span class="line">        <span class="comment"># Required CGI variables</span></span><br><span class="line">        env[<span class="string">"REQUEST_METHOD"</span>] = self.request_method</span><br><span class="line">        env[<span class="string">"PATH_INFO"</span>] = self.path.decode(<span class="string">"utf-8"</span>)</span><br><span class="line">        env[<span class="string">"SERVER_NAME"</span>] = self.server_name</span><br><span class="line">        env[<span class="string">"SERVER_PORT"</span>] = str(self.server_port)</span><br><span class="line">        <span class="keyword">return</span> env</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">start_response</span><span class="params">(self, status, response_headers, exc_info=None)</span>:</span></span><br><span class="line">        <span class="comment"># 按照 WSGI 规范提供一个 start_response 给 application</span></span><br><span class="line">        <span class="comment"># Add necessary必要的 server headers</span></span><br><span class="line">        server_headers = [</span><br><span class="line">            (<span class="string">"Date"</span>, <span class="string">"Tue, 31 Mar 2020 12:51:48 GMT"</span>),</span><br><span class="line">            (<span class="string">"Server"</span>, <span class="string">"WSGIServer 0.2"</span>)</span><br><span class="line">        ]</span><br><span class="line">        self.headers_set = [status, response_headers + server_headers]</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 按照 WSGI 协议，应该在这里返回一个 write()，但这里为了简便就省略了</span></span><br><span class="line">        <span class="comment"># 会在后续分析 wsgiref 源码时提及此处</span></span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">finish_response</span><span class="params">(self, result)</span>:</span></span><br><span class="line">        <span class="comment"># 通过现有参数整理出一个响应体</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            status, response_headers = self.headers_set</span><br><span class="line">            <span class="comment"># 响应第一部分：HTTP 协议以及状态码</span></span><br><span class="line">            response = <span class="string">f"HTTP/1.1 <span class="subst">&#123;status&#125;</span>\r\n"</span></span><br><span class="line">            <span class="comment"># 响应第二部分：将生成好的响应头递归式的传入响应体内</span></span><br><span class="line">            <span class="keyword">for</span> header <span class="keyword">in</span> response_headers:</span><br><span class="line">                response += <span class="string">"&#123;0&#125;: &#123;1&#125;\r\n"</span>.format(*header)</span><br><span class="line">            <span class="comment"># 通过 \r\n 进行空行</span></span><br><span class="line">            response += <span class="string">"\r\n"</span></span><br><span class="line">            <span class="comment"># 响应第三部分：将响应主题信息追加到响应体内</span></span><br><span class="line">            <span class="keyword">for</span> data <span class="keyword">in</span> result:</span><br><span class="line">                response += data</span><br><span class="line">            <span class="comment"># 通过 senall 将响应发送给客户端</span></span><br><span class="line">            <span class="comment"># 注意：在 Python3 下，如果你构建的响应体为 str 类型，需要进行 encode 转换为 bytes</span></span><br><span class="line">            self.client_connection.sendall(response.encode())</span><br><span class="line">        <span class="keyword">finally</span>:</span><br><span class="line">            <span class="comment"># 关闭连接</span></span><br><span class="line">            self.client_connection.close()</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /path_to_code/middleware.py</span></span><br><span class="line"><span class="comment"># Examples of wsgi middleware</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TestMiddleware</span><span class="params">(object)</span>:</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, application)</span>:</span></span><br><span class="line">        self.application = application</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">core</span><span class="params">(self, environ, start_response)</span>:</span></span><br><span class="line">        old_response = self.application(environ, start_response)</span><br><span class="line">        new_response = old_response + [<span class="string">"middleware add this message\n"</span>]</span><br><span class="line">        <span class="keyword">return</span> new_response</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__call__</span><span class="params">(self, environ, start_response)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> self.core(environ, start_response)</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /path_to_code/application.py</span></span><br><span class="line"><span class="comment"># Examples of wsgi application</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">application</span><span class="params">(environ, start_response)</span>:</span></span><br><span class="line">    status = <span class="string">"200 OK"</span></span><br><span class="line">    response_headers = [(<span class="string">"Content-Type"</span>, <span class="string">"text/plain"</span>)]</span><br><span class="line">    start_response(status, response_headers)</span><br><span class="line">    <span class="keyword">return</span> [<span class="string">"hello world from a simple WSGI application!\n"</span>]</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /path_to_code/run.py</span></span><br><span class="line"><span class="comment"># running Example</span></span><br><span class="line"><span class="keyword">from</span> server <span class="keyword">import</span> WSGIServer</span><br><span class="line"><span class="keyword">from</span> application <span class="keyword">import</span> application</span><br><span class="line"><span class="keyword">from</span> middleware <span class="keyword">import</span> TestMiddleware</span><br><span class="line"></span><br><span class="line"><span class="comment"># 规定 server host 和 server port</span></span><br><span class="line">server_address = (host, port) = <span class="string">""</span>, <span class="number">8888</span></span><br><span class="line"><span class="comment"># 创建 server 实例</span></span><br><span class="line">server = WSGIServer(server_address)</span><br><span class="line"><span class="comment"># 设置本 server 对应的 middleware 以及 application</span></span><br><span class="line">server.set_app(TestMiddleware(application))</span><br><span class="line"><span class="comment"># 输出提示性语句</span></span><br><span class="line">print(<span class="string">f"WSGIServer: Serving HTTP on port: <span class="subst">&#123;port&#125;</span>...\n"</span>)</span><br><span class="line"><span class="comment"># 进入 server socket 监听循环</span></span><br><span class="line">server.server_forever()</span><br></pre></td></tr></table></figure><h4 id="运行"><a href="#运行" class="headerlink" title="运行"></a>运行</h4><p>将四段代码分别复制到同一目录的四个文件（如果没有按照示例给出的命名记得更改一下 run 模块中相应的 import 的模块名）中。</p><p>注：以下操作默认你完全按照示例代码中给出的命名进行文件命名</p><ol><li>启动 server：<code>python /path_to_code/run.py</code></li><li>通过浏览器浏览 <code>127.0.0.1:8888</code> 查看效果</li><li>通过 curl 命令 <code>curl -v http://127.0.0.1:8888</code> 查看完整输出</li><li>对比 <code>curl -v https://baidu.com</code> 的输出查看区别</li></ol><h4 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h4><h5 id="代码运行流程分析"><a href="#代码运行流程分析" class="headerlink" title="代码运行流程分析"></a>代码运行流程分析</h5><p>上面我根据 WSGI 协议编写了三个文件（模块）：server.py middleware.py application.py，分别对应 WSGI 里 server middleware application 这三个概念。然后通过 run.py 引入三个模块组成了一个完整的 server-middleware-application Web 程序并监听本地 8888 端口。</p><p>通过 run.py 中的代码我们能够清晰的看到一个 WSGI 类型的 Web 程序的运行流程：</p><ol><li>创建 wsgi server socket 实例对象（调用 <code>server.__init__</code> 方法）</li><li>将准备好的 middleware 以及 application 对象导入给 server 实例（调用 <code>server.set_app</code> 方法）</li><li>运行 server 监听指定端口（调用 <code>server.server_forever</code> 方法）</li></ol><p>通过 server.py 中的代码能够清晰的看到一个 WSGI 类型的 Web 程序是如何处理 HTTP 请求的：</p><ol><li>通过 <code>server_forever</code> <strong>监听到</strong>客户端请求并<strong>记录请求信息</strong></li><li>调用 <code>handle_one_request</code> 方法处理此请求<ol><li>通过请求 socket <strong>获取请求数据</strong></li><li>通过 <code>parse_request</code> 方法将请求数据<strong>解析</strong>成所需格式</li><li>通过 <code>get_environ</code> 方法利用现有数据构造环境变量<strong>字典</strong></li><li>将生成好的 environ 参数和 start_response 方法传给 application 对象（<strong>也可能是 middleware 伪装的 application 对象</strong>），并获取响应结果</li><li>将响应结果传给 <code>finish_response</code> 方法构造一个<strong>可迭代的</strong>响应对象返回给客户端并结束本次请求</li></ol></li></ol><p>通过 middleware.py 中的代码就能够理解一个 WSGI 中间件是如何工作的：</p><ol><li>通过在 <code>__init__</code> 方法中接收一个 application 将自己伪装成一个 server</li><li>通过在 <code>__call__</code> 方法中接收 environ 和 start_response 参数将自己伪装成一个 application<br>通过这两点伪装 middleware 能够很好的粘合在 server 和 application 之间完成中间逻辑处理，在 <a href="https://www.python.org/dev/peps/pep-3333/#middleware-components-that-play-both-sides" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 中指明了中间件的几点常见用途。</li></ol><p>至于 application.py 在这里就真的只是一个简单的单文件 WSGI 应用。当然也可以尝试用写好的 server.py 和 middleware.py 对接像 Django 这样的框架，但需要对代码做一些修改，这里就不展开讨论了，有兴趣可以自己尝试。</p><h5 id="浏览器结果分析"><a href="#浏览器结果分析" class="headerlink" title="浏览器结果分析"></a>浏览器结果分析</h5><p>在运行 run.py 之后使用浏览器浏览 <code>127.0.0.1:8888</code> 并查看结果如下：</p><p><img src="https://img.blanc.site//wiki/img/20200630114909.png" alt="浏览器结果 1"><br><img src="https://img.blanc.site//wiki/img/20200630114908.png" alt="浏览器结果 2"><br><img src="https://img.blanc.site//wiki/img/20200630114910.png" alt="浏览器结果 3"></p><p>通过控制台可以清晰地看到响应头和响应主体的内容是符合我们预期的</p><h5 id="curl-结果分析"><a href="#curl-结果分析" class="headerlink" title="curl 结果分析"></a>curl 结果分析</h5><p>通过 <code>curl http://127.0.0.1:8888</code> 可以看到响应主体：</p><p><img src="https://img.blanc.site//wiki/img/20200630115755.png" alt="curl 结果 1"></p><p>通过 <code>curl -v http://127.0.0.1:8888</code> 可以看到详细的请求和响应内容：</p><p><img src="https://img.blanc.site//wiki/img/20200630115754.png" alt="curl 结果 2"></p><p>通过 <code>curl -v https://baidu.com</code> 获取百度首页的响应内容以作比较：</p><p><img src="https://img.blanc.site//wiki/img/20200630115753.png" alt="curl 结果 3"></p><p>可以看到目前浏览网页常用的正常请求要比自己构建的测试示例要复杂的多，这也是为什么经常使用 Web 框架而非单文件应用来处理这些请求的原因。</p><h2 id="解读-PEP-3333-中的某些细节"><a href="#解读-PEP-3333-中的某些细节" class="headerlink" title="解读 PEP-3333 中的某些细节"></a>解读 PEP-3333 中的某些细节</h2><p><a href="https://www.python.org/dev/peps/pep-3333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 我只读到了 <a href="https://www.python.org/dev/peps/pep-3333/#buffering-and-streaming" target="_blank" rel="external nofollow noopener noreferrer">Buffering and Streaming</a> 章节，并且没能很好的理解此章节所描述的东西，因此在下面的细节分析中大都是此章节之前的一些内容。</p><h3 id="可迭代对象和可调用对象"><a href="#可迭代对象和可调用对象" class="headerlink" title="可迭代对象和可调用对象"></a>可迭代对象和可调用对象</h3><p>可迭代对象（callable）和可迭代对象（iterable）在 PEP 3333 中最常见的两个词汇，在 WSGI 规范中它们分别代表：实现了 <code>__call__</code> 的对象和实现了 <code>__iter__</code> 的对象。</p><h3 id="Unicode-bytes-str"><a href="#Unicode-bytes-str" class="headerlink" title="Unicode | bytes | str"></a>Unicode | bytes | str</h3><p>这是一组比较基础的概念：</p><ol><li>Unicode 是一种字符编码标准</li><li>bytes 和 str 是 Python 中两种不同的数据类型</li></ol><p>Python3 中字符串的默认类型是 str，在内存中以 Unicode 表示。如果要在网络中传输或保存为磁盘文件，需要将 str 转换为 bytes 类型。</p><h4 id="Unicode-UCS-UTF"><a href="#Unicode-UCS-UTF" class="headerlink" title="Unicode | UCS | UTF"></a>Unicode | UCS | UTF</h4><ol><li>Unicode（万国码、国际码、统一码、单一码）是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码，使得电脑可以用更为简单的方式来呈现和处理文字。Unicode 伴随着通用字符集的标准而发展，同时也以书本的形式对外发表。</li><li>UCS（Universal Character Set，通用字符集）是由ISO制定的ISO 10646（或称ISO/IEC 10646）标准所定义的标准字符集。</li><li>UTF（Unicode Transformation Format），Unicode 定义了两种映射方式：一种叫 the Unicode Transformation Format (UTF) 编码, 还有一种叫 Universal Character Set (UCS) 编码。一种编码映射一定范围（可能是子集）的 Unicode 码点（code points ）成代码值（code value）的序列。编码名字后面的数字代表一个代码值的位数（UTF使用位数，UCS 使用字节数）,UTF-8 和UTF-16是最常使用的编码。</li></ol><h4 id="bytes-str"><a href="#bytes-str" class="headerlink" title="bytes | str"></a>bytes | str</h4><blockquote><p>Python3 里面的 str 是在内存中对文本数据进行使用的，bytes 是对二进制数据使用的。</p><p>str 可以 encode 为 bytes，但是 bytes 不一定可以 decode 为 tr。实际上 <code>bytes.decode(‘latin1’)</code> 可以称为 str，也就是说 decode 使用的编码决定了 <code>decode()</code> 的成败，同样的，UTF-8 编码的 bytes 字符串用 GBK 去 <code>decode()</code> 也会出错。</p><p>bytes一般来自网络读取的数据、从二进制文件（图片等）读取的数据、以二进制模式读取的文本文件(.txt, .html, .py, .cpp等)</p><p>from <a href="https://zhuanlan.zhihu.com/p/56901822" target="_blank" rel="external nofollow noopener noreferrer">知乎-猿人学-Python 3 中str 和 bytes 的区别</a></p></blockquote><h4 id="WSGI-中的-String"><a href="#WSGI-中的-String" class="headerlink" title="WSGI 中的 String"></a>WSGI 中的 String</h4><p>WSGI 中规定了两种 String：</p><ol><li>Native String（常说的 str）用来表示 request/response headers and metadata</li><li>ByteString（Python3 中用 byte type 来表示）用于 request/response 的 body（例如：PUT/POST 输入和 HTML 页面输出）</li></ol><p>在 <a href="https://www.python.org/dev/peps/pep-3333/#unicode-issues" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a> 中有对这部分的详细说明。</p><h3 id="三个主要组成部件"><a href="#三个主要组成部件" class="headerlink" title="三个主要组成部件"></a>三个主要组成部件</h3><p>了解了以上基础概念之后再具体的看一下 WSGI 的三个主要组成部件：</p><h4 id="Application-Framework-下文简称-application"><a href="#Application-Framework-下文简称-application" class="headerlink" title="Application/Framework | 下文简称 application"></a>Application/Framework | 下文简称 application</h4><ol><li>application 是一个<strong>必须且只能</strong>接收两个参数的 callable，形如 <code>application(environ, start_response)</code>。而且这两个参数只能以<strong>位置参数</strong>的形式被传入。</li><li>environ 和 start_response 只是习惯性命名，对于具体传入的对象名称没有做要求。</li><li>application 必须可被<strong>多次调</strong>用，因为所有的 server/gateway（CGI 除外）都会发出此类的重复请求。</li><li><a href="https://www.python.org/dev/peps/pep-3333/#environ-variables" target="_blank" rel="external nofollow noopener noreferrer">environ 是一个字典参数</a>，包含了 CGI 风格的环境变量。必须使用<strong>内置的 Python 字典类型</strong>（不能是子类或自定义的 UserDict），并且允许 application <strong>以任何它想要的方式修改</strong>。字典还包括某些 <strong>WSGI 变量</strong>，并且还可能包括 <strong>server 特定的拓展参数</strong>，它们的命名需要遵守<strong>相应规范</strong>。</li><li>start_response 参数也是一个 callable，接收两个必要的未知参数和一个可选参数，三个参数依次默认命名为：status, response_headers, exc_info，即 <code>start_response(status, response_headers, exc_info=None)</code>。</li><li>status 是一个状态<strong>字符串</strong>（str），例如：<code>&quot;200 OK&quot;</code></li><li>response_headers 是一个描述 HTTP Response Headers 的 (header_name, header_value) <strong>元组列表</strong>。</li><li>可选参数 exc_info 只有当 application 捕获到错误并且视图向浏览器（客户端）显示时才会调用。</li><li>start_response callable 必须返回一个 write(body_data) callable，这个 callable 需要一个位置参数：一个要作为 HTTP 响应体一部分的 bytestring（<strong>注意：wirte callabel 只是为了支持某些现有框架的必要输出 API 而提供的；如果可以避免的话，新的 application/gateway 应该避免使用它</strong>）。</li><li>当 callable（<strong>如果实现了 write 这个 callable 指的就是 write；如果没有，这个 callable 指的就是 start_response 本身</strong>）被 server 调用时，<strong>必须</strong>返回一个产生零个或多个字符串的 iterable。可以通过多种方式实现，如：一个字符串列表、application 是一个 generator 函数或 application 是一个实现了 <code>__iter__</code> 的对象。<strong>无论如何</strong>，application 必须返回一个能够产生零个或多个字符串 iterable。</li><li>application 应该负责确保被写入的字符串是<strong>适合</strong> client 的格式的。</li><li>如果 <code>len(iterable)</code> 能够被成功执行（这里的 iterable 指的是第 10 条中的 iterable）则其返回的必须是<strong>一个 server 能够信赖的结果</strong>。也就是说 application 返回的 iterable 如果提供了一个有效的 <code>__len__</code> 方法就必须能够获得<strong>准确值</strong>。</li><li>如果 application 返回的 iterable 有 close 方法，server <strong>必须</strong>在当前请求<strong>完成后</strong>调用它，无论请求是否正常完成（为了支持 application 释放资源）。</li><li>application 应该检查其所需要的变量是否存在并对变量不存在的情况做好处理方案。</li></ol><h4 id="Server-Gateway-下文简称-server"><a href="#Server-Gateway-下文简称-server" class="headerlink" title="Server/Gateway | 下文简称 server"></a>Server/Gateway | 下文简称 server</h4><ol><li>server 必须以无缓冲（unbuffered）的方式将 yielded bytestrings 传输到 client，在下一次请求之前完成每一个 bytestring 的传输。换句话说 application 应该自己实现缓存。（对于这部分我理解的不是很透彻，大多都是直译的 <a href="https://www.python.org/dev/peps/pep-3333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333</a>）</li><li>server 不能直接使用 application 返回的 iterable 的其他属性。</li><li>server 应该尽可能多的提供 CGI 变量。</li><li>符合 WSGI 规范的 server 应该记录所提供的变量。</li></ol><h4 id="Middleware"><a href="#Middleware" class="headerlink" title="Middleware"></a>Middleware</h4><ol><li>middleware 是一个单独的对象，可能在一些 application 中扮演 server 同时在一些 server 中扮演 application。</li></ol><h3 id="WSGI-中的坑"><a href="#WSGI-中的坑" class="headerlink" title="WSGI 中的坑"></a>WSGI 中的坑</h3><ol><li>要确定在那些地方使用 str，在那些地方使用 bytes</li></ol><h2 id="Python-wsgiref-官方库源码分析"><a href="#Python-wsgiref-官方库源码分析" class="headerlink" title="Python wsgiref 官方库源码分析"></a>Python wsgiref 官方库源码分析</h2><p>可以参考我的开源库 <a href="https://github.com/ryomahan/read-python" target="_blank" rel="external nofollow noopener noreferrer">read-python</a> 中 practices/for_wsgiref 目录下的 <a href="https://github.com/ryomahan/read-python/blob/master/practices/for_wsgiref/server.py" target="_blank" rel="external nofollow noopener noreferrer">server.py</a> 文件。</p><p>在这个文件中我提取了 Python wsgiref 官方库的必要代码汇聚成一个文件实现了一个和 <code>wsgiref.WSGIServer</code> 大致同样功能的 <code>WSGIServer</code> 类。</p><p>Python wsgiref 官方库对 WSGI 规范的实现更加抽象，加上一些历史原因使得代码分布在多个官方库中，我在抽离代码的过程中学到了很多但是同样也产生了很多困惑，我在源码中使用 <code>TODO 疑惑 XXX</code> 的形式将我的困惑表达出来了，如果你感兴趣并且恰好知道解决我疑惑的方法，欢迎直接给我的代码仓库提交 Issues。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://www.python.org/dev/peps/pep-333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 333 Python Web Server Gateway Interface v1.0</a></li><li><a href="https://www.python.org/dev/peps/pep-3333/" target="_blank" rel="external nofollow noopener noreferrer">PEP 3333 Python Web Server Gateway Interface v1.0.1</a></li><li><a href="https://zhuanlan.zhihu.com/p/22544725" target="_blank" rel="external nofollow noopener noreferrer">知乎-方应杭-「每日一题」什么是 Web 服务器（server）</a></li><li><a href="https://skyline75489.github.io/post/2014-9-8_python-wsgi-learning.html" target="_blank" rel="external nofollow noopener noreferrer">Skyline75489-Python WSGI学习笔记</a></li><li><a href="https://mozillazg.com/tag/rang-wo-men-yi-qi-lai-gou-jian-yi-ge-web-fu-wu-qi.html" target="_blank" rel="external nofollow noopener noreferrer">Huang Huang 的博客-翻译项目系列-让我们一起来构建一个 Web 服务器</a></li><li><a href="https://juejin.im/post/5ccb8bb8f265da03981fd577" target="_blank" rel="external nofollow noopener noreferrer">掘金- liaochangjiang-Python Web开发：开发wsgi中间件</a></li><li><a href="https://zh.wikipedia.org/wiki/Unicode" target="_blank" rel="external nofollow noopener noreferrer">维基百科-Unicode</a></li><li><a href="https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%AD%97%E7%AC%A6%E9%9B%86#Unicode%E5%92%8CISO_10646%E7%9A%84%E5%85%B3%E7%B3%BB" target="_blank" rel="external nofollow noopener noreferrer">维基百科-通用字符集</a></li><li><a href="https://zhuanlan.zhihu.com/p/56901822" target="_blank" rel="external nofollow noopener noreferrer">知乎-猿人学-Python 3 中str 和 bytes 的区别</a></li><li><a href="https://docs.python.org/zh-cn/3/glossary.html" target="_blank" rel="external nofollow noopener noreferrer">Python 官方文档-术语对照表</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;注意&quot;&gt;&lt;a href=&quot;#注意&quot; class=&quot;headerlink&quot; title=&quot;注意&quot;&gt;&lt;/a&gt;注意&lt;/h2&gt;&lt;p&gt;如果你也想阅读 WSGI 相关的 PEP 规范，建议直接阅读 &lt;a href=&quot;https://www.python.org/dev/pep
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Python" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Python/"/>
    
    
      <category term="Python" scheme="https://wiki.blanc.site/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>Shell 区分不同 Unix 系统</title>
    <link href="https://wiki.blanc.site/archives/b422aa7d.html"/>
    <id>https://wiki.blanc.site/archives/b422aa7d.html</id>
    <published>2020-04-29T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="区分-macOS-和-Linux"><a href="#区分-macOS-和-Linux" class="headerlink" title="区分 macOS 和 Linux"></a>区分 macOS 和 Linux</h2><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">OS=`uname <span class="literal">-s</span>`</span><br><span class="line"><span class="keyword">if</span> [ <span class="variable">$</span>&#123;<span class="type">OS</span>&#125; == <span class="string">"Darwin"</span>  ];then</span><br><span class="line">    sudo brew install git</span><br><span class="line">elif [ <span class="variable">$</span>&#123;<span class="type">OS</span>&#125; == <span class="string">"Linux"</span>  ];then</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><h2 id="区分不同-Linux-系统"><a href="#区分不同-Linux-系统" class="headerlink" title="区分不同 Linux 系统"></a>区分不同 Linux 系统</h2><p>在 <code>Linux</code> 系统中有一个记录 OS 的发行版本的 <code>os-release</code> 文件，位置在 <code>/etc/os-release</code>。可以利用 <code>source /etc/os-release</code> 将文件中的 <code>key-value</code> 数据导入到上下文中，然后通过不同系统 <code>ID</code> 值不同的特性进行区分。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">source /etc/os<span class="literal">-release</span></span><br><span class="line">case <span class="variable">$ID</span> <span class="keyword">in</span></span><br><span class="line">    debian|ubuntu|devuan)</span><br><span class="line">        sudo apt<span class="literal">-get</span> install git</span><br><span class="line">        ;;</span><br><span class="line">    centos|fedora|rhel)</span><br><span class="line">        yumdnf=<span class="string">"yum"</span></span><br><span class="line">        <span class="keyword">if</span> test <span class="string">"<span class="variable">$</span>(echo "</span><span class="variable">$VERSION_ID</span> &gt;= <span class="number">22</span><span class="string">" | bc)"</span> <span class="operator">-ne</span> <span class="number">0</span>; then</span><br><span class="line">            yumdnf=<span class="string">"dnf"</span></span><br><span class="line">        fi</span><br><span class="line">        sudo <span class="variable">$yumdnf</span> install <span class="literal">-y</span> git</span><br><span class="line">        ;;</span><br><span class="line">    *)</span><br><span class="line">        <span class="keyword">exit</span> <span class="number">1</span></span><br><span class="line">        ;;</span><br><span class="line">esac</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://wxnacy.com/2017/11/24/get-os-name/" target="_blank" rel="external nofollow noopener noreferrer">温欣爸比：如何判断 Unix 系统 OS 版本</a></li><li><a href="http://billie66.github.io/TLCL/book/chap32.html" target="_blank" rel="external nofollow noopener noreferrer">The Linux Command Line：流程控制：case 分支</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;区分-macOS-和-Linux&quot;&gt;&lt;a href=&quot;#区分-macOS-和-Linux&quot; class=&quot;headerlink&quot; title=&quot;区分 macOS 和 Linux&quot;&gt;&lt;/a&gt;区分 macOS 和 Linux&lt;/h2&gt;&lt;figure class=&quot;hi
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Shell" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Shell/"/>
    
    
      <category term="Shell" scheme="https://wiki.blanc.site/tags/Shell/"/>
    
  </entry>
  
  <entry>
    <title>实现 echo 不换行输出</title>
    <link href="https://wiki.blanc.site/archives/dbac95ae.html"/>
    <id>https://wiki.blanc.site/archives/dbac95ae.html</id>
    <published>2020-04-28T00:00:00.000Z</published>
    <updated>2024-11-10T15:39:52.936Z</updated>
    
    <content type="html"><![CDATA[<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p><code>echo</code> 命令用于在 <code>shell</code> 中打印 <code>shell</code> 变量的值，或者直接输出指定的字符串。</p><p>语法：<code>echo [SHORT-OPTION]... [STRING]...</code></p><h2 id="详情"><a href="#详情" class="headerlink" title="详情"></a>详情</h2><h3 id="SHORT-OPTION"><a href="#SHORT-OPTION" class="headerlink" title="SHORT-OPTION"></a>SHORT-OPTION</h3><ol><li><code>-n</code>：不输出换行</li><li><code>-e</code>：开启对反斜线转移的解释</li><li><code>-E</code>：取消对反斜线转义的解释（默认开启）</li></ol><h3 id="常见反斜线转义"><a href="#常见反斜线转义" class="headerlink" title="常见反斜线转义"></a>常见反斜线转义</h3><table><thead><tr><th>表达方式</th><th>含义</th></tr></thead><tbody><tr><td><code>\a</code></td><td>发出警告声</td></tr><tr><td><code>\b</code></td><td>删除前一个字符</td></tr><tr><td><code>\c</code></td><td>最后不加上换行符号</td></tr><tr><td><code>\f</code></td><td>换行但光标仍旧停留在原来的位置</td></tr><tr><td><code>\n</code></td><td>换行且光标移至行首</td></tr><tr><td><code>\r</code></td><td>光标移至行首，但不换行</td></tr><tr><td><code>\t</code></td><td>插入 <code>tab</code></td></tr><tr><td><code>\v</code></td><td>与 <code>\f</code> 相同</td></tr><tr><td><code>\\</code></td><td>插入 <code>\</code> 字符</td></tr><tr><td><code>\nnn</code></td><td>插入 <code>nnn</code>（八进制）所代表的 <code>ASCII</code> 字符</td></tr></tbody></table><h2 id="具体问题"><a href="#具体问题" class="headerlink" title="具体问题"></a>具体问题</h2><h3 id="实现-echo-不换行输出"><a href="#实现-echo-不换行输出" class="headerlink" title="实现 echo 不换行输出"></a>实现 echo 不换行输出</h3><h4 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h4><ol><li>编写 <code>shell</code> 脚本编写用户输入提示</li></ol><h4 id="方法一：利用参数-e"><a href="#方法一：利用参数-e" class="headerlink" title="方法一：利用参数 -e"></a>方法一：利用参数 -e</h4><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># test.sh</span></span><br><span class="line">echo <span class="literal">-e</span> <span class="string">"hello world\c"</span></span><br></pre></td></tr></table></figure><h4 id="方法二：利用参数-n"><a href="#方法二：利用参数-n" class="headerlink" title="方法二：利用参数 -n"></a>方法二：利用参数 -n</h4><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># test.sh</span></span><br><span class="line">echo <span class="literal">-n</span> <span class="string">"hello world"</span></span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://blog.csdn.net/lllxy/article/details/3423580" target="_blank" rel="external nofollow noopener noreferrer">lllxy：echo不换行的实现</a></li><li><code>man echo</code></li><li><a href="https://man.linuxde.net/echo" target="_blank" rel="external nofollow noopener noreferrer">Linux 命令大全-echo 命令</a></li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;简介&quot;&gt;&lt;a href=&quot;#简介&quot; class=&quot;headerlink&quot; title=&quot;简介&quot;&gt;&lt;/a&gt;简介&lt;/h2&gt;&lt;p&gt;&lt;code&gt;echo&lt;/code&gt; 命令用于在 &lt;code&gt;shell&lt;/code&gt; 中打印 &lt;code&gt;shell&lt;/code&gt; 变量的值
      
    
    </summary>
    
    
      <category term="3-计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="1-理论计算机科学" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    
      <category term="4-编程语言和编译器" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
      <category term="Shell" scheme="https://wiki.blanc.site/categories/3-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/1-%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/4-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E5%92%8C%E7%BC%96%E8%AF%91%E5%99%A8/Shell/"/>
    
    
      <category term="Shell" scheme="https://wiki.blanc.site/tags/Shell/"/>
    
  </entry>
  
</feed>
