无知的 tonyseekhttps://blog.tonyseek.com/2019-03-26T03:17:00+08:00矩阵的四个基础子空间2019-03-26T03:17:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2019-03-26:post/linear-algebra-subspace-of-matrix/<p>这是十周年重修工科数学系列课程之… 线性代数基础。重修这件事,有效缓解了焦虑和失眠,让自己觉得,生活除了吃饭睡觉和<strike>想象空间愈发匮乏的</strike>工作,还是有一些其他内容的。</p>
<div class="section" id="id2">
<h2>向量空间和其(线性)子空间的定义</h2>
<p>线性空间需要满足加法封闭和数乘封闭:</p>
<dl class="docutils">
<dt>加法封闭(可加性)</dt>
<dd>空间内任意两向量相加(减),结果必须还在空间中:<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>a</mi><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mi>n</mi></msup><mo separator="true">,</mo><mi>b</mi><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mi>n</mi></msup><mo>⇒</mo><mo>(</mo><mi>a</mi><mo>+</mo><mi>b</mi><mo>)</mo><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mi>n</mi></msup></mrow><annotation encoding="application/x-tex">a \in \mathbb{R}^n, b \in \mathbb{R}^n \Rightarrow (a+b) \in \mathbb{R}^n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mord mathdefault">a</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.8888799999999999em;vertical-align:-0.19444em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">n</span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathdefault">b</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.68889em;vertical-align:0em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">n</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">⇒</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathdefault">a</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.68889em;vertical-align:0em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">n</span></span></span></span></span></span></span></span></span></span></span>
</dd>
<dt>数乘封闭(齐次性)</dt>
<dd>空间内任意向量乘以标量,结果必须还在空间中:<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>a</mi><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mi>n</mi></msup><mo separator="true">,</mo><mi>λ</mi><mo>∈</mo><mi>F</mi><mo>⇒</mo><mi>λ</mi><mi>a</mi><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mi>n</mi></msup></mrow><annotation encoding="application/x-tex">a \in \mathbb{R}^n, \lambda \in F \Rightarrow {\lambda}a \in \mathbb{R}^n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mord mathdefault">a</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.8888799999999999em;vertical-align:-0.19444em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">n</span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathdefault">λ</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault" style="margin-right:0.13889em;">F</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">⇒</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.73354em;vertical-align:-0.0391em;"></span><span class="mord"><span class="mord mathdefault">λ</span></span><span class="mord mathdefault">a</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.68889em;vertical-align:0em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">n</span></span></span></span></span></span></span></span></span></span></span>
</dd>
</dl>
<p>注意“数乘封闭”隐含了: 线性空间必须包括零向量(当 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>λ</mi><mo>=</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">\lambda = 0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.69444em;vertical-align:0em;"></span><span class="mord mathdefault">λ</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.64444em;vertical-align:0em;"></span><span class="mord">0</span></span></span></span>
时);因为线性变换不研究坐标系原点的移动(假设所有向量都从同一原点出发)。</p>
<p>确立一个线性空间子空间需要知道:</p>
<ul class="simple">
<li>空间的维数(假设为 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">n</span></span></span></span>
)</li>
<li>空间中一组 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">n</span></span></span></span>
个线性无关的向量,称为基(basis)</li>
</ul>
<p>线性空间的两个封闭要求刚好对应了向量的两种线性组合方式,所以可以理解子空间是由基向量所有的线性组合张成(span)的向量空间。</p>
</div>
<div class="section" id="id3">
<h2>基的正交标准化</h2>
<p>空间的基不要求正交和模长为 1,但是如果它满足这两点,会更方便计算。对应的术语是正交化(orthogonal)和标准化(normal),两点都具备时可以用合成词正交标准化(orthonormal)称呼。</p>
<p>有意思的是,我阅读了一些书籍和维基百科,得知根据以上向量空间的定义,“长度”和“夹角”并不是一个向量空间的内禀特性。我们需要额外引入“内积”的定义,即让两个向量点乘之后得到一个标量,才借此产生了模长和夹角,进而可以定义什么是标准化(模长为 1)和正交化(夹角垂直)。引入了内积的线性空间称为内积空间。</p>
<p>当然高维空间不能像二维或者三维空间那样直观地想象长度、夹角乃至夹角垂直,但是可以根据向量和自己的内积来定义模长的平方,根据两向量内积为 0 来定义垂直。</p>
<p>代数方法研究向量空间往往不强依赖坐标系统,但是如果引入坐标轴,套用二维或者三维空间的几何意义,那么最常见的一组标准正交基就是坐标轴上的单位向量。以其为列向量合成矩阵,即得到仅对角线为 1 其他位置都是 0 的方阵——单位矩阵(identity matrix)。</p>
</div>
<div class="section" id="id4">
<h2>矩阵的四个基本子空间的定义</h2>
<p>先要<strong>非正式地</strong>定义一下矩阵的秩:秩(rank)即矩阵最大线性无关组中包含的向量数 —— 刨除所有因为和其他向量线性相关,而在作为基向量的时候对张成空间的维数“毫无贡献”的向量之后,剩下的有效向量数目。</p>
<p>无论是否是方阵,都可以根据行向量或者列向量是否线性无关,来得到行秩和列秩。行秩一定等于列秩,不在这里证明这一点。但知道这一点时,仅取行秩或列秩就可以知道矩阵的秩。</p>
<p>假设 A 是 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>m</mi><mo>×</mo><mi>n</mi></mrow><annotation encoding="application/x-tex">m \times n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.66666em;vertical-align:-0.08333em;"></span><span class="mord mathdefault">m</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">n</span></span></span></span>
的矩阵,其秩为 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>r</mi></mrow><annotation encoding="application/x-tex">r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault" style="margin-right:0.02778em;">r</span></span></span></span>
:</p>
<dl class="docutils">
<dt>列空间(column space)</dt>
<dd><p class="first">A 中所有列向量张成的子空间,维度是 r,记作:</p>
<p class="last"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>C</mi><mo>(</mo><mi>A</mi><mo>)</mo><mo>=</mo><mi>S</mi><mi>p</mi><mi>a</mi><mi>n</mi><mo>{</mo><mi>v</mi><mo>=</mo><msubsup><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>m</mi></msubsup><msub><mi>λ</mi><mi>i</mi></msub><msub><mi>c</mi><mi>i</mi></msub><mi mathvariant="normal">∣</mi><mi>v</mi><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mi>r</mi></msup><mo separator="true">,</mo><mi>λ</mi><mo>∈</mo><mi>F</mi><mo separator="true">,</mo><msub><mi>c</mi><mi>i</mi></msub><mo>∈</mo><mtext>column vectors of A</mtext><mo>}</mo></mrow><annotation encoding="application/x-tex">C(A) = Span\{v = \sum_{i=1}^m{\lambda_i}{c_i}|v \in \mathbb{R^{r}}, \lambda \in F, c_i \in \text{column vectors of A} \}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.07153em;">C</span><span class="mopen">(</span><span class="mord mathdefault">A</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.05764em;">S</span><span class="mord mathdefault">p</span><span class="mord mathdefault">a</span><span class="mord mathdefault">n</span><span class="mopen">{</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1.104002em;vertical-align:-0.29971000000000003em;"></span><span class="mop"><span class="mop op-symbol small-op" style="position:relative;top:-0.0000050000000000050004em;">∑</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.804292em;"><span style="top:-2.40029em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathdefault mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2029em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">m</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.29971000000000003em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord"><span class="mord"><span class="mord mathdefault">λ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span><span class="mord"><span class="mord"><span class="mord mathdefault">c</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span><span class="mord">∣</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.8888799999999999em;vertical-align:-0.19444em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathdefault mtight" style="margin-right:0.02778em;">r</span></span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathdefault">λ</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.8777699999999999em;vertical-align:-0.19444em;"></span><span class="mord mathdefault" style="margin-right:0.13889em;">F</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord"><span class="mord mathdefault">c</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">column vectors of A</span></span><span class="mclose">}</span></span></span></span>
</p>
</dd>
<dt>行空间(row space)</dt>
<dd><p class="first">A 中所有行向量张成的子空间,维度是 r,记作:</p>
<p class="last"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>C</mi><mo>(</mo><msup><mi>A</mi><mi>T</mi></msup><mo>)</mo><mo>=</mo><mi>S</mi><mi>p</mi><mi>a</mi><mi>n</mi><mo>{</mo><mi>v</mi><mo>=</mo><msubsup><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></msubsup><msub><mi>λ</mi><mi>i</mi></msub><msub><mi>r</mi><mi>i</mi></msub><mi mathvariant="normal">∣</mi><mi>v</mi><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mi>r</mi></msup><mo separator="true">,</mo><mi>λ</mi><mo>∈</mo><mi>F</mi><mo separator="true">,</mo><msub><mi>r</mi><mi>i</mi></msub><mo>∈</mo><mtext> row vectors of A</mtext><mo>}</mo></mrow><annotation encoding="application/x-tex">C(A^T) = Span\{v = \sum_{i=1}^n{\lambda_i}{r_i}|v \in \mathbb{R^{r}}, \lambda \in F, r_i \in \text{ row vectors of A} \}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0913309999999998em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.07153em;">C</span><span class="mopen">(</span><span class="mord"><span class="mord mathdefault">A</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413309999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight" style="margin-right:0.13889em;">T</span></span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.05764em;">S</span><span class="mord mathdefault">p</span><span class="mord mathdefault">a</span><span class="mord mathdefault">n</span><span class="mopen">{</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1.104002em;vertical-align:-0.29971000000000003em;"></span><span class="mop"><span class="mop op-symbol small-op" style="position:relative;top:-0.0000050000000000050004em;">∑</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.804292em;"><span style="top:-2.40029em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathdefault mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2029em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">n</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.29971000000000003em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord"><span class="mord"><span class="mord mathdefault">λ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span><span class="mord"><span class="mord"><span class="mord mathdefault" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:-0.02778em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span><span class="mord">∣</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.8888799999999999em;vertical-align:-0.19444em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathdefault mtight" style="margin-right:0.02778em;">r</span></span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathdefault">λ</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.8777699999999999em;vertical-align:-0.19444em;"></span><span class="mord mathdefault" style="margin-right:0.13889em;">F</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord"><span class="mord mathdefault" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:-0.02778em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord"> row vectors of A</span></span><span class="mclose">}</span></span></span></span>
</p>
</dd>
<dt>零空间(nullspace)</dt>
<dd><p class="first">以 A 为系数矩阵的齐次线性方程组的所有非平凡解集张成的子空间,说人话就是“能令 Av = 0 的所有非零 v”:</p>
<p class="last"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>N</mi><mo>(</mo><mi>A</mi><mo>)</mo><mo>=</mo><mi>S</mi><mi>p</mi><mi>a</mi><mi>n</mi><mo>{</mo><mi>v</mi><mi mathvariant="normal">∣</mi><mi>v</mi><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mrow><mi>m</mi><mo>−</mo><mi>r</mi></mrow></msup><mo separator="true">,</mo><mi>A</mi><mi>v</mi><mo>=</mo><mn>0</mn><mo>}</mo></mrow><annotation encoding="application/x-tex">N(A) = Span\{v|v \in \mathbb{R}^{m - r}, Av = 0\}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.10903em;">N</span><span class="mopen">(</span><span class="mord mathdefault">A</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.05764em;">S</span><span class="mord mathdefault">p</span><span class="mord mathdefault">a</span><span class="mord mathdefault">n</span><span class="mopen">{</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mord">∣</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.9657709999999999em;vertical-align:-0.19444em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.771331em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathdefault mtight">m</span><span class="mbin mtight">−</span><span class="mord mathdefault mtight" style="margin-right:0.02778em;">r</span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathdefault">A</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">0</span><span class="mclose">}</span></span></span></span>
</p>
</dd>
<dt>左零空间(left nullspace)</dt>
<dd><p class="first">类似列空间和行空间的关系,左零空间是 A 的转置的零空间;“左零”的说法来自其定义中矩阵是“左乘”到向量上的,而不是习惯上的右乘:</p>
<p class="last"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>N</mi><mo>(</mo><msup><mi>A</mi><mi>T</mi></msup><mo>)</mo><mo>=</mo><mi>S</mi><mi>p</mi><mi>a</mi><mi>n</mi><mo>{</mo><mi>v</mi><mi mathvariant="normal">∣</mi><mi>v</mi><mo>∈</mo><msup><mi mathvariant="double-struck">R</mi><mrow><mi>m</mi><mo>−</mo><mi>r</mi></mrow></msup><mo separator="true">,</mo><msup><mi>v</mi><mi>T</mi></msup><mi>A</mi><mo>=</mo><mn>0</mn><mo>}</mo></mrow><annotation encoding="application/x-tex">N(A^T) = Span\{v|v \in \mathbb{R}^{m - r}, v^TA = 0\}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0913309999999998em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.10903em;">N</span><span class="mopen">(</span><span class="mord"><span class="mord mathdefault">A</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413309999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight" style="margin-right:0.13889em;">T</span></span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.05764em;">S</span><span class="mord mathdefault">p</span><span class="mord mathdefault">a</span><span class="mord mathdefault">n</span><span class="mopen">{</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mord">∣</span><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1.035771em;vertical-align:-0.19444em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.771331em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathdefault mtight">m</span><span class="mbin mtight">−</span><span class="mord mathdefault mtight" style="margin-right:0.02778em;">r</span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord"><span class="mord mathdefault" style="margin-right:0.03588em;">v</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413309999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight" style="margin-right:0.13889em;">T</span></span></span></span></span></span></span></span><span class="mord mathdefault">A</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">0</span><span class="mclose">}</span></span></span></span>
</p>
</dd>
</dl>
</div>
<div class="section" id="id5">
<h2>非奇异(可逆)矩阵的子空间</h2>
<p>矩阵非奇异(可逆)的充分必要条件是矩阵的行、列数都等于其秩,即满秩矩阵。无论是行向量之间,还是列向量之间,都线性无关——矩阵中的每一行都为行空间贡献了维度,每一列都为列空间贡献了维度。</p>
<p>正因如此,非奇异矩阵的零空间和左零空间就成了零维的——其中只包含零向量(参考定义中关于子空间维数)。非正式地说法可以是“非奇异矩阵没有零空间和左零空间”。</p>
<p>实际上两个行列空间和两个零空间构成某种补集关系,见下节。</p>
</div>
<div class="section" id="id6">
<h2>行列空间和零空间的正交补关系</h2>
<img alt="Four linear subspaces of a matrix" src="/images/four-linear-subspaces.png" />
<p>这张出处不详的图,很好地描述了四个基本子空间的关系。维系这个关系的关键动作,在于矩阵被用于线性变换(线性映射)。</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi><mi>x</mi><mo>=</mo><mi>b</mi></mrow><annotation encoding="application/x-tex">Ax = b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span><span class="mord mathdefault">x</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.69444em;vertical-align:0em;"></span><span class="mord mathdefault">b</span></span></span></span></span>
<p>上述线性方程组的矩阵写法,可以诠释为矩阵 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
对(作为未知数集的)<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">x</span></span></span></span>
向量进行了线性变换,产生了向量 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.69444em;vertical-align:0em;"></span><span class="mord mathdefault">b</span></span></span></span>
。</p>
<p>这个变换的过程中,向量 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">x</span></span></span></span>
来自 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
的行空间或零空间(左侧的两个空间),变化后到达列空间。因为按照矩阵乘法的定义,<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.69444em;vertical-align:0em;"></span><span class="mord mathdefault">b</span></span></span></span>
一定是 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
中各列按照 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">x</span></span></span></span>
的线性组合(或者说以 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
中各列为基,<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">x</span></span></span></span>
的坐标表示)。</p>
<p>对于非奇异矩阵,因为它是满秩的,行空间足以张成整个 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msup><mi mathvariant="double-struck">R</mi><mi>n</mi></msup></mrow><annotation encoding="application/x-tex">\mathbb{R}^n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68889em;vertical-align:0em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">n</span></span></span></span></span></span></span></span></span></span></span>
空间,列空间足以张成整个 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msup><mi mathvariant="double-struck">R</mi><mi>m</mi></msup></mrow><annotation encoding="application/x-tex">\mathbb{R}^m</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68889em;vertical-align:0em;"></span><span class="mord"><span class="mord"><span class="mord mathbb">R</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight">m</span></span></span></span></span></span></span></span></span></span></span>
空间(假设 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
是 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>m</mi><mo>×</mo><mi>n</mi></mrow><annotation encoding="application/x-tex">m \times n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.66666em;vertical-align:-0.08333em;"></span><span class="mord mathdefault">m</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">n</span></span></span></span>
的矩阵),那么行空间中所有向量一定可以一一映射到列空间中,行空间中零向量也只映射到列空间的零向量上。</p>
<p>对于奇异矩阵,因为行空间中存在浑水摸鱼的向量,其只能张成 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>r</mi></mrow><annotation encoding="application/x-tex">r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault" style="margin-right:0.02778em;">r</span></span></span></span>
维空间(<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>r</mi><mo><</mo><mi>n</mi></mrow><annotation encoding="application/x-tex">r < n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span><span class="mord mathdefault" style="margin-right:0.02778em;">r</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">n</span></span></span></span>
),剩下的 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>n</mi><mo>−</mo><mi>r</mi></mrow><annotation encoding="application/x-tex">n - r</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.66666em;vertical-align:-0.08333em;"></span><span class="mord mathdefault">n</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault" style="margin-right:0.02778em;">r</span></span></span></span>
维构成了零空间,零空间中的向量可能不是零,但在 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
的线性变换作用下却映射到列空间的零向量上,即方程说的 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi><mi>x</mi><mo>=</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">Ax = 0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span><span class="mord mathdefault">x</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.64444em;vertical-align:0em;"></span><span class="mord">0</span></span></span></span>
的非平凡(<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">x</span></span></span></span>
不为 0 的)解。</p>
<p>也就是说,零空间中的任何向量,和 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
的任何一个行向量的内积都是 0,按照内积空间的定义,它们正交。整个零空间和整个行空间构成正交补的关系。引用 <a class="reference external" href="http://zhangyet.github.io/">Dante 老师</a>的话:</p>
<blockquote>
行空间的正交可以理解:它的 nullspace 就是跟行空间垂直的向量组成的</blockquote>
<p>在上面这个变化过程中,左零空间没有参与。其实可以将 A 转置来得出类似于“行空间正交于零空间”的结论,列空间也正交于左零空间。</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><msup><mi>x</mi><mi>T</mi></msup><mi>A</mi><mo>=</mo><msup><mi>b</mi><mi>T</mi></msup></mrow><annotation encoding="application/x-tex">x^TA = b^T</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8913309999999999em;vertical-align:0em;"></span><span class="mord"><span class="mord mathdefault">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8913309999999999em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight" style="margin-right:0.13889em;">T</span></span></span></span></span></span></span></span><span class="mord mathdefault">A</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.8913309999999999em;vertical-align:0em;"></span><span class="mord"><span class="mord mathdefault">b</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8913309999999999em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathdefault mtight" style="margin-right:0.13889em;">T</span></span></span></span></span></span></span></span></span></span></span></span>
<p>这个变换其实是将右乘的线性变换两边转置,以交换 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
的行和列。此时行向量 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">x</span></span></span></span>
存在于列空间中或左零空间中。若 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">x</span></span></span></span>
存在于列空间,则在矩阵左乘的变换下映射到行空间中的 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.69444em;vertical-align:0em;"></span><span class="mord mathdefault">b</span></span></span></span>
;若 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathdefault">x</span></span></span></span>
存在于左零空间中,则在矩阵左乘的变换下映射到零向量。</p>
</div>
<div class="section" id="id7">
<h2>其他的称呼</h2>
<p>矩阵可以表示线性变换(一般习惯还是用右乘,即矩阵在左向量在右),在语境为线性变换的时候,列空间和零空间有别的称呼:</p>
<ul class="simple">
<li>列空间称为线性变换的“像”(image)</li>
<li>行空间称为线性变换的“原像”(preimage)</li>
<li>零空间称为线性变换的“核”(kernel)</li>
</ul>
<p>非常的形象了,零空间变换后是要坍缩成一个点(零向量)的,不就是“核”么。</p>
</div>
<div class="section" id="id8">
<h2>用到左零空间的地方</h2>
<p>看起来左零空间在(正向的)线性变换中根本不出现,而且只是列空间的正交补,比较没有意思。</p>
<p>不过往后学习一点,还是会用到的。默认在定义矩阵的逆的时候,只对可逆的方阵(非奇异)才有意义,因为可逆矩阵的零空间真的是个零(只有零向量),那么行空间中每个向量都可以一一映射到列空间中,自然映射也就可逆——找出逆矩阵,将原矩阵列空间中的向量再映射回行空间。</p>
<p>对于奇异矩阵,因为零空间的存在,零空间中的非零向量会被矩阵映射到零向量上。那么无论如何,我们都没法再找出一个矩阵,能把零向量映射回原零空间中的非零向量了——变成零后信息已经丢失了。这也是“不可逆”的原因。</p>
<p>但是对于不可逆的矩阵(可以是奇异方阵,甚至可以是长方矩阵),可以定义其“伪逆”(pseudo-inverse),即在正向变换的时候,仍然是行空间映射到列空间,零空间映射到零向量;逆向变换的时候,将列空间映射回行空间,左零空间则映射到零向量。</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi><mi>x</mi><mo>=</mo><mi>b</mi><mtext> </mtext><mi>o</mi><mi>r</mi><mtext> </mtext><mn>0</mn><mtext> then </mtext><msup><mi>A</mi><mo>+</mo></msup><mi>b</mi><mo>=</mo><mi>x</mi><mtext> </mtext><mi>o</mi><mi>r</mi><mtext> </mtext><mn>0</mn></mrow><annotation encoding="application/x-tex">Ax = b\ or\ 0 \text{ then } A^+b = x\ or\ 0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span><span class="mord mathdefault">x</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.821331em;vertical-align:0em;"></span><span class="mord mathdefault">b</span><span class="mspace"> </span><span class="mord mathdefault">o</span><span class="mord mathdefault" style="margin-right:0.02778em;">r</span><span class="mspace"> </span><span class="mord">0</span><span class="mord text"><span class="mord"> then </span></span><span class="mord"><span class="mord mathdefault">A</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.821331em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mbin mtight">+</span></span></span></span></span></span></span></span><span class="mord mathdefault">b</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.64444em;vertical-align:0em;"></span><span class="mord mathdefault">x</span><span class="mspace"> </span><span class="mord mathdefault">o</span><span class="mord mathdefault" style="margin-right:0.02778em;">r</span><span class="mspace"> </span><span class="mord">0</span></span></span></span></span>
<p>嗯… 就是接受有的分量(在左零空间)中会丢失(退化)这件事,因为反正它们也是从零空间来的。所以伪逆的概念里蕴含了某种投影的思想…</p>
<p>这个在最小二乘法里比较有用,因为反复收集数据很可能因为数据重复导致矩阵奇异。这个时候我们希望“浑水摸鱼的向量就不要出声”,求伪逆当作逆用就好。</p>
<p>求伪逆的方法,其中一种是对矩阵 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
做奇异值分解(SVD),将其变成两个正交矩阵和一个对角矩阵的乘积。正交矩阵一定可逆,其伪逆就是其逆;对角矩阵如果对角线不含零元素(对角矩阵也可逆),求逆时取对角线元素的倒数即可;不可逆矩阵的 SVD 中,对角矩阵一定含有零元素,那么求伪逆时只对非零元素取倒数,零元素仍然取零(对应左零空间仍然映射到零向量),得出对角矩阵伪逆。再将三个逆和伪逆重新合成,得到 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi></mrow><annotation encoding="application/x-tex">A</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathdefault">A</span></span></span></span>
的伪逆。</p>
</div>
<div class="section" id="id9">
<h2>线性方程组的通解和特解</h2>
<p>线性方程组的通解为什么等于特解加上零空间的向量,就可以解释了。因为零空间中的向量在线性变换中中只产生零向量,根据线性变换基本法:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>T</mi><mo>(</mo><mi>a</mi><mo>+</mo><mi>b</mi><mo>)</mo><mo>=</mo><mi>T</mi><mo>(</mo><mi>a</mi><mo>)</mo><mo>+</mo><mi>T</mi><mo>(</mo><mi>b</mi><mo>)</mo></mrow><annotation encoding="application/x-tex">T(a + b) = T(a) + T(b)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.13889em;">T</span><span class="mopen">(</span><span class="mord mathdefault">a</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.13889em;">T</span><span class="mopen">(</span><span class="mord mathdefault">a</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.13889em;">T</span><span class="mopen">(</span><span class="mord mathdefault">b</span><span class="mclose">)</span></span></span></span></span>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>T</mi><mo>(</mo><mi>a</mi><mo>+</mo><mi>b</mi><mo>)</mo><mo>=</mo><mi>T</mi><mo>(</mo><mi>a</mi><mo>)</mo><mtext> </mtext><mi>i</mi><mi>f</mi><mtext> </mtext><mi>T</mi><mo>(</mo><mi>b</mi><mo>)</mo><mo>=</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">T(a + b) = T(a)\ \ if\ \ T(b) = 0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.13889em;">T</span><span class="mopen">(</span><span class="mord mathdefault">a</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathdefault" style="margin-right:0.13889em;">T</span><span class="mopen">(</span><span class="mord mathdefault">a</span><span class="mclose">)</span><span class="mspace"> </span><span class="mspace"> </span><span class="mord mathdefault">i</span><span class="mord mathdefault" style="margin-right:0.10764em;">f</span><span class="mspace"> </span><span class="mspace"> </span><span class="mord mathdefault" style="margin-right:0.13889em;">T</span><span class="mopen">(</span><span class="mord mathdefault">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.64444em;vertical-align:0em;"></span><span class="mord">0</span></span></span></span></span>
<p>来自零空间的 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.69444em;vertical-align:0em;"></span><span class="mord mathdefault">b</span></span></span></span>
对线性变换没有贡献,带上它才能在“值域”不变的情况,让“定义域”完整。</p>
</div>
<div class="section" id="id10">
<h2>参考资料</h2>
<ul class="simple">
<li><a class="reference external" href="https://ocw.mit.edu/courses/mathematics/18-06-linear-algebra-spring-2010/">MIT-Open-Course-Ware 18.06 by Prof. Gilbert Strang</a></li>
<li><a class="reference external" href="https://book.douban.com/subject/26824921/">Introduction to Linear Algebra</a></li>
<li><a class="reference external" href="https://book.douban.com/subject/1425950/">Linear Algebra and Its Applications</a></li>
<li><a class="reference external" href="https://zh.wikipedia.org/wiki/%E8%A1%8C%E7%A9%BA%E9%97%B4%E4%B8%8E%E5%88%97%E7%A9%BA%E9%97%B4">中文维基百科:行空间与列空间</a></li>
<li><a class="reference external" href="https://zh.wikipedia.org/wiki/%E9%9B%B6%E7%A9%BA%E9%97%B4">中文维基百科:零空间</a></li>
</ul>
</div>
GraphQL 和服务化随想碎片2018-09-02T17:33:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2018-09-02:post/graphql-and-microservices/<p>我大概 2015 年时候对 GraphQL 有过<a class="reference external" href="https://blog.tonyseek.com/post/graphql-vs-restful/">一些想法</a>,当时还在从事用户产品研发,想法主要还是围绕业务公网 API 的。现在我的工作有很长一段时间是围绕服务化相关问题,视角也就有所改变了。加之重温了<a class="reference external" href="https://github.com/PyConChina/PyConChina2016/blob/c2cd009/src/data/_speakers.yaml#L388-L399">华翼老师当年的 slide</a>,产生了一些随想。很碎,但是记录下来。</p>
<div class="section" id="id2">
<h2>优点</h2>
<ul class="simple">
<li>将数据获取需求一步送到服务端,减少 IO 往返次数</li>
<li>涉及数据过滤、剪裁时,使用简单(和 SQL 类似),下游可以较少考虑 RPC 开销优化之类的问题</li>
<li>灵活,下游无须为了新需求而请求上游排期开发、部署特定接口,自己就可以搞掂(同时也是缺点)</li>
</ul>
</div>
<div class="section" id="id3">
<h2>缺点</h2>
<ul class="simple">
<li>接口语义模糊,太过于“通用”,抽象泄漏</li>
<li>难以设计服务端缓存(无法预知查询语句)</li>
<li>难以治理(完全由用户定制查询类型,服务端难以做监控、告警、QoS 和保护熔断等)</li>
<li>IDL 不明确,向后兼容困难(不知道下游是怎么用的)</li>
</ul>
</div>
<div class="section" id="id4">
<h2>折衷方案</h2>
<p>首先要有 service mesh with sidecar(不能是 smart sdk 形式的)。</p>
<p>在 sidecar 上引入类似 API Gateway 的方式,允许服务提供方使用 GraphQL 设计接口,对内(后端服务)剪裁过滤数据处理查询,对外(调用方)暴露的是业务语义明确的简单
RPC 接口。</p>
<p>下游业务需求有变动时,可以在不变更 codebase 的情况下,单独向 sidecar
发布新增或者修改的 RPC 接口以及背后的 GraphQL 查询语句。</p>
</div>
<div class="section" id="id5">
<h2>公网出口</h2>
<p>针对公网出口(提供客户端接入)而非内网 SOA 的情况,设计部署一个 API Gatway
比让客户端用 GraphQL 更好。理由同上。</p>
<p>RESTful API 在公网出口上有着许许多多的优点,相应的开发调试工具链、软件库和中间件也都很成熟。很多文章阐述了这点,这里就不再重复。</p>
</div>
<div class="section" id="id6">
<h2>识别拐点</h2>
<p>评估引入这样一套机制的 ROI 时,除了看业务类型(内容主导?交易主导?),还有就是看业务现在的实际状况和短期未来的发展趋势。</p>
<p>下游的查询需求真的有这么多样化、变更频繁吗?如果不是,针对下游业务需求定制
RPC 接口,比丢一个万能的 GraphQL 接口给下游好。</p>
<p>查询真的有这么复杂吗?机房内网往返次数带来的延时放大真的有这么明显吗?业务本身真的对延时这么敏感吗?如果都不是,设计细粒度、正交的 RPC
接口(业务原语)由下游自行组装定制更好。</p>
<p>针对内网服务(SOA),如果公司研发人数超过 1000 且组织架构复杂,那么“治理”的优先级应该远远高于“优化”的,人灾带来的复杂度比技术灾影响概念更长远、更难事后优化挽回,因此也就更需要未雨绸缪。</p>
<p>即使是“折衷方案”,也在拐点靠前一点的位置做变更,不要为不存在或者还有很久才会来的需求做设计,后者很可能会随着时间推进演变为前者。</p>
</div>
TCP TIME-WAIT 笔记 - 概览2017-02-03T17:18:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2017-02-03:post/tcp-tw-overview/<p>这是我阅读 Vincent Bernat 的 <a class="reference external" href="https://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux">Coping with the TCP TIME-WAIT state on busy Linux
servers</a>
一文整理的笔记。虽然是消化过再表述, 但是仍然一篇(阅读 2 分钟)写不完的感觉,
所以先把概览整理了出来, 即 "<em>TIME-WAIT</em> 是个啥" 。</p>
<p>首先得放个 TCP 状态机图:</p>
<img alt="TCP 状态机" src="https://www.ibm.com/support/knowledgecenter/SSLTBW_2.1.0/com.ibm.zos.v2r1.halu101/dwgl0004.gif" />
<div class="section" id="time-wait">
<h2>怎样的连接会进入 TIME-WAIT</h2>
<p>首先观 TCP 状态机可得到一点: 被动断开连接的一方结束连接前只会依次进入
<em>CLOSE-WAIT</em> 和 <em>LAST-ACK</em> 两个状态, <em>TIME-WAIT</em>
<strong>永远只出现在主动断开连接的一方</strong> 。此外, 非正常断开连接的方式 (例如发送
<em>RST</em> 断开连接) 不经历四次握手, 也不会有 <em>TIME-WAIT</em> 状态。</p>
</div>
<div class="section" id="id1">
<h2>TIME-WAIT 的连接占用了什么</h2>
<p>一个 TCP 连接由四元组确立(源 IP、源端口、目标 IP、目标端口),
所以同一个四元组结束一个连接期间如果进入了 <em>TIME-WAIT</em> 状态,
那么同一四元组当然是不能再被用于新的连接——直到回归 <em>CLOSED</em> 状态。</p>
<p>所以, <em>TIME-WAIT</em> 状态的连接至少占用了四元组。</p>
</div>
<div class="section" id="id2">
<h2>为什么要有 TIME-WAIT</h2>
<p>然后就是为什么要设计这个 <em>TIME-WAIT</em> 状态, 有两个原因:</p>
<p>首先, <em>TIME-WAIT</em> 持续时间为 <tt class="docutils literal">2*MSL</tt>, 其中 <tt class="docutils literal">MSL</tt> 即 <a class="reference external" href="https://en.wikipedia.org/wiki/Maximum_segment_lifetime">TCP 分段的最大寿命</a>,
等待了这段时间再中止连接可以确保新的连接复用同一四元组时
<strong>不会收到前一个连接的分段</strong> 。为什么经历了 <em>FIN-ACK</em>
握手还会有前一个连接的分段乱入呢?因为传输层之下是网络层,
同一连接的分段可能经由不同的路由被传输。如果一个分段因为拥堵等原因延迟了,
对等实体并不知道, 没等到 <em>ACK</em> 就简单重发了事。而姗姗来迟的分段如果没有经历
<em>TIME-WAIT</em> 这段时间而被蒸发掉, 完全是有可能乱入到新的连接中的。</p>
<p>其次, 被动关闭连接一方发出 <em>FIN</em> 之后会进入 <em>LAST-ACK</em> 状态。如果最后一个 <em>ACK</em>
丢失, 主动关闭连接一方的 <em>TIME-WAIT</em> 占住了坑, 就留下了足够的时间让对方补发
<em>FIN</em>, 或最终超出最大重试次数(最大重试时间)而双方结束连接。如果不占这个坑,
有可能被动关闭方还停留在 <em>LAST-ACK</em> 状态时主动方就结束了连接,
同一四元组再被建立起新连接的时候, 三次握手的 <em>SYN</em> 将被处于 <em>LAST-ACK</em>
状态的(前)被动关闭连接方以 <em>RST</em> 回应, 造成“这个端口坏掉了,
刚握手就被重置连接”的现象。</p>
<p>所以默认情况下, 还必须得保留这个 <em>TIME-WAIT</em> 状态且为足料的 <tt class="docutils literal">2*MSL</tt> 时长。</p>
</div>
ZooKeeper Session ID 的组成2016-11-10T19:17:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2016-11-10:post/zookeeper-sessionid/<p>ZooKeeper 的 Session ID 是每个会话的唯一标识,记录为一个 64 位的长整型。
只要对应会话未被服务器端主动关闭,它就一定能唯一关联上。且会话初次建立时
Session ID 会被写入 transaction log,这意味着会话是跨节点共享的。
客户端和一个节点断开连接后,只要会话还没过期,就仍然可以使用相同的 Session ID
连到另一个节点。这个会话创建的所有临时 ZNodes 也会相应保留。</p>
<p>指标显示 ZooKeeper 经常出现大量 outstanding requests (即节点上排队待处理的请求)
时,我们就需要诊断大量请求来自哪个 ZooKeeper 节点。无论是 leader、follower 还是
observer 节点都在本地处理读请求而将写请求转发给 leader 发起投票,所以节点自身的
outstanding requests 指标可能因参与投票而不是处理本节点的请求而飙高,
从而不一定能准确体现出请求来源。这个时候从 transaction log 中找出大量请求的
Session ID 就成了一种有效的诊断方法。</p>
<p>ZooKeeper 的官方文档非常惜墨,介绍了 Session ID 的作用而没有介绍其组成。
所以只能从源码看:</p>
<figure class="code"><figcaption><span>./src/org/apache/zookeeper/server/SessionTrackerImpl.java</span></figcaption><div class="highlight"><pre> <span class="c1">// ...</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">long</span> <span class="nf">initializeNextSession</span><span class="o">(</span><span class="kt">long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">long</span> <span class="n">nextSid</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="n">nextSid</span> <span class="o">=</span> <span class="o">(</span><span class="n">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o"><<</span> <span class="mi">24</span><span class="o">)</span> <span class="o">>>></span> <span class="mi">8</span><span class="o">;</span>
<span class="n">nextSid</span> <span class="o">=</span> <span class="n">nextSid</span> <span class="o">|</span> <span class="o">(</span><span class="n">id</span> <span class="o"><<</span><span class="mi">56</span><span class="o">);</span>
<span class="k">return</span> <span class="n">nextSid</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// ...</span>
<span class="k">this</span><span class="o">.</span><span class="na">nextSessionId</span> <span class="o">=</span> <span class="n">initializeNextSession</span><span class="o">(</span><span class="n">sid</span><span class="o">);</span>
<span class="c1">// ...</span>
</pre></div>
</figure><p>很显然了,Session ID 的 64 个位的布局是这样的:</p>
<table border="1" class="docutils">
<colgroup>
<col width="25%" />
<col width="25%" />
<col width="25%" />
<col width="25%" />
</colgroup>
<tbody valign="top">
<tr><th class="stub">字段 (高位到低位)</th>
<td>sid</td>
<td>时间戳</td>
<td>空白</td>
</tr>
<tr><th class="stub">长度</th>
<td>8</td>
<td>40</td>
<td>16</td>
</tr>
</tbody>
</table>
<p>其中时间戳部分为会话创建时间被截断的低 40 位,而 sid 就是 ZooKeeper 节点的
<tt class="docutils literal">myid</tt> 数字。对比 <tt class="docutils literal">zoo.cfg</tt> 中的成员列表,就能找出会话创建时所在的
ZooKeeper 节点了。</p>
<p>借助社区第三方实现的 Python 脚本 <a class="reference external" href="https://github.com/phunt/zk-txnlog-tools">zk-txnlog-tools</a> 可以很容易地在 ZooKeeper 的
transaction log 中找出历史操作和相应的 Session ID, 结合 shell 工具过滤之后再从
Session ID 抽出 <tt class="docutils literal">myid</tt> 汇总即得到每个节点的操作连接数。</p>
<figure class="code"><figcaption><span>example.py</span></figcaption><div class="highlight"><pre> <span class="kn">import</span> <span class="nn">collections</span>
<span class="n">sessionid_list</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'sessionid_list.txt'</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">)</span>
<span class="n">counter</span> <span class="o">=</span> <span class="n">collections</span><span class="o">.</span><span class="n">Counter</span><span class="p">(</span>
<span class="nb">int</span><span class="p">(</span><span class="n">sessionid</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span> <span class="o">>></span> <span class="mi">56</span> <span class="k">for</span> <span class="n">sessionid</span> <span class="ow">in</span> <span class="n">sessionid_list</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">counter</span><span class="p">)</span> <span class="c"># output: Counter({1: 1171, 2: 1442, 3: 541})</span>
</pre></div>
</figure><p>嗯… 很惭愧,只写了一点微小的姿势,谢谢大家。</p>
GraphQL vs RESTful API 的一些想法2016-09-17T02:25:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2016-09-17:post/graphql-vs-restful/<p>嗯其实又是一篇早就写了的碎碎念, 在我的 <a class="reference external" href="https://simplenote.com">simplenote</a> 里躺了有一年了。也发出来充数吧,好显得我并没有把这个博客弃坑…</p>
<p>GraphQL 出来的时候似乎有老司机说它会“重新定义后端”,当然我们心里知道这实际上是重新定义了重新定义。综合官方文章来看,它要解决的问题都很明确 —— 我认为在 HTTP 1.1 时代它是很有工业价值的。然而它并非唯一的解决方案,RESTful API + HTTP/2 就是一种不错的备选。</p>
<div class="section" id="id1">
<h2>一个对比 不一定对</h2>
<div class="section" id="id2">
<h3>细粒度请求的开销</h3>
<blockquote>
Fetching complicated object graphs require multiple round trips between the client and server to render single views. For mobile applications operating in variable network conditions, these multiple roundtrips are highly undesirable.</blockquote>
<p>这是 HTTP/2 要解决的问题。自 SPDY 作为先锋试水之后,HTTP 2 已标准化。其不仅解决了首部开销、多路复用等 HTTP 1.1 面临的问题,还做到了完全向后兼容。主流 Web Server (Apache httpd、Nginx 等)均已实现或计划实现 HTTP 2 的支持。所以在 H2 得到推广应用可以预期的背景下,细粒度请求的问题将是有解的。</p>
</div>
<div class="section" id="id3">
<h3>字段不可选</h3>
<blockquote>
Invariably fields and additional data are added to REST endpoints as the system requirements change. However, old clients also receive this additional data as well, because the data fetching specification is encoded on the server rather than the client.</blockquote>
<p>这个实际上是一种字段协商的需求,可以通过 HTTP API 首部添加 <tt class="docutils literal"><span class="pre">X-Accept-Fields</span></tt> 之类的方法解决。</p>
<p>另外按照 RESTful API 的设计风格,资源应该倾向于细粒度的。如果粒度够细,似乎也不用多一个字段少一个字段地斤斤计较了。</p>
</div>
<div class="section" id="id4">
<h3>标识弱类型</h3>
<blockquote>
REST endpoints are usually weakly-typed and lack machine-readable metadata.</blockquote>
<p>如果资源对应的是细粒度的 API,那么 URI 中能出现的只有资源的标识而已。似乎区别标识的类型并没有很大的意义?</p>
</div>
</div>
<div class="section" id="id5">
<h2>一个想法 不一定对</h2>
<p>我觉得 GraphQL 在解决 Facebook 自己的需求层面自然是有其实用意义的,但它并不适合作为一个通用解决方案或作为长期的标准。原因无外乎和很多资源查询语言一样 —— 它造了 HTTP 的轮子。</p>
<p>HTTP 无状态、以 URI 定位资源是有其道理的,针对这个特点可以非常容易实现对应用层透明的缓存。而 GraphQL 重新定义了资源的定位方式,相应的空缺需要重新补上。</p>
<p>当然,GraphQL 本身如果作为数据查询的 DSL,和 React.js 等结合将会很大提高前端开发的清爽度。实质上把 GraphQL 翻译成标准 RESTful API 访问是可行的。当 HTTP 2 得以广泛应用的一天,F2E 圈也许可以透明地将 RESTful API 策略放在 GraphQL 编译器的后面。而服务器端仍然能利用 RESTful API 的优势。</p>
</div>
<div class="section" id="id6">
<h2>其他顺手搜到的资源</h2>
<p><a class="reference external" href="http://jsonapi.org">json:api</a> 非常类似 Linked Data 应用的一种表现层标准。</p>
</div>
Git 中的状态2016-09-17T01:17:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2016-09-17:post/git-status-note/<p>嗯,这是去年 lttxzmj 初学 Git 的时候写给她的小纸条。两年没写博客了,发出来充个数…</p>
<div class="section" id="id1">
<h2>有哪些状态</h2>
<dl class="docutils">
<dt>untracked</dt>
<dd>版本库里以前不存在,现在新增加且还没有 <tt class="docutils literal">git add</tt> 过的文件</dd>
<dt>unstaged</dt>
<dd>版本库里以前存在,现在修改了但没有 <tt class="docutils literal">git add</tt> 的文件</dd>
<dt>staged</dt>
<dd><dl class="first last docutils">
<dt>所有 <tt class="docutils literal">git add</tt> 过的文件,可以是:</dt>
<dd><ul class="first last simple">
<li>版本库里以前不存在,现在新增且 <tt class="docutils literal">git add</tt> 了的文件,</li>
<li>也可以是版本库里以前存在,现在修改了且 <tt class="docutils literal">git add</tt> 了的文件</li>
</ul>
</dd>
</dl>
</dd>
</dl>
<p>当然还有针对 <tt class="docutils literal">merge</tt> / <tt class="docutils literal">rebase</tt> / <tt class="docutils literal"><span class="pre">cherry-pick</span></tt> 时的一些中间状态存在,
例如 <strong>both modified</strong> 状态。初学的时候可以不必关注。</p>
</div>
<div class="section" id="id2">
<h2>提交的是什么</h2>
<p>下次 <tt class="docutils literal">git commit</tt> 的时候,提交的是 <strong>staged</strong> 状态的修改。</p>
<p>同一个文件可能同时有两部分修改,一部分是 <strong>staged</strong> 状态,另一部分是
<strong>unstaged</strong> 状态。其中 <strong>unstaged</strong> 状态是上次 <tt class="docutils literal">git add</tt> 之后作出的还没有再次
<tt class="docutils literal">git add</tt> 的修改。</p>
</div>
<div class="section" id="id3">
<h2>怎么查看修改</h2>
<ul class="simple">
<li><tt class="docutils literal">git diff</tt> 可以看到 <strong>unstaged</strong> 的修改</li>
<li><tt class="docutils literal">git diff <span class="pre">--cached</span></tt> 可以看到 <strong>staged</strong> 的修改</li>
<li><strong>untracked</strong> 的修改无法通过原生 Git 命令看到</li>
</ul>
<p><a class="reference external" href="https://github.com/jonas/tig">tig</a> 是一个拥有终端 GUI 的 Git 历史查看工具,
其中可以看到 <strong>untracked</strong> 状态的修改。</p>
</div>
告别过去的 2014 年2014-12-31T21:30:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2014-12-31:post/goodbye-2014/<p>其实我不想写,不如就这样吧。</p>
杀死 subprocess.Popen 的子子孙孙2014-11-09T21:15:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2014-11-09:post/kill-the-descendants-of-subprocess/<p>Python 标准库 <tt class="docutils literal">subprocess.Popen</tt> 是 shellout 一个外部进程的首选,它在
Linux/Unix 平台下的实现方式是 <tt class="docutils literal">fork</tt> 产生子进程然后 <tt class="docutils literal">exec</tt>
载入外部可执行程序。</p>
<p>于是问题就来了,如果我们需要一个类似“夹具”的子进程(比如运行 Web
集成测试的时候跑起来的那个被测试 Server),
那么就需要在退出上下文的时候清理现场,也就是结束被跑起来的子进程。</p>
<p>最简单粗暴的做法可以是这样:</p>
<figure class="code"><figcaption><span>process_fixture.py</span></figcaption><div class="highlight"><pre> <span class="nd">@contextlib.contextmanager</span>
<span class="k">def</span> <span class="nf">process_fixture</span><span class="p">(</span><span class="n">shell_args</span><span class="p">):</span>
<span class="n">proc</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">Popen</span><span class="p">(</span><span class="n">shell_args</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">yield</span>
<span class="k">finally</span><span class="p">:</span>
<span class="c"># 无论是否发生异常,现场都是需要清理的</span>
<span class="n">proc</span><span class="o">.</span><span class="n">terminate</span><span class="p">()</span>
<span class="n">proc</span><span class="o">.</span><span class="n">wait</span><span class="p">()</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
<span class="k">with</span> <span class="n">process_fixture</span><span class="p">([</span><span class="s">'python'</span><span class="p">,</span> <span class="s">'SimpleHTTPServer'</span><span class="p">,</span> <span class="s">'8080'</span><span class="p">])</span> <span class="k">as</span> <span class="n">proc</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">'pid </span><span class="si">%d</span><span class="s">'</span> <span class="o">%</span> <span class="n">proc</span><span class="o">.</span><span class="n">pid</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">urllib</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="s">'http://localhost:8080'</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">())</span>
</pre></div>
</figure><p>那个 <tt class="docutils literal">proc.wait()</tt> 是不可以偷懒省掉的,否则如果子进程被中止了而父进程继续运行,
子进程就会一直占用 <tt class="docutils literal">pid</tt> 而成为僵尸,直到父进程也中止了才被托孤给 <tt class="docutils literal">init</tt> 清理掉。</p>
<p>这个简单粗暴版对简单的情况可能有效,但是被运行的程序可能没那么听话。被运行程序可能会再
<tt class="docutils literal">fork</tt> 一些子进程来工作,自己则只当监工 —— 这是不少 Web Server 的做法。
对这种被运行程序如果简单地 <tt class="docutils literal">terminate</tt>,也即对其 <tt class="docutils literal">pid</tt> 发 <tt class="docutils literal">SIGTERM</tt>,
那就相当于谋杀了监工进程,真正的工作进程也就因此被托孤给 <tt class="docutils literal">init</tt>,变成畸形的守护进程……
嗯没错,这就是我一开始遇到的问题,CI Server 上明明已经中止了 Web Server
进程了,下一轮测试跑起来的时候端口仍然是被占用的。</p>
<p>这个问题稍微有点棘手,因为自从被运行程序 <tt class="docutils literal">fork</tt> 以后,产生的子进程都享有独立的进程空间和
<tt class="docutils literal">pid</tt>,也就是它超出了我们触碰的范围。好在 <tt class="docutils literal">subprocess.Popen</tt> 有个
<tt class="docutils literal">preexec_fn</tt> 参数,它接受一个回调函数,并在 <tt class="docutils literal">fork</tt> 之后 <tt class="docutils literal">exec</tt>
之前的间隙中执行它。我们可以利用这个特性对被运行的子进程做出一些修改,比如执行
<tt class="docutils literal">setsid()</tt> 成立一个独立的进程组。</p>
<p>Linux 的进程组是一个进程的集合,任何进程用系统调用 <tt class="docutils literal">setsid</tt>
可以创建一个新的进程组,并让自己成为首领进程。首领进程的子子孙孙只要没有再调用
<tt class="docutils literal">setsid</tt> 成立自己的独立进程组,那么它都将成为这个进程组的成员。
之后进程组内只要还有一个存活的进程,那么这个进程组就还是存在的,即使首领进程已经死亡也不例外。
而这个存在的意义在于,我们只要知道了首领进程的 <tt class="docutils literal">pid</tt> (同时也是进程组的 <tt class="docutils literal">pgid</tt>),
那么可以给整个进程组发送 <tt class="docutils literal">signal</tt>,组内的所有进程都会收到。</p>
<p>因此利用这个特性,就可以通过 <tt class="docutils literal">preexec_fn</tt> 参数让 <tt class="docutils literal">Popen</tt> 成立自己的进程组,
然后再向进程组发送 <tt class="docutils literal">SIGTERM</tt> 或 <tt class="docutils literal">SIGKILL</tt>,中止 <tt class="docutils literal">subprocess.Popen</tt>
所启动进程的子子孙孙。当然,前提是这些子子孙孙中没有进程再调用 <tt class="docutils literal">setsid</tt> 分裂自立门户。</p>
<p>前文的例子经过修改是这样的:</p>
<figure class="code"><figcaption><span>better_process_fixture.py</span></figcaption><div class="highlight"><pre> <span class="kn">import</span> <span class="nn">signal</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">contextlib</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">import</span> <span class="nn">warnings</span>
<span class="nd">@contextlib.contextmanager</span>
<span class="k">def</span> <span class="nf">process_fixture</span><span class="p">(</span><span class="n">shell_args</span><span class="p">):</span>
<span class="n">proc</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">Popen</span><span class="p">(</span><span class="n">shell_args</span><span class="p">,</span> <span class="n">preexec_fn</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">setsid</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">yield</span>
<span class="k">finally</span><span class="p">:</span>
<span class="n">proc</span><span class="o">.</span><span class="n">terminate</span><span class="p">()</span>
<span class="n">proc</span><span class="o">.</span><span class="n">wait</span><span class="p">()</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">os</span><span class="o">.</span><span class="n">killpg</span><span class="p">(</span><span class="n">proc</span><span class="o">.</span><span class="n">pid</span><span class="p">,</span> <span class="n">signal</span><span class="o">.</span><span class="n">SIGTERM</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">OSError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">warnings</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</pre></div>
</figure><p>Python 3.2 之后 <tt class="docutils literal">subprocess.Popen</tt> 新增了一个选项 <tt class="docutils literal">start_new_session</tt>,
<tt class="docutils literal">Popen(args, start_new_session=True)</tt> 即等效于 <tt class="docutils literal">preexec_fn=os.setsid</tt> 。</p>
<p>这种利用进程组来清理子进程的后代的方法,比简单地中止子进程本身更加“干净”。基于
Python 实现的 Procfile 进程管理工具 <a class="reference external" href="https://github.com/nickstenning/honcho">Honcho</a>
也采用了这个方法。当然,因为不能保证被运行进程的子进程一定不会调用 <tt class="docutils literal">setsid</tt>,
所以这个方法不能算“通用”,只能算“相对可用”。如果真的要百分之百通用,那么像
systemd 那样使用 cgroups 来追溯进程创建过程也许是唯一的办法。也难怪说 systemd
是第一个能正确地关闭服务的 init 工具。</p>
Flask 的 Context 机制2014-07-21T00:07:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2014-07-21:post/the-context-mechanism-of-flask/<p>用过 Flask 做 Web 开发的同学应该不会不记得 App Context 和 Request Context 这两个名字——这两个 Context 算是 Flask 中比较特色的设计。<a class="footnote-reference" href="#id11" id="id1">[1]</a></p>
<p>从一个 Flask App 读入配置并启动开始,就进入了 App Context,在其中我们可以访问配置文件、打开资源文件、通过路由规则反向构造 URL。<a class="footnote-reference" href="#id12" id="id2">[2]</a> 当一个请求进入开始被处理时,就进入了 Request Context,在其中我们可以访问请求携带的信息,比如 HTTP Method、表单域等。<a class="footnote-reference" href="#id13" id="id3">[3]</a></p>
<p>所以,这两个 Context 也成了 Flask 框架复杂度比较集中的地方,对此有评价认为 Flask 的这种设计比 Django、Tornado 等框架的设计更为晦涩。<a class="footnote-reference" href="#id14" id="id4">[4]</a> 我不认同这种评价。对于一个 Web 应用来说,“应用” 和 “请求” 的两级上下文在理念上是现实存在的,如果理解了它们,那么使用 Flask 并不会晦涩;即使是使用 Django、Tornado,理解了它们的 Context 也非常有利于做比官网例子更多的事情(例如编写 Middleware)。</p>
<p>我因为开发 Flask 扩展,对这两个 Context 的具体实现也研究了一番,同时还解决了一些自己之前“知道结论不知道过程”的疑惑,所以撰写本文记录下来。</p>
<div class="section" id="thread-local">
<h2>Thread Local 的概念</h2>
<p>从面向对象设计的角度看,对象是保存“状态”的地方。Python 也是如此,一个对象的状态都被保存在对象携带的一个特殊字典中,可以通过 <tt class="docutils literal">vars</tt> 函数拿到它。</p>
<p>Thread Local 则是一种特殊的对象,它的“状态”对线程隔离 —— 也就是说每个线程对一个 Thread Local 对象的修改都不会影响其他线程。这种对象的实现原理也非常简单,只要以线程的 ID 来保存多份状态字典即可,就像按照门牌号隔开的一格一格的信箱。</p>
<p>在 Python 中获得一个这样的 Thread Local 最简单的方法是 <tt class="docutils literal">threading.local()</tt>:</p>
<figure class="code"><figcaption><span>Python Shell</span></figcaption><div class="highlight"><pre><span class="o">>>></span> <span class="kn">import</span> <span class="nn">threading</span>
<span class="o">>>></span> <span class="n">storage</span> <span class="o">=</span> <span class="n">threading</span><span class="o">.</span><span class="n">local</span><span class="p">()</span>
<span class="o">>>></span> <span class="n">storage</span><span class="o">.</span><span class="n">foo</span> <span class="o">=</span> <span class="mi">1</span>
<span class="o">>>></span> <span class="k">print</span><span class="p">(</span><span class="n">storage</span><span class="o">.</span><span class="n">foo</span><span class="p">)</span>
<span class="mi">1</span>
<span class="o">>>></span> <span class="k">class</span> <span class="nc">AnotherThread</span><span class="p">(</span><span class="n">threading</span><span class="o">.</span><span class="n">Thread</span><span class="p">):</span>
<span class="o">...</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="o">...</span> <span class="n">storage</span><span class="o">.</span><span class="n">foo</span> <span class="o">=</span> <span class="mi">2</span>
<span class="o">...</span> <span class="k">print</span><span class="p">(</span><span class="n">storage</span><span class="o">.</span><span class="n">foo</span><span class="p">)</span> <span class="c"># 这这个线程里已经修改了</span>
<span class="o">>>></span>
<span class="o">>>></span> <span class="n">another</span> <span class="o">=</span> <span class="n">AnotherThread</span><span class="p">()</span>
<span class="o">>>></span> <span class="n">another</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
<span class="mi">2</span>
<span class="o">>>></span> <span class="k">print</span><span class="p">(</span><span class="n">storage</span><span class="o">.</span><span class="n">foo</span><span class="p">)</span> <span class="c"># 但是在主线程里并没有修改</span>
<span class="mi">1</span>
</pre></div>
</figure><p>这样来说,只要能构造出 Thread Local 对象,就能够让同一个对象在多个线程下做到状态隔离。这个“线程”不一定要是系统线程,也可以是用户代码中的其他调度单元,例如 Greenlet。<a class="footnote-reference" href="#id15" id="id5">[5]</a></p>
</div>
<div class="section" id="werkzeug-local-stack-local-proxy">
<h2>Werkzeug 实现的 Local Stack 和 Local Proxy</h2>
<p>Werkzeug 没有直接使用 <tt class="docutils literal">threading.local</tt>,而是自己实现了 <tt class="docutils literal">werkzeug.local.Local</tt> 类。后者和前者有一些区别:</p>
<ul class="simple">
<li>后者会在 Greenlet 可用的情况下优先使用 Greenlet 的 ID 而不是线程 ID 以支持 Gevent 或 Eventlet 的调度,前者只支持多线程调度;</li>
<li>后者实现了 Werkzeug 定义的协议方法 <tt class="docutils literal">__release_local__</tt>,可以被 Werkzeug 自己的 release_pool 函数释放(析构)掉当前线程下的状态,前者没有这个能力。</li>
</ul>
<p>除 Local 外,Werkzeug 还实现了两种数据结构:LocalStack 和 LocalProxy。</p>
<p>LocalStack 是用 Local 实现的栈结构,可以将对象推入、弹出,也可以快速拿到栈顶对象。当然,所有的修改都只在本线程可见。和 Local 一样,LocalStack 也同样实现了支持 release_pool 的接口。</p>
<p>LocalProxy 则是一个典型的代理模式实现,它在构造时接受一个 callable 的参数(比如一个函数),这个参数被调用后的返回值本身应该是一个 Thread Local 对象。对一个 LocalProxy 对象的所有操作,包括属性访问、方法调用(当然方法调用就是属性访问)甚至是二元操作 <a class="footnote-reference" href="#id16" id="id6">[6]</a> 都会转发到那个 callable 参数返回的 Thread Local 对象上。</p>
<p>LocalProxy 的一个使用场景是 LocalStack 的 <tt class="docutils literal">__call__</tt> 方法。比如 <tt class="docutils literal">my_local_stack</tt> 是一个 LocalStack 实例,那么 <tt class="docutils literal">my_local_stack()</tt> 能返回一个 LocalProxy 对象,这个对象始终指向 <tt class="docutils literal">my_local_stack</tt> 的栈顶元素。如果栈顶元素不存在,访问这个 LocalProxy 的时候会抛出 <tt class="docutils literal">RuntimeError</tt>。</p>
</div>
<div class="section" id="flask-local-stack-context">
<h2>Flask 基于 Local Stack 的 Context</h2>
<p>Flask 是一个基于 Werkzeug 实现的框架,所以 Flask 的 App Context 和 Request Context 也理所当然地基于 Werkzeug 的 Local Stack 实现。</p>
<p>在概念上,App Context 代表了“应用级别的上下文”,比如配置文件中的数据库连接信息;Request Context 代表了“请求级别的上下文”,比如当前访问的 URL。</p>
<p>这两种上下文对象的类定义在 <tt class="docutils literal">flask.ctx</tt> 中,它们的用法是推入 <tt class="docutils literal">flask.globals</tt> 中创建的 <tt class="docutils literal">_app_ctx_stack</tt> 和 <tt class="docutils literal">_request_ctx_stack</tt> 这两个单例 Local Stack 中。因为 Local Stack 的状态是线程隔离的,而 Web 应用中每个线程(或 Greenlet)同时只处理一个请求,所以 App Context 对象和 Request Context 对象也是请求间隔离的。</p>
<p>当 <tt class="docutils literal">app = Flask(__name__)</tt> 构造出一个 Flask App 时,App Context 并不会被自动推入 Stack 中。所以此时 Local Stack 的栈顶是空的,<tt class="docutils literal">current_app</tt> 也是 unbound 状态。</p>
<figure class="code"><figcaption><span>Python Shell</span></figcaption><div class="highlight"><pre><span class="o">>>></span> <span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>
<span class="o">>>></span> <span class="kn">from</span> <span class="nn">flask.globals</span> <span class="kn">import</span> <span class="n">_app_ctx_stack</span><span class="p">,</span> <span class="n">_request_ctx_stack</span>
<span class="o">>>></span>
<span class="o">>>></span> <span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="o">>>></span> <span class="n">_app_ctx_stack</span><span class="o">.</span><span class="n">top</span>
<span class="o">>>></span> <span class="n">_request_ctx_stack</span><span class="o">.</span><span class="n">top</span>
<span class="o">>>></span> <span class="n">_app_ctx_stack</span><span class="p">()</span>
<span class="o"><</span><span class="n">LocalProxy</span> <span class="n">unbound</span><span class="o">></span>
<span class="o">>>></span>
<span class="o">>>></span> <span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">current_app</span>
<span class="o">>>></span> <span class="n">current_app</span>
<span class="o"><</span><span class="n">LocalProxy</span> <span class="n">unbound</span><span class="o">></span>
</pre></div>
</figure><p>这也是一些 Flask 用户可能被坑的地方 —— 比如编写一个离线脚本时,如果直接在一个 Flask-SQLAlchemy 写成的 Model 上调用 <tt class="docutils literal">User.query.get(user_id)</tt>,就会遇到 <tt class="docutils literal">RuntimeError</tt>。因为此时 App Context 还没被推入栈中,而 Flask-SQLAlchemy 需要数据库连接信息时就会去取 <tt class="docutils literal">current_app.config</tt>,current_app 指向的却是 <tt class="docutils literal">_app_ctx_stack</tt> 为空的栈顶。</p>
<p>解决的办法是运行脚本正文之前,先将 App 的 App Context 推入栈中,栈顶不为空后 <tt class="docutils literal">current_app</tt> 这个 Local Proxy 对象就自然能将“取 config 属性” 的动作转发到当前 App 上了:</p>
<figure class="code"><figcaption><span>Python Shell</span></figcaption><div class="highlight"><pre><span class="o">>>></span> <span class="n">ctx</span> <span class="o">=</span> <span class="n">app</span><span class="o">.</span><span class="n">app_context</span><span class="p">()</span>
<span class="o">>>></span> <span class="n">ctx</span><span class="o">.</span><span class="n">push</span><span class="p">()</span>
<span class="o">>>></span> <span class="n">_app_ctx_stack</span><span class="o">.</span><span class="n">top</span>
<span class="o"><</span><span class="n">flask</span><span class="o">.</span><span class="n">ctx</span><span class="o">.</span><span class="n">AppContext</span> <span class="nb">object</span> <span class="n">at</span> <span class="mh">0x102eac7d0</span><span class="o">></span>
<span class="o">>>></span> <span class="n">_app_ctx_stack</span><span class="o">.</span><span class="n">top</span> <span class="ow">is</span> <span class="n">ctx</span>
<span class="bp">True</span>
<span class="o">>>></span> <span class="n">current_app</span>
<span class="o"><</span><span class="n">Flask</span> <span class="s">'__main__'</span><span class="o">></span>
<span class="o">>>></span>
<span class="o">>>></span> <span class="n">ctx</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span>
<span class="o">>>></span> <span class="n">_app_ctx_stack</span><span class="o">.</span><span class="n">top</span>
<span class="o">>>></span> <span class="n">current_app</span>
<span class="o"><</span><span class="n">LocalProxy</span> <span class="n">unbound</span><span class="o">></span>
</pre></div>
</figure><p>那么为什么在应用运行时不需要手动 <tt class="docutils literal"><span class="pre">app_context().push()</span></tt> 呢?因为 Flask App 在作为 WSGI Application 运行时,会在每个请求进入的时候将请求上下文推入 <tt class="docutils literal">_request_ctx_stack</tt> 中,而请求上下文一定是 App 上下文之中,所以推入部分的逻辑有这样一条:如果发现 <tt class="docutils literal">_app_ctx_stack</tt> 为空,则隐式地推入一个 App 上下文。</p>
<p>所以,请求中是不需要手动推上下文入栈的,但是离线脚本需要手动推入 App Context。如果没有什么特殊困难,我更建议用 Flask-Script 来写离线任务。<a class="footnote-reference" href="#id17" id="id7">[7]</a></p>
</div>
<div class="section" id="id8">
<h2>两个疑问</h2>
<p>到此为止,就出现两个疑问:</p>
<ul class="simple">
<li>为什么 App Context 要独立出来:既然在 Web 应用运行时里,App Context 和 Request Context 都是 Thread Local 的,那么为什么还要独立二者?</li>
<li>为什么要放在“栈”里:在 Web 应用运行时中,一个线程同时只处理一个请求,那么 <tt class="docutils literal">_req_ctx_stack</tt> 和 <tt class="docutils literal">_app_ctx_stack</tt> 肯定都是只有一个栈顶元素的。那么为什么还要用“栈”这种结构?</li>
</ul>
<p>我最初也被这两个疑问困惑过。后来看了一些资料,就明白了 Flask 为何要设计成这样。这两个做法给予我们 <strong>多个 Flask App 共存</strong> 和 <strong>非 Web Runtime 中灵活控制 Context</strong> 的可能性。</p>
<p>我们知道对一个 Flask App 调用 <tt class="docutils literal">app.run()</tt> 之后,进程就进入阻塞模式并开始监听请求。此时是不可能再让另一个 Flask App 在主线程运行起来的。那么还有哪些场景需要多个 Flask App 共存呢?前面提到了,一个 Flask App 实例就是一个 WSGI Application,那么 WSGI Middleware 是允许使用组合模式的,比如:</p>
<figure class="code"><figcaption><span>wsgi</span></figcaption><div class="highlight"><pre><span class="kn">from</span> <span class="nn">werkzeug.wsgi</span> <span class="kn">import</span> <span class="n">DispatcherMiddleware</span>
<span class="kn">from</span> <span class="nn">biubiu.app</span> <span class="kn">import</span> <span class="n">create_app</span>
<span class="kn">from</span> <span class="nn">biubiu.admin.app</span> <span class="kn">import</span> <span class="n">create_app</span> <span class="k">as</span> <span class="n">create_admin_app</span>
<span class="n">application</span> <span class="o">=</span> <span class="n">DispatcherMiddleware</span><span class="p">(</span><span class="n">create_app</span><span class="p">(),</span> <span class="p">{</span>
<span class="s">'/admin'</span><span class="p">:</span> <span class="n">create_admin_app</span><span class="p">()</span>
<span class="p">})</span>
</pre></div>
</figure><p>这个例子就利用 Werkzeug 内置的 Middleware 将两个 Flask App 组合成一个一个 WSGI Application。这种情况下两个 App 都同时在运行,只是根据 URL 的不同而将请求分发到不同的 App 上处理。</p>
<div class="note">
<p class="first admonition-title">Note</p>
<p class="last">需要注意的是,这种用法和 Flask 的 Blueprint 是有区别的。Blueprint 虽然和这种用法很类似,但前者自己没有 App Context,只是同一个 Flask App 内部整理资源的一种方式,所以多个 Blueprint 可能共享了同一个 Flask App;后者面向的是所有 WSGI Application,而不仅仅是 Flask App,即使是把一个 Django App 和一个 Flask App 用这种用法整合起来也是可行的。</p>
</div>
<p>如果仅仅在 Web Runtime 中,多个 Flask App 同时工作倒不是问题。毕竟每个请求被处理的时候是身处不同的 Thread Local 中的。但是 Flask App 不一定仅仅在 Web Runtime 中被使用 —— 有两个典型的场景是在非 Web 环境需要访问上下文代码的,一个是离线脚本(前面提到过),另一个是测试。这两个场景即所谓的“Running code outside of a request”。</p>
</div>
<div class="section" id="web-flask">
<h2>在非 Web 环境运行 Flask 关联的代码</h2>
<p>离线脚本或者测试这类非 Web 环境和和 Web 环境不同 —— 前者一般只在主线程运行。</p>
<p>设想,一个离线脚本需要操作两个 Flask App 关联的上下文,应该怎么办呢?这时候栈结构的 App Context 优势就发挥出来了。</p>
<figure class="code"><figcaption><span>offline_script.py</span></figcaption><div class="highlight"><pre><span class="kn">from</span> <span class="nn">biubiu.app</span> <span class="kn">import</span> <span class="n">create_app</span>
<span class="kn">from</span> <span class="nn">biubiu.admin.app</span> <span class="kn">import</span> <span class="n">create_app</span> <span class="k">as</span> <span class="n">create_admin_app</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">create_app</span><span class="p">()</span>
<span class="n">admin_app</span> <span class="o">=</span> <span class="n">create_admin_app</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">copy_data</span><span class="p">():</span>
<span class="k">with</span> <span class="n">app</span><span class="o">.</span><span class="n">app_context</span><span class="p">():</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">read_data</span><span class="p">()</span> <span class="c"># fake function for demo</span>
<span class="k">with</span> <span class="n">admin_app</span><span class="o">.</span><span class="n">app_context</span><span class="p">():</span>
<span class="n">write_data</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="c"># fake function for demo</span>
<span class="n">mark_data_copied</span><span class="p">()</span> <span class="c"># fake function for demo</span>
</pre></div>
</figure><p>无论有多少个 App,只要主动去 Push 它的 App Context,Context Stack 中就会累积起来。这样,栈顶永远是当前操作的 App Context。当一个 App Context 结束的时候,相应的栈顶元素也随之出栈。如果在执行过程中抛出了异常,对应的 App Context 中注册的 <tt class="docutils literal">teardown</tt> 函数被传入带有异常信息的参数。</p>
<p>这么一来就解释了两个疑问 —— 在这种单线程运行环境中,只有栈结构才能保存多个 Context 并在其中定位出哪个才是“当前”。而离线脚本只需要 App 关联的上下文,不需要构造出请求,所以 App Context 也应该和 Request Context 分离。</p>
<p>另一个手动推入 Context 的场景是测试。测试中我们可能会需要构造一个请求,并验证相关的状态是否符合预期。例如:</p>
<figure class="code"><figcaption><span>tests.py</span></figcaption><div class="highlight"><pre><span class="k">def</span> <span class="nf">test_app</span><span class="p">():</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">create_app</span><span class="p">()</span>
<span class="n">client</span> <span class="o">=</span> <span class="n">app</span><span class="o">.</span><span class="n">test_client</span><span class="p">()</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'/'</span><span class="p">)</span>
<span class="k">assert</span> <span class="s">'Home'</span> <span class="ow">in</span> <span class="n">resp</span><span class="o">.</span><span class="n">data</span>
</pre></div>
</figure><p>这里调用 <tt class="docutils literal">client.get</tt> 时,Request Context 就被推入了。其特点和 App Context 非常类似,这里不再赘述。</p>
</div>
<div class="section" id="app-factory">
<h2>为何建议使用 App Factory 模式</h2>
<p>从官方文档来看,Flask 有 Singleton 和 App Factory 两种用法。前一种用法和其他的一些 Web 框架(如 Bottle、Sinatra)的门面广告很相似,因为代码精简,所以显得非常的“帅”:</p>
<figure class="code"><figcaption><span>app.py</span></figcaption><div class="highlight"><pre><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">render_template</span>
<span class="kn">from</span> <span class="nn">flask.ext.sqlalchemy</span> <span class="kn">import</span> <span class="n">SQLAlchemy</span>
<span class="kn">from</span> <span class="nn">flask.ext.login</span> <span class="kn">import</span> <span class="n">LoginManager</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="n">db</span> <span class="o">=</span> <span class="n">SQLAlchemy</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
<span class="n">login_manager</span> <span class="o">=</span> <span class="n">LoginManager</span><span class="p">()</span>
<span class="nd">@app.route</span><span class="p">(</span><span class="s">'/'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">home</span><span class="p">():</span>
<span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">'home.html'</span><span class="p">)</span>
</pre></div>
</figure><p>但是这种“帅”是有代价的。一个最麻烦的问题就是编写测试的时候:</p>
<figure class="code"><figcaption><span>test_app.py</span></figcaption><div class="highlight"><pre><span class="k">class</span> <span class="nc">TestApp</span><span class="p">(</span><span class="n">unittest</span><span class="o">.</span><span class="n">TestCase</span><span class="p">):</span>
<span class="n">DEBUG</span> <span class="o">=</span> <span class="bp">False</span>
<span class="n">TESTING</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">SQLALCHEMY_DATABASE_URI</span> <span class="o">=</span> <span class="bp">None</span>
<span class="k">def</span> <span class="nf">setUp</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">app</span> <span class="o">=</span> <span class="n">create_app</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">from_object</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">client</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="o">.</span><span class="n">test_client</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">test_app</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nd">@self.app.route</span><span class="p">(</span><span class="s">'/test/<int:id_>'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">my_view</span><span class="p">(</span><span class="n">id_</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'#</span><span class="si">%d</span><span class="s">'</span> <span class="o">%</span> <span class="n">id_</span>
<span class="n">resp</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'/test/42'</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">data</span><span class="p">,</span> <span class="s">'#42'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_home</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">resp</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'/'</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertIn</span><span class="p">(</span><span class="s">'Welcome'</span><span class="p">,</span> <span class="n">resp</span><span class="o">.</span><span class="n">data</span><span class="p">)</span>
</pre></div>
</figure><p>在上面的例子中,我为了测试给 App 新挂载了一个 View 函数。这是很常见的一个测试需求。但是如果 Flask App 实例是单例的,这种做法就会“弄脏”下一个测试的运行。更加麻烦的是,上述例子中如果 <tt class="docutils literal">test_home</tt> 在 <tt class="docutils literal">test_app</tt> 之前运行了,Flask 的开发者防御机制会认为这是一个“已经开始处理 Web 请求了,又挂载了视图” <a class="footnote-reference" href="#id18" id="id9">[8]</a> 的失误,从而抛出 <tt class="docutils literal">RuntimeError</tt>。</p>
<p>所以除非是应用简单到不需要 Web 层测试,否则还是尽量使用 App Factory 模式比较好。况且配合 Blueprint 的情况下,App Factory 还能帮助我们良好地组织应用结构:</p>
<figure class="code"><figcaption><span>happytree/app.py</span></figcaption><div class="highlight"><pre><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>
<span class="kn">from</span> <span class="nn">werkzeug.utils</span> <span class="kn">import</span> <span class="n">import_string</span>
<span class="n">extensions</span> <span class="o">=</span> <span class="p">[</span>
<span class="s">'happytree.ext:db'</span><span class="p">,</span>
<span class="s">'happytree.ext:login_manager'</span><span class="p">,</span>
<span class="p">]</span>
<span class="n">blueprints</span> <span class="o">=</span> <span class="p">[</span>
<span class="s">'happytree.views:bp'</span><span class="p">,</span>
<span class="p">]</span>
<span class="k">def</span> <span class="nf">create_app</span><span class="p">():</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="k">for</span> <span class="n">ext_name</span> <span class="ow">in</span> <span class="n">extensions</span><span class="p">:</span>
<span class="n">ext</span> <span class="o">=</span> <span class="n">import_string</span><span class="p">(</span><span class="n">ext_name</span><span class="p">)</span>
<span class="n">ext</span><span class="o">.</span><span class="n">init_app</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
<span class="k">for</span> <span class="n">bp_name</span> <span class="ow">in</span> <span class="n">blueprints</span><span class="p">:</span>
<span class="n">bp</span> <span class="o">=</span> <span class="n">import_string</span><span class="p">(</span><span class="n">bp_name</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">register_blueprint</span><span class="p">(</span><span class="n">bp</span><span class="p">)</span>
<span class="k">return</span> <span class="n">app</span>
</pre></div>
</figure><p>这样就能彻底摆脱 <tt class="docutils literal">app.py</tt> 和 View 模块“互相 Import”的纠结了。</p>
<p>好吧其实这一节和 Context 没啥关系……</p>
</div>
<div class="section" id="id10">
<h2>拖延不好</h2>
<p>这篇文章动笔开始写是 6 月 21 日,到今天发布出来,已经过去了整整一个月。而事实上我开始列提纲准备写这篇文章已经是三四月份的事情了。</p>
<p><cite>_(:з」∠)_</cite></p>
<table class="docutils footnote" frame="void" id="id11" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id1">[1]</a></td><td>Flask 文档对 <a class="reference external" href="http://flask.pocoo.org/docs/appcontext/">Application Context</a> 和 <a class="reference external" href="http://flask.pocoo.org/docs/reqcontext/">Request Context</a> 作出了详尽的解释;</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id12" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id2">[2]</a></td><td>通过访问 <tt class="docutils literal">flask.current_app</tt>;</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id13" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id3">[3]</a></td><td>通过访问 <tt class="docutils literal">flask.request</tt>;</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id14" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id4">[4]</a></td><td>Flask(Werkzeug) 的 Context 基于 Thread Local 和代理模式实现,只要身处 Context 中就能用近似访问全局变量的的方式访问到上下文信息,例如 <tt class="docutils literal">flask.current_app</tt> 和 <tt class="docutils literal">flask.request</tt>;Django 和 Tornado 则将上下文封装在对象中,只有明确获取了相关上下文对象才能访问其中的信息,例如在视图函数中或按照规定模板实现的 Middleware 中;</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id15" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id5">[5]</a></td><td>基于 Flask 的 Web 应用可以在 Gevent 或 Eventlet 异步网络库 patch 过的 Python 环境中正常工作。这二者都使用 Greenlet 而不是系统线程作为调度单元,而 Werkzeug 考虑到了这点,在 Greenlet 可用时用 Greenlet ID 代替线程 ID。</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id16" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id6">[6]</a></td><td>Python 的对象方法是 Descriptior 实现的,所以方法就是一种属性;而 Python 的二元操作可以用双下划线开头和结尾的一系列协议,所以 <tt class="docutils literal">foo + bar</tt> 等同于 <tt class="docutils literal">foo.__add__(bar)</tt>,本质还是属性访问。</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id17" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id7">[7]</a></td><td><a class="reference external" href="http://flask-script.readthedocs.org/">Flask-Script</a> 是一个用来写 <tt class="docutils literal">manage.py</tt> 管理脚本的 Flask 扩展,用它运行的任务会在开始前自动推入 App Context。将来这个“运行任务”的功能将被整合到 Flask 内部。</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id18" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id9">[8]</a></td><td>详见 Flask 源码中的 <tt class="docutils literal">setup_method</tt> 装饰器。</td></tr>
</tbody>
</table>
</div>
Git 忽略某个目录2014-03-27T11:45:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2014-03-27:post/git-ignore-directories/<p>有时候我们在给开源项目贡献代码的时候,会使用自己熟悉的工具链。比如 Python 项目,我可能就用 <a class="reference external" href="https://tox.readthedocs.org/en/latest">tox</a> 来构建发型包并运行单元测试。或者有不习惯用 <a class="reference external" href="https://github.com/yyuu/pyenv">pyenv</a> 或者 <a class="reference external" href="https://virtualenvwrapper.readthedocs.org/en/latest/">virtualenvwrapper</a> 的同学,就可能直接使用 <a class="reference external" href="https://virtualenv.readthedocs.org/en/latest/">virtualenv</a> 命令创建一个隔离的环境。</p>
<div class="section" id="id1">
<h2>忽略但不提交</h2>
<p>于是问题就来了,这些工具链都是会在项目目录中生成自己的工作目录的。 <a class="reference external" href="https://tox.readthedocs.org/en/latest">tox</a> 有 <tt class="docutils literal">.tox</tt> 目录, <a class="reference external" href="https://virtualenv.readthedocs.org/en/latest/">virtualenv</a> 有 <tt class="docutils literal">venv</tt> 目录。这些目录对版本控制来说应该被忽略,但我们不是项目的主人,只是贡献者,直接修改项目根目录下的 <tt class="docutils literal">.gitignore</tt> 会引起处女座的项目主人严重不适。</p>
<p>这个时候有个取巧的办法:进入这个要被忽略的目录,在目录下创建一个 <tt class="docutils literal">.gitignore</tt> 文件,然后里面写 <tt class="docutils literal">*</tt> 。这样这个目录就会被 git 默默忽略,而且不需要修改项目根目录的 <tt class="docutils literal">.gitignore</tt> 。</p>
</div>
<div class="section" id="id2">
<h2>提交且要忽略</h2>
<p>类似的一个问题是项目根目录下可能有 <tt class="docutils literal">logs</tt> 一类的目录,我们希望他人把仓库 <tt class="docutils literal">clone</tt> 下来的时候能够已经携带了这个目录,但又不希望让这个目录中的日志文件进版本库。</p>
<p>之前看到一些项目用了一种比较 ugly 的做法:在 <tt class="docutils literal">logs</tt> 下建立一个
<tt class="docutils literal">.gitkeep</tt> 空文件(git 无法版本控制没有任何文件的空目录),然后再在项目根目录``.gitignore`` 中写入一行 <tt class="docutils literal">logs/*</tt> 。</p>
<p>其实完全没有这样的必要,我们可以直接在 <tt class="docutils literal">logs</tt> 里面写一个 <tt class="docutils literal">.gitignore</tt> 文件,内容如下:</p>
<figure class="code"><figcaption><span>.gitignore</span></figcaption><div class="highlight"><pre> *
!.gitignore
</pre></div>
</figure><p>然后 <tt class="docutils literal">git add <span class="pre">logs/.gitignore</span> && git commit</tt> 提交。</p>
<p>这个 <tt class="docutils literal">.gitignore</tt> 既留住了空目录,又声明了忽略。而且某天如果要删除 <tt class="docutils literal">logs</tt> 目录,直接 <tt class="docutils literal">git rm <span class="pre">-r</span> logs</tt> 即可,无需再修改项目根目录下的 <tt class="docutils literal">.gitignore</tt> 。</p>
</div>
回顾即将结束的 2013 年2013-12-31T23:16:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-12-31:post/the-end-of-2013/<p>2013 年已走向尾声,还有一个小时不到新的一年就要来了。这一年对我来说意义非凡,充满了不同的结束,也充满了不同的开始。</p>
<p>去年偷懒,最后也没写年终总结。2013 年的转折点更多,自己都觉得不记录下来太过分。所以还是流水帐,略记一笔。</p>
<p>年初寒假伊始,和某人在考完试又未回老家之前去了趟广州,总算是见了家长。说来我自己也挺坑爹,回去之前才让爸爸知道我已有女友,弄得爸爸还有点凌乱。某人淑女模式开启,爸爸当然很是喜欢,不仅开启了话痨模式,还小孩子一样带了我们回去看家里的兔子。见面之前某人紧张得不行,见面后总算放下了重石。可惜在家里待到一天不到,我们两人马上又按计划赴中山市。爸爸其实对此很遗憾。我每次回家都是匆匆而回,匆匆而去,很少有被挽留成功的。我也一直有着奇怪的矛盾心理,一方面觉得自己这样刚来便走很让家人难过,另一方面又实在不愿意在家里久居。</p>
<p>中山一行短短三天,去了一个叫小榄的干净小镇,和深圳的城市气质截然不同。但重要的不是去哪里,而是与谁携手同行吧。后离中山,各回各家,两地隔山隔水,只能电话往来,甚为难熬。实习三个月的分别也没有这般感受,可能是因为在一起时间愈发长了,举手投足都已深入脑海,所以才刻骨铭心吧。</p>
<p>在老家一个月除了亲戚串门,大部分时间还是在写代码。本科四年即将结束,我也想在毕业设计的机会挑战一下自己,顺便平衡发展技能树,所以没有选择我比较熟悉的 Web 项目开发,而是转而基于 libuv 和 Lua VM 实现了一个 Application Server (类似 Python WSGI Server)。坑多,边跳边填地走了下去。</p>
<p>三月,与某人在广州相见,同回深圳。之后就开始了最闲的几个月。折腾了很多幺蛾子,睡了很多觉,看了很多电影,零零散散的时间做着毕业设计。</p>
<p>四月,找到了一份实习工作,在小象网做了两个月 Web 开发,从此也终于是写过 Ruby 的人了,虽然写的很渣。同时也认识了很厉害的 Tech Leader,感觉相比之下自己在许多基础知识方面薄弱的要命。</p>
<p>六月七月毕业季,到了宿舍搬空那一天才意识到真的要离开了。那种莫名的失落感一下冲了上来,很难受。同学朋友各奔东西,他日再见也不同当年了。</p>
<p>毕业后就一路奔来北京,算正式成为北漂族一员了吧。刚来那天先到的厂里,看到熟悉的小二楼上只剩下两个人,不由得脑补了去年的小二楼盛况,对比之下更是有种物是人非的感觉。</p>
<p>转眼到如今,工作已有近半年,也从看什么都陌生进化到了现在闭着眼睛能找到哪个类放在哪个模块。短短数月,踩了许多坑,造了许多轮子,还终于如愿以偿维护了一个能见人的开源项目 <a class="reference external" href="https://github.com/tonyseek/brownant">Brownant</a> 。</p>
<p>再次异地恋,吵了几次架之后反而愈发知心,哄妹纸的水平日渐提高。某人要准备考研,同时还要面临期末考试,压力很大,至今离会考也只剩下几天了。而这一切所为的,是明年的今天二人能同处一个城市,不再分隔两地。</p>
<p>曾经说过要为某人学会做双皮奶,如今该技能已得到。曾经说过工作了也要保持阅读,做的很不好。曾经说过要保持作息节奏,结局惨不忍睹。</p>
<p>工作上,对比去年实习,真实的工作给了我实习无法得到的经验、感悟以及忙碌,同时也在厂里目睹了很多遗憾的变故。我也开始学着去应对这个世界,应对如意的事情和不如意的事情。一个人经历越多就应该会坎坷越多,而我看到了几位历练比我要多好多年的老师仍在以正能量应对着,做他们喜欢做的事情,参加他们喜欢的社区,组织他们喜欢的活动。所以我想我更没有理由因为丑陋的存在而不去喜欢这个世界本身。</p>
<p>2014 年来了,谁也不知道这将是怎样的一年,但这一年必是值得期待。这一年里,我还想修剪技能树打小怪兽升级,还想朝着让某人幸福的方向继续努力,还想多读几本书(是 <a class="reference external" href="https://www.douban.com/group/buybook/">移山</a> 吧)。当然,也可能有新的坑在等着我跳。但只要是未知的,都还是值得期待的。</p>
为 C/C++ 库定制 Python Binding2013-12-10T06:23:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-12-10:post/make-python-binding-for-c-library/<p>这应该是个非常常见的需求了吧。可能因为性能原因需要自己编写一部分 C 代码,可能因为需要的第三方库是 C/C++ 编写的。</p>
<p>我遇到这个问题是因为希望将淘宝发的一个切图实现 <a class="reference external" href="https://github.com/exinnet/tclip">tclip</a> 放到 Python 上用。和几个同事都尝试折腾了这个问题,总结出了几种可行的方法。</p>
<div class="section" id="id1">
<h2>可行的办法</h2>
<div class="section" id="port-python">
<h3>直接 port 到 Python 上</h3>
<p>实现例子: <a class="reference external" href="https://github.com/CNBorn/pytclip">https://github.com/CNBorn/pytclip</a></p>
<p>tclip 是使用 OpenCV 实现的,所以一个可行的途径是直接使用 OpenCV 的 Python Binding,将逻辑在 Python 中实现一遍。</p>
<p>所以如果目标库本身代码比较简单,但是用到了第三方 C/C++ 库,可以寻找有没有第三方库的 Python Binding。如果有,可以将逻辑 port 到 Python 上实现。</p>
<p>优点:</p>
<ul class="simple">
<li>编写快,只需要编写 Python 代码,不需要写 C/C++;</li>
<li>构建过程简单。由于将构建过程推给了第三方 Python Binding,所以直接用 setuptools 内置的依赖管理指向 PyPI 上的包即可。如果源中提供了打包好的依赖,部署环境不需要安装编译器或 foobar-devel 库。</li>
</ul>
<p>缺点:</p>
<ul class="simple">
<li>视具体情况不同,性能折扣可能比较大;</li>
<li>而且最终质量依赖第三方 Python Binding 库的质量。如果第三方实现的很多坑,那我们作为使用者就会掉坑;</li>
<li>跨平台、跨解释器受第三方限制。如果第三方 Python Binding 库只支持 CPython 或 Python 2.x 或只支持某个操作系统,那么我们编写的库也将受到这个限制。</li>
</ul>
</div>
<div class="section" id="cpython-abi">
<h3>使用 CPython ABI 绑定</h3>
<p>实现例子: <a class="reference external" href="https://github.com/xtao/tclip">https://github.com/xtao/tclip</a></p>
<p>这个对 CPython 而言“受官方支持”的做法,大部分库也都会选择这么做。用 C/C++ 编写一个绑定模块并在其中 <tt class="docutils literal">#include <Python.h></tt> 和定义 <tt class="docutils literal">PyMODINIT_FUNC</tt> ,使用 setuptools 编译后就可以在 Python 中直接 import,非常好用。</p>
<p>除去直接使用 C/C++ 访问 CPython ABI,借助 Pyrex 或者 Cython 能编码过程进一步简化。使用 Cython 后除了访问 C/C++ 接口处,其他地方就像编写普通 Python 代码。</p>
<p>优点:</p>
<ul class="simple">
<li>CPython 的官方指导做法,适用面广;</li>
<li>构建过程简单,在 <tt class="docutils literal">setup.py</tt> 中定义好后, <tt class="docutils literal">python setup.py install</tt> 或者 <tt class="docutils literal">pip install foobar</tt> 就可以直接编译扩展并安装到 site-packages 目录。部署环境要求安装编译器,但编译过程不用人为干预。如果在源中打包好,部署环境的编译器也可以省去。</li>
</ul>
<p>缺点:</p>
<ul class="simple">
<li>仅适用于 CPython,无法适用其它富有潜力的解释器,如 PyPy。</li>
</ul>
</div>
<div class="section" id="ctypes-cffi">
<h3>使用 ctypes 或者 cffi 绑定</h3>
<p>实现例子: <a class="reference external" href="https://github.com/tonyseek/python-tclip">https://github.com/tonyseek/python-tclip</a></p>
<p><a class="reference external" href="http://docs.python.org/dev/library/ctypes.html">ctypes</a> 是 Python 标准库模块,能在运行时载入动态链接库(Windows 平台上的 <tt class="docutils literal">foo.dll</tt> 、Linux/Unix/OSX 平台上的 <tt class="docutils literal">foo.so</tt> ),在 CPython 2.x/3.x 和 PyPy 上均受支持。</p>
<p><a class="reference external" href="http://cffi.readthedocs.org">cffi</a> 是 PyPy 开发者编写的一套 Python 与 C 的 FII 实现,和 ctypes 类似,但提供了更高层的接口,支持直接内嵌 C 源码,运行时将其编译。cffi 同时支持 CPython 和 PyPy 两种解释器。在 PyPy 中 cffi 是内置的,ctypes 实际上是 cffi 的一个 wrapper。</p>
<p>其中我个人更推荐使用 cffi,因为它能接受和 setuptools 一样的扩展选项,例如指定 <tt class="docutils literal">include</tt> 路径、 <tt class="docutils literal">library</tt> 路径,方便编译需要外部库支持的 C/C++ 源码。如果配合 <a class="reference external" href="https://github.com/tonyseek/python-tclip/blob/e041c974f409bcc4075121a38d16fda83fe8d9cc/tclip.py#L13-L23">一个简单的函数</a> 调用系统的 <tt class="docutils literal"><span class="pre">pkg-config</span></tt> 来寻找这个路径,就能在不同的平台下不修改参数而直接编译,方便程度不亚于 CPython ABI。</p>
<p>另外,cffi 支持基础类型到 Python 的自动转换,例如 <tt class="docutils literal">str</tt> 对 <tt class="docutils literal">char *</tt> 、<tt class="docutils literal">int</tt> 对 <tt class="docutils literal">int</tt> 等。用 cffi 产生的接口对于不合法的输入不是一个粗暴的段错误或 core dump 回应,而是抛出 Python 语言级别的可恢复异常,例如 <tt class="docutils literal">TypeError</tt> 。</p>
<p>一个简单的例子:</p>
<figure class="code"><div class="highlight"><pre><span class="kn">import</span> <span class="nn">cffi</span>
<span class="n">ffi</span> <span class="o">=</span> <span class="n">cffi</span><span class="o">.</span><span class="n">FFI</span><span class="p">()</span>
<span class="n">ffi</span><span class="o">.</span><span class="n">cdef</span><span class="p">(</span><span class="s">"""</span>
<span class="s"> int hello(char *);</span>
<span class="s">"""</span><span class="p">)</span>
<span class="n">ffi</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span><span class="s">"""</span>
<span class="s"> #include <stdio.h></span>
<span class="s"> int hello(char *name) { printf("hello, </span><span class="si">%s</span><span class="s">", name); return 0; }</span>
<span class="s">"""</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">ffi</span><span class="o">.</span><span class="n">hello</span><span class="p">(</span><span class="s">"world"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span>
</pre></div>
</figure><p>如果 C 的源码独立成文件,也可以用 <tt class="docutils literal"><span class="pre">ffi.verify(sources=['foo.c'])</span></tt> 指定。 <tt class="docutils literal">verify</tt> 接受的参数和 setuptools/distutils 的 <tt class="docutils literal">Extension</tt> 类接受的参数是兼容的。</p>
<p>如果目标代码不是 ANSI C 而是 C++ 的,暴露给 cffi 的接口需要包在 <tt class="docutils literal">extern "C" {}</tt> 内。</p>
<p>优点:</p>
<ul class="simple">
<li>跨解释器兼容,同时支持 CPython 2.x/3.x 和 PyPy;</li>
<li>开发和部署都非常简单,编译过程由 cffi 库默默完成。</li>
</ul>
<p>缺点:</p>
<ul class="simple">
<li>由于是运行时编译,所以即使做了安装源的二进制打包,部署环境还是 <strong>必须</strong> 安装编译器和 <tt class="docutils literal"><span class="pre">foobar-devel</span></tt> 库;</li>
<li>同样由于运行时编译,所以对于频繁运行而每次运行时间又特别短的小脚本来说, <tt class="docutils literal">verify</tt> 的执行有些不“低碳”(对于驻守进程的应用则可以忽略这个开销,因为 <tt class="docutils literal">verify</tt> 只在首次调时编译)。</li>
</ul>
</div>
<div class="section" id="boost-python">
<h3>基于 boost-python 的封装</h3>
<p>实现例子:tclip 的例子就没有了。有一个 tesseract 的 Python Binding 实现 tesserwrap, <a class="reference external" href="https://github.com/gregjurman/tesserwrap/blob/3edfbe1942a6ba6983e32564b99442fdae8550c2/tesserwrap/cpp/tesseract_wrap.cpp#L70-L95">早期</a> 使用了这个方法。</p>
<p>据说性能捉急,坑又多多。所以 tesserwrap 现在也迁移到 <tt class="docutils literal">ctypes</tt> 了。这里就不详述了。</p>
</div>
</div>
<div class="section" id="id4">
<h2>做出选择</h2>
<p>我个人来说最推崇的方法是 cffi/ctypes,理由是“跨解释器”。我认为作为一个库实现,对 PyPy 的支持重要程度不亚于对 Python 3.x 的支持。</p>
<p>我现在发起的开源 Python 库一律对 CPython 2.7、CPython 3.3 和 PyPy latest stable release 提供 CI 保证和维护支持。因为我认为作为维护者,不应该让自己的库成为用户选择优秀解释器的阻碍。cffi/ctypes 很完美地符合了这个要求。</p>
<p>目前为止,我了解的还有 <a class="reference external" href="http://docs.wand-py.org">Wand</a> (ImageMagick 的 binding)和现行版本的 <a class="reference external" href="https://github.com/gregjurman/tesserwrap">tesserwrap</a> (Tesseract 的 binding)使用了 cffi/ctypes ;此外如果要在 PyPy 下使用 PostgreSQL,选择之一也是 <a class="reference external" href="https://github.com/mvantellingen/psycopg2-ctypes">psycopg2-ctypes</a> 驱动。</p>
</div>
用装饰器注册 Python 函数2013-08-13T18:28:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-08-13:post/how-to-register-python-function-by-decorator/<p>注册回调函数应该是开发中很常见的一种行为。这在 Python 中通常通过装饰器来实现,看起来比较漂亮:</p>
<figure class="code"><figcaption><span>flaskr.py</span></figcaption><div class="highlight"><pre> <span class="nd">@app.route</span><span class="p">(</span><span class="s">"/"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">home</span><span class="p">():</span>
<span class="k">return</span> <span class="s">"It works."</span>
</pre></div>
</figure><p>但是这种用法常常带来一种隐藏的“惊讶”,比如说:</p>
<figure class="code"><figcaption><span>admin_menu.py</span></figcaption><div class="highlight"><pre> <span class="nd">@permission</span><span class="p">([</span><span class="s">"manager"</span><span class="p">,</span> <span class="s">"developer"</span><span class="p">])</span>
<span class="nd">@app.route</span><span class="p">(</span><span class="s">"/admin"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">admin_menu</span><span class="p">():</span>
<span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">"admin/menu.html"</span><span class="p">)</span>
</pre></div>
</figure><p>这个 <tt class="docutils literal">@permission</tt> 根本不会生效,因为在它装饰 <tt class="docutils literal">admin_menu</tt> 之前,<tt class="docutils literal">admin_menu</tt> 就已经被注册到 <tt class="docutils literal">app</tt> 里了。最终我们执行的是未经过装饰的函数。</p>
<p>类似的陷阱还有可能出现在“猴子补丁”使用的场景。比如下面这段代码将一系列 <tt class="docutils literal">filter</tt> 注册到一个对象中:</p>
<figure class="code"><figcaption><span>filter_manager.py</span></figcaption><div class="highlight"><pre> <span class="k">class</span> <span class="nc">FilterManager</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="sd">"""The filter registry center."""</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_filters</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">def</span> <span class="nf">register</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">decorator</span><span class="p">(</span><span class="n">func</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_filters</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">func</span>
<span class="k">return</span> <span class="n">func</span>
<span class="k">return</span> <span class="n">decorator</span>
<span class="k">def</span> <span class="nf">filter</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">type_name</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_filters</span><span class="p">[</span><span class="n">type_name</span><span class="p">](</span><span class="n">value</span><span class="p">)</span>
<span class="n">filter_manager</span> <span class="o">=</span> <span class="n">FilterManager</span><span class="p">()</span>
<span class="nd">@filter_manager.register</span><span class="p">(</span><span class="s">"datetime"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">datetime_filter</span><span class="p">(</span><span class="n">dt</span><span class="p">):</span>
<span class="n">dt</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%Y-%m-</span><span class="si">%d</span><span class="s"> %H:%M"</span><span class="p">)</span>
</pre></div>
</figure><p>看起来似乎很漂亮,但是如果我在写单元测试的时候,希望用一个 Stub 来取代 <tt class="docutils literal">datetime_filter</tt> ,就会小小惊讶一下:</p>
<figure class="code"><figcaption><span>test_filter_manager.py</span></figcaption><div class="highlight"><pre> <span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
<span class="kn">from</span> <span class="nn">mock</span> <span class="kn">import</span> <span class="n">patch</span>
<span class="kn">from</span> <span class="nn">filter_manager</span> <span class="kn">import</span> <span class="n">filter_manager</span><span class="p">,</span> <span class="n">datetime_filter</span>
<span class="nd">@patch</span><span class="p">(</span><span class="s">"filter_manager.datetime_filter"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_datetime_filter</span><span class="p">():</span>
<span class="n">filter_manager</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="s">"datetime"</span><span class="p">,</span> <span class="n">datetime</span><span class="o">.</span><span class="n">utcnow</span><span class="p">())</span>
<span class="k">assert</span> <span class="n">datetime_filter</span><span class="o">.</span><span class="n">called</span>
</pre></div>
</figure><p>说到底还是用装饰器注册函数,不是 <strong>Lazy Binding</strong> 的。</p>
<p>这是我个人感觉 Python 的装饰器使用中比较违反直觉的一个地方。虽然装饰器这种看上去很像“声明”的语法很酷,却也很 <a class="reference external" href="http://www.python.org/dev/peps/pep-0020/">implicit</a> 。当然因为装饰器的这种用法实在太流行,很多 Python 开发者已经具备了绕过这个陷阱的技能了(Orz,小白和非小白的区别之一)。但是我还是觉得这种东西需要开发者用经验去绕过的,并不符合我比较认同的最小惊讶原则。</p>
<p>所以…… 我可能还是更喜欢用 Qualified Name 来引用要注册的函数,以实现真正的 Lazy Binding:</p>
<figure class="code"><div class="highlight"><pre><span class="k">def</span> <span class="nf">import_object</span><span class="p">(</span><span class="n">qualname</span><span class="p">):</span>
<span class="k">try</span><span class="p">:</span>
<span class="kn">from</span> <span class="nn">werkzeug.utils</span> <span class="kn">import</span> <span class="n">import_string</span>
<span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
<span class="kn">from</span> <span class="nn">importlib</span> <span class="kn">import</span> <span class="n">import_module</span>
<span class="n">module_name</span><span class="p">,</span> <span class="n">object_name</span> <span class="o">=</span> <span class="n">qualname</span><span class="o">.</span><span class="n">rsplit</span><span class="p">(</span><span class="s">"."</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">module</span> <span class="o">=</span> <span class="n">import_module</span><span class="p">(</span><span class="n">module_name</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">object_name</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">import_string</span><span class="p">(</span><span class="n">qualname</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_qualname</span><span class="p">(</span><span class="n">o</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">o</span><span class="p">,</span> <span class="s">"__qualname__"</span><span class="p">,</span> <span class="s">"</span><span class="si">%s</span><span class="s">.</span><span class="si">%s</span><span class="s">"</span> <span class="o">%</span> <span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="n">__module__</span><span class="p">,</span> <span class="n">o</span><span class="o">.</span><span class="n">__name__</span><span class="p">))</span>
<span class="k">class</span> <span class="nc">FilterManager</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="sd">"""The filter registry center."""</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_filters_qualname</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">def</span> <span class="nf">register</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">decorator</span><span class="p">(</span><span class="n">func</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_filters_qualname</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">get_qualname</span><span class="p">(</span><span class="n">func</span><span class="p">)</span>
<span class="k">return</span> <span class="n">func</span>
<span class="k">return</span> <span class="n">decorator</span>
<span class="k">def</span> <span class="nf">filter</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">type_name</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
<span class="n">qualname</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_filters_qualname</span><span class="p">[</span><span class="n">type_name</span><span class="p">]</span>
<span class="n">func</span> <span class="o">=</span> <span class="n">import_object</span><span class="p">(</span><span class="n">qualname</span><span class="p">)</span>
<span class="k">return</span> <span class="n">func</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
</pre></div>
</figure>[翻译] Lua 应用程序编程接口2013-05-28T16:55:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-05-28:post/the-application-program-interface-of-lua/<p><img alt="知识共享署名-非商业性使用-相同方式共享 3.0 许可协议" src="https://i.creativecommons.org/l/by-nc-sa/3.0/88x31.png" /></p>
<p>本文翻译采用 <a class="reference external" href="https://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh">知识共享署名-非商业性使用-相同方式共享 3.0 许可协议</a> 进行许可。</p>
<p>本文翻译节选自 <a class="reference external" href="http://www.lua.org/manual/5.1/manual.html#3">Lua 5.1 Reference Manual</a> ,只包含了描述部分,未包含 API 函数文档。</p>
<div class="section" id="id2">
<h2>概述</h2>
<p>这部分描述了 Lua 的 C 语言绑定 API,这是一个 C 语言函数集合,用来提供宿主程序和 Lua 之见的信息交换。所有 API 函数和关联的类型、常量都被声明在头文件 <tt class="docutils literal">lua.h</tt> 里。</p>
<p>即使我们使用了“函数”这个术语,许多 API 中的接口仍然以宏的方式提供。所有这样的宏只会精确地将每个参数使用一次(除了第一个参数,第一个参数始终为 Lua State),并且不会产生任何隐藏的副作用。</p>
<p>几乎在所有的 C 语言库中,Lua API 函数都不会检查它们收到参数的有效性和一致性。然而,你可以通过修改 <tt class="docutils literal">luaconf.h</tt> 中的宏定义 <tt class="docutils literal">luai_apicheck</tt> 来改变这个行为。</p>
</div>
<div class="section" id="id3">
<h2>状态栈</h2>
<p><strong>译者注:本文中,带有双引号的“栈”特别指代 Lua State 状态栈。</strong></p>
<p>Lua 使用一个虚拟的“栈”来和 C 语言之间传值和取值。每个栈中的元素都表示一个 Lua 值(nil 空值、number 数值型、string 字符串,诸如此类)。</p>
<p>无论何时,Lua 调用 C 语言时,被调用函数会获得一个新的“栈”,它和前一个“栈”以及 C 语言函数的活动栈相互独立。这个“栈”初始化的时候包含了任何传入 C 语言函数的参数,同时 C 语言函数也在这个“栈”上推入值作为调用者的返回值。详见 <tt class="docutils literal">lua_CFunction</tt> API 的文档。</p>
<p>为了方便,大多数 API 中的查询操作都不会遵循严格的栈访问规则(指严格的先进后出顺序访问,译者注)。相反,调用者可以通过索引引用“栈”上任何位置的元素:一个正数的索引代表“栈”上的绝对位置(从 1 开始);一个负数的索引代表相对“栈”顶的位移位置。更明确来说,如果一个“栈”有 n 个元素,那么索引 1 代表“栈”上的第一个元素(也即第一个被推入栈中的元素)(即栈底,译者注),索引 -1 却代表最后一个元素(也即栈顶元素),索引 -n 代表第一个元素(即栈底,译者注)。如果一个所有的值在 1 到栈顶之间(也即 <tt class="docutils literal">1 ≤ abs(index) ≤ top</tt> ),我们就说这个索引是有效的。</p>
</div>
<div class="section" id="id4">
<h2>状态栈大小</h2>
<p>当你和 Lua API 交互时,你作为开发者有责任自己对一致性问题提供保证。往细里说,你有责任控制“栈”不要溢出。你可以使用 C 语言函数 <tt class="docutils literal">lua_checkstack</tt> 来检视“栈”的大小。</p>
<p>无论何时 Lua 调用 C 语言,都能确保“栈”中有 <tt class="docutils literal">LUA_MINSTACK</tt> (API 中的宏定义,译者注)个位置可供使用。 <tt class="docutils literal">LUA_MINSTACK</tt> 的定义是 20,所以一般情况下你不需要担心“栈”的大小不够你的代码循环推入元素。</p>
<p>大多数查询函数接受去获取有效“栈”空间中的任何值,也即通过 <tt class="docutils literal">lua_checkstack</tt> 检查“栈”的最大大小。这样的索引被称为“可接受索引”。更具有普遍适用意义地,我们给可接受索引一个定义,如下:</p>
<figure class="code"><div class="highlight"><pre><span class="p">(</span><span class="n">index</span> <span class="o"><</span> <span class="mi">0</span> <span class="o">&&</span> <span class="n">abs</span><span class="p">(</span><span class="n">index</span><span class="p">)</span> <span class="o"><=</span> <span class="n">top</span><span class="p">)</span> <span class="o">||</span>
<span class="p">(</span><span class="n">index</span> <span class="o">></span> <span class="mi">0</span> <span class="o">&&</span> <span class="n">index</span> <span class="o"><=</span> <span class="n">stackspace</span><span class="p">)</span>
</pre></div>
</figure><p>注意, <tt class="docutils literal">0</tt> 在任何时候都不是一个可接受索引。</p>
</div>
<div class="section" id="id5">
<h2>伪索引</h2>
<p>无需额外注意,所有函数都接受被称为“伪索引”的值作为有效的索引。伪索引表示一些可被 C 语言中 Lua API 函数接受,但却不在“栈”中的 Lua 值。伪索引常常被用来访问线程的环境、函数的环境、注册表以及 C 语言函数中的“上值”(upvalue,函数式编程中指闭包函数引用了的外部作用域变量,译者注)(参考 3.4 节)。</p>
<p>线程环境(全局变量存活的地方)始终位于伪索引 <tt class="docutils literal">LUA_GLOBALSINDEX</tt> ,正在运行的 C 语言函数始终位于伪索引 <tt class="docutils literal">LUA_ENVIRONINDEX</tt> 。</p>
<p>如果需要访问和修改全局变量的值,你需要对环境“表”(Lua 中一种类似关联数组的数据结构,译者注)使用常规的表操作。举个例子,访问全局变量,只需要:</p>
<figure class="code"><div class="highlight"><pre><span class="n">lua_getfield</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="n">LUA_GLOBALSINDEX</span><span class="p">,</span> <span class="n">varname</span><span class="p">);</span>
</pre></div>
</figure></div>
<div class="section" id="c">
<h2>C 语言闭包</h2>
<p>当一个 C 语言函数被创建出来,就有可能将它和某些值关联起来,从而创建了一个 C 语言的闭包(并非编译原理或离散数学中的闭包,这里特指函数式编程的“词法闭包”,译者注);这些值被称之为“上值”(upvalues),无论绑定的函数在何时被调用,它们都能保证是可访问的。参考 <tt class="docutils literal">lua_pushcclosure</tt> 函数 API 文档。</p>
<p>无论何时,一个 C 语言函数被调用,它的上值都位于一个特殊的伪索引上。这个伪索引由 <tt class="docutils literal">lua_upvalueindex</tt> 宏提供。第一个关联到函数的值位于 <tt class="docutils literal">lua_upvalueindex(1)</tt> 位置,依此类推。任何对 <tt class="docutils literal">lua_upvalueindex(n)</tt> 的访问,n 总是大于当前函数中上值的数量(但不会大于 256),提供一个可被接受(但无效)的特殊索引。</p>
</div>
<div class="section" id="id6">
<h2>注册表</h2>
<p>Lua 提供了一个注册表,一个预定义的表,可以被任何 C 语言代码使用,以便于储存人呢和需要储存的 Lua 值。这个表始终位于伪索引 <tt class="docutils literal">LUA_REGISTRYINDEX</tt> 之上。任何 C 语言库都能将数据存储到这个表中,但存储者应该要小心地选择键名,以和其他库所使用的不同,避免冲突。典型来说,你应该选择一个包含库名的字符串,或者一个内存地址位于你自己的库中的 C 对象所封装的“用户数据”(Lua 对 C 语言指针的一种封装,译者注),来作为你的键名。</p>
<p>注册表中,整型的键名被引用机制所使用,这是 auxiliary 库(Lua API 的一套上层封装工具集,译者注)所实现的。所以不应该将整型键名用作其他用途。</p>
</div>
<div class="section" id="id7">
<h2>C 语言方面的错误处理</h2>
<p>在内部,Lua 使用 C 语言的 <tt class="docutils literal">longjmp</tt> 机制来处理错误。(如果你使用 C++,你也可以使用 C++ 异常,详见 <tt class="docutils literal">luaconf.h</tt> ) 当 Lua 面对任何错误(例如内存分配错误、类型错误、语法错误以及运行时错误)它会将其抛出——也就是做一次跳出(long jump)。在保护环境中,Lua 使用 <tt class="docutils literal">setjmp</tt> 来设置一个恢复指针;从任何错误中都会跳出到最近激活的恢复指针。</p>
<p>大多数 API 函数都能抛出一个错误,例如遇到了内存分配失败。本文档为每一个函数明确声明了可能抛出的错误。</p>
<p>在 C 语言函数中,你可以调用 <tt class="docutils literal">lua_error</tt> 来抛出一个错误。</p>
</div>
[翻译] SCGI 协议规格说明书2013-05-28T12:33:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-05-28:post/scgi-protocol-specification/<p><img alt="知识共享署名-非商业性使用-相同方式共享 3.0 许可协议" src="https://i.creativecommons.org/l/by-nc-sa/3.0/88x31.png" /></p>
<ul class="simple">
<li>原文:Neil Schemenauer <nas at python dot ca> 2008-06-23</li>
<li>翻译:Jiangge Zhang <tonyseek at gmail dot com> 2013-03-17</li>
</ul>
<p>原文地址: <a class="reference external" href="http://python.ca/scgi/protocol.txt">http://python.ca/scgi/protocol.txt</a></p>
<div class="section" id="id1">
<h2>简介</h2>
<p>SCGI 协议是通用网关协议(CGI)的替代协议,它描述了一套应用程序和 HTTP 服务器之间的接口标准。它和 FastCGI 协议非常类似,但设计得更加简单和易于实现。</p>
<p>在本文档中,一个 8 位字节串可能被写成两种形式:</p>
<ul class="simple">
<li>以一系列十六进制数字的形式,写在尖括号中;</li>
<li>以一系列 ASCII 字符的形式,写在双引号中。</li>
</ul>
<p>例如,<68 65 6c 6c 6f 20 77 6f 72 6c 64 21> 是一个长度为 12 的字符串,它和字符串 "hello world!" 是等价的。注意,这仅仅是本文档的一种约定,并不是协议规格的一部分。</p>
</div>
<div class="section" id="id2">
<h2>协议</h2>
<p>客户端通过一个可靠的流协议连接到 SCGI 服务器,这个流协议允许传输 8 位字节串。客户端的工作由发送一个请求开始。请参考第三部分对请求格式的描述。</p>
<p>当 SCGI 服务器读取到请求结束,它会向回发送一个响应并关闭本次连接。本规格说明不约定响应的格式。</p>
</div>
<div class="section" id="id3">
<h2>请求的格式</h2>
<p>一个请求由一系列的请求头和一个请求体构成。请求头的格式如下:</p>
<pre class="literal-block">
headers ::= header*
header ::= name NUL value NUL
name ::= notnull+
value ::= notnull*
notnull ::= <01> | <02> | <03> | ... | <ff>
NUL = <00>
</pre>
<p>请求头中不允许出现同样的项名。第一个请求头的项名必须为 <tt class="docutils literal">CONENTE_LENGTH</tt> ,值为一个非空的 ASCII 字符串,包含了请求体长度的字符串形式数字。</p>
<p>请求头项 <tt class="docutils literal">CONTENT_LENGTH</tt> 必须始终被提供,即使它的值是 <tt class="docutils literal">"0"</tt> 。</p>
<p>除此之外,另一个必须始终被提供的请求头项名是 <tt class="docutils literal">"SCGI"</tt> ,值是 <tt class="docutils literal">"1"</tt> 。</p>
<p>为了促进从 CGI 到 SCGI 的过渡,应该将标准 CGI 环境变量作为 SCGI 的请求头项提供。</p>
<p>请求头在被发送时,会被编码成 netstring。Netstring 编码方式在本文第四部分解释。请求体在被发送时则尾随在请求头后面,请求体的长度由请求头中的 <tt class="docutils literal">"CONTENT_LENGTH"</tt> 规定。</p>
</div>
<div class="section" id="netstring">
<h2>Netstring 编码</h2>
<p>任何 8 位字节串都可以被编码成 <tt class="docutils literal"><span class="pre">[长度]":"[内容]","</span></tt> 的格式。其中 <tt class="docutils literal">[内容]</tt> 是字节串本身,而 <tt class="docutils literal">[长度]</tt> 是一个非空字节串,以整数的 ASCII 字符串形式记录 <tt class="docutils literal">[内容]</tt> 的长度。这个 ASCII 字符串中,<30> ( <tt class="docutils literal">"0"</tt> )代表整数 0,<31> 代表整数 1,依此类推,直到 <39> 表示整数 9。 <tt class="docutils literal">[长度]</tt> 前面放置多余的 <tt class="docutils literal">"0"</tt> 是不被允许的:如果 <tt class="docutils literal">[内容]</tt> 为空, <tt class="docutils literal">[长度]</tt> 应该以 <30> 打头。</p>
<p>举例说明,字符串 <tt class="docutils literal">"hello world!"</tt> 的编码是 <31 32 3a 68 65 6c 6c 6f 20 77 6f 72 6c 64 21 2c>,即 <tt class="docutils literal">"12:hello <span class="pre">world!,"</span></tt> ;空的字符串的编码是 <tt class="docutils literal"><span class="pre">"0:,"</span></tt> 。 <tt class="docutils literal"><span class="pre">[长度]":"[内容]","</span></tt> 的格式被称为 netstring,而 <tt class="docutils literal">[内容]</tt> 则是该 netstring 的“解释取值结果”。</p>
</div>
<div class="section" id="id4">
<h2>范例</h2>
<p>前端 Web 服务器(即 SCGI 客户端)打开一个连接,并发送以下字符串内容到 SCGI 服务器:</p>
<pre class="literal-block">
"70:"
"CONTENT_LENGTH" <00> "27" <00>
"SCGI" <00> "1" <00>
"REQUEST_METHOD" <00> "POST" <00>
"REQUEST_URI" <00> "/deepthought" <00>
","
"What is the answer to life?"
</pre>
<p>SCGI 服务器回发了如下响应:</p>
<pre class="literal-block">
"Status: 200 OK" <0d 0a>
"Content-Type: text/plain" <0d 0a>
"" <0d 0a>
"42"
</pre>
<p>然后 SCGI 服务器关闭该请求&响应的网络连接。</p>
</div>
<div class="section" id="id5">
<h2>版权声明</h2>
<p>本文档(指原文,译者注)置于公共领域发表。</p>
<p>本文翻译采用 <a class="reference external" href="https://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh">知识共享署名-非商业性使用-相同方式共享 3.0 许可协议</a> 进行许可。</p>
</div>
C 语言程序设计笔记2013-05-19T01:38:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-05-19:post/the-learning-summary-of-c-programming/<p>呵呵,看标题像是大一同学的学习笔记,事实上我是一个大四将毕业的人了。我在大一入学之后认认真真学习的第一个程序语言是 C 语言,当然,是在在课堂上。可是快四年来,我用的最多的却是各路动态语言(人生短暂我用 Python)。后来毕业设计需要选题,我跟了一个很有黑客精神的导师 <a class="footnote-reference" href="#id5" id="id1">[0]</a> ,所以也就希望能挑战一下自己,选择了用我并不熟悉的 C 语言来做系统编程&网络编程。</p>
<p>那个项目 <a class="footnote-reference" href="#id7" id="id2">[1]</a> 总算还花了我不少时间和精力,虽然答辩时遇到了奇葩的人并发生了不愉快的事情,但我终究是学到了不少东西。这篇博客用来记录我掉的那些坑,并让读者看到后可以对我说:“你还是回大一去重学吧”。</p>
<div class="section" id="id3">
<h2>没承诺的事情它就不会去做</h2>
<p>用 C 语言读取一个文件,当然可以用 POSIX 系统调用 <tt class="docutils literal">read</tt> 函数。不过如果想兼容其他非 POSIX 平台,比如 Windows,比如某些特定的嵌入式环境,最好的选择还是使用 ANSI C 的 <tt class="docutils literal">stdio.h</tt> 提供的 <tt class="docutils literal">fread</tt> 函数。</p>
<p>我起初是用了这样一段代码去读取一个文本文件:</p>
<figure class="code"><figcaption><span>test_data.c</span></figcaption><div class="highlight"><pre> <span class="kt">char</span> <span class="o">*</span> <span class="nf">alloc_read_test_data</span><span class="p">(</span><span class="kt">FILE</span> <span class="o">*</span><span class="n">stream</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">max_len</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">char</span> <span class="o">*</span><span class="n">buffer</span><span class="p">;</span>
<span class="cm">/* allocate buffer */</span>
<span class="n">buffer</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">char</span><span class="p">)</span> <span class="o">*</span> <span class="n">max_len</span><span class="p">);</span>
<span class="n">memset</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">char</span><span class="p">)</span> <span class="o">*</span> <span class="n">max_len</span><span class="p">);</span>
<span class="cm">/* read test data */</span>
<span class="k">if</span> <span class="p">(</span><span class="n">fread</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">char</span><span class="p">),</span> <span class="n">max_len</span><span class="p">,</span> <span class="n">stream</span><span class="p">)</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span>
<span class="k">return</span> <span class="n">buffer</span><span class="p">;</span>
<span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</figure><p>这段代码在 Linux 下完全工作正常,但是在 OSX 下总是会出现“乱入”:</p>
<figure class="code"><figcaption><span>bash</span></figcaption><div class="highlight"><pre><span class="go"> $ cat test_data.txt</span>
<span class="go"> What is the answer to life?</span>
<span class="go"> $ ./test_read_data test_data.txt</span>
<span class="go"> What is the answer to life?</span>
<span class="go"> he answer to life?</span>
<span class="go"> $ # WTF ↑↑</span>
</pre></div>
</figure><p>我不相信这是脏内存导致的,因为为了保险起见我已经用 <tt class="docutils literal">memset</tt> 将新分配的内存块填 0 了。而且非常奇异的是,“乱入”的字符总是正常读取字符的节选。为此我搜索了很多资料都未寻到解答,还以为是 OSX 下 libc 的 Bug。最后翻查文档 <a class="footnote-reference" href="#id8" id="id4">[2]</a> 对 <tt class="docutils literal">fread</tt> 的描述:</p>
<blockquote>
<p>Reads an array of count elements, each one with a size of size bytes, from the stream and stores them in the block of memory specified by ptr.</p>
<p>The position indicator of the stream is advanced by the total amount of bytes read. The total amount of bytes read if successful is <tt class="docutils literal">(size*count)</tt> .</p>
</blockquote>
<p>貌似文档只承诺了 <tt class="docutils literal">fread</tt> 会做这么几点:</p>
<ul class="simple">
<li>将 n 组指定尺寸的元素从流( <tt class="docutils literal">FILE *</tt> )读入,然后存储到第一个参数指针所指向的内存区域</li>
<li>将流的位置指针向后移动,读了多少移动多少</li>
<li>读入的总字节数为提供的 <tt class="docutils literal">count</tt> 参数</li>
</ul>
<p>然后,关于返回值文档的描述是:</p>
<blockquote>
The total number of elements successfully read is returned. If this number
differs from the count parameter, either a reading error occurred or the
end-of-file was reached while reading. ...</blockquote>
<p>好了,再多承诺一点:</p>
<ul class="simple">
<li>返回值如果小于 <tt class="docutils literal">count</tt> 参数,则说明发生了读取错误,或者已经到达了文件结尾</li>
</ul>
<p>貌似在这些承诺中,有两点没有被提及:</p>
<ol class="arabic simple">
<li>将读取到的内容存入指定内存区域后,是否要在结尾补上字符串终结符 <tt class="docutils literal">'\0'</tt> ?</li>
<li>如果因为 EOF 或者其他原因,读到的字节数 M 小于指定字节数 COUNT, <tt class="docutils literal">fread</tt> 是否仅仅对大小为 M 的内存区域做出修改呢?</li>
</ol>
<p>首先可以确定第一点是否定的, <tt class="docutils literal">fread</tt> 并非仅仅为文本流读取设计,它不会自己去补这个终结符;第二点则是我遇到的奇怪问题所在:文档承诺对传入指针参数指向的内存区域写入,并且预期的写入尺寸由 <tt class="docutils literal">count</tt> 参数指定,而读取到的实际尺寸小于预期尺寸一律视为一种异常情况,包括 EOF 导致的截断。异常情况下文档的约定**并没有承诺**仅仅写入 M 大小的区域,而不污染 (M, COUNT] 的区域。尽管 Linux 下 glibc 做了这件没承诺的事情,但 libc 并没有这个责任。</p>
<p>所以,如果不考虑 EOF 以外的读取异常,用 <tt class="docutils literal">fread</tt> 读取一个最终读到尺寸可能小于预期尺寸的文件,应该手动补上终结符 <tt class="docutils literal">'\0'</tt> 以解决余下的区域可能的污染问题。</p>
<figure class="code"><figcaption><span>test_data.c</span></figcaption><div class="highlight"><pre> <span class="kt">char</span> <span class="o">*</span> <span class="nf">alloc_read_test_data</span><span class="p">(</span><span class="kt">FILE</span> <span class="o">*</span><span class="n">stream</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">max_len</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">char</span> <span class="o">*</span><span class="n">buffer</span><span class="p">;</span>
<span class="kt">size_t</span> <span class="n">nread</span><span class="p">;</span>
<span class="cm">/* allocate buffer */</span>
<span class="n">buffer</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">char</span><span class="p">)</span> <span class="o">*</span> <span class="n">max_len</span><span class="p">);</span>
<span class="n">memset</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">char</span><span class="p">)</span> <span class="o">*</span> <span class="n">max_len</span><span class="p">);</span>
<span class="cm">/* read test data */</span>
<span class="n">nread</span> <span class="o">=</span> <span class="n">fread</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">char</span><span class="p">),</span> <span class="n">max_len</span><span class="p">,</span> <span class="n">stream</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">nread</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="n">buffer</span><span class="p">[</span><span class="n">nread</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span> <span class="cm">/* for safe */</span>
<span class="k">return</span> <span class="n">buffer</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</figure><p>困了想睡觉…… 要不明天接着写吧?</p>
<table class="docutils footnote" frame="void" id="id5" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id1">[0]</a></td><td><a class="reference external" href="http://rsc.szu.edu.cn/fengcai/list.asp?ProdId=000842">尹剑飞老师,在深大的同学可以考虑选或者旁听他的课</a></td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id7" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id2">[1]</a></td><td><a class="reference external" href="https://github.com/tonyseek/tinyscgi">Yet Another Application Server Based on Lua and SCGI</a></td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id8" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id4">[2]</a></td><td><a class="reference external" href="http://www.cplusplus.com/reference/cstdio/fread/">fread - C++ Reference</a></td></tr>
</tbody>
</table>
</div>
香港藝術館2013-03-28T17:47:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-03-28:post/silent-hill-hong-kong-museum-of-art/<p>Silent Hill (在 Hong Kong Museum of Art 香港藝術館)</p>
<a class="reference external image-reference" href="https://instagram.com/p/XZY1WRvUC5/"><img alt="Silent Hill (在 Hong Kong Museum of Art 香港藝術館)" src="https://instagram.com/p/XZY1WRvUC5/media?size=l" /></a>
西汉南越王博物馆2013-03-24T14:53:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-03-24:post/the-museum-of-the-nanyue-king/<p>安卓… (在 西汉南越王博物馆 - The Museum of The Nanyue King Mausoleum)</p>
<a class="reference external image-reference" href="https://instagram.com/p/XOxAMRPUAQ/"><img alt="安卓… (在 西汉南越王博物馆 - The Museum of The Nanyue King Mausoleum)" src="https://instagram.com/p/XOxAMRPUAQ/media?size=l" /></a>
五羊雕塑2013-03-24T12:47:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-03-24:post/five-goats-statue/<p>五羊 (在 五羊雕塑 Five Goats Statue)</p>
<a class="reference external image-reference" href="https://instagram.com/p/XOjTiYvUEW/"><img alt="五羊 (在 五羊雕塑 Five Goats Statue)" src="https://instagram.com/p/XOjTiYvUEW/media?size=l" /></a>
木棉花 (深大文科楼)2013-02-27T18:38:00+08:00Jiangge Zhangtag:blog.tonyseek.com,2013-02-27:post/bombax-ceiba-in-szu/<a class="reference external image-reference" href="https://instagram.com/p/WOzoP6vUL3/"><img alt="木棉花 (在 深大文科楼)" src="https://instagram.com/p/WOzoP6vUL3/media?size=l" /></a>