<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ qiwsir - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ qiwsir - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 16 Jun 2026 21:16:06 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/qiwsir/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 pandas 的 read_html() 来读取表格数据 ]]>
                </title>
                <description>
                    <![CDATA[ 引言 pandas中的read_html()函数是将HTML的表格转换为DataFrame的一种快速方便的方法，这个函数对于快速合并来自不同网页上的表格非常有用。 在合并时，不需要用爬虫获取站点的HTML。但是，在分析数据之前，数据的清理和格式化可能会遇到一些问题。在本文中，我将讨论如何使用pandas的 read_html()来读取和清理来自维基百科的多个HTML表格，以便对它们做进一步的数值分析。 基本方法 在第一个例子中，我们将尝试解析一个表格。这个表格来自维基百科页面中明尼苏达州的政治部分（ https://en.wikipedia.org/wiki/Minnesota） [https://en.wikipedia.org/wiki/Minnesota)%E3%80%82]。 read_html的基本用法非常简单，在许多维基百科页面上都能运行良好，因为表格并不复杂。首先，要导入一些库 ，在后面的数据清理中都会用到： import pandas as pd import numpy as np import matplotlib.pyplot as plt from uni ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/read-form-data-with-pandas-read-html/</link>
                <guid isPermaLink="false">6146a302b201730648bda21d</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pandas ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Fri, 17 Sep 2021 04:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/09/lukas-blazek-mcSDtbWXUZU-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-">引言</h2><p>pandas中的<code>read_html()</code>函数是将HTML的表格转换为DataFrame的一种快速方便的方法，这个函数对于快速合并来自不同网页上的表格非常有用。 在合并时，不需要用爬虫获取站点的HTML。但是，在分析数据之前，数据的清理和格式化可能会遇到一些问题。在本文中，我将讨论如何使用pandas的<code>read_html()</code>来读取和清理来自维基百科的多个HTML表格，以便对它们做进一步的数值分析。</p><h2 id="--1">基本方法</h2><p>在第一个例子中，我们将尝试解析一个表格。这个表格来自维基百科页面中明尼苏达州的政治部分（<a href="https://en.wikipedia.org/wiki/Minnesota)%E3%80%82" rel="noopener">https://en.wikipedia.org/wiki/Minnesota）</a>。</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-16/1600236785993-r1.png" class="kg-image" alt="1600236785993-r1" width="600" height="400" loading="lazy"></figure><p><code>read_html</code>的基本用法非常简单，在许多维基百科页面上都能运行良好，因为表格并不复杂。首先，要导入一些库 ，在后面的数据清理中都会用到：</p><pre><code class="language-python">import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from unicodedata import normalize

table_MN = pd.read_html('https://en.wikipedia.org/wiki/Minnesota')
</code></pre><p>特别注意，上面代码中得到的<code>table_MN</code>是页面上所有表格的列表：</p><pre><code class="language-python">print(f'Total tables: {len(table_MN)}')

Total tables: 38</code></pre><p>很难在38张表格中找到你需要的那张，要想容易地找出来，可以设置<code>match</code>参数，如下面的代码所示，用<code>mathch</code>参数指明要选择标题为“Election results from statewide races”的那张表格。</p><pre><code class="language-python">table_MN = pd.read_html('https://en.wikipedia.org/wiki/Minnesota', match='Election results from statewide races')
len(table_MN)

# 输出
1</code></pre><pre><code class="language-python">df = table_MN[0]
df.head()</code></pre><p>输出：</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-16/1600237073054-r2.png" class="kg-image" alt="1600237073054-r2" width="600" height="400" loading="lazy"></figure><p>显然，用Pandas能够很容易地读取到了表格，此外，从上面的输出结果可以看出，跨多行的<code>Year</code>列也得到了很好地处理，这要比自己写爬虫工具专门收集数据简单多了。</p><p>总的来说，这样的操作看起来还不错，然而，如果用<code>df.info()</code>来查看数据类型：</p><pre><code class="language-python">&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 24 entries, 0 to 23
Data columns (total 5 columns):
#   Column  Non-Null Count  Dtype
---  ------  --------------  -----
0   Year    24 non-null     int64
1   Office  24 non-null     object
2   GOP     24 non-null     object
3   DFL     24 non-null     object
4   Others  24 non-null     object
dtypes: int64(1), object(4)
memory usage: 1.1+ KB</code></pre><p>如果想对这些数据进行分析，需要将<code>GOP</code>、<code>DFL</code>和其他类型为<code>object</code>的列转换为数值。</p><p>如果这么操作：</p><pre><code class="language-python">df['GOP'].astype('float')
</code></pre><p>系统就会报错：</p><pre><code class="language-python">ValueError: could not convert string to float: '42.4%'</code></pre><p>最有可能的罪魁祸首是<code>%</code>，下面用pandas的<code>replace()</code>函数删除它。</p><pre><code class="language-python">df['GOP'].replace({'%':''}, regex=True).astype('float')
</code></pre><p>效果看起来不错：</p><pre><code class="language-python">0     42.4
1     36.2
2     42.4
3     44.9
&lt;...&gt;
21    63.3
22    49.1
23    31.9
Name: GOP, dtype: float64
</code></pre><p>注意，必须使用参数<code>regex=True</code>才能完美地删除，因为<code>%</code>是字符串的一部分，而不是完整的字符串值。</p><p>现在，我们可以用<code>pd.to_numeric()</code>和<code>apply()</code>替换所有的<code>%</code>值，并将其转换为数字。</p><pre><code class="language-python">df = df.replace({'%': ''}, regex=True)
df[['GOP', 'DFL', 'Others']] = df[['GOP', 'DFL', 'Others']].apply(pd.to_numeric)
df.info()

# 输出
&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 24 entries, 0 to 23
Data columns (total 5 columns):
#   Column  Non-Null Count  Dtype
---  ------  --------------  -----
0   Year    24 non-null     int64
1   Office  24 non-null     object
2   GOP     24 non-null     float64
3   DFL     24 non-null     float64
4   Others  24 non-null     float64
dtypes: float64(3), int64(1), object(1)
memory usage: 1.1+ KB</code></pre><pre><code class="language-python">df.head()
</code></pre><p>输出：</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-16/1600237869486-r3.png" class="kg-image" alt="1600237869486-r3" width="600" height="400" loading="lazy"></figure><p>这个基本过程进展顺利，下面看一个有点难度的。</p><h2 id="--2">高级的数据清理方法</h2><p>前面的例子展示了基本概念，数据清理是任何数据科学项目都不可或缺的，下面看一个有点难度的示例。在接下来的示例中继续使用维基百科，但是这些方法同样适用于其他含有表格的HTML页面。</p><p>例如读取美国GDP的数据表：</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-16/1600237927753-us_gdp.png" class="kg-image" alt="1600237927753-us_gdp" width="600" height="400" loading="lazy"></figure><p>现在，就不能用<code>match</code>参数指定要获得的那个表格标题——因为这表格没有标题，但是可以将其值设置为“Nominal GDP”，这样依然能匹配到我们想要的表格。</p><pre><code class="language-python">table_GDP = pd.read_html('https://en.wikipedia.org/wiki/Economy_of_the_United_States', match='Nominal GDP')
df_GDP = table_GDP[0]
df_GDP.info()

# 输出
&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 41 entries, 0 to 40
Data columns (total 9 columns):
#   Column                                            Non-Null Count  Dtype
---  ------                                            --------------  -----
0   Year                                              41 non-null     object
1   Nominal GDP(in bil. US-Dollar)                    41 non-null     float64
2   GDP per capita(in US-Dollar)                      41 non-null     int64
3   GDP growth(real)                                  41 non-null     object
4   Inflation rate(in percent)                        41 non-null     object
5   Unemployment (in percent)                         41 non-null     object
6   Budget balance(in % of GDP)[107]                  41 non-null     object
7   Government debt held by public(in % of GDP)[108]  41 non-null     object
8   Current account balance(in % of GDP)              41 non-null     object
dtypes: float64(1), int64(1), object(7)
memory usage: 3.0+ KB</code></pre><p>不出所料，数据清理是避免不了得了。根据前面的经验，先删除<code>%</code>。</p><pre><code class="language-python">df_GDP['GDP growth(real)'].replace({'%': ''}, regex=True).astype('float')
</code></pre><p>很遗憾，报错了：</p><pre><code class="language-python">ValueError: could not convert string to float: '−5.9\xa0'
</code></pre><p>问题的根源在于有一个隐藏字符<code>xa0</code>，它导致了错误，它是一个特殊字符，即“non-breaking Latin1 (ISO 8859-1) space”，对应的实体是 <code>&amp;nbsp</code>，即空格。</p><p>我所使用的一个方法是使用<code>replace</code>直接替换，这种方法奏效了，但我担心它将来是否会与其他字符产生冲突。</p><p>在深入研究了Unicode这个坑之后，我决定使用<code>normalize</code>来清理这个值。</p><p>我还发现，在其他的一些表格的数据中也有多余的空格。于是编写了一个函数，对所有文本进行清理。</p><pre><code class="language-python">from unicodedata import normalize

def clean_normalize_whitespace(x):
    if isinstance(x, str):
        return normalize('NFKC', x).strip()
    else:
        return x</code></pre><p>用<code>applymap</code>将这个函数用于整个DataFrame上：</p><pre><code class="language-python">df_GDP = df_GDP.applymap(clean_normalize_whitespace)
</code></pre><p>需要注意的是：<code>applymap</code>函数非常慢，所以在使用<code>applymap</code>时应该慎重。</p><p><code>applymap</code>函数是一个非常低效的pandas函数，不推荐你经常使用它。但在本例中，DataFrame很小，像这样的清理又很棘手，所以我认为这是一个有用的权衡。</p><p><code>applymap</code>不能处理列名称，例如：</p><pre><code class="language-python">df_GDP.columns[7]

# 输出
'Government debt held by public(in\xa0% of GDP)[108]'</code></pre><p>在列的名称中有可怕的<code>xa0%</code>。解决此问题的方法有多种，在这里还是继续使用<code>clean_normalize_whitespace()</code>函数，将列转换为Series对象，并使用<code>apply</code>来调用这个函数。有点麻烦了，不知道pandas在以后的版本是否会考虑到这里的问题，让操作简化。</p><pre><code class="language-python">df_GDP.columns = df_GDP.columns.to_series().apply(clean_normalize_whitespace)
df_GDP.columns[7]

# 输出
'Government debt held by public(in % of GDP)[108]'</code></pre><p>现在我们清理掉了一些隐藏的字符。下一步会怎样呢？</p><p>再试一次：</p><pre><code class="language-python">df_GDP['GDP growth(real)'].replace({'%': ''}, regex=True).astype('float')

# 输出
ValueError: could not convert string to float: '−5.9 '</code></pre><p>真的很棘手。如果你仔细观察，你可能会发现：<code>−</code>和<code>-</code>看起来有点不同，但真的很难看出，在Unicode中，破折号和减号之间实际上是有区别的。</p><p>幸运的是，我们也可以使用<code>replace</code>来清理：</p><pre><code class="language-python">df_GDP['GDP growth(real)'].replace({'%': '', '−': '-'}, regex=True).astype('float')

# 输出
0    -5.9
1     2.2
2     3.0
3     2.3
4     1.7
&lt;...&gt;
38   -1.8
39    2.6
40   -0.2
Name: GDP growth(real), dtype: float64</code></pre><p>现在来关注列<code>Year</code>，例如表示“2020年”的值是<code>2020(est)</code>，需要去掉其中的<code>(est)</code>，还要将列转换为整数型。</p><pre><code class="language-python">df['Year'].replace({'%': '', '−': '-', '\(est\)': ''}, regex=True).astype('int')

# 输出
0     2020
1     2019
2     2018
3     2017
4     2016
&lt;...&gt;
40    1980
Name: Year, dtype: int64</code></pre><p>在DataFrame中的各列的值，除了整数型之外，其他的是浮点数型，在转化的时候，如果使用<code>pd.numeric()</code>虽然能够实现，但略显笨拙。我们可以使用<code>astype()</code>同时又不需要为每一列手动输入类型信息。</p><p><code>astype()</code>函数可以接受含有列名和数据类型的字典。这真的很有用，直到我写了这篇文章我才知道这一点。下面是对列与其数据类型映射字典：</p><pre><code class="language-python">col_type = {
    'Year': 'int',
    'Nominal GDP(in bil. US-Dollar)': 'float',
    'GDP per capita(in US-Dollar)': 'int',
    'GDP growth(real)': 'float',
    'Inflation rate(in percent)': 'float',
    'Unemployment (in percent)': 'float',
    'Budget balance(in % of GDP)[107]': 'float',
    'Government debt held by public(in % of GDP)[108]': 'float',
    'Current account balance(in % of GDP)': 'float'
}</code></pre><p>如果你觉得键入上面这个词典很慢，可以用下面的快捷方法。要注意，这样建立的字典，默认值为<code>float</code>，还需要手动将<code>Year</code>对应的值修改为<code>int</code>：</p><pre><code class="language-python">dict.fromkeys(df_GDP.columns, 'float')

# 输出
{'Year': 'float',
'Nominal GDP(in bil. US-Dollar)': 'float',
'GDP per capita(in US-Dollar)': 'float',
'GDP growth(real)': 'float',
'Inflation rate(in percent)': 'float',
'Unemployment (in percent)': 'float',
'Budget balance(in % of GDP)[107]': 'float',
'Government debt held by public(in % of GDP)[108]': 'float',
'Current account balance(in % of GDP)': 'float'}</code></pre><p>再创建了一个字典，其中包含要替换的值：</p><pre><code class="language-python">clean_dict = {'%': '', '−': '-', '\(est\)': ''}
</code></pre><p>现在我们可以调用这个DataFrame的<code>replace</code>方法，转换为所需的类型，并获得干净的数据：</p><pre><code class="language-python">df_GDP = df_GDP.replace(clean_dict, regex=True).replace({'-n/a ': np.nan}).astype(col_type)
df_GDP.info()

# 输出
&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 41 entries, 0 to 40
Data columns (total 9 columns):
#   Column                                            Non-Null Count  Dtype
---  ------                                            --------------  -----
0   Year                                              41 non-null     int64
1   Nominal GDP(in bil. US-Dollar)                    41 non-null     float64
2   GDP per capita(in US-Dollar)                      41 non-null     int64
3   GDP growth(real)                                  41 non-null     float64
4   Inflation rate(in percent)                        41 non-null     float64
5   Unemployment (in percent)                         41 non-null     float64
6   Budget balance(in % of GDP)[107]                  40 non-null     float64
7   Government debt held by public(in % of GDP)[108]  41 non-null     float64
8   Current account balance(in % of GDP)              40 non-null     float64
dtypes: float64(7), int64(2)
memory usage: 3.0 KB</code></pre><p>结果如下所示：</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-16/1600238907819-r4.png" class="kg-image" alt="1600238907819-r4" width="600" height="400" loading="lazy"></figure><p>为了证明上述操作的效果，我们可以把这些数据绘制成图表：</p><pre><code class="language-python">plt.style.use('seaborn-whitegrid')
df_clean.plot.line(x='Year', y=['Inflation rate(in percent)', 'Unemployment (in percent)'])
</code></pre><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-16/1600238986033-us_gdp_chart.png" class="kg-image" alt="1600238986033-us_gdp_chart" width="600" height="400" loading="lazy"></figure><p>如果你紧跟我的思路，可能已经注意到链式方式调用<code>replace</code>的方法：</p><pre><code class="language-python">.replace({'-n/a ': np.nan})
</code></pre><p>我这样做的原因是我不知道如何使用第一个字典<code>replace</code>来清理<code>n/a</code>。我认为问题的症结在于：我无法预测这些数据的清理顺序，所以不得不分两个阶段来执行替换。</p><p>如果读者有更好的方法，请不吝赐教。</p><h2 id="--3">完整的代码</h2><p>最后，把上面的过程，集中用下面的代码实现。从HTML网页上的表格获取数据，并把这些数据转化为DataFrame对象。</p><pre><code class="language-python">import pandas as pd
import numpy as np
from unicodedata import normalize

def clean_normalize_whitespace(x):
    """ 
    Normalize unicode characters and strip trailing spaces
    """
    if isinstance(x, str):
        return normalize('NFKC', x).strip()
    else:
        return x

# Read in the Wikipedia page and get the DataFrame
table_GDP = pd.read_html(
    'https://en.wikipedia.org/wiki/Economy_of_the_United_States',
    match='Nominal GDP')
df_GDP = table_GDP[0]

# Clean up the DataFrame and Columns
df_GDP = df_GDP.applymap(clean_normalize_whitespace)
df_GDP.columns = df_GDP.columns.to_series().apply(clean_normalize_whitespace)

# Determine numeric types for each column
col_type = {
    'Year': 'int',
    'Nominal GDP(in bil. US-Dollar)': 'float',
    'GDP per capita(in US-Dollar)': 'int',
    'GDP growth(real)': 'float',
    'Inflation rate(in percent)': 'float',
    'Unemployment (in percent)': 'float',
    'Budget balance(in % of GDP)[107]': 'float',
    'Government debt held by public(in % of GDP)[108]': 'float',
    'Current account balance(in % of GDP)': 'float'
}

# Values to replace
clean_dict = {'%': '', '−': '-', '\(est\)': ''}

# Replace values and convert to numeric values
df_GDP = df_GDP.replace(clean_dict, regex=True).replace({
    '-n/a ': np.nan
}).astype(col_type)</code></pre><h2 id="--4">总结</h2><p>pandas的<code>read_html()</code>函数对于快速解析页面中的 HTML表格非常有用，尤其是维基百科页面。从HTML页面直接获得的数据，通常不会像你所需要的那样干净，并且清理各种Unicode字符可能会非常耗时。本文展示的几种技术可以用于清理数据、并将其转换为正确的数字格式。如果你需要从维基百科或其他HTML表格中获取数据，这些技巧应该可以为你节省一些时间。</p><p>参考资料：<a href="https://pbpython.com/pandas-html-table.html" rel="noopener">https://pbpython.com/pandas-html-table.html</a></p><p>欢迎在<a href="https://qiwsir.github.io/">我的博客</a>阅读更多内容。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python 中的 type 和 isinstance ]]>
                </title>
                <description>
                    <![CDATA[ Python是一种动态语言，比如创建一个变量，一开始引用的是字符串，随后就可以再引用整数或者浮点数，解释器对这种变换也接受。这与类似Java那样的语言就完全不同了。 name = "Sebastian" # 下面演示的就是动态语言特点 name = 42 name = None name = Exception()    # 引用一个实例对象 在程序中，检查变量所引用的对象是什么类型，对于Python程序也是必要的。一般我们会实用type()或者isinstance()这两个内置函数。 >>> variable = "hello" >>> type(variable) is str True >>> isinstance(variable, str) True 下面比较一下这两个函数的性能： $ python -m timeit -s "variable = 'hello'" "type(variable) is int" 2000000 loops, best of ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/python-type-and-isinstance/</link>
                <guid isPermaLink="false">611b7654020863066536abd8</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Tue, 17 Aug 2021 08:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/08/walling-e_MdMMKrgdY-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Python是一种动态语言，比如创建一个变量，一开始引用的是字符串，随后就可以再引用整数或者浮点数，解释器对这种变换也接受。这与类似Java那样的语言就完全不同了。</p><pre><code class="language-python">name = "Sebastian"
# 下面演示的就是动态语言特点
name = 42
name = None
name = Exception()    # 引用一个实例对象</code></pre><p>在程序中，检查变量所引用的对象是什么类型，对于Python程序也是必要的。一般我们会实用<code>type()</code>或者<code>isinstance()</code>这两个内置函数。</p><pre><code class="language-python">&gt;&gt;&gt; variable = "hello"
&gt;&gt;&gt; type(variable) is str
True
&gt;&gt;&gt; isinstance(variable, str)
True</code></pre><p>下面比较一下这两个函数的性能：</p><pre><code class="language-python">$ python -m timeit -s "variable = 'hello'" "type(variable) is int"
2000000 loops, best of 5: 102 nsec per loop
$ python -m timeit -s "variable = 'hello'" "isinstance(variable, str)"
5000000 loops, best of 5: 72.8 nsec per loop</code></pre><p><code>type</code>比<code>instance</code>慢了 40% (102/72.8 = 1.40).</p><p>有人也实用 <code>type(variable) == str</code>种方式判断某个对象的类型，虽然此方法是可行的，但不提倡，因为：</p><ul><li><code>==</code>应该用于检查对象是否与另外一个对象相等。我们可以用它来查看变量的值是否等于<code>hello</code>，但是想要检查变量是否是一个字符串时，不要用这个符号，而是改用<code>is</code>操作符更合适。</li><li><code>==</code>的执行速度更慢，可以用下面的代码检验：</li></ul><pre><code class="language-python">$ python -m timeit -s "variable = 'hello'" "type(variable) == str"
2000000 loops, best of 5: 114 nsec per loop</code></pre><p><code>isinstance</code>和<code>type</code>之间除了前面演示的执行速度不同之外，还有别的区别吗？</p><p>有！而且下面要说的区别，比执行速度还重要。</p><ul><li><code>type</code>的返回值是一个对象的类型（类），可以用它来检查<code>variable</code>的类型是否为<code>str</code>。</li><li><code>isinstance</code>要检查第一个参数对象是不是第二个参数所指定的类的实例，例如<code>variable</code>是<code>str</code>类的一个实例吗？或者，检查是不是第二个参数所指定的类的子类的示例，例如<code>variable</code>是<code>str</code>子类的一个实例吗?</li></ul><p>这在实践很有用。假设自定义一个类，它类似于列表，但方法可以更多一些。所以我们可以把<code>list</code>作为这个类的父类，然后在这个类里面写其他的方法，基本样式如下：</p><pre><code class="language-python">class MyAwesomeList(list):
    # Add additional functions here
`</code></pre><p>但是现在，如果我们将这个新类与一个列表进行比较，<code>type</code> 和<code>isinstance</code>会返回不同的结果！</p><pre><code class="language-python">&gt;&gt;&gt; my_list = MyAwesomeList()
&gt;&gt;&gt; type(my_list) is list
False
&gt;&gt;&gt; isinstance(my_list, list)
True</code></pre><p>输出结果不同。</p><p><code>isinstance</code>检查<code>my_list</code>是否是<code>list</code>的一个实例(它不是)、或者是否是<code>list</code>的一个子类的实例(它是，因为<code>MyAwesomeList</code>是<code>list</code>的一个子类)。这个细节，有时候会导致BUG。</p><p><code>isinstance</code>通常是判断对象类型的首选方法。它不仅更快，而且还考虑了继承，这通常是我们所需要的。不过，在Python中，我们通常不需要检查某个对象的类型，只需要关注它能不能具备像字符串或列表那样的方法和属性，这就是著名的“鸭子检验”。因此，只需要使用<code>isinstance</code>即可。</p><p>另一方面，如果想显式地检查给定对象是否属于某一特定类型(而不是它的子类)，可以使用<code>type</code>，但通常用这样的语句<code>type(var) is some_type</code>，而不是<code>type(var) == some_type</code>。</p><p>记住，编写函数的时候，不检查对象类型，是Python的惯例，不要把Java的习惯带过来。</p><p>欢迎在<a href="http://www.itdiffer.com/">我的网站</a>阅读更多内容。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python 中 __name__ 有什么用 ]]>
                </title>
                <description>
                    <![CDATA[ 在Python程序中，你会经常看到 __name__，例如： if __name__ == '__main__':     main() 本文将介绍如何正确使用和理解这个变量。 请注意，__name__  在程序中是一个变量，只不过这个变量的命名有点奇怪，用双下划线开头和结尾。这种命名的方法，在Pyhton的类对象设计中，常常用于一些具有特殊作用的属性或者方法名称。 如果将编写的一个Python文件，即 .py 为扩展名的文件，作为模块被其他程序引入的时候，我们需要通过 __name__  这个变量，决定在引入的时候是否要运行该文件。 例如创建一个文件 namescript.py，内容如下： def my_function():     print('the value of __name__ is ' + __name__)      def main():     my_function()  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/python-name/</link>
                <guid isPermaLink="false">6119281e17b8810648f27a72</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Sat, 14 Aug 2021 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/08/ales-nesetril-Im7lZjxeLhg-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在Python程序中，你会经常看到 <code>__name__</code>，例如：</p><pre><code class="language-python">if __name__ == '__main__':
    main()</code></pre><p>本文将介绍如何正确使用和理解这个变量。</p><p>请注意，<code>__name__</code> 在程序中是一个变量，只不过这个变量的命名有点奇怪，用双下划线开头和结尾。这种命名的方法，在Pyhton的类对象设计中，常常用于一些具有特殊作用的属性或者方法名称。</p><p>如果将编写的一个Python文件，即 <code>.py</code> 为扩展名的文件，作为模块被其他程序引入的时候，我们需要通过 <code>__name__</code> 这个变量，决定在引入的时候是否要运行该文件。</p><p>例如创建一个文件 <code>namescript.py</code>，内容如下：</p><pre><code class="language-python">def my_function():
    print('the value of __name__ is ' + __name__)
    
def main():
    my_function()
    
if __name__ == "__main__":
    main()</code></pre><p>如果执行 <code>namescript.py</code> 文件，其执行流程如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/08/image-4.png" class="kg-image" alt="image-4" width="600" height="400" loading="lazy"></figure><p>在<code>importscript.py</code>中，变量<code>__name__</code>被赋值为<code>__main__</code>，然后执行<code>import namescript</code>，Python解析器会自动在模块名字后面增加<code>.py</code>并在检索目录中查找该文件——所以，在引入模块的时候不要写成<code>import namescript.py</code>。找到了，就引入该文件中的所有代码。</p><p>然后，将<code>namescript.py</code>文件所运行的空间中的<code>__name__</code>设置为<code>namescript</code>。结合上图，在<code>importingscript.py</code>和<code>namescript.py</code>中，分别有两个同名的<code>__name__</code>变量，但是，它们的值不同。如此，在<code>namescript.py</code>中，因为<code>__name__</code>的值是<code>namescript</code>了，于是该文件中的<code>if __name__ == "__main__"</code>条件不再成立，所以此条件下的<code>main()</code>函数不再执行。</p><p>在<code>importingscript.py</code>中，调用了<code>namescript.my_function()</code>，打印的结果是：<code>the value of __name__ is namescript</code>，这里的<code>__name__</code>当然是<code>namescript.py</code>中的变量。</p><p>如果在<code>importingscript.py</code>中，增加<code>print(__name__)</code>，打印出来的结果应该是<code>__main__</code>。</p><p>本文内容作为<a href="http://www.itdiffer.com/python_course.html">《Python大学实用教程》</a>第7章的补充和拓展，帮助你理解<code>__name__</code>变量的作用及其在模块编写中的应用效果。</p><h2 id="-">参考文献：</h2><p>[1]. <a href="https://chinese.freecodecamp.org/news/name-in-python/">What’s in a (Python’s) __name__?</a></p><p>[2]. Python大学实用教程. 齐伟. 北京：电子工业出版社. 2019.3，第1版</p><p>欢迎在<a href="http://www.itdiffer.com/">我的网站</a>阅读更多内容。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 用Python读写文件的方法 ]]>
                </title>
                <description>
                    <![CDATA[ 在文中，我们将研习如何用Python读取文件，然后，向文件写入内容并再次保存它。使用Python读写某种特别类型的文件，例如：JSON、CSV、Excel等，一般会有专门的模块。但是，在这里，我们将用Python打开文本文件(.txt)。 若使用Python的open 函数，它将返回一个文件对象，此对象将包含一些方法和属性。我们可以使用这些方法和属性获得已打开文件的相关信息，并且，可以使用这些方法来更改所打开的文件。 用 open()读取文件 在本节中，我们将学习如何使用open()函数在Python中加载文件，最简单的例子是打开一个文件并创建一个文件对象。 当使用Python的open()函数打开一个文件时，有若干个参数可用。然而，最常用的参数只有前两个。注意，第一个是强制性的，其余的是可选的。如果不添加mode 参数，文件将在Python中以只读模式打开。 open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) mode参 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/python-read-and-write-files/</link>
                <guid isPermaLink="false">60fd64ed651f1b064ee80c87</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Sat, 24 Jul 2021 12:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/07/james-harrison-vpOeXr5wmR4-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在文中，我们将研习如何用Python读取文件，然后，向文件写入内容并再次保存它。使用Python读写某种特别类型的文件，例如：JSON、CSV、Excel等，一般会有专门的模块。但是，在这里，我们将用Python打开文本文件(.txt)。</p><p>若使用Python的<code>open</code>函数，它将返回一个文件对象，此对象将包含一些方法和属性。我们可以使用这些方法和属性获得已打开文件的相关信息，并且，可以使用这些方法来更改所打开的文件。</p><h2 id="-open-">用 <code>open()</code>读取文件</h2><p>在本节中，我们将学习如何使用<code>open()</code>函数在Python中加载文件，最简单的例子是打开一个文件并创建一个文件对象。</p><p>当使用Python的<code>open()</code>函数打开一个文件时，有若干个参数可用。然而，最常用的参数只有前两个。注意，第一个是强制性的，其余的是可选的。如果不添加<code>mode</code>参数，文件将在Python中以只读模式打开。</p><pre><code class="language-python">open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)</code></pre><h3 id="mode-"><code>mode</code>参数</h3><p>读取文件有不同模式。如前所述，如果不带有<code>mode</code>参数，文件就会以只读方式打开，如下所示，列出了常用的几种打开模式。</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598926360596-file01.jpg" class="kg-image" alt="1598926360596-file01" width="600" height="400" loading="lazy"></figure><p>其中，<code>mode='r'</code>表示制度；<code>mode='w'</code>表示只写；<code>mode='a'</code>表示追加。<code>mode='r+'</code>表示可读写，但是文件必须存在，否则报错。</p><h3 id="-">一个简单的示例</h3><p>在下面的代码示例中使用<code>open()</code>代开一个文件，此处假设文件与Python脚本在同一个目录中，否则要增加路径。</p><pre><code class="language-python">exfile = open('example_file')
print(exfile)</code></pre><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598926533962-file02.jpg" class="kg-image" alt="1598926533962-file02" width="600" height="400" loading="lazy"></figure><p>在上图中，很明显我们有一个以只读模式打开的文件对象，在<code>open()</code>中除了文件名之外没有任何其他参数。因此，无法向该文件写入任何内容。如果要打印文件名，只需键入<code>print(exfile.name)</code>。</p><h3 id="--1">创建文本文件并写入内容</h3><p>下面使用<code>open()</code>创建一个新文件。现在，要使用<code>mode='w'</code>参数，这样能够打开一个文件对象，并可以使用“文件对象写入”方法。</p><pre><code class="language-python">exfile = open('example_file2', 'w')
print(exfile)</code></pre><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598926597909-file03.jpg" class="kg-image" alt="1598926597909-file03" width="600" height="400" loading="lazy"></figure><p>在上图中，可以当前文件对象是写入模式(‘ w ‘)，在下面的代码块中，我们将向这个文件中添加一行文本：</p><pre><code class="language-python">exfile.write('This is example file 2 \n')
</code></pre><p>当然，也可以添加更多的行：</p><pre><code class="language-python">exfile.write('Line number 2, in example file 2')
exfile.close()</code></pre><p>注意，在最后一行务必要使用<code>close()</code> 关闭文件。在下图中，我们可以看到用Python创建的示例文件。</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598926726878-file04.jpg" class="kg-image" alt="1598926726878-file04" width="600" height="400" loading="lazy"></figure><h3 id="-open-python-">如何使用open()读取Python中的文本文件</h3><p>在下一个用Python读取文件的示例中，我们将学习如何在Python中打开文本文件（.txt）。当然，这很简单，我们基本上已经掌握了如何使用Python实现这一目的。也就是说，如果我们只想在Python中读取.txt文件，我们可以使用open函数和read模式：</p><pre><code class="language-python">txtfile = open('example_file.txt')
</code></pre><h3 id="read-">read()示例</h3><p>这个操作很简单。现在，如果我们想打印文本文件的内容，可以有三个方法。第一个，使用文件对象的<code>read()</code>方法，读取整个文件内容。也就是说，用<code>txtfile.read()</code>可以得到以下输出:</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598926871330-file05.jpg" class="kg-image" alt="1598926871330-file05" width="600" height="400" loading="lazy"></figure><p>第二个是用<code>readlines()</code>将文件读取到列表中：</p><pre><code class="language-python">txtfile = open('example_file.txt')  
print(txtfile.readlines())</code></pre><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598926958528-file06.jpg" class="kg-image" alt="1598926958528-file06" width="600" height="400" loading="lazy"></figure><p>在这个方法中，还可以使用通过提供参数，说明读取某些行。例如，下面的代码将把前两行读入，然后将其打印出来：</p><pre><code class="language-python">txtfile = open('example_file.txt')
line = txtfile.readlines(1)
print(line)

line2 = txtfile.readlines(2)
print(line2)</code></pre><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598927028854-file07.jpg" class="kg-image" alt="1598927028854-file07" width="600" height="400" loading="lazy"></figure><p>最后一个方法，通过循环方式，把文件的内容逐行打印出来：</p><pre><code class="language-python">txtfile = open('example_file.txt')
for line in txtfile:
    print(line)</code></pre><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598927101308-file08.jpg" class="kg-image" alt="1598927101308-file08" width="600" height="400" loading="lazy"></figure><h2 id="--2">写入文件内容</h2><p>在示例中，打开一个<code>.txt</code>文件，并向其中以追加的方式增加内容，故需要用<code>'a'</code>模式打开。</p><pre><code class="language-python">open('example_file2.txt', 'a')</code></pre><p>接下来，使用<code>write()</code>向其追加内容。</p><pre><code class="language-python">txtfile.write('\n More text here.')
</code></pre><p>在添加文本时，至少在Windows 10中，必须在行前添加<code>\n</code>。否则，新的一行将添加到最后一个字符的后面（在文件的最后一行）。如果我们要添加更多的行，也必须记住这样操作；</p><pre><code class="language-python">txtfile.write(‘\nLast line of text, I promise.)
txtfile.close()</code></pre><p>可以使用文本编辑器(例如，Notepad, Gedit)打开文本文件，会看到添加的最后两行:</p><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598927206346-file09.jpg" class="kg-image" alt="1598927206346-file09" width="600" height="400" loading="lazy"></figure><h2 id="-with-">使用with语句</h2><p>使用with语句打开文件是一个非常好的习惯，这样就不必记住关闭文件，并且使用with语句的语法清晰易读：</p><pre><code class="language-python">with open('example_file2.txt') as txtfile2:
    print(txtfile2.read())</code></pre><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598927307211-f1.jpg" class="kg-image" alt="1598927307211-f1" width="600" height="400" loading="lazy"></figure><p>现在，如果我们使用<code>read()</code>方法，Python会抛出ValueError：</p><pre><code class="language-python">txtfile2.read()</code></pre><figure class="kg-card kg-image-card"><img src="https://gitee.com/qiwsir/images/raw/master/2020-9-1/1598927364414-f2.jpg" class="kg-image" alt="1598927364414-f2" width="600" height="400" loading="lazy"></figure><h2 id="--3">分词和统计</h2><p>在读取文件后，可以使用字符串的<code>split()</code>方法将文本文件中的句子分割成单词，然后用collections模块中的<code>Counter</code>类来统计打开的文件中的单词数量。</p><pre><code class="language-python">from collections import Counter

with open('example_file2.txt') as txtfile2:
    wordcount = Counter(txtfile2.read().split())
    
print(len(wordcount))
# Output: 43</code></pre><p>现在，<code>Counter</code>类返回了一个字典，该字典包含所有单词和每个单词出现的次数。因此，可以这样来打印所有单词和单词总数：</p><pre><code class="language-python">for k in sorted(wordcount, key=wordcount.get, reverse=True):
    print(k, wordcount[k])</code></pre><p>在上面的代码示例中，我们循环遍历字典中的键并对它们进行排序。这样，就把最常见的词排在最上面。当然，如果用Python读取包含多个单词的文件、并像这样打印结果，这种操作就是不可行的。</p><p>以上介绍了以不同的模式读取文件、创建和写入文件、将数据追加到文件的方法，以及如何使用with语句读取文件。</p><p>欢迎在我的<a href="http://www.itdiffer.com/">个人网站</a>阅读更多文章。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 详解 Python 中的 filter() 函数 ]]>
                </title>
                <description>
                    <![CDATA[ Python内置的filter() 函数能够从可迭代对象（如字典、列表）中筛选某些元素，并生成一个新的迭代器。可迭代对象是一个可以被“遍历”的Python对象，也就是说，它将按顺序返回各元素，这样我们就可以在 for循环中使用它。 filter()函数的基本语法是： filter(function, iterable) 返回一个可迭代的filter对象，可以使用list()函数将其转化为列表，这个列表包含过滤器对象中返回的所有的项。 filter() 函数所提供的过滤方法，通常比用列表解析更有效，特别是当我们处理更大的数据集时。例如，列表解析会生成一个新列表，这会增加该处理的运行时间。当列表解析执行完毕它的表达式后，内存中会有两个列表。但是， filter()将生成一个简单的对象，该对象包含对原始列表的引用、提供的函数以及原始列表中位置的索引，这样操作占用的内存更少。 下面介绍filter()的不同用法。 在filter()中使用特殊函数 filter()的第一个参数是一个函数，用它来决定第二个参数所引用的可迭代对象中的每一项的去留。此函数被调用后，当返回False 时， ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/python-filter-function/</link>
                <guid isPermaLink="false">60f6e0bccffc7f0618708fbc</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Mon, 19 Jul 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/07/chris-ried-ieic5Tq8YMk-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Python内置的<code>filter()</code>函数能够从可迭代对象（如字典、列表）中筛选某些元素，并生成一个新的迭代器。可迭代对象是一个可以被“遍历”的Python对象，也就是说，它将按顺序返回各元素，这样我们就可以在<code>fo</code>r循环中使用它。</p><p><code>filter()</code>函数的基本语法是：</p><pre><code class="language-python">filter(function, iterable)
</code></pre><p>返回一个可迭代的filter对象，可以使用<code>list()</code>函数将其转化为列表，这个列表包含过滤器对象中返回的所有的项。</p><p><code>filter()</code>函数所提供的过滤方法，通常比用列表解析更有效，特别是当我们处理更大的数据集时。例如，列表解析会生成一个新列表，这会增加该处理的运行时间。当列表解析执行完毕它的表达式后，内存中会有两个列表。但是，<code>filter()</code>将生成一个简单的对象，该对象包含对原始列表的引用、提供的函数以及原始列表中位置的索引，这样操作占用的内存更少。</p><p>下面介绍<code>filter()</code>的不同用法。</p><h2 id="-filter-">在<code>filter()</code>中使用特殊函数</h2><p><code>filter()</code>的第一个参数是一个函数，用它来决定第二个参数所引用的可迭代对象中的每一项的去留。此函数被调用后，当返回<code>False</code>时，第二个参数中的可迭代对象里面相应的值就会被删除。针对这个函数，可以是一个普通函数，也可以使用<code>lambda</code>函数，特别是当表达式不那么复杂的时候。</p><p>下面是<code>filter()</code>中使用<code>lambda</code>函数的方法：</p><pre><code class="language-python">filter(lambda item: item[] expression, iterable)
</code></pre><p>将下面的列表，用于<code>lambda</code>函数，根据<code>lambda</code>函数表达式筛选列表中的元素。</p><pre><code class="language-python">creature_names = ['Sammy', 'Ashley', 'Jo', 'Olly', 'Jackie', 'Charlie']
</code></pre><p>要筛选此列表以元音开头的水族馆生物的名称，<code>lambda</code>函数如下：</p><pre><code class="language-python">print(list(filter(lambda x: x[0].lower() in 'aeiou', creature_names)))
</code></pre><p>在这里，我们将列表中的一个项声明为<code>x</code>，并以<code>x[0]</code>的方式访问每个字符串的第一个字符，并且要将字母转化为小写，以确保将字母与<code>'aeiou'</code>中的字符匹配。</p><p>最后，要提供可迭代对向<code>creature_name</code>。与上一节一样，用<code>list()</code>将返回结果转化为列表表。</p><p>输出如下：</p><pre><code class="language-python">['Ashley', 'Olly']
</code></pre><p>当然，写一个函数，也能够实现类似的结果：</p><pre><code class="language-python">creature_names = ['Sammy', 'Ashley', 'Jo', 'Olly', 'Jackie', 'Charlie']

def names_vowels(x):
    return x[0].lower() in 'aeiou'

filtered_names = filter(names_vowels, creature_names)

print(list(filtered_names))</code></pre><p>在<code>names_vowels</code>函数中用一个表达式，完成了对<code>creature_names</code>的过滤。</p><p>同样，输出如下：</p><pre><code class="language-python">['Ashley', 'Olly']
</code></pre><p>总的来说，在<code>filter()</code>函数中使用lambda函数得到的结果与使用常规函数得到的结果相同。如果所要过滤数据更复杂了，还可能要使用正则表达式，这可能会提高代码的可读性。</p><h2 id="-filter-none">在<code>filter()</code>中使用<code>None</code></h2><p>我们也可以将<code>None</code>作为<code>filter()</code>的第一个参数，让迭代器过滤掉Python中布尔值是<code>False</code>的对象，比如长度为0的对象（如空列表或空字符串）或在数字上等于0的对象。</p><p>下面的示例中要过滤一个列表，去掉其中布尔值是<code>False</code>的元素。</p><pre><code class="language-python">aquarium_tanks = [11, False, 18, 21, "", 12, 34, 0, [], {}]

filtered_tanks = filter(None, aquarium_tanks)</code></pre><p>这段代码在<code>filter()</code>中使用了None，并将<code>aquarium_tanks</code>列表作为可迭代项传入。将<code>None</code>作为第一个参数，可以检查列表中的元素是否为<code>False</code>。</p><pre><code class="language-python">print(list(filtered_tanks))
</code></pre><p>然后再将<code>filtered_tanks</code>传给<code>list()</code>函数，这样就得到了一个列表。</p><p>从输出结果中可以看出，我们得到了想要的整数，那些布尔值是<code>False</code>的项都筛选掉了。</p><pre><code class="language-python">[11, 18, 21, 12, 34]
</code></pre><p>注意：如果不使用<code>list()</code>并打印<code>filtered_tanks</code>，将得到一个类似于<code>&lt;filter object at 0x7fafd5903240&gt;</code>这样的filter对象。filter对象是可迭代的，因此我们可以使用for循环它，也可以使用<code>list()</code>将其转换为列表。</p><p>借助<code>None</code>，用<code>filter()</code>快速地从列表中删除被认为<code>False</code>的项。</p><h2 id="-filter--1">将<code>filter()</code>用于复杂场景</h2><p>对于复杂的数据结构，<code>filter()</code>也可以胜任，例如，有一个由字典组成的列表，我们不仅要遍历列表中的每项（字典）， 还可能要遍历字典中的每个键值对，以便得到所有的数据。</p><p>举个例子，假设我们有水族馆里每种生物的一个列表以及每种生物的不同细节，用下面的列表显示此数据。</p><pre><code class="language-python">aquarium_creatures = [
  {"name": "sammy", "species": "shark", "tank number": "11", "type": "fish"},
  {"name": "ashley", "species": "crab", "tank number": "25", "type": "shellfish"},
  {"name": "jo", "species": "guppy", "tank number": "18", "type": "fish"},
  {"name": "jackie", "species": "lobster", "tank number": "21", "type": "shellfish"},
  {"name": "charlie", "species": "clownfish", "tank number": "12", "type": "fish"},
  {"name": "olly", "species": "green turtle", "tank number": "34", "type": "turtle"}
]</code></pre><p>下面就写一个函数，用这个函数来过滤这些数据。为了让<code>filter()</code>访问每个字典和字典中的每个元素，这需要构造一个嵌套函数，如下所示：</p><pre><code class="language-python">def filter_set(aquarium_creatures, search_string):
    def iterator_func(x):
        for v in x.values():
            if search_string in v:
                return True
        return False
    return filter(iterator_func, aquarium_creatures)
</code></pre><p>定义<code>filter_set()</code>函数，以<code>aquarium_creatures</code>和<code>search_string</code>作为参数。在<code>filter_set()</code>中，将内部函数<code>iterator_func()</code>作为<code>filter()</code>的参数。<code>filter_set()</code>函数将返回由<code>filter()</code>生成的迭代器。</p><p><code>iterator_func()</code>以<code>x</code>作为参数，它代表列表中的一个项（即单个字典）。</p><p>接下来，<code>for</code>循环访问字典中每个键值对，然后使用条件语句检查<code>search_string</code>是键值对中的值。</p><p><code>iterator_func</code>函数作为<code>filter</code>函数的参数对象，用它对迭代对象进行筛选。例如：用<code>filter_set()</code>搜索字符串：</p><pre><code class="language-python">filtered_records = filter_set(aquarium_creatures, "2")
</code></pre><p>一旦函数执行完毕，过滤器对象存储在<code>filtered_records</code>变量中，我们将其转换为一个列表并打印：</p><pre><code class="language-python">print(list(filtered_records))
</code></pre><p>输出内容：</p><pre><code class="language-python">[{'name': 'ashley', 'species': 'crab', 'tank number': '25', 'type': 'shellfish'}, {'name': 'jackie', 'species': 'lobster', 'tank number': '21', 'type': 'shellfish'}, {'name': 'charlie', 'species': 'clownfish', 'tank number': '12', 'type': 'fish'}]</code></pre><p>刚才的示例中，我们用<code>filter()</code>实现了在字典组成的列表中过滤制定字符。</p><h2 id="-">结论</h2><p>本文中列举了<code>filter()</code>函数的不同使用方法。如果你打算深入了解，请阅读《Python大学实用教程》（电子工业出版社）一书，这是针对零起点读者，并特别注重工程实践的不可多得的读物。</p><p>欢迎在我的<a href="http://www.itdiffer.com/">个人网站</a>阅读更多文章。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 深入理解 Python 中的循环和迭代 ]]>
                </title>
                <description>
                    <![CDATA[ 循环，特别是for循环，是Python中常见的语句，甚至于Guido van Rossum（Python创始人）在评论递归的时候说过在Python中“递归已死”，我想这句话的意思不是说在Python中不能用递归，而是说因为Python中的 for循环语句足够强大，可以不考虑递归，而是用for循环实现原本用递归做的事情。 本来，在《Python大学实用教程》和《跟老齐学Python：轻松入门》两本书中都对for 循环语句做了很完整地介绍，并且在这两本书中也有关于可迭代等概念，但是，如何将两者融合起来理解，从而能够更好地实现for循环，对新手还是有挑战的。 本文就在以上两本书所述基础上，从更深入和综合的角度进行阐述，以便能更好地使用for循环。 踩过的坑 在实用for循环中，特别是初学者，会遇到很多坑，这里列举几个，看看你是否遇到过？ 1、第二次无果 假设有一个数字组成的列表和一个生成器，生成器给出这些数字的平方： >>> numbers = [1, 2, 3, 5, 7]  >>> squares = (n**2 for n in numbers) 用tuple函数，将sq ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/loops-and-iterations-in-python/</link>
                <guid isPermaLink="false">60e85c760f76ab0660b1438d</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Fri, 09 Jul 2021 12:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/07/safar-safarov-MSN8TFhJ0is-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>循环，特别是<code>for</code>循环，是Python中常见的语句，甚至于Guido van Rossum（Python创始人）在评论递归的时候说过在Python中“递归已死”，我想这句话的意思不是说在Python中不能用递归，而是说因为Python中的<code>for</code>循环语句足够强大，可以不考虑递归，而是用<code>for</code>循环实现原本用递归做的事情。</p><p>本来，在《Python大学实用教程》和《跟老齐学Python：轻松入门》两本书中都对<code>for</code>循环语句做了很完整地介绍，并且在这两本书中也有关于可迭代等概念，但是，如何将两者融合起来理解，从而能够更好地实现<code>for</code>循环，对新手还是有挑战的。</p><p>本文就在以上两本书所述基础上，从更深入和综合的角度进行阐述，以便能更好地使用<code>for</code>循环。</p><h2 id="-">踩过的坑</h2><p>在实用<code>for</code>循环中，特别是初学者，会遇到很多坑，这里列举几个，看看你是否遇到过？</p><h3 id="1-">1、第二次无果</h3><p>假设有一个数字组成的列表和一个生成器，生成器给出这些数字的平方：</p><pre><code class="language-python">&gt;&gt;&gt; numbers = [1, 2, 3, 5, 7] 
&gt;&gt;&gt; squares = (n**2 for n in numbers)
</code></pre><p>用<code>tuple</code>函数，将<code>squares</code>转化为元组。</p><pre><code class="language-python">&gt;&gt;&gt; tuple(squares) 
(1, 4, 9, 25, 49)</code></pre><p>现在，又向计算这个生成器对象<code>squares</code>里面所有数字的和，观察一下，应该能看出来，其和是<code>88</code>，然而：</p><pre><code class="language-python">&gt;&gt;&gt; sum(squares) 
0</code></pre><p>这里计算结果为<code>0</code>，是Python的BUG吗？</p><h3 id="2-">2、检查无效</h3><p>再用下面的方法得到那个生成器对象：</p><pre><code class="language-python">&gt;&gt;&gt; numbers = [1, 2, 3, 5, 7] 
&gt;&gt;&gt; squares = (n**2 for n in numbers)
</code></pre><p>如果检查<code>9</code>是否在<code>squares</code>生成器中，显然这是真的<code>True</code>。但是同样的检查如果再做一遍，就不是这个结果了——不可重复，不科学？</p><pre><code class="language-python">&gt;&gt;&gt; 9 in squares 
True
&gt;&gt;&gt; 9 in squares 
False</code></pre><h3 id="3-">3、解包</h3><p>创建一个包含两个键值对的字典：</p><pre><code class="language-python">&gt;&gt;&gt; counts = {'apples': 2, 'oranges': 1}
</code></pre><p>用多变量的赋值语句对字典解包：</p><pre><code class="language-python">&gt;&gt;&gt; x, y = counts
</code></pre><p>先猜一下，这样做会有什么结果？报错，还是两个变量分别引用了两个键值对——引用键值对，兼职不可能吧，除非将键值对包裹在字典里。</p><p>但是，一没有报错，二没有返回键值对，而是：</p><pre><code class="language-python">&gt;&gt;&gt; x 
'apples'</code></pre><p>这似乎也合乎情理和逻辑。</p><h2 id="-for-">复习for循环</h2><p>温故而知新，先来回顾一下<code>for</code>循环。</p><p>严格地说，Python中的<code>for</code>循环并不“传统”，或者说不符合众多语言中所继承的C语言风格的<code>for</code>循环。</p><p>先看一看所谓的C语言风格的<code>for</code>循环，以JavaScript为例：</p><pre><code class="language-python">var numbers = [1, 2, 3, 5, 7]; 
for (var i = 0; i &lt; numbers.length; i += 1) {
    print(numbers[i]) }</code></pre><p>像人们熟知的JavaScript, C, C++, Java, PHP等很多编程语言的<code>for</code>循环，都是这个样子的，所以，不少人认为这样的才是真正的<code>for</code>循环。</p><p>但是，Python盲从，而是特立独行地创造了自己的<code>for</code>循环。它不是C语言风格的，而是Python风格的：</p><pre><code class="language-python">numbers = [1, 2, 3, 5, 7] 
for n in numbers:
    print(n)</code></pre><p>与传统的C语言风格的<code>for</code>循环不同，Python的<code>for</code>循环不需要创建索引，不需要对索引变量进行初始化，不需要进行边界检查，也不需要让索引递增。Python的<code>for</code>循环为我们完成了在<code>numbers</code>列表上循环的所有工作。</p><p>因此，Python中虽有<code>for</code>循环，但并非传统的C风格，那么其工作原理亦与之不同。</p><h2 id="--1">可迭代对象和序列</h2><p>在Python中，可迭代对象就是可以用<code>for</code>来循环的东西。</p><pre><code class="language-python">for item in some_iterable:
    print(item)</code></pre><p>序列是一种非常常见的可迭代对象，例如列表、元组和字符串都是序列。</p><pre><code class="language-python">&gt;&gt;&gt; numbers = [1, 2, 3, 5, 7] 
&gt;&gt;&gt; coordinates = (4, 5, 7) 
&gt;&gt;&gt; words = "hello there"</code></pre><p>序列是具有一组特定特征的可迭代对象，它们可以从<code>0</code>开始索引，并在比序列长度少一个元素的地方结束。它们有长度，并且可以切片。列表、元组、字符串和所有其他序列都是这样工作的。</p><pre><code class="language-python">&gt;&gt;&gt; numbers[0] 
1 
&gt;&gt;&gt; coordinates[2] 
7 
&gt;&gt;&gt; words[4] 
'o'</code></pre><p>Python中的很多东西都是可迭代对象，但并非所有的可迭代对象都是序列。集合、字典、文件和生成器都是可迭代对象，但它们都不是序列。</p><pre><code class="language-python">&gt;&gt;&gt; my_set = {1, 2, 3} 
&gt;&gt;&gt; my_dict = {'k1': 'v1', 'k2': 'v2'} 
&gt;&gt;&gt; my_file = open('some_file.txt') 
&gt;&gt;&gt; squares = (n**2 for n in my_set)</code></pre><p>因此，任何可以用<code>for</code>来循环的东西都是一个可迭代对象，例如序列，但是并非所有可迭代对象都是序列。</p><h2 id="python-for-">Python的for循环</h2><p>前面已经显示了，Python的<code>for</code>循环不使用索引——这是不同于C语言分割的<code>for</code>循环之处。</p><p>不过，你可能会悄悄滴认为，如果非要用，Python的<code>for</code>循环肯定也能实现C语言风格，因为我们一向认为“C语言是任何东西的基础”。为此，我们使用<code>while</code> 循环和索引手动遍历一个可迭代对象：</p><pre><code class="language-python">numbers = [1, 2, 3, 5, 7] 
i = 0 
while i &lt; len(numbers):
    print(numbers[i])  
    i += 1</code></pre><p>很显然，上面的循环方式只适合于序列类对象，对其它的并非完全使用，比如字典、集合。</p><p>比如使用索引手动遍历一个集合，我们将看到报错：</p><pre><code class="language-python">&gt;&gt;&gt; fruits = {'lemon', 'apple', 'orange', 'watermelon'}
&gt;&gt;&gt; i = 0 
&gt;&gt;&gt; while i &lt; len(fruits): 
...     print(fruits[i]) 
...     i += 1 
... Traceback (most recent call last): File "&lt;stdin&gt;", line 2, in &lt;module&gt; 
    TypeError: 'set' object does not support indexing</code></pre><p>集合不是序列，因此它们不支持索引。</p><p>在Python中，我们<em>不能</em>通过使用索引手动遍历每个可迭代对象。这对于不是序列的可迭代对象根本不起作用。</p><h2 id="--2">迭代器</h2><p>在Python中，迭代器可以用于<code>for</code>循环。</p><p>什么是迭代器？它是驱动可迭代对象的一类对象。我们可以从任意可迭代对象那里生成迭代器。</p><p>这里有三个可迭代对象：集合、元组和字符串。</p><pre><code class="language-python">&gt;&gt;&gt; numbers = {1, 2, 3, 5, 7} 
&gt;&gt;&gt; coordinates = (4, 5, 7) 
&gt;&gt;&gt; words = "hello there"</code></pre><p>可以用Python内置的<code>iter</code>函数用上面的可迭代对象生成迭代器。</p><pre><code class="language-python">&gt;&gt;&gt; iter(numbers) 
&lt;set_iterator object at 0x7f2b9271c860&gt; 
&gt;&gt;&gt; iter(coordinates) 
&lt;tuple_iterator object at 0x7f2b9271ce80&gt; 
&gt;&gt;&gt; iter(words) 
&lt;str_iterator object at 0x7f2b9271c860&gt;</code></pre><p>有了迭代器，就把它传给内置函数<code>next</code>，从而获得它的下一项。</p><pre><code class="language-python">&gt;&gt;&gt; numbers = [1, 2, 3] 
&gt;&gt;&gt; my_iterator = iter(numbers) 
&gt;&gt;&gt; next(my_iterator) 
1 
&gt;&gt;&gt; next(my_iterator) 
2</code></pre><p>每从迭代器中取出一项，那一项就从迭代器中“消失”了。如果到了迭代器的最后一项，还执行<code>next</code>，而实际上后面已经没有其他项了，这时候就会报出<code>StopIteration</code>异常。</p><pre><code class="language-python">&gt;&gt;&gt; next(iterator) 
3 
&gt;&gt;&gt; next(iterator) 
Traceback (most recent call last):  File "&lt;stdin&gt;", line 1, 
in &lt;module&gt; StopIteration</code></pre><h2 id="-for--1">不用for的循环</h2><p>在了解了迭代器、以及<code>iter</code>和<code>next</code>函数后，我们将尝试手动遍历一个可迭代对象，而不使用<code>for</code>循环。</p><p>不用<code>for</code>，就得用<code>while</code>了，Python中只有这么两个循环语句。</p><pre><code class="language-python">def funky_for_loop(iterable, action_to_do):    
    for item in iterable:
        action_to_do(item)</code></pre><p>为了去掉<code>for</code>，需要：</p><ol><li>根据给定的可迭代对象生成迭代器</li><li>从迭代器中重复获取下一项</li><li>如果成功获得了下一项，则相当于执行<code>for</code>循环了</li><li>如果在获取下一项时遇到“StopIteration”异常，则停止循环</li></ol><pre><code class="language-python">def funky_for_loop(iterable, action_to_do):    
    iterator = iter(iterable)
    done_looping = False    
    while not done_looping:
        try:
            item = next(iterator)
        except StopIteration:
            done_looping = True        
        else:
            action_to_do(item)</code></pre><p>这里，其实是用<code>while</code>循环和迭代器重新发明了<code>for</code>循环。</p><p>上面的代码基本上定义了Python中循环的工作方式。如果你了解内置的<code>iter</code>和<code>next</code>函数在遍历对象时的工作方式，那么你就了解了Python的<code>for</code>循环是如何工作的，它们的工作过程是类似的。</p><p>实际上，通过上面的代码，不仅仅展示了<code>for</code>循环的工作原理，所有可迭代对象的循环都如此。</p><p>总结一下，<strong>迭代器协议</strong>是描述“Python中可迭代对象的循环如何工作的”的一种基本方式，它本质上是Python中<code>iter</code>和<code>next</code>函数所定义的，Python中所有形式的迭代都由迭代器协议提供支持。</p><p>迭代器协议也被用于<code>for</code>：</p><pre><code class="language-python">for n in numbers:    
    print(n)</code></pre><p>多重赋值也使用迭代器协议：</p><pre><code class="language-python">x, y, z = coordinates
</code></pre><p>下面这种使用<code>*</code>的表达式也使用迭代器协议：</p><pre><code class="language-python">a, b, *rest = numbers print(*numbers)
</code></pre><p>许多内置函数依赖于迭代器协议：</p><pre><code class="language-python">unique_numbers = set(numbers)</code></pre><p>Python中任何与可迭代对象一起工作的东西都可能以某种方式使用迭代器协议。在Python中，每当你遍历一个可迭代对象时，都依赖于迭代器协议。</p><h2 id="--3">生成器是迭代器</h2><p>迭代器看起来很酷，不过，它是不是用途有限呢？或者说作为普通的Python编程者，是不是不需要关心它呢？</p><p>非也。</p><p>迭代器很常见。</p><pre><code class="language-python">&gt;&gt;&gt; numbers = [1, 2, 3] 
&gt;&gt;&gt; squares = (n**2 for n in numbers)
</code></pre><p>此处得到的<code>squares</code>是一个生成器，生成器也是迭代器，这意味着你可以对生成器调用<code>next</code>，以获取其下一项：</p><pre><code class="language-python">&gt;&gt;&gt; next(squares) 
1 
&gt;&gt;&gt; next(squares) 
4</code></pre><p>用<code>for</code>循环同样可以遍历生成器：</p><pre><code class="language-python">&gt;&gt;&gt; squares = (n**2 for n in numbers) 
&gt;&gt;&gt; for n in squares: 
...     print(n) 
... 1 4 9</code></pre><p>下面这句话，貌似废话，但是重要：<strong>迭代器是可迭代对象</strong>。</p><p>这就意味着，可以将迭代器对象作为<code>iter</code>函数的参数，生成一个新的迭代器对象。不是吗？</p><pre><code class="language-python">&gt;&gt;&gt; numbers = [1, 2, 3] 
&gt;&gt;&gt; iterator1 = iter(numbers) 
&gt;&gt;&gt; iterator2 = iter(iterator1)  # 迭代器对象作为参数
</code></pre><p>以上最终得到的<code>iterator2</code>是一个迭代器。不过，要注意，<code>iterator1</code>和<code>iterator2</code>的关系：</p><pre><code class="language-python">&gt;&gt;&gt; iterator1 is iterator2 
True</code></pre><p><code>iter</code>函数的参数如果是一个迭代器，返回对象仍然是该迭代器对象自身。</p><p>结论：迭代器是可迭代对象，所有迭代器都是自己的迭代器。</p><pre><code class="language-python">def is_iterator(iterable):    
    return iter(iterable) is iterable
</code></pre><p>困惑了吗？</p><p>继续。</p><p>迭代器没有长度，因此无法索引。这个认识必须要建立起来。</p><pre><code class="language-python">&gt;&gt;&gt; numbers = [1, 2, 3, 5, 7] 
&gt;&gt;&gt; iterator = iter(numbers) 
&gt;&gt;&gt; len(iterator) 
TypeError: object of type 'list_iterator' has no len() 
&gt;&gt;&gt; iterator[0] 
TypeError: 'list_iterator' object is not subscriptable</code></pre><p>从Python程序员的角度来看，使用迭代器可以做的唯一有用的事情就是：将迭代器传给内置的<code>next</code>函数、或遍历迭代器：</p><pre><code class="language-python">&gt;&gt;&gt; next(iterator) 
1 
&gt;&gt;&gt; list(iterator)
[2, 3, 5, 7]</code></pre><p>如果我们第二次遍历迭代器，我们将一无所获：</p><pre><code class="language-python">&gt;&gt;&gt; list(iterator) 
[]</code></pre><p>这就是说，迭代器可以认为是<strong>一次性</strong>的惰性的可迭代对象，这意味着它们只能遍历一次。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/07/image-17.png" class="kg-image" alt="image-17" width="600" height="400" loading="lazy"></figure><p>正如上表中所示，可迭代对象并不总是迭代器，但迭代器总是可迭代对象：</p><p>所谓迭代器协议，即：</p><ol><li>可以作为<code>next</code>函数的参数，从而获得对象的下一项，或者在没有其他项时引发<code>StopIteration</code>异常。</li><li>可以作为<code>iter</code>函数的参数，并返回自身。</li></ol><p>反过来说，也成立：</p><ol><li>任何可以传给<code>iter</code>而没有引发<code>TypeError</code>的对象都是可迭代对象。</li><li>任何可以传给<code>next</code>而没有引发<code>TypeError</code>的对象都是迭代器。</li><li>任何在传给<code>iter</code>时返回自身的对象都是迭代器。</li></ol><p>这是Python中的迭代器协议。</p><p>迭代器还能创建包含无限多个元素的对象，关于这方面的内容请参阅《Python大学实用教程》中的有关内容。</p><h2 id="--4">迭代器无处不在</h2><p>Python中的迭代器很多，例如：</p><pre><code class="language-python">&gt;&gt;&gt; letters = ['a', 'b', 'c'] 
&gt;&gt;&gt; e = enumerate(letters) 
&gt;&gt;&gt; e 
&lt;enumerate object at 0x7f112b0e6510&gt; 
&gt;&gt;&gt; next(e) 
(0, 'a')</code></pre><p>在Python3中，<code>zip</code>、<code>map</code>和<code>filter</code>对象也是迭代器。</p><pre><code class="language-python">&gt;&gt;&gt; numbers = [1, 2, 3, 5, 7] 
&gt;&gt;&gt; letters = ['a', 'b', 'c'] 
&gt;&gt;&gt; z = zip(numbers, letters) 
&gt;&gt;&gt; z 
&lt;zip object at 0x7f112cc6ce48&gt; 
&gt;&gt;&gt; next(z) 
(1, 'a')</code></pre><p>Python中的文件对象也是迭代器。</p><pre><code class="language-python">&gt;&gt;&gt; next(open('hello.txt')) 
'hello world\n'</code></pre><p>在Python、标准库和第三方Python库中还有许多内置的迭代器。</p><p>至此，本文开始时所提到的那三个坑，已经可以给出完美的解释了。</p><p>最后，要强调，这里所介绍的有关迭代器概念，只是对《Python大学实用教程》中没有特别强调或者容易忽视的地方给予补充和强调，在这本书中，对循环、迭代和迭代器、生成器有比较全面的介绍，请参阅。</p><p>欢迎在我的<a href="http://www.itdiffer.com/">个人网站</a>阅读更多文章。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 解密 Python 实例、类方法和静态方法 ]]>
                </title>
                <description>
                    <![CDATA[ 在本文中，我将帮助你揭开类方法、静态方法和常规实例方法背后的奥秘。 如果对它们的差异有了直观的理解，那么你将能够编写面向对象的 Python 程序，以便更清楚地传达某种意图，并且从长远来看更易于维护。 实例、类方法和静态方法概述 我们首先编写一个类，其中包含实例、类方法和静态方法： class MyClass:     def method(self):         return 'instance method called', self     @classmethod     def classmethod(cls):         return 'class method called', ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/python-instances-class-methods-and-static-methods/</link>
                <guid isPermaLink="false">5de65b91ca1efa04e196a885</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Tue, 06 Jul 2021 12:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/12/WechatIMG2103.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在本文中，我将帮助你揭开类方法、静态方法和常规实例方法背后的奥秘。</p><p>如果对它们的差异有了直观的理解，那么你将能够编写面向对象的 Python 程序，以便更清楚地传达某种意图，并且从长远来看更易于维护。</p><h2 id="-">实例、类方法和静态方法概述</h2><p>我们首先编写一个类，其中包含实例、类方法和静态方法：</p><pre><code>class MyClass:
    def method(self):
        return 'instance method called', self

    @classmethod
    def classmethod(cls):
        return 'class method called', cls

    @staticmethod
    def staticmethod():
        return 'static method called'</code></pre><h3 id="--1">实例方法</h3><p><code>MyClass</code>的第一个方法叫做“方法”，它是一个普通的实例方法，也是你将会在大多数情况下使用的最基本、最简单的方法。该方法接受一个参数 self，它在调用该方法时指向<code>MyClass</code>的一个实例（当然，实例方法可以接受的参数不止一个）。</p><p>通过<code>self</code>参数，实例方法可以自由地访问同一对象上的属性和其他方法。当涉及到修改对象的状态时，实例方法就非常给力。</p><p>实例方法不仅可以修改对象状态，还可以通过<code>self.__class__</code>属性来访问类本身，这就意味着实例方法也可以修改类的状态。</p><h3 id="--2">类方法</h3><p>比较<code>MyClass.method</code>与第二个方法<code>MyClass.classmethod</code>。我用一个<code>@classmethod</code>装饰符把这个方法标记为类方法。</p><p>类方法不接受<code>self</code>参数，而是接受一个<code>cls</code>参数。<code>cls</code>参数在调用方法时指向的是类而不是对象实例。</p><p>由于类方法只能访问此<code>cls</code>参数，因此它无法修改对象实例的状态——那需要访问<code>self</code>。但是，类方法仍然可以对应用于类的所有实例的类状态进行修改。</p><h3 id="--3">静态方法</h3><p>第三个方法<code>MyClass.staticmethod</code>用一个装饰符<code>@staticmethod</code>标记为静态方法。</p><p>这个方法既不接受<code>self</code>参数，也不接受<code>cls</code>参数（但是，它可以接受任意数量的其他参数）。</p><p>因此静态方法既不能修改对象状态，也不能修改类状态。静态方法在它们可以访问的数据方面受到限制，它们主要用于对你的方法进行空间命名。</p><h2 id="--4">见证它们的作用！</h2><p>我知道到目前为止，这个讨论是相当理论化的。我相信：对这些方法在实践中的差异有一个直观的理解。我们现在来复习一些具体的例子。</p><p>让我们来看看这些方法在被调用时是如何运行的。我们从创建类的实例开始，然后对其调用三个不同的方法。</p><p>类<code>MyClass</code>的每个方法，都返回一个元组，元组包含踪迹信息，以及该方法可以访问的类或对象的有关信息。</p><p>下面是调用实例方法时发生的情况：</p><pre><code>&gt;&gt;&gt; obj = MyClass()
&gt;&gt;&gt; obj.method()
('instance method called', &lt;MyClass instance at 0x101a2f4c8&gt;)
</code></pre><p>这些代码确认了<code>method</code>(实例方法)可以通过<code>self</code>参数访问对象实例(打印为<code>&lt;MyClass instance&gt;</code>)。</p><p>此方法被调用时，Python 用实例对象<code>obj</code>替换了<code>self</code>参数。我们可以用句点“.”调用方法的方式(<code>obj.method()</code>)，手动传递实例对象来获得相同的结果:</p><pre><code>&gt;&gt;&gt; MyClass.method(obj)
('instance method called', &lt;MyClass instance at 0x101a2f4c8&gt;)
</code></pre><p>你能猜到在不创建实例的情况下调用该方法会怎么样吗？</p><p>顺便说一下，实例方法还可以通过<code>self.__class__</code>属性访问类本身。这使得实例方法在访问受限时非常给力——它们可以修改对象实例和类本身的状态。</p><p>接下来让我们尝试一下类方法：</p><pre><code>&gt;&gt;&gt; obj.classmethod()
('class method called', &lt;class MyClass at 0x101a2f4c8&gt;)
</code></pre><p>调用<code>classmethod()</code> 向我们展示了它不能访问<code>&lt;MyClass instance&gt;</code>对象，而是只能访问代表类本身的<code>&lt;class MyClass&gt;</code>对象（在 Python中，一切皆对象。即使是类本身也不列外）。</p><p>请注意，当我们调用<code>MyClass.classmethod()</code>时，Python 如何自动将类作为第一个参数传递给函数，通过句点“.”的方式自动实现这种行为，这与实例方法上的<code>self</code>参数的工作方式相同。</p><p>将这些参数命名为<code>self</code>和<code>cls</code>只是一种约定。你可以将它们命名为<code>the_object</code> 和<code>the_class</code>，也能得到相同的结果。重要的是，它们在方法的参数列表中处于第一个位置。</p><p>现在到了调用静态方法的时候：</p><pre><code>&gt;&gt;&gt; obj.staticmethod()
'static method called'
</code></pre><p>你看到我们如何成功地在对象上调用<code>staticmethod()</code>了吗？某些开发者看到通过实例调用静态方法时会感到惊讶。</p><p>在后台，当使用句点“.”的方式调用静态方法时，Python 只是通过不传入<code>self</code>或<code>cls</code>参数来执行访问限制。</p><p>这就确保静态方法既不能访问对象实例状态，也不能访问类状态。静态方法像普通函数一样工作，但它属于类（和每个实例）的命名空间。</p><p>现在，让我们看一看：如果没有预先创建对象实例，就在类本身上调用静态方法时，会发生什么情况：</p><pre><code>&gt;&gt;&gt; MyClass.classmethod()
('class method called', &lt;class MyClass at 0x101a2f4c8&gt;)

&gt;&gt;&gt; MyClass.staticmethod()
'static method called'

&gt;&gt;&gt; MyClass.method()
TypeError: unbound method method() must
    be called with MyClass instance as first
    argument (got nothing instead)
</code></pre><p>我们可以顺利地调用<code>classmethod()</code> 和<code>staticmethod()</code>，但调用实例方法<code>method()</code> 时，显示<code>TypeError</code>异常。</p><p>这是意料之中的。这次我们没有创建实例，而是尝试直接通过在类本身调用实例方法。这意味着 Python 无法填充<code>self</code>参数，因此调用失败。</p><p>由上可见这三种方法之间的区别，但我不会就此罢休，在接下来的两部分中，我将介绍两个稍微实际一些的示例，以便大家了解何时使用这些特殊的方法类型。</p><p>以这个简单的<code>Pizza</code>类为例：</p><pre><code>class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __repr__(self):
        return f'Pizza({self.ingredients!r})'

&gt;&gt;&gt; Pizza(['cheese', 'tomatoes'])
Pizza(['cheese', 'tomatoes'])
</code></pre><p>注意：本文中的代码示例使用 Python3.6 的 f 字符串来实现<code>__repr__</code>方法中返回的字符串。在 Python2 和 3.6 之前的 Python3 版本中，你将使用不同的格式化字符串表达式，例如:</p><pre><code>def __repr__(self):
        return 'Pizza(%r)' % self.ingredients
</code></pre><h2 id="-classmethod-">带有<code>@classmethod</code>的披萨工厂</h2><p>如果你在现实世界中接触过披萨，你就会知道有很多不同的美味：</p><pre><code>Pizza(['mozzarella', 'tomatoes'])
Pizza(['mozzarella', 'tomatoes', 'ham', 'mushrooms'])
Pizza(['mozzarella'] * 4)
</code></pre><p>几个世纪前，意大利人就弄清了比萨饼的分类，所以这些不同口味的比萨饼都有自己的名字。我们最好利用这一点，为<code>Pizza</code>类的用户提供更好的接口，以便于创建他们所渴望的 Pizza 对象。</p><p>一个好用的、整洁的方法是使用类方法作为工厂函数来存放我们可以创建的不同类型的比萨饼：</p><p>★<strong>译者注：</strong>在原文中，作者将类方法称为“工厂函数”。所谓工厂函数，是一个比喻说法。比如在现在的代码中，可以通过类方法<code>margherita</code>创建关于类<code>Pizza</code>的实例。这就好比<code>margherita</code>是一个“生产比萨的工厂”，故名之曰：工厂函数。</p><pre><code>class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __repr__(self):
        return f'Pizza({self.ingredients!r})'

    @classmethod
    def margherita(cls):
        return cls(['mozzarella', 'tomatoes'])

    @classmethod
    def prosciutto(cls):
        return cls(['mozzarella', 'tomatoes', 'ham'])
</code></pre><p>请注意看我是如何在<code>margherita</code>和<code>prosciutto</code>方法中使用<code>cls</code>参数，而不是直接调用<code>Pizza</code>类生成实例对象。</p><p>这是一个小窍门，你可以遵循“不要自我重复（DRY）”的原则。如果我们决定在某个时候重命名这个类，我们就不必在所有的类方法中更新类名称。</p><p>现在，能用这些方法做什么？让我们试试看：</p><pre><code>&gt;&gt;&gt; Pizza.margherita()
Pizza(['mozzarella', 'tomatoes'])

&gt;&gt;&gt; Pizza.prosciutto()
Pizza(['mozzarella', 'tomatoes', 'ham'])
</code></pre><p>如你所见，我们可以使用工厂函数来创建新的<code>Pizza</code>对象，这些对象是按照我们希望的方式配置的。它们都在内部使用相同的<code>__init__</code>初始化方法，并且仅仅提供了一个快捷方式来记住所有不同的成分。</p><p>查看类方法的这种用途的另一种途径是：它们允许为你的类定义替代性的初始化方法。</p><p>Python 只允许每个类使用一个<code>__init__</code>方法。使用类方法，有可能根据需要添加尽可能多的替代性的初始化方法。这可以（在一定程度上）使类的接口自文档化，并简化它们的使用。</p><p>★<strong>译者注：</strong> 对于上述内容的进一步理解，可以参考《Python大学实用教程》6.4.2节中的例题6-4-1。</p><h2 id="--5">什么时候使用静态方法</h2><p>在这里想出一个好的例子有点困难，但告诉你吧，我会继续把“比萨饼”抻得越来越薄......</p><p>我想到的是：</p><pre><code>import math

class Pizza:
    def __init__(self, radius, ingredients):
        self.radius = radius
        self.ingredients = ingredients

    def __repr__(self):
        return (f'Pizza({self.radius!r}, '
                f'{self.ingredients!r})')

    def area(self):
        return self.circle_area(self.radius)

    @staticmethod
    def circle_area(r):
        return r ** 2 * math.pi
</code></pre><p>我在这里做了哪些更改？首先，我修改了初始化方法和<code>__repr__</code>方法，以便接受另外一个关于半径的参数。</p><p>★<strong>译者注：</strong> 原文作者没有区分类中的“初始化方法”（<code>__init__</code>）和“构造方法”（<code>__new__</code>）。原文中将<code>__init__</code>称为constructor，这种说法欠妥当。对此，在《Python大学实用教程》的“6.9构造方法”一节中有详细说明。</p><p>我还添加了一个<code>area()</code>实例方法来计算并返回比萨饼的面积（这对于<code>@property</code>也是一个很好的候选项——但是，这只是一个虚构的示例）。</p><p>我没有直接在<code>area()</code>中计算面积，而是使用众所周知的圆面积公式，将其分解为独立的<code>circle_area()</code>静态方法。</p><p>我们试试吧！</p><pre><code>&gt;&gt;&gt; p = Pizza(4, ['mozzarella', 'tomatoes'])
&gt;&gt;&gt; p
Pizza(4, ['mozzarella', 'tomatoes'])
&gt;&gt;&gt; p.area()
50.26548245743669
&gt;&gt;&gt; Pizza.circle_area(4)
50.26548245743669
</code></pre><p>当然，这是一个有点简陋的例子，但是它可以帮助解释静态方法所带来的一些好处。</p><p>正如我们所了解到的，静态方法不能访问类或实例状态，因为它们不接受<code>cls</code>或<code>self</code>参数。这是一个很大的限制，但这也是一个很好的信号，表明一个特定的方法独立于它周围的一切。</p><p>在上面的例子中，<code>circle_area()</code>显然不能以任何方式修改类或类实例。（当然，你总是可以用全局变量来解决这个问题，但这不是重点。）</p><p>静态方法为什么有用呢？</p><p>将一个方法标记为静态方法，不仅意味着这个方法不会修改类或实例状态，而且这种强制性要求在执行 Python 程序时还会强化。</p><p>这样的技术允许你明确地对类的各个部分进行调用，从而自然地引导新的开发工作在规定的范围内展开。当然，挑战这些限制是很容易的，但在实践中，它们常常有助于避免与原始设计相反的意外修改。</p><p>换言之，使用静态方法和类方法是传达开发者意图的方法，同时又能充分执行这种意图，以避免由于疏忽而造成的大多数的错误和 bug，因为这些 bug 可能会破坏原有的设计。</p><p>如果不将静态方法滥用，而是应用得恰到好处，那么所编写的程序则具有良好的可维护性，并降低了其他开发者犯错误的可能性。</p><p>静态方法对编写测试代码也有用处。</p><p>因为 <code>circle_area()</code>方法完全独立于类的其他部分，所以测试起来容易得多。</p><p>在单元测试中测试这种方法之前，我们不必担心创建一个完整的实例，可以像测试常规函数一样不停地测试。同样，这使得以后的维护更加容易。</p><h2 id="--6">重要收获</h2><p>实例方法需要实例，并且可以通过<code>self</code>访问该实例。</p><p>类方法不需要类实例，不能访问实例 (<code>self</code>) ，但是可以通过<code>cls</code>访问类本身。</p><p>静态方法无权访问 <code>cls</code> 或<code>self</code>，可以像普通函数一样工作，但属于类的命名空间。</p><p>静态方法和类方法便于调用，并（在一定程度上）限制了开发者对类的设计意图，这有益于代码的维护。</p><p>译者：老齐，研途教育科技有限公司 CTO，Python 畅销书作者，出版书籍有《跟老齐学 Python：轻松入门》、《跟老齐学 Python：Django 实战》、《跟老齐学 Python：数据分析》、《Python 大学实用教程》（大学教材），并在苏州大学数科院为应用统计专业硕士生开设《机器学习实践》课程。欢迎访问<a href="http://www.itdiffer.com/">个人网站</a>阅读更多。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python教程：从DataFrame中删除列 ]]>
                </title>
                <description>
                    <![CDATA[ 在操作数据的时候，DataFrame对象中删除一个或多个列是常见的操作，并且实现方法较多，然而这中间有很多细节值得关注。 首先，一般被认为是“正确”的方法，是使用DataFrame的drop方法，之所以这种方法被认为是标准的方法，可能是收到了SQL语句中使用drop 实现删除操作的影响。 import pandas as pd import numpy as np df = pd.DataFrame(np.arange(25).reshape((5,5)), columns=list("abcde")) display(df) try:     df.drop('b') except KeyError as ke:     print(ke)    a   b   c   d   e 0   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/delete-columns-from-python-dataframe/</link>
                <guid isPermaLink="false">60d88f59fff62a063e5769ca</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Sun, 27 Jun 2021 12:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/06/jessica-ruscello-OQSCtabGkSY-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在操作数据的时候，<code>DataFrame</code>对象中删除一个或多个列是常见的操作，并且实现方法较多，然而这中间有很多细节值得关注。</p><p>首先，一般被认为是“正确”的方法，是使用<code>DataFrame</code>的<code>drop</code>方法，之所以这种方法被认为是标准的方法，可能是收到了SQL语句中使用<code>drop</code>实现删除操作的影响。</p><pre><code class="language-python">import pandas as pd
import numpy as np

df = pd.DataFrame(np.arange(25).reshape((5,5)), columns=list("abcde"))

display(df)

try:
    df.drop('b')
except KeyError as ke:
    print(ke)</code></pre><pre><code class="language-python">   a   b   c   d   e
0   0   1   2   3   4
1   5   6   7   8   9
2  10  11  12  13  14
3  15  16  17  18  19
4  20  21  22  23  24
"['b'] not found in axis"
</code></pre><p>上面的操作中出现了报错信息，什么原因？这是因为<code>drop</code>方法中，默认是删除行。</p><p>如果用<code>axis=0</code>或<code>axis='rows'</code>，都表示展出行，也可用<code>labels</code>参数删除行。</p><pre><code class="language-python">df.drop(0)                # drop a row, on axis 0 or 'rows'
df.drop(0, axis=0)        # same
df.drop(0, axis='rows')   # same
df.drop(labels=0)         # same
df.drop(labels=[0])       # same

# 结果
    a   b   c   d   e
1   5   6   7   8   9
2  10  11  12  13  14
3  15  16  17  18  19
4  20  21  22  23  24</code></pre><h2 id="-">如何删除列</h2><p>如何删除列？可以指定<code>axis</code>或使用<code>columns</code>参数，如下所示：</p><pre><code class="language-python">df.drop('b', axis=1)         # drop a column
df.drop('b', axis='columns') # same
df.drop(columns='b')         # same
df.drop(columns=['b'])       # same

# 输出
    a   c   d   e
0   0   2   3   4
1   5   7   8   9
2  10  12  13  14
3  15  17  18  19
4  20  22  23  24</code></pre><p>这样就删除了一列，注意，删除之后，返回了新的对象，这意味着，你可以用一个新的变量引用删除后得到的结果。如果要改变原有的DataFrame，可以增加一个参数<code>inplace=True</code>。</p><pre><code class="language-python">df2 = df.drop('b', axis=1)

print(df2.columns)
print(df.columns)

# result
Index(['a', 'c', 'd', 'e'], dtype='object')
Index(['a', 'b', 'c', 'd', 'e'], dtype='object')
</code></pre><p>同样值得注意的是，你可以通过同时使用<code>index</code>和<code>columns</code>，同时删除行和列，并且你可以传入多个值，即删除多行或者多列。</p><pre><code class="language-python">df.drop(index=[0,2], columns=['b','c'])

# result
    a   d   e
1   5   8   9
3  15  18  19
4  20  23  24</code></pre><p>如果不使用<code>drop</code>方法，还可以通过索引实现同样的操作。有多种方式，这里列举一种，如下所示，用<code>.loc</code>和<code>.isin</code>并取反。</p><pre><code class="language-python">df.loc[~df.index.isin([0,2]), ~df.columns.isin(['b', 'c'])]

# result
    a   d   e
1   5   8   9
3  15  18  19
4  20  23  24</code></pre><p>如果这些对你来说都不是很清楚，建议参阅<a href="http://www.itdiffer.com/data.html" rel="noopener">《跟老齐学Python：数据分析》</a>中对此的详细说明。</p><h2 id="--1">另外的方法</h2><p>除了上面演示的方法之外，还有别的方法可以删除列。</p><pre><code class="language-python">del df['a']
df

# result
    b   c   d   e
0   1   2   3   4
1   6   7   8   9
2  11  12  13  14
3  16  17  18  19
4  21  22  23  24</code></pre><p>原来的<code>df['a']</code>没了，这就如同前面用<code>drop</code>方法时参数中使用了<code>inplace=True</code>一样，原地修改。</p><p>但是，不要认为<code>del</code>就能百试百灵，它会让你有迷茫的时候。</p><p>我们知道，如果用类似<code>df.b</code>这样访问属性的形式，也能得到DataFrame对象的列，虽然这种方法我不是很提倡使用，但很多数据科学的民工都这么干。</p><pre><code class="language-python">df.b

# result
0     1
1     6
2    11
3    16
4    21
Name: b, dtype: int64
</code></pre><p>这么干，如果仅仅是查看，也无所谓，但是：</p><pre><code class="language-python">del df.b

# result
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
&lt;ipython-input-10-0dca358a6ef9&gt; in &lt;module&gt;
----&gt; 1 del df.b

AttributeError: b</code></pre><p>这就报错了。是不是很迷惑，为什<code>del df['b']</code>奏效，而<code>del df.b</code>无效？</p><p>这就是接下来要研究的了。必须通过对细节的剖析，才能搞清楚问题的根源。</p><p>首先，<code>del df['b']</code>有效，是因为DataFrame对象中实现了<code>__delitem__</code>方法，在执行<code>del df['b']</code>时会调用该方法。但是<code>del df.b</code>呢，有没有调用此方法呢？</p><p>为此，可以定义一个简单的类，这里暂用<code>dict</code>作为保存数据的容器，当然，这个类不是真正的<code>DataFrame</code>。</p><pre><code class="language-python">class StupidFrame:
    def __init__(self, columns):
        self.columns = columns
        
    def __delitem__(self, item):
        del self.columns[item]
        
    def __getitem__(self, item):
        return self.columns[item]
    
    def __setitem__(self, item, val):
        self.columns[item] = val
            
f = StupidFrame({'a': 1, 'b': 2, 'c': 3})
print("StupidFrame value for a:", f['a'])
print("StupidFrame columns: ", f.columns)
del f['b']
f.d = 4
print("StupidFrame columns: ", f.columns)

# result
StupidFrame value for a: 1
StupidFrame columns:  {'a': 1, 'b': 2, 'c': 3}
StupidFrame columns:  {'a': 1, 'c': 3}</code></pre><p>认真观察上面的操作和<code>StupidFrame</code>代码，如果用<code>[]</code>对所创建的实例进行数据操作，可以实现删除、赋值、读取等。但是，当我们执行<code>f.d = 4</code>的操作时，并没有在<code>StupidFrame</code>中所创建的<code>columns</code>属性中增加键为<code>d</code>的键值对，而是为实例<code>f</code>增加了一个普通属性，名称是<code>d</code>。</p><p>因此，如果要让<code>f.d</code>与<code>f['d']</code>等效，还必须要在<code>StupidFrame</code>类中添加 <code>__getattr__</code> 方法，并使用<code>__setattr__</code>方法来处理设置问题（关于这两个方法的使用，请参阅<a href="http://www.itdiffer.com/python_course.html" rel="noopener">《Python大学实用教程》</a>中的详细介绍）。</p><pre><code class="language-python">class StupidFrameAttr:
    def __init__(self, columns):
        self.__dict__['columns'] = columns
        
    def __delitem__(self, item):
        del self.__dict__['columns'][item]
        
    def __getitem__(self, item):
        return self.__dict__['columns'][item]
    
    def __setitem__(self, item, val):
        self.__dict__['columns'][item] = val
        
    def __getattr__(self, item):
        if item in self.__dict__['columns']:
            return self.__dict__['columns'][item]
        elif item == 'columns':
            return self.__dict__[item]
        else:
            raise AttributeError
    
    def __setattr__(self, item, val):
        if item != 'columns':
            self.__dict__['columns'][item] = val
        else:
            raise ValueError("Overwriting columns prohibited") 

            
f = StupidFrameAttr({'a': 1, 'b': 2, 'c': 3})
print("StupidFrameAttr value for a", f['a'])
print("StupidFrameAttr columns: ", f.columns)
del f['b']
print("StupidFrameAttr columns: ", f.columns)
print("StupidFrameAttr value for a", f.a)
f.d = 4
print("StupidFrameAttr columns: ", f.columns)
del f['d']
print("StupidFrameAttr columns: ", f.columns)
f.d = 5
print("StupidFrameAttr columns: ", f.columns)
del f.d

# result
StupidFrameAttr value for a 1
StupidFrameAttr columns:  {'a': 1, 'b': 2, 'c': 3}
StupidFrameAttr columns:  {'a': 1, 'c': 3}
StupidFrameAttr value for a 1
StupidFrameAttr columns:  {'a': 1, 'c': 3, 'd': 4}
StupidFrameAttr columns:  {'a': 1, 'c': 3}
StupidFrameAttr columns:  {'a': 1, 'c': 3, 'd': 5}
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
&lt;ipython-input-12-fd29f59ea01e&gt; in &lt;module&gt;
     39 f.d = 5
     40 print("StupidFrameAttr columns: ", f.columns)
---&gt; 41 del f.d

AttributeError: d</code></pre><p>现在删除属性也能够奏效了。</p><p>另外，还可以在类中重写<code>__delattr__</code>方法，如下所示：</p><pre><code class="language-python">class StupidFrameDelAttr(StupidFrameAttr):
    def __delattr__(self, item):
        # trivial implementation using the data model methods
        del self.__dict__['columns'][item]

f = StupidFrameDelAttr({'a': 1, 'b': 2, 'c': 3})
print("StupidFrameDelAttr value for a", f['a'])
print("StupidFrameDelAttr columns: ", f.columns)
del f['b']
print("StupidFrameDelAttr columns: ", f.columns)
print("StupidFrameDelAttr value for a", f.a)
f.d = 4
print("StupidFrameDelAttr columns: ", f.columns)
del f.d 
print("StupidFrameDelAttr columns: ", f.columns)

# result
StupidFrameDelAttr value for a 1
StupidFrameDelAttr columns:  {'a': 1, 'b': 2, 'c': 3}
StupidFrameDelAttr columns:  {'a': 1, 'c': 3}
StupidFrameDelAttr value for a 1
StupidFrameDelAttr columns:  {'a': 1, 'c': 3, 'd': 4}
StupidFrameDelAttr columns:  {'a': 1, 'c': 3}</code></pre><p>现在，就理解了前面使用<code>del</code>删除<code>DataFrame</code>对象属性的方法出问题的根源了。当然，并不是说DataFrame对象的类就是上面那样的，而是用上面的方式简要说明了一下原因。</p><p>所以，在Pandas中要删除DataFrame的列，最好是用对象的<code>drop</code>方法。</p><p>另外，特别提醒，如果要创建新的列，也不要用<code>df.column_name</code>的方法，这也容易出问题。</p><p>欢迎在<a href="https://qiwsir.github.io/2021/03/10/remove-column-from-dataframe/">老齐教室</a>阅读更多Python相关的文章。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python 教程之如何用 OpenCV 处理图像 ]]>
                </title>
                <description>
                    <![CDATA[ 本文将介绍如何使用Python语言进行图像处理。我们不会局限于一个单独的库或框架，然而，有一个库的使用率将会是最高的，那就是OpenCV。我们一开始会讨论一些图像处理的基本知识，然后继续探讨不同的应用/场景，也就是图像处理的用武之地。 什么是图像处理? 在深入研究图像处理的方法之前，重要的是要了解什么是图像处理，特别是这项技术在处理大量图片方面的角色。图像处理完整的说法是“数字图像处理”，经常使用图像处理的领域是“计算机视觉”。对这两个术语不要混淆，图像处理算法和计算机视觉(CV)算法都以图像为输入，然而，在图像处理中，输出也是图像，而在计算机视觉中，输出可以是关于图像的一些特征或信息。 为什么需要图像处理? 我们收集或生成的数据大部分是原始数据，也就是说，由于一些可能的原因，这些数据不适合直接用于应用程序。因此，我们需要首先分析它，执行必要的预处理，然后使用它——特别推荐《数据准备和特征工程》，此书即为这方面最佳读物。 例如，我们正在尝试构建一个关于猫的分类器。我们的程序会把一个图像作为输入，然后告诉我们这个图像是否包含一只猫。构建这个分类器的第一步是收集数百张含有猫的图片。一 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/image-processing-with-opencv/</link>
                <guid isPermaLink="false">5e7a1788db4be8080eb703cb</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Tue, 22 Jun 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/03/8.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文将介绍如何使用Python语言进行图像处理。我们不会局限于一个单独的库或框架，然而，有一个库的使用率将会是最高的，那就是OpenCV。我们一开始会讨论一些图像处理的基本知识，然后继续探讨不同的应用/场景，也就是图像处理的用武之地。</p><h2 id="-"><strong>什么是图像处理?</strong></h2><p>在深入研究图像处理的方法之前，重要的是要了解什么是图像处理，特别是这项技术在处理大量图片方面的角色。图像处理完整的说法是“数字图像处理”，经常使用图像处理的领域是“计算机视觉”。对这两个术语不要混淆，图像处理算法和计算机视觉(CV)算法都以图像为输入，然而，在图像处理中，输出也是图像，而在计算机视觉中，输出可以是关于图像的一些特征或信息。</p><h2 id="--1"><strong>为什么需要图像处理?</strong></h2><p>我们收集或生成的数据大部分是原始数据，也就是说，由于一些可能的原因，这些数据不适合直接用于应用程序。因此，我们需要首先分析它，执行必要的预处理，然后使用它——特别推荐《数据准备和特征工程》，此书即为这方面最佳读物。</p><p>例如，我们正在尝试构建一个关于猫的分类器。我们的程序会把一个图像作为输入，然后告诉我们这个图像是否包含一只猫。构建这个分类器的第一步是收集数百张含有猫的图片。一个常见的问题是，收集的所有图片的大小都不相同，因此在将它们提供给模型进行训练之前，需要调整它们的大小或者把它们进行预处理，使尺寸符合标准。</p><p>为什么图像处理对于任何计算机视觉应用序都是必不可少的？以上提到的只是众多原因之一。</p><h2 id="--2"><strong>预备知识</strong></h2><p>为了轻松地学习本文内容，你需要已经具备如下知识。</p><p>首先，应该具备一定的编程语言技能，本文使用的是Python语言。</p><p>其次，你应该了解什么是机器学习以及它的基本工作原理。因为在本文中我们将使用一些机器学习算法来进行图像处理。</p><p>另外，如果你之前接触过或掌握了OpenCV的基本知识，也会有所帮助。但这不是必需的。</p><p>还有，你一定要了解图像在内存中究竟是如何表示的。每幅图像都由一组像素表示，即像素值矩阵。对于灰度图像，像素值的范围是0到255，它们表示该像素的强度。例如，如果你有一个20×20维的图像，它将由一个20x20的矩阵表示(像素值总共是400)。</p><p>如果你正在处理彩色图像，你应该知道它有三个通道——红、绿、蓝(RGB)。因此，一个彩色图像有三个这样的矩阵。</p><h2 id="--3"><strong>安装</strong></h2><p>注意: 由于我们将通过Python使用OpenCV，所以你必须会实用它，前面推荐了关于Python的书籍。下面依次说明在不同操作系统中OpenCV的安装方法：</p><ul><li>Windows</li></ul><pre><code>$ pip install opencv-python</code></pre><ul><li>MacOS</li></ul><pre><code>$ brew install opencv3 --with-contrib --with-python3</code></pre><ul><li>Linux</li></ul><pre><code>$ sudo apt-get install libopencv-dev python-opencv</code></pre><p>要检查是否安装成功，请在Python交互模式中运行以下命令:</p><pre><code>import cv2</code></pre><h2 id="--4"><strong>必备的基础知识</strong></h2><p>在进行图像处理之前，要先做一些准备。</p><p>在本文中，我们将使用以下图像：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/1.jpeg" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>注意: 为了在本文中显示该图像，对其进行了缩放，但是我们使用的图像原始大小约为1180x786。</p><p>你可能注意到图像现在是彩色的，这意味着它由三个颜色通道表示，即红色、绿色和蓝色。我们将把图像转换成灰度，并使用下面的代码将图像分割成单独的通道。</p><h3 id="--5"><strong>找到图像细节</strong></h3><p>使用<code>imread()</code>函数加载图像后，我们可以得到关于它的一些简单属性，比如像素的数量和尺寸:</p><pre><code>import cv2

img = cv2.imread('rose.jpg')

print("Image Properties")
print("- Number of Pixels: " + str(img.size))
print("- Shape/Dimensions: " + str(img.shape))</code></pre><p>Output:</p><pre><code>Image Properties
- Number of Pixels: 2782440
- Shape/Dimensions: (1180, 786, 3)</code></pre><h3 id="--6"><strong>将图像分割成单独的通道</strong></h3><p>现在，我们将使用OpenCV将图像分割成红色、绿色和蓝色的部分，并显示它们:</p><pre><code>from google.colab.patches import cv2_imshow

blue, green, red = cv2.split(img)    # Split the image into its channels
img_gs = cv2.imread('rose.jpg', cv2.IMREAD_GRAYSCALE)    # Convert image to grayscale

cv2_imshow(red) # Display the red channel in the image
cv2_imshow(blue) # Display the red channel in the image
cv2_imshow(green) # Display the red channel in the image
cv2_imshow(img_gs) # Display the grayscale version of image</code></pre><p>为了简单起见，我们只显示灰度图像。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/2.jpeg" class="kg-image" alt="2" width="600" height="400" loading="lazy"></figure><h2 id="--7"><strong>图像阈值</strong></h2><p>阈值的概念非常简单。正如上面在图像表示中所讨论的，像素值可以是0到255之间的任何值。假设我们想要将一幅图像转二值化，即指定一个像素值为0或1。为此，我们可以设置阈值。例如，如果阈值(T)为125，那么所有大于125的像素将被赋值为1，所有小于或等于该值的像素将被赋值为0。下面，我们通过代码来更好地理解它。</p><p>将下面的图像用上述方法进行转换：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/3.png" class="kg-image" alt="3" width="600" height="400" loading="lazy"></figure><pre><code>import cv2

# Read image
img = cv2.imread('image.png', 0)

# Perform binary thresholding on the image with T = 125
r, threshold = cv2.threshold(img, 125, 255, cv2.THRESH_BINARY)
cv2_imshow(threshold)</code></pre><p>输出：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/4.png" class="kg-image" alt="4" width="600" height="400" loading="lazy"></figure><p>正如你所看到的, 二值化之后，出现了两个区域，即黑色区域(像素值0)和白色区域(像素值1)。原来, 我们设置的阈值正好在图像的中间，这就是为什么黑白值在那里被分割。</p><h2 id="--8"><strong>应用</strong></h2><h3 id="1-"><strong>1:去除图像中的噪声</strong></h3><p>现在你已经对图像处理的概念和用途有了基本的了解，接下来让我们来了解一下它的一些具体应用。</p><p>在大多数情况下，我们收集的原始数据有噪声，也就是说，不需要的特征使图像很难被感知。虽然这些图像可以直接用于特征抽取，但是算法的准确性会受到很大的影响。这就是为什么在将图像传递给算法以获得更好的精度之前，要对图像进行处理的原因。</p><p>有许多不同类型的噪声，如高斯噪声，椒盐噪声等。我们可以通过应用滤波器来去除图像中的噪声，或者至少将其影响降到最低。在滤波器方面也有很多选择，每一个滤波器都有不同的优点。因此，对于特定类型的噪声来说，总有一个是最好的。</p><p>为了更好地理解这一点，我们将在上面的玫瑰色图像的灰度版本中添加“盐和胡椒粉”噪声，然后尝试使用不同的滤波器去除图像中的噪声，看看哪一个最适合这种类型。</p><pre><code>import numpy as np

# Adding salt &amp; pepper noise to an image

def salt_pepper(prob):
      # Extract image dimensions
      row, col = img_gs.shape

      # Declare salt &amp; pepper noise ratio
      s_vs_p = 0.5
      output = np.copy(img_gs)

      # Apply salt noise on each pixel individually
      num_salt = np.ceil(prob * img_gs.size * s_vs_p)
      coords = [np.random.randint(0, i - 1, int(num_salt))
            for i in img_gs.shape]
      output[coords] = 1

      # Apply pepper noise on each pixel individually
      num_pepper = np.ceil(prob * img_gs.size * (1. - s_vs_p))
      coords = [np.random.randint(0, i - 1, int(num_pepper))
            for i in img_gs.shape]
      output[coords] = 0
      cv2_imshow(output)

      return output

# Call salt &amp; pepper function with probability = 0.5
# on the grayscale image of rose
sp_05 = salt_pepper(0.5)

# Store the resultant image as 'sp_05.jpg'
cv2.imwrite('sp_05.jpg', sp_05)</code></pre><p>好的，我们已经把噪声添加到玫瑰图像，这是它现在的样子：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/5.jpeg" class="kg-image" alt="5" width="600" height="400" loading="lazy"></figure><p>让我们现在应用不同的滤波器，并记下观察结果，即每个滤波器降噪的效果。</p><h4 id="--9"><strong>锐化滤波器</strong></h4><pre><code># Create our sharpening kernel, the sum of all values must equal to one for uniformity
kernel_sharpening = np.array([[-1,-1,-1],
                              [-1, 9,-1],
                              [-1,-1,-1]])

# Applying the sharpening kernel to the grayscale image &amp; displaying it.
print("\n\n--- Effects on S&amp;P Noise Image with Probability 0.5 ---\n\n")

# Applying filter on image with salt &amp; pepper noise
sharpened_img = cv2.filter2D(sp_05, -1, kernel_sharpening)
cv2_imshow(sharpened_img)</code></pre><p>在有椒盐噪声的图像上应用滤波器得到的图像如下所示。通过与原始灰度图的对比，我们可以看出，它把图像调得太亮了，也无法突出玫瑰上的亮点。因此，我们可以得出结论，锐化滤波器并不能去除噪声。</p><p>锐化滤波器输出：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/6.jpeg" class="kg-image" alt="6" width="600" height="400" loading="lazy"></figure><h4 id="--10"><strong>中值滤波器</strong></h4><pre><code>from scipy.ndimage import maximum_filter, minimum_filter

def midpoint(img):
    maxf = maximum_filter(img, (3, 3))
    minf = minimum_filter(img, (3, 3))
    midpoint = (maxf + minf) / 2
    cv2_imshow(midpoint)

print("\n\n---Effects on S&amp;P Noise Image with Probability 0.5---\n\n")
midpoint(sp_05)</code></pre><p>在有噪声的图像上应用中值滤波器，得到的图像如下所示。通过与原始灰度图像的对比，我们可以看出，与上面的核方法一样，图像的亮度调高了很多，然而，它能够突出玫瑰上的亮斑（即噪声）。因此，我们可以说，中值滤波器是比锐化滤波器更好的选择，但它仍然不能完全恢复原始图像。</p><p>中值滤波器输出：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/7.jpeg" class="kg-image" alt="7" width="600" height="400" loading="lazy"></figure><h4 id="--11"><strong>逆谐波均值滤波器</strong></h4><p>注意: 对这些滤波器的工作原理的阐述，超出了本文范畴，读者可以在网上搜索，我们还是从应用的层面来研究。</p><pre><code>def contraharmonic_mean(img, size, Q):
    num = np.power(img, Q + 1)
    denom = np.power(img, Q)
    kernel = np.full(size, 1.0)
    result = cv2.filter2D(num, -1, kernel) / cv2.filter2D(denom, -1, kernel)
    return result

print("\n\n--- Effects on S&amp;P Noise Image with Probability 0.5 ---\n\n")
cv2_imshow(contraharmonic_mean(sp_05, (3,3), 0.5))</code></pre><p>在有椒盐噪声的图像上应用逆谐波均值滤波器(https://en.wikipedia.org/wiki/Contraharmonic_mean)得到的图像如下图所示。通过与原始灰度图像的对比，我们可以看到：它几乎完美再现了原始图像。它的强度或亮度级别与原图是相同的，它突出了玫瑰上的亮点。因此，我们可以得出结论，逆谐波均值滤波器在处理椒盐噪声方面是非常有效的。</p><p>逆谐波均值滤波器输出：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/8.jpeg" class="kg-image" alt="8" width="600" height="400" loading="lazy"></figure><p>现在我们已经找到了最佳滤波器，它可以有效地把有噪声的图像恢复到原始图像。我们可以继续下一个应用了。</p><h3 id="2-canny-"><strong>2:使用Canny算子进行边缘检测</strong></h3><p>到目前为止，我们使用的玫瑰图像的背景是不变的，也就是黑色的，因此，我们将把这个应用用于不同的图像，以更好地展示算法的功效。原因是，如果背景是恒定的，边缘检测任务就变得相当简单，这不是我们所希望的。</p><p>在本文开始部分，我们提到了一个关于猫的分类器。现在我们延用这个例子，看看图像处理如何在其中扮演一个完整的角色。</p><p>在分类算法中，首先扫描图像寻找“对象”。也就是说，当你输入一幅图像时，算法会找到图像中的所有对象，然后将它们与你试图寻找的对象进行特征比较。对于猫分类器，它会将在图像中找到的所有对象与猫图像的特征进行比较，如果找到匹配项，它会告诉我们输入图像中包含了一只猫。</p><p>对于这个猫分类器，仅以一张猫的图像为例，以下是我们将要使用的图像：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/9.jpg" class="kg-image" alt="9" width="600" height="400" loading="lazy"></figure><pre><code>import cv2
import numpy as np
from matplotlib import pyplot as plt

# Declaring the output graph's size
plt.figure(figsize=(16, 16))

# Convert image to grayscale
img_gs = cv2.imread('cat.jpg', cv2.IMREAD_GRAYSCALE)
cv2.imwrite('gs.jpg', img_gs)

# Apply canny edge detector algorithm on the image to find edges
edges = cv2.Canny(img_gs, 100,200)

# Plot the original image against the edges
plt.subplot(121), plt.imshow(img_gs)
plt.title('Original Gray Scale Image')
plt.subplot(122), plt.imshow(edges)
plt.title('Edge Image')

# Display the two images
plt.show()</code></pre><p>边缘检测输出：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/10.jpeg" class="kg-image" alt="10" width="600" height="400" loading="lazy"></figure><p>正如你所看到的，图像中包含对象的部分（在本例中是一只猫）已经通过边缘检测用虚线标出或分隔开。现在你一定想知道，什么是边缘检测的Canny算子，它是怎么工作的？现在就讨论一下。</p><p>要理解上述内容，需要讨论三个关键步骤。首先，它对图像进行降噪，降噪方式与前面讨论的方式类似。其次，它使用每个像素的一阶导数来找到边缘。这背后的逻辑是，在边缘存在的地方，会有一个突然的强度变化，导致一阶导数值达到峰值，从而使该像素成为“边缘像素”。</p><p>最后，进行滞后阈值化；上面我们说过，在一个边缘的一阶导数值会有一个峰值，但是我们没有说：这个峰值需要有多高，才能被归类为一个边缘——这叫做阈值!在本文的前面，我们讨论了什么是简单的阈值。迟滞阈值法是在此基础上的一种改进，它利用两个阈值来代替一个阈值。这背后的原因是，如果阈值过高，我们可能会错过一些真正的边缘(真负例)，如果阈值过低，我们会得到很多被归类为边缘的点，而实际上不是边缘(假正例)。一个阈值设置为高，一个设置为低，将所有高于“高阈值”的点标识为边缘，然后对所有高于“低阈值”但低于“高阈值”的点进行评估；边缘上的点确定之后，与边缘点靠近或相邻的点也被确定为边缘，其余的点被丢弃。</p><p>这些是Canny算子用于识别图像边缘的基本概念/方法。</p><blockquote><strong>译者注：</strong> Canny算子是澳洲计算机科学家约翰·坎尼（John F. Canny）于1986年开发出来的一个多级边缘检测算法，其目标是找到一个最优的边缘。</blockquote><h2 id="--12"><strong>结论</strong></h2><p>在本文中，我们学习了如何在不同的平台(如Windows、MacOS和Linux)上安装OpenCV，以及如何验证安装成功。OpenCV是Python中最流行的图像处理库。</p><p>接着我们讨论了什么是图像处理，以及它在机器学习的计算机视觉领域中的应用。我们讨论了一些常见的噪声类型，以及如何使用不同的滤波器将噪声从图像中去除，以便在应用中使用这些图像。</p><p>此外，我们还了解了图像处理如何在高端应用（如：对象检测或分类）中发挥不可或缺的作用。请注意，这篇文章只是冰山一角，数字图像处理还有更多的内容，不可能在一篇短文中全部涵盖。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python 教程之如何在 Django 中实现分组查询 ]]>
                </title>
                <description>
                    <![CDATA[ 聚合在任何类型的ORM中都会引起一些乱七八糟的事情，Django也不例外。虽然在官方文档中已经对ORM中的分组和聚合做了说明，但我还是要从另一个角度来说明如何解决这个问题。 在本文中，我将QuerySets和SQL放在一起。如果SQL令你最舒服，那么这就是适合你的Django分组资料。 准备 为了演示不同的GROUP BY，我将使用Django内置的django.contrib.auth中的模型。 >>> from django.contrib.auth.models import User Django ORM生成的SQL语句具有很长的别名。为了简洁起见，我将演示一种简化但等效的执行过程。 统计行数 让我们统计一下我们有多少用户： SELECT     COUNT(*) FROM     auth_user; User.objects.count() 行的统计查询是如此常见，以至于Django的QuerySet包含了一个函数，与我们接下来将看到的其他QuerySets不同，count返回一个数字。 如何使用aggregate函数 Django还提供了另外两种统计查询 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/introduction-to-django-group/</link>
                <guid isPermaLink="false">5e70dde9ca1efa04e196bdb1</guid>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Tue, 17 Mar 2020 14:48:24 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/03/WechatIMG2342.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>聚合在任何类型的ORM中都会引起一些乱七八糟的事情，Django也不例外。虽然在官方文档中已经对ORM中的分组和聚合做了说明，但我还是要从另一个角度来说明如何解决这个问题。</p><p>在本文中，我将QuerySets和SQL放在一起。如果SQL令你最舒服，那么这就是适合你的Django分组资料。</p><h2 id="-"><strong>准备</strong></h2><p>为了演示不同的GROUP BY，我将使用Django内置的django.contrib.auth中的模型。</p><pre><code>&gt;&gt;&gt; from django.contrib.auth.models import User</code></pre><p>Django ORM生成的SQL语句具有很长的别名。为了简洁起见，我将演示一种简化但等效的执行过程。</p><h2 id="--1"><strong>统计行数</strong></h2><p>让我们统计一下我们有多少用户：</p><pre><code>SELECT
    COUNT(*)
FROM
    auth_user;</code></pre><pre><code>User.objects.count()</code></pre><p>行的统计查询是如此常见，以至于Django的QuerySet包含了一个函数，与我们接下来将看到的其他QuerySets不同，<code>count</code>返回一个数字。</p><h2 id="-aggregate-"><strong>如何使用<code>aggregate</code>函数</strong></h2><p>Django还提供了另外两种统计查询方法，首先来看看<code>aggregate</code>：</p><pre><code>SELECT
    COUNT(id) AS id__count
FROM
    auth_user;</code></pre><pre><code>from django.db.models import Count

User.objects.aggregate(Count('id'))</code></pre><p>为了使用<code>aggregate</code>，我们导入了<code>Count</code>函数，<code>aggregate</code>以另外一个实现统计查询的表达式为参数，在本例中，我们使用主键<code>id</code>来查询数据库表中的行的数量。</p><p><code>aggregate</code>的结果是一个字典对象：</p><pre><code>&gt;&gt;&gt; from django.db.models import Count
&gt;&gt;&gt; User.objects.aggregate(Count('id'))
{"id__count": 891}</code></pre><p>键的名称是从字段的名称和查询函数的名称派生的，在本例中，键名是<code>id__count</code>。我们最好不要使用这样的命名方式，而是要自己设定名称：</p><pre><code>SELECT
    COUNT(id) as total
FROM
    auth_user;</code></pre><pre><code>&gt;&gt;&gt; from django.db.models import Count
&gt;&gt;&gt; User.objects.aggregate(total=Count('id'))
{"total": 891}</code></pre><p><code>aggregate</code>参数的名称，就是返回值字典的键。</p><h2 id="-group-by"><strong>如何实现Group By</strong></h2><p>使用<code>aggregate</code>，我们得到数据表进行聚合查询结果，这很有用，但我们还希望对指定的行应用此操作。</p><p>让我们根据用户的活动状态来统计用户数：</p><pre><code>SELECT
    is_active,
    COUNT(id) AS total
FROM
    auth_user
GROUP BY
    is_active</code></pre><pre><code>(User.objects
.values('is_active')
.annotate(total=Count('id')))</code></pre><p>这次使用了<code>annotate</code>函数。我们使用<code>values</code>和<code>annotate</code>的组合来完成分组查询：</p><ul><li><code>values('is_active')</code>：分组依据</li><li><code>annotate(total=Count('id'))</code>：要查询的内容</li></ul><p>顺序很重要：如果在<code>annotate</code>之前调用<code>values</code>失败，不会产生查询结果。</p><p>与<code>aggregate</code>一样，<code>annotate</code>的参数名称是QuerySet返回值的键，示例中就是<code>total</code>。</p><h2 id="--2"><strong>分组筛选</strong></h2><p>若要对筛选查询应用聚合功能，可以在查询的任何位置使用<code>filter</code>，例如，仅按员工用户的活动状态对其进行统计：</p><pre><code>ELECT
    is_active,
    COUNT(id) AS total
FROM
    auth_user
WHERE
    is_staff = True
GROUP BY
    is_active</code></pre><pre><code>(User.objects
.values('is_active')
.filter(is_staff=True)
.annotate(total=Count('id')))</code></pre><h2 id="--3"><strong>如何进行分组排序</strong></h2><p>与<code>filter</code>类似，要对分组结果排序，可以在查询中使用<code>order_by</code>：</p><pre><code>SELECT
    is_active,
    COUNT(id) AS total
FROM
    auth_user
GROUP BY
    is_active
ORDER BY
    is_active,
    total</code></pre><pre><code>(User.objects
.values('is_active')
.annotate(total=Count('id'))
.order_by('is_staff', 'total'))</code></pre><p>请注意：你可以按分组的关键词<code>is_active</code>和聚合的关键词<code>total</code>进行排序。</p><h2 id="--4"><strong>如何联合聚合查询</strong></h2><p>要生成同分组的多个聚合查询，请添加多个annotation：</p><pre><code>SELECT
    is_active,
    COUNT(id) AS total,
    MAX(date_joined) AS last_joined
FROM
    auth_user
GROUP BY
    is_active</code></pre><pre><code>from django.db.models import Max

(User.objects
.values('is_active')
.annotate(
    total=Count('id'),
    last_joined=Max('date_joined'),
))</code></pre><p>该查询将得到活动用户和非活动用户的数量，以及用户加入每个组的最后日期。</p><h2 id="--5"><strong>根据多个字段进行分组</strong></h2><p>就像执行多个聚合一样，我们可能也希望根据多个字段进行分组。例如，按活动状态和人员状态分组:</p><pre><code>SELECT
    is_active,
    is_staff,
    COUNT(id) AS total
FROM
    auth_user
GROUP BY
    is_active,
    is_staff</code></pre><pre><code>(User.objects
.values('is_active', 'is_staff')
.annotate(total=Count('id')))</code></pre><p>此查询的结果包括<code>is_active</code>、<code>is_staff</code>和每个组中的用户数。</p><h2 id="--6"><strong>根据表达式分组</strong></h2><p>分组的另一个常见用例是按表达式分组。例如，统计每年加入的用户数：</p><pre><code>SELECT
    EXTRACT('year' FROM date_joined),
    COUNT(id) AS total
FROM
    auth_user
GROUP BY
    EXTRACT('year' FROM date_joined)</code></pre><pre><code>(User.objects
.values('date_joined__year')
.annotate(total=Count('id')))</code></pre><p>注意，为了从日期开始获取年份，我们在第一次调用<code>values()</code>时使用了特殊表达式<code>&lt;field&gt;__year</code>。查询的结果是一个dict，键的名称将是<code>date_joined__year</code>。</p><p>有时，内置表达式不够，需要在更复杂的表达式上进行聚合。例如，按注册后登录的用户分组：</p><pre><code>SELECT
  last_login &gt; date_joined AS logged_since_joined,
  COUNT(id) AS total
FROM
  auth_user
GROUP BY
  last_login &gt; date_joined</code></pre><pre><code>from django.db.models import (
    ExpressionWrapper,
    Q, F, BooleanField,
)

(User.objects
.annotate(
    logged_since_joined=ExpressionWrapper(
        Q(last_login__gt=F('date_joined')),
        output_field=BooleanField(),
    )
)
.values('logged_since_joined')
.annotate(total=Count('id'))
.values('logged_since_joined', 'total')</code></pre><p>这里的表达式相当复杂。我们首先使用<code>annotate</code>构建表达式，然后在下面对<code>values()</code>的调用中通过引用表达式将其标记为按该关键词分组。后面的代码就跟前述一样了。</p><h2 id="--7"><strong>根据条件聚合</strong></h2><p>根据条件，只能对组的一部分进行聚合。当有多个聚合时，条件就很有用了。例如，按注册年份统计员工用户和非员工用户的数量：</p><pre><code>SELECT
    EXTRACT('year' FROM date_joined),

    COUNT(id) FILTER (
        WHERE is_staff = True
    ) AS staff_users,

    COUNT(id) FILTER (
        WHERE is_staff = False
    ) AS non_staff_users

FROM
    auth_user
GROUP BY
    EXTRACT('year' FROM date_joined)</code></pre><pre><code>from django.db.models import F, Q

(User.objects
.values('date_joined__year')
.annotate(
    staff_users=(
        Count('id', filter=Q(is_staff=True))
    ),
    non_staff_users=(
        Count('id', filter=Q(is_staff=False))
    ),
))</code></pre><p>上面的SQL来自PostgreSQL，它和SQLite是目前唯一支持<code>FILTER</code>语法快捷方式（正式名称为“选择性聚合”）的数据库。对于其他数据库，ORM将使用<code>CASE ... WHEN</code>来代替。</p><h2 id="-having"><strong>如何使用Having</strong></h2><p>HAVING子句用于筛选聚合函数的结果。例如，查找超过100多个用户加入的年份：</p><pre><code>SELECT
    is_active,
    COUNT(id) AS total
FROM
    auth_user
GROUP BY
    is_active
HAVING
    COUNT(id) &gt; 100</code></pre><pre><code>(User.objects
.annotate(year_joined=F('date_joined__year'))
.values('is_active')
.annotate(total=Count('id'))
.filter(total__gt=100))</code></pre><p>对<code>annotate</code>中的<code>total</code>查询结果进行过滤，即后面的<code>filter</code>，它与SQL中的HAVING子句等效。</p><h2 id="-distinct-"><strong>根据Distinct分组</strong></h2><p>对于某些聚合函数（如“COUNT”），有时只需要计算不同的出现次数。例如，每一种活动状态中的用户有多少不同的姓氏：</p><pre><code>SELECT
    is_active,
    COUNT(id) AS total,
    COUNT(DISTINCT last_name) AS unique_names
FROM
    auth_user
GROUP BY
    is_active</code></pre><pre><code>(User.objects
.values('is_active')
.annotate(
    total=Count('id'),
    unique_names=Count('last_name', distinct=True),
))</code></pre><p>注意在<code>Count</code>的参数中使用了<code>distinct=True</code>。</p><h2 id="--8"><strong>使用聚合字段创建表达式</strong></h2><p>聚合字段通常只是解决较大问题的第一步。例如，按用户活动状态列出的唯一姓氏的百分比是多少：</p><pre><code>SELECT
    is_active,
    COUNT(id) AS total,
    COUNT(DISTINCT last_name) AS unique_names,
    (COUNT(DISTINCT last_name)::float
        / COUNT(id)::float) AS pct_unique_names
FROM
    auth_user
GROUP BY
    is_active</code></pre><pre><code>from django.db.models import FloatField
from django.db.models.functions import Cast

(User.objects
.values('is_active')
.annotate(
    total=Count('id'),
    unique_names=Count('last_name', distinct=True),
)
.annotate(pct_unique_names=(
    Cast('unique_names', FloatField())
    / Cast('total', FloatField())
))</code></pre><p>第一个<code>annotate()</code>定义聚合字段。第二个<code>annotate()</code>使用聚合函数构造表达式。</p><h2 id="--9"><strong>跨表分组</strong></h2><p>到目前为止，我们只是在一个模型中进行各种数据查询操作，但聚合也能在不同模型（即不同数据库表）之间实现，比较简单的情况是一对一或外键关系。例如，假设我们有一个与用户一一对应的<code>User profile</code>模型，并且我们希望按配置文件的类型统计用户：</p><pre><code>SELECT
    p.type,
    COUNT(u.id) AS total
FROM
    auth_user u
    JOIN user_profile p ON u.id = p.user_id
GROUP BY
    p.type</code></pre><pre><code>(User.objects
.values('user_profile__type')
.annotate(total=Count('id')))')))</code></pre><p>就像分组表达式一样，在<code>values</code>中使用关联表，并按该该字段分组。请注意：表示关联数据库包的名称将是<code>user_profile__type</code>。</p><h2 id="--10"><strong>根据多对多关系分组</strong></h2><p>更复杂的还是多对多的关系。例如，计算每个用户参与了多少个小组：</p><pre><code>SELECT
    u.id,
    COUNT(ug.group_id) AS memberships
FROM
    auth_user
    LEFT OUTER JOIN auth_user_groups ug ON (
        u.id = ug.user_id
    )
GROUP BY
    u.id</code></pre><pre><code>(User.objects
.annotate(memberships=Count('groups'))
.values('id', 'memberships'))</code></pre><p>用户可以是多个组的成员，要统计用户所属的组数，我们在<code>User</code>模型中使用了相关名称<code>groups</code>。如果未显式设置相关名称（且未显式禁用），Django将自动生成格式为<code>{related model model}_set</code>的名称。例如<code>group_set</code>。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 一文了解 Python 3.8 中的海象运算符 ]]>
                </title>
                <description>
                    <![CDATA[ Python3.8引入了一种叫做海象运算符（walrus）的新语法（译者注： 对于walrus的翻译，目前尚未出现对于Python的专门术语翻译，所以，此处姑且用字面意思“海象”），它其实是一种赋值语句，用于解决Python语言中长期存在的、可能导致代码重复的问题。正常的赋值语句是 a=b，读作“a等于b”，而海象赋值语句是a:=b，读作“a walrus /ˈwɔːlrəs/ b”（因为:= 看起来像一对眼球和獠牙，类似于海象。注意：此语句还没有适合的中文读法，总不能读作“a海象b”吧）。 海象运算符的优势在于能在不允许赋值的地方（如if语句的条件表达式中）使用赋值变量。海象运算符左侧有个标识符，赋值表达式的值等于分配给这个标识符的值。 例如，假设我有一篮子新鲜水果，我正试图经营一家果汁店。在这里，我定义了篮子里的东西： fresh_fruit = {     'apple': 10,     'banana': 8,     'lemon': 5, } 当顾客到柜台点柠檬水时，我需要确保篮子里至少有一个柠檬用来榨果汁。我的操作方法是检索柠檬的数量，然后使用if语句查询非零的 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/introduction-to-the-walrus-operator-in-python/</link>
                <guid isPermaLink="false">5e660631ca1efa04e196b80c</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Mon, 09 Mar 2020 07:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1583621913946-91a67678ec1b.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Python3.8引入了一种叫做海象运算符（walrus）的新语法（<strong>译者注：</strong>对于walrus的翻译，目前尚未出现对于Python的专门术语翻译，所以，此处姑且用字面意思“海象”），它其实是一种赋值语句，用于解决Python语言中长期存在的、可能导致代码重复的问题。正常的赋值语句是<code>a=b</code>，读作“a等于b”，而海象赋值语句是<code>a:=b</code>，读作“a walrus /ˈwɔːlrəs/ b”（因为<code>:=</code>看起来像一对眼球和獠牙，类似于海象。注意：此语句还没有适合的中文读法，总不能读作“a海象b”吧）。</p><p>海象运算符的优势在于能在不允许赋值的地方（如if语句的条件表达式中）使用赋值变量。海象运算符左侧有个标识符，赋值表达式的值等于分配给这个标识符的值。</p><p>例如，假设我有一篮子新鲜水果，我正试图经营一家果汁店。在这里，我定义了篮子里的东西：</p><pre><code>fresh_fruit = {
    'apple': 10,
    'banana': 8,
    'lemon': 5,
}</code></pre><p>当顾客到柜台点柠檬水时，我需要确保篮子里至少有一个柠檬用来榨果汁。我的操作方法是检索柠檬的数量，然后使用if语句查询非零的值：</p><pre><code>def make_lemonade(count):
    ...

def out_of_stock():
    ...

count = fresh_fruit.get('lemon', 0)
if count:
    make_lemonade(count)
else:
    out_of_stock()</code></pre><p>这个看似简单的代码问题吸引了过多的关注。<code>count</code>变量仅在if语句的第一个代码块中使用，在if语句上方定义<code>count</code>会使它看起来比实际情况更为重要，好像后面的所有代码（包括else块）都需要访问<code>count</code>变量，然而事实并非如此。</p><p>我们获取一个值，检查它是否为非零，然后使用它。这种模式在Python中非常常见。许多程序员试图绕过多次出现<code>count</code>的情况，甚至不惜使用各种损害可读性的招数。现在好了，在Python3.8中增加了海象运算符，可以简化上面的代码。</p><pre><code>if count := fresh_fruit.get('lemon', 0):
    make_lemonade(count)
else:
    out_of_stock()</code></pre><p>虽然现在只是少了一行，但可读性提高很多。因为现在可以清楚地看到<code>count</code>只与if语句的第一行相关。赋值表达式首先为<code>count</code>变量赋值，然后在if语句的上下文中使用该值，以确定如何继续控制流程。这两步行为——分配和评估——是海象运算符的基本性质。</p><p>柠檬是非常有效的，所以我的柠檬水配方中只需要一个，这意味着非零检查就足够了。不过，如果顾客点了苹果酒，我需要确保至少有四个苹果。在这里，我从<code>fruit_basket</code>字典中获取计数，然后在if语句中使用比较表达式：</p><pre><code>def make_cider(count):
    ...

count = fresh_fruit.get('apple', 0)
if count &gt;= 4:
    make_cider(count)
else:
    out_of_stock()</code></pre><p>这个问题和柠檬水的例子一样，<code>count</code>的赋值会分散对这个变量的注意力。在这里，我还使用了海象运算符来提高代码的清晰度：</p><pre><code>if (count := fresh_fruit.get('apple', 0)) &gt;= 4:
    make_cider(count)
else:
    out_of_stock()</code></pre><p>这样做可以收到预期的效果，并使代码缩短了一行。需要注意的是，我需要用圆括号将赋值表达式括起来，以便与if语句中的4进行比较。在柠檬水的例子中不需要使用圆括号，因为赋值表达式本身就是一个非零检查；它不是一个较大表达式的子表达式。与其他表达式一样，应尽量避免使用圆括号把赋值表达式括起来。</p><p>有时候会出现一种类似的重复模式，那就是当我需要根据某些条件在封闭范围内分配一个变量，然后在函数中的稍后位置引用该变量。例如，假设客户订购了一些香蕉冰沙。为了制作它们，我需要至少两个香蕉的香蕉片，否则将引发<code>OutOfBananas</code>异常。在这里，我以一种典型的方式来实现这个逻辑：</p><pre><code>def slice_bananas(count):
    ...

class OutOfBananas(Exception):
    pass

def make_smoothies(count):
    ...

pieces = 0
count = fresh_fruit.get('banana', 0)
if count &gt;= 2:
    pieces = slice_bananas(count)

try:
    smoothies = make_smoothies(pieces)
except OutOfBananas:
    out_of_stock()</code></pre><p>另一种常见的方法是将<code>pieces=0</code>赋值放入else块：</p><pre><code>count = fresh_fruit.get('banana', 0)
if count &gt;= 2:
    pieces = slice_bananas(count)
else:
    pieces = 0

try:
    smoothies = make_smoothies(pieces)
except OutOfBananas:
    out_of_stock()</code></pre><p>第二种方法可能会让人觉得奇怪，因为这意味着<code>pieces</code>变量出现在了条件语句中两个不同的位置，可以在这两个位置进行初始定义。由于Python的作用域规则，这种分割定义在技术上是可行的，但它的可读性不好，也不优雅。这就是许多人喜欢上面那种结构的原因，在它里面的<code>pieces = 0</code>的赋值在前面出现。</p><p>海象运算符也可以用一行代码来缩短这个例子。这个小变化消除了对<code>count</code>变量的任何强调。现在，很明显，除了if语句之外，<code>pieces</code>也很重要：</p><pre><code>pieces = 0
if (count := fresh_fruit.get('banana', 0)) &gt;= 2:
    pieces = slice_bananas(count)

try:
    smoothies = make_smoothies(pieces)
except OutOfBananas:
    out_of_stock()</code></pre><p>使用海象运算符还可以提高在条件语句中分别在两个分支中的<code>pieces</code>复制的可读性。当<code>count</code>定义不再位于if语句之前时，跟踪<code>pieces</code>变量变得更容易:</p><pre><code>if (count := fresh_fruit.get('banana', 0)) &gt;= 2:
    pieces = slice_bananas(count)
else:
    pieces = 0

try:
    smoothies = make_smoothies(pieces)
except OutOfBananas:
    out_of_stock()</code></pre><p>初学Python的程序员经常遇到的一个难题是缺少灵活的<code>switch/case</code>语句，与此类功能近似的一般做法是使用多个if、elif和else语句的深度嵌套。</p><p>例如，假设我想实现一个优先级系统，这样每个客户都可以自动获得最好的果汁，而不必预定。在这里，我设置这样的流程，让它先供应香蕉冰沙，然后供应苹果酒，最后供应柠檬水：</p><pre><code>count = fresh_fruit.get('banana', 0)
if count &gt;= 2:
    pieces = slice_bananas(count)
    to_enjoy = make_smoothies(pieces)
else:
    count = fresh_fruit.get('apple', 0)
    if count &gt;= 4:
        to_enjoy = make_cider(count)
    else:
        count = fresh_fruit.get('lemon', 0)
        if count:
            to_enjoy = make_lemonade(count)
        else:
            to_enjoy = 'Nothing'</code></pre><p>像这样难看的结构在Python代码中司空见惯，幸运的是，海象运算符提供了一个优雅的解决方案，它几乎可以像switch/case语句的专用语法一样通用：</p><pre><code>if (count := fresh_fruit.get('banana', 0)) &gt;= 2:
    pieces = slice_bananas(count)
    to_enjoy = make_smoothies(pieces)
elif (count := fresh_fruit.get('apple', 0)) &gt;= 4:
    to_enjoy = make_cider(count)
elif count := fresh_fruit.get('lemon', 0):
    to_enjoy = make_lemonade(count)
else:
    to_enjoy = 'Nothing'</code></pre><p>使用海象运算符版本只比原来的版本短五行，但是由于嵌套和缩进的减少，可读性有了很大提高。如果在你的代码中看到像上面那样丑陋的代买，我建议你尽量使用海象运算符重写。</p><p>初学Python的程序员常常遇到的另一个挫折是缺少do/while循环构造。例如，假设我想在新水果到货时将果汁装入瓶中，直到没有剩余的水果为止。在这里，我用while循环实现这个逻辑：</p><pre><code>def pick_fruit():
    ...

def make_juice(fruit, count):
    ...

bottles = []
fresh_fruit = pick_fruit()
while fresh_fruit:
    for fruit, count in fresh_fruit.items():
        batch = make_juice(fruit, count)
        bottles.extend(batch)
    fresh_fruit = pick_fruit()</code></pre><p>这里存在重复，先后执行了两次<code>fresh_fruit = pick_fruit()</code>，一个在循环前设置初始条件，另一个在循环结束时补充到货的水果列表。</p><p>在这种情况下，改进代码复用的策略是使用loop-and-a-half（如果出现这种情况，需要立即退出并跳过循环体中的任何剩余语句）。这消除了多余的行，但它也破坏了while循环，使其成为一个愚蠢的无限循环。现在，循环的所有流控制都依赖于break条件语句：</p><pre><code>bottles = []
while True:                     # Loop
    fresh_fruit = pick_fruit()
    if not fresh_fruit:         # And a half
        break
    for fruit, count in fresh_fruit.items():
        batch = make_juice(fruit, count)
        bottles.extend(batch)</code></pre><p>海象运算符消除了对loop-and-a-half的需要。方法是：允许重新设置<code>fresh_fruit</code>变量，然后每次都通过while循环有条件地求值。此解决方案简短易读，应该是代码中的首选方法：</p><pre><code>bottles = []
while fresh_fruit := pick_fruit():
    for fruit, count in fresh_fruit.items():
        batch = make_juice(fruit, count)
        bottles.extend(batch)</code></pre><p>在许多其他情况下，可以使用还有海象运算符的赋值表达式来消除冗余。通常，当你发现自己在许多行中多次重复同一个表达式或赋值时，应该考虑使用海象运算符来提高可读性。</p><h2 id="-"><strong>牢记</strong></h2><ul><li>赋值表达式使用walrus运算符(:=)在单个表达式中同时对变量名进行赋值和计算，从而减少重复。</li><li>当赋值表达式是一个较大表达式的子表达式时，它必须用圆括号括起来。</li><li>尽管在Python中不能用switch/case语句和do/while循环，但是通过使用海象运算符的赋值表达式可以更清楚地模拟它们的功能。</li></ul><p>原文链接：https://effectivepython.com/2020/02/02/prevent-repetition-with-assignment-expressions</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 介绍 Python 线程及其实现 ]]>
                </title>
                <description>
                    <![CDATA[ Python 线程允许程序的不同部分同时运行，并可以简化设计。如果你对 Python 有一些经验，并且希望使用线程为程序加速，那么本文就是为你准备的！ 什么是线程？ 线程是一个独立的流，这意味着你的程序可以同时做两件事，但是，对于大多数Python程序，不同的线程实际上并不同时执行，它们只是看起来像是同时执行。 人们很容易认为线程是在程序上运行两个（或更多）不同的处理器，每个处理器同时执行一个独立的任务。这种看法大致正确，线程可能在不同的处理器上运行，但一个处理器一次只能运行一个线程。 要同时运行多个任务，不能用Python的标准方式实现，可以用不同的编程语言，或者多个进程实现，这样做的开发成本就高了。 由于用CPython实现了Python业务，线程可能不会加速所有任务，这是GIL（全称Global Interpreter Lock）的原因，一次只能运行一个Python线程。 如果某项任务需要花费大量时间等待外部事件，那么就可以应用多线程。如果是需要对CPU占用高并且花费很少时间等待外部事件，多线程可能枉费。 对于用Python编写并在标准CPython实现上运行的代码， ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/introduction-to-python-threading/</link>
                <guid isPermaLink="false">5e533910ca1efa04e196b671</guid>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Mon, 02 Mar 2020 02:58:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/02/danielle-macinnes-IuLgi9PWETU-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Python 线程允许程序的不同部分同时运行，并可以简化设计。如果你对 Python 有一些经验，并且希望使用线程为程序加速，那么本文就是为你准备的！</p><h2 id="-"><strong><strong><strong>什么是线程？</strong></strong></strong></h2><p>线程是一个独立的流，这意味着你的程序可以同时做两件事，但是，对于大多数Python程序，不同的线程实际上并不同时执行，它们只是看起来像是同时执行。</p><p>人们很容易认为线程是在程序上运行两个（或更多）不同的处理器，每个处理器同时执行一个独立的任务。这种看法大致正确，线程可能在不同的处理器上运行，但一个处理器一次只能运行一个线程。</p><p>要同时运行多个任务，不能用Python的标准方式实现，可以用不同的编程语言，或者多个进程实现，这样做的开发成本就高了。</p><p>由于用CPython实现了Python业务，线程可能不会加速所有任务，这是GIL（全称Global Interpreter Lock）的原因，一次只能运行一个Python线程。</p><p>如果某项任务需要花费大量时间等待外部事件，那么就可以应用多线程。如果是需要对CPU占用高并且花费很少时间等待外部事件，多线程可能枉费。</p><p>对于用Python编写并在标准CPython实现上运行的代码，这是正确的。如果你的线程是用C编写的，那么它们就能够释放GIL、并发运行。如果你在不同的Python实现上运行，也可以查看文档，了解它如何处理线程。</p><p>如果你正在运行一个标准的Python程序，只使用Python编写，并且有一个CPU受限的问题，那么你应该用多进程解决此问题。</p><p>将程序架构为使用线程也可以提高设计的清晰度。你将在下文中学习的大多数示例不一定会运行得更快，因为它们使用线程。在这些示例中使用线程有助于使设计更清晰、更易于推理。</p><p>所以，让我们停止谈论线程并开始使用它！</p><h2 id="--1"><strong><strong><strong>创建一个线程</strong></strong></strong></h2><p>现在你已经知道了什么是线程，让我们来学习如何制作线程。Python标准库提供了线程模块<code>threading</code>，它包含了你将在本文中看到的大部分内容。在这个模块中，<code>Thread</code>是对线程的封装，提供了简单的实现接口。</p><p>要创建一个线程，需要创建<code>Thread</code>的实例，然后调用它的<code>.start()</code>方法:</p><pre><code class="language-python">import logging
import threading
import time

def thread_function(name):
    logging.info("Thread %s: starting", name)
    time.sleep(2)
    logging.info("Thread %s: finishing", name)

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    logging.info("Main : before creating thread")
    x = threading.Thread(target=thread_function, args=(1,))
    logging.info("Main : before running thread")
    x.start()
    logging.info("Main : wait for the thread to finish")
    # x.join()
    logging.info("Main : all done")</code></pre><p>如果你查看日志，可以看到在<code>main</code>部分正在创建和启动线程:</p><pre><code class="language-python">x = threading.Thread(target=thread_function, args=(1,))
x.start()</code></pre><p>用函数<code>thread_function()</code>和<code>arg(1,)</code>创建一个<code>Thread</code>实例。在本文中用整数作为线程的名称，<code>threading.get_ident()</code>可以返回线程的名称，但可读性较差。</p><p><code>thread_function()</code>函数的作用不大，它只是记录一些日志消息，在这些消息之间加上<code>time.sleep()</code>。</p><p>当你执行此程序时，输出将如下所示:</p><pre><code class="language-python">$ ./single_thread.py
Main : before creating thread
Main : before running thread
Thread 1: starting
Main : wait for the thread to finish
Main : all done
Thread 1: finishing</code></pre><p>你会注意到代码的<code>main</code>部分结束之后，<code>Thread</code>才结束。后面会揭示这么做的原因。</p><h3 id="--2"><strong><strong><strong>守护线程</strong></strong></strong></h3><p>在计算机科学中，<code>daemon</code>是在后台运行的程序。</p><p>Python的<code>threading</code>模块对<code>daemon</code>有更具体的含义。当程序退出时，守护线程会立即关闭。考虑这些定义的一种方法是将<code>daemon</code>视为在后台运行的线程，而不必担心关闭它。</p><p>如果程序中正在执行的<code>Threads</code>不是<code>daemons</code>，则程序将在终止之前等待这些线程完成。然而，如果<code>Threads</code>是<code>daemons</code>，当程序退出时，它们就终止了。</p><p>让我们更仔细地看看上面程序的输出，最后两行是有点意思的。当运行这个程序时，在<code>__main__</code>打印完<code>all done</code>后以及线程结束之前会暂停大约2秒。</p><p>这个暂停是Python等待非后台线程完成。当Python程序结束时，关闭操作是清除线程中的程序。</p><p>如果查看<code>threading</code>模块的源代码，你将看到<code>threading._shutdown()</code>方法，它会遍历所有正在运行的线程，并在每一个没有设置<code>daemon</code>标志的线程上调用<code>.join()</code>方法。</p><p>因此，程序在退出时会等待，因为线程本身正在sleep（<code>time.sleep(2)</code>）中。一旦完成并打印了消息，<code>.join()</code> 将返回，程序才可以退出。</p><p>通常，这是你想要的，但是我们还有其他的选择。让我们首先使用一个<code>daemon</code>线程来重复这个程序。你可以修改<code>Thread</code>实例化时的参数，添加<code>daemon=True</code>:</p><pre><code class="language-python">x = threading.Thread(target=thread_function, args=(1,), daemon=True)</code></pre><p>现在运行程序时，应看到以下输出：</p><pre><code class="language-python">$ ./daemon_thread.py
Main : before creating thread
Main : before running thread
Thread 1: starting
Main : wait for the thread to finish
Main : all done</code></pre><p>与前面不同的是，前面所输出的最后一行在这里没有了。<code>thread_function()</code>没有执行完，它是一个<code>daemon</code>线程，所以当<code>_main__</code>执行到达它的末尾时，程序结束，后台线程也就结束了。</p><h3 id="-join-"><strong><strong><strong>线程实例的<code>.join()</code>方法</strong></strong></strong></h3><p>守护线程很方便，但是，如果要实现线程完全执行，而不是被迫退出，应该怎么办？现在让我们回到原始程序，看看注释掉的那一行:</p><pre><code># x.join()</code></pre><p>要让一个线程等待另一个线程完成，可以调用<code>.join()</code>。取消对该行的注释，主线程将暂停并等待线程<code>x</code>，直到它运行结束。</p><p>你是否在程序中用守护线程或普通线程测试了这个问题？这并不重要。如果执行某个线程的<code>.join()</code>方法，该语句将一直等待，直到每个线程都完成。</p><h2 id="--3"><strong><strong><strong>使用多线程</strong></strong></strong></h2><p>到目前为止，示例代码只使用了两个线程：一个是主线程，另一个是以<code>threading.Thread</code>对象开始的线程。</p><p>通常，您会希望启动更多线程并让它们做一些有趣的工作。我们先来看看比复杂的方法，然后再看比较简单的方法。</p><p>启动多线程比较复杂的方法是你已经知道的:</p><pre><code class="language-python">import logging
import threading
import time

def thread_function(name):
    logging.info("Thread %s: starting", name)
    time.sleep(2)
    logging.info("Thread %s: finishing", name)

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    threads = list()
    for index in range(3):
        logging.info("Main : create and start thread %d.", index)
        x = threading.Thread(target=thread_function, args=(index,))
        threads.append(x)
        x.start()

    for index, thread in enumerate(threads):
        logging.info("Main : before joining thread %d.", index)
        thread.join()
        logging.info("Main : thread %d done", index)</code></pre><p>这段代码使用与上面看到的相同机制来启动线程，创建一个<code>Thread</code>实例对象，然后调用<code>.start()</code>。程序中生成一个由<code>Thread</code>实例组成的列表，后面再调用每个实例<code>.join()</code>方法。</p><p>多次运行此代码可能会产生一些有趣的结果。下面是我的机器的输出示例：</p><pre><code>$ ./multiple_threads.py
Main : create and start thread 0.
Thread 0: starting
Main : create and start thread 1.
Thread 1: starting
Main : create and start thread 2.
Thread 2: starting
Main : before joining thread 0.
Thread 2: finishing
Thread 1: finishing
Thread 0: finishing
Main : thread 0 done
Main : before joining thread 1.
Main : thread 1 done
Main : before joining thread 2.
Main : thread 2 done</code></pre><p>如果仔细检查输出，你将看到所有三个线程都按照你可能期望的顺序开始，但在本例中，它们是按照相反的顺序完成的！多次运行将产生不同的排序，可以通过查找<code>Thread x: finishing</code>消息来了解每个线程何时完成。</p><p>线程的运行顺序由操作系统决定，很难预测，它可能（而且很可能）因运行而异，因此在设计使用线程的算法时需要注意这一点。</p><p>幸运的是，Python提供了几个模块，你稍后将看到这些模块用来帮助协调线程并使它们一起运行。在此之前，让我们看看如何更简单地管理一组线程。</p><h2 id="-threadpoolexecutor"><strong><strong><strong>使用ThreadPoolExecutor</strong></strong></strong></h2><p>有一种比上面看到的更容易启动多线程的方法，它被称为<code>ThreadPoolExecutor</code>，是标准库中的<code>concurrent.futures</code>的一员（从Python3.2开始）。</p><p>创建它的最简单方法是使用上下文管理器的<code>with</code>语句，用它实现对线程池的创建和销毁。</p><p>下面是为了使用<code>ThreadPoolExecutor</code>而重写的上一个示例中的<code>__main__</code>部分代码：</p><pre><code>import concurrent.futures

# [rest of code]

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        executor.map(thread_function, range(3))</code></pre><p>代码创建了一个<code>ThreadPoolExecutor</code>作为上下文管理器，告诉它需要在线程池中有多少个工作线程。然后它使用<code>.map()</code>遍历可迭代对象，在上面的例子中是<code>range(3)</code>，将每个可迭代对象传递给线程池中的一个线程。</p><p><code>with</code>语句块的尾部，默认会调用<code>ThreadPoolExecutor</code>的每个线程的<code>.join()</code>方法，建议你尽可能使用<code>ThreadPoolExecutor</code>作为上下文管理器，这样你就永远不会忘记对执行线程<code>.join()</code>。</p><p>注意：使用<code>ThreadPoolExecutor</code>可能会导致一些混乱的错误。</p><p>例如，如果调用不带参数的函数，但在<code>.map()</code>中传了参数，则线程应当抛出异常。</p><p>不幸的是，<code>ThreadPoolExecutor</code>隐藏了该异常，并且（在上面的情况下）程序将在没有输出的情况下终止。一开始调试可能会很混乱。</p><p>运行正确的示例代码将生成如下输出：</p><pre><code>$ ./executor.py
Thread 0: starting
Thread 1: starting
Thread 2: starting
Thread 1: finishing
Thread 0: finishing
Thread 2: finishing</code></pre><p>同样，请注意<code>Thread 1</code>是在<code>Thread 0</code>之前完成的，线程执行顺序的调度是由操作系统完成的，所遵循的计划也不易理解。</p><h2 id="--4">竞态条件</h2><p>在讨论Python线程的其他特性之前，让我们先讨论一下编写线程程序时遇到的一个更困难的问题：竞态条件。</p><p>一旦你了解了什么是竞态条件，并看到了正在发生的情况，然后就使用标准库提供的模块，以防止这些竞态条件的出现。</p><p>当两个或多个线程访问共享数据或资源时，可能会出现竞态情况。在本例中，你将创建一个每次都发生的大型竞态条件，但请注意，大多数它并不是很明显。示例中的情况通常很少发生，而且会产生令人困惑的结果。可以想象，因为竞态条件而引起的bug很难被发现。</p><p>幸运的是，在下述示例中竞态问题每次都会发生，你将详细地了解它以便解释发生了什么。</p><p>对于本例，将编写一个更新数据库的类。你不会真的有一个数据库：你只是要伪造它，因为这不是本文的重点。</p><p><code>FakeDatabase</code>类中有<code>.__init__()</code> 和 <code>.update()</code>方法：</p><pre><code>class FakeDatabase:
    def __init__(self):
        self.value = 0

    def update(self, name):
        logging.info("Thread %s: starting update", name)
        local_copy = self.value
        local_copy += 1
        time.sleep(0.1)
        self.value = local_copy
        logging.info("Thread %s: finishing update", name)</code></pre><p><code>FakeDatabase</code>中的属性<code>.value</code>，用于作为竞态条件中共享的数据。</p><p><code>.__init__()</code>中将<code>.value</code>值初始化为<code>0.</code>，到目前为止，一切正常。</p><p><code>.update()</code> 看起来有点奇怪，它模拟从数据库中读取一个值，对其进行一些计算，然后将一个新值写回数据库。</p><p>所谓从数据库中读取，即将<code>.value</code>的值复制到本地变量。计算就是在原值上加1，然后<code>.sleep()</code> 一小会儿。最后，它通过将本地值复制回<code>.value</code>，将值写回去。</p><p>下面是<code>FakeDatabase</code>的使用方法：</p><pre><code>if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    database = FakeDatabase()
    logging.info("Testing update. Starting value is %d.", database.value)
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
        for index in range(2):
            executor.submit(database.update, index)
    logging.info("Testing update. Ending value is %d.", database.value)</code></pre><p>程序中创建了两个<code>ThreadPoolExecutor</code>，然后对每个线程调用<code>.submit()</code>，告诉它们运行<code>database.update()</code>。</p><p><code>.submit()</code>有一个明显特征，它允许将位置参数和命名参数传给线程中运行的函数:</p><pre><code>.submit(function, *args, **kwargs)</code></pre><p>在上面的用法中，<code>index</code>作为第一个也是唯一一个位置参数传给<code>database.update()</code>。你将在本文后面看到，可以用类似的方式传多个参数。</p><p>由于每个线程都运行<code>.update()</code>，而<code>.update()</code>会让<code>.value</code>的值加1，因此在最后打印时，你可能会希望<code>database.value</code>为2。但如果是这样的话，你就不会看这个例子了。如果运行上述代码，则输出如下：</p><pre><code>$ ./racecond.py
Testing unlocked update. Starting value is 0.
Thread 0: starting update
Thread 1: starting update
Thread 0: finishing update
Thread 1: finishing update
Testing unlocked update. Ending value is 1.</code></pre><p>你可能已经预料到这种情况会发生，但是让我们来看看实际情况的细节，因为这将使这个问题的解决方案更容易理解。</p><h3 id="--5">单线程</h3><p>在用两个线程深入讨论这个问题之前，让我们先退一步，谈谈线程工作流程的一些细节。</p><p>我们不会在这里深入讨论所有的细节，因为这种全面深入的讨论现在并不重要。我们还将简化一些事情，这种做法虽然在技术上并不准确，但会让你对正在发生的事情有正确的认识。</p><p>当你告诉<code>ThreadPoolExecutor</code>运行每个线程时，也就是告诉它要运行哪个函数以及要传给它的参数：<code>executor.submit(database.update, index)</code>。</p><p>其结果是线程池中的每个线程都将调用<code>database.update(index)</code>。注意，<code>database</code>是<code>__main__</code>中创建的<code>FakeDatabase</code>实例对象，调用它的方法<code>.update()</code>。</p><p>每个线程都将引用同一个<code>FakeDatabase</code>的实例<code>database</code>，每个线程还将有一个唯一的值<code>index</code>。为了让上述过程更容易理解，请看下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/02/image-25.png" class="kg-image" alt="image-25" width="1080" height="441" loading="lazy"></figure><p>当某线程开始运行<code>.update()</code>时，它有此方法的本地的数据，即<code>.update()</code>中的<code>local_copy</code>。这绝对是件好事，否则，在两个线程中运行同一个函数就会互相干扰了。这意味着该函数的所有作用域（或本地）变量对于线程来说都是安全的。</p><p>现在，你已经理解，如果使用单个线程和对<code>.update()</code>的单个调用来运行上面的程序会发生什么情况。</p><p>如果只运行一个线程，如下图所示，会一步一步地执行<code>.update()</code>。下图中，语句显示在上面，下面用图示方式演示了线程中的<code>local_value</code>和共享的<code>database.value</code> 中的值的变化：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/02/image-26.png" class="kg-image" alt="image-26" width="1080" height="2265" loading="lazy"></figure><p>按照时间顺序，从上到下观察上面的示意图，从创建线程<code>Thread 1</code>开始，到<code>Thread 1</code>结束终止。</p><p><code>Thread 1</code>启动时，<code>FakeDatabase.value</code>为零。方法中的第一行代码<code>local_copy=self.value</code>将0复制到局部变量。接下来，使用<code>local_copy+=1</code>语句增加<code>local_copy</code>的值。你可以看到<code>Thread 1</code>中的<code>.value</code>值为1。</p><p>然后，调用下一个<code>time.sleep()</code>，这将使当前线程暂停并允许其他线程运行。因为在这个例子中只有一个线程，所以这没有影响。</p><p>当<code>Thread 1</code>唤醒并继续时，它将新值从<code>local_copy</code>复制到<code>FakeDatabase.value</code>，然后线程完成。你可以看到<code>database.value</code>为1。</p><p>到目前为止，一切正常。你只运行了一次<code>.update()</code>并且将<code>FakeDatabase.value</code>递增为1。</p><h3 id="--6">两个线程</h3><p>回到竞态条件，两个线程并行，但不是同时运行。每个线程都有自己的<code>local_copy</code>，并指向相同的<code>database</code>，正是这个共享数据库对象导致了这些问题。</p><p>程序还是从<code>Thread 1</code>执行<code>.update()</code>开始：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/02/image-27.png" class="kg-image" alt="image-27" width="1080" height="907" loading="lazy"></figure><p>当<code>Thread 1</code>调用<code>time.sleep()</code>时，它允许另一个线程开始运行。这就是事情变得有趣的地方。</p><p><code>Thread 2</code>启动并执行相同的操作。它也将<code>database.value</code>复制到其私有的<code>local_copy</code>，而此时共享的<code>database.value</code>尚未更新：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/02/image-28.png" class="kg-image" alt="image-28" width="1080" height="880" loading="lazy"></figure><p>当<code>Thread 1</code>进入睡眠状态时，共享的<code>database.value</code>仍然未被修改，还是0，而此时的<code>local_copy</code>的两个私有版本的值都为1。</p><p><code>Thread 1</code>现在醒来并保存其<code>local_copy</code>的值，然后线程终止，给<code>Thread 2</code>机会。<code>Thread 2</code>不知道在它睡眠时<code>Thread 1</code>运行并更新了<code>database.value</code>的值。<code>Thread 2</code>也将它的<code>local_copy</code>值存储到<code>database.value</code>中，并将其设置为1：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/02/image-29.png" class="kg-image" alt="image-29" width="1080" height="832" loading="lazy"></figure><p>这两个线程交替访问一个共享对象，覆盖彼此的结果。当一个线程释放内存或在另一个线程完成访问之前关闭文件句柄时，可能会出现类似的竞态。</p><h3 id="--7">为什么这不是一个愚蠢的示例</h3><p>上面的例子是刻意而为，目的是确保每次运行程序时都会发生竞态。因为操作系统可以在任何时候交换线程，所以在读取<code>x</code>的值之后，并且在写回递增的值之前，可以中断类似<code>x=x+1</code>的语句。</p><p>发生这种情况的原因细节非常有趣，但这篇文章的其余部分并不需要这些细节，所以可以跳过这个隐藏的部分。</p><p>既然你已经看到了运行过程中的竞态条件，让我们找出解决问题的方法！</p><h2 id="--8">使用锁实现同步</h2><p>有很多方法可以避免或解决竞态。你不会在这里看到所有这些方法，但是有一些方法是经常使用的。让我们从<code>Lock</code>开始。</p><p>要解决上述竞态条件，需要找到一种方法，使得在代码的“读-修改-写”操作中一次只允许一个线程。最常见的方法是使用Python中名为<code>Lock</code>的方法。在其他的一些语言中，类似的被称为<code>Mutex</code>，<code>Mutex</code>源于MUTual EXclusion，这正是<code>Lock</code>的作用。</p><p><code>Lock</code>像是通行证，一次只能有一个线程拥有<code>Lock</code>，任何其他想要<code>Lock</code>的线程都必须等到<code>Lock</code>的所有者放弃它。</p><p>执行此操作的基本函数是<code>.acquire()</code> 和 <code>.release()</code>。线程将调用<code>my_lock.acquire()</code>来获取自己的锁。如果锁已经被其他线程所有，则将等待它被释放。这里有一点很重要，如果一个线程得到了锁，但尚未返回，你的程序将被卡住。你稍后会读到更多关于这方面的内容。</p><p>幸运的是，Python的<code>Lock</code>也将作为上下文管理器运行，因此你可以在一个带有with的语句中使用它，并且当with代码块由于任何原因退出时，锁也会自动释放。</p><p>让我们看看添加了锁的<code>FakeDatabase</code>，其所调用函数保持不变：</p><pre><code>class FakeDatabase:
    def __init__(self):
        self.value = 0
        self._lock = threading.Lock()

    def locked_update(self, name):
        logging.info("Thread %s: starting update", name)
        logging.debug("Thread %s about to lock", name)
        with self._lock:
            logging.debug("Thread %s has lock", name)
            local_copy = self.value
            local_copy += 1
            time.sleep(0.1)
            self.value = local_copy
            logging.debug("Thread %s about to release lock", name)
        logging.debug("Thread %s after release", name)
        logging.info("Thread %s: finishing update", name)</code></pre><p>除了添加一堆调试日志以便更清楚地看到锁操作之外，这里的大变化是添加一个名为<code>._lock</code>的属性，它是一个<code>threading.Lock()</code>实例对象。这个<code>._lock</code>在未锁定状态下初始化，并由with语句锁定和释放。</p><p>这里值得注意的是，运行此方法的线程将一直保持<code>Lock</code>，直到完全完成对数据库的更新。在这种情况下，这意味着函数将在复制、更新、休眠时保持锁定，然后将值写回数据库。</p><p>如果在日志记录设置为警告级别的情况下运行此版本，你将看到以下内容：</p><pre><code>$ ./fixrace.py
Testing locked update. Starting value is 0.
Thread 0: starting update
Thread 1: starting update
Thread 0: finishing update
Thread 1: finishing update
Testing locked update. Ending value is 2.</code></pre><p>看看这个。你的程序终于成功了！</p><p>在<code>__main__</code>中配置日志输出后，可以通过添加以下语句将级别设置为<code>DEBUG</code>来打开完整日志记录：</p><pre><code>logging.getLogger().setLevel(logging.DEBUG)</code></pre><p>在启用<code>DEBUG</code>后，运行此程序，如下所示：</p><pre><code>$ ./fixrace.py
Testing locked update. Starting value is 0.
Thread 0: starting update
Thread 0 about to lock
Thread 0 has lock
Thread 1: starting update
Thread 1 about to lock
Thread 0 about to release lock
Thread 0 after release
Thread 0: finishing update
Thread 1 has lock
Thread 1 about to release lock
Thread 1 after release
Thread 1: finishing update
Testing locked update. Ending value is 2.</code></pre><p>在输出中，你可以看到<code>Thread 0</code>得到了锁，并在进入睡眠状态时仍保持锁定。然后<code>Thread 1</code>启动并尝试获取相同的锁。因为<code>Thread 0</code>仍在持有锁，<code>Thread 1</code>必须等待。这就是<code>Lock</code>的互斥性。</p><p>本文其余部分中的许多示例将日志设置为<code>WARNING</code>和<code>DEBUG</code>级别。我们通常只是<code>DEBUG</code>级别的输出，因为<code>DEBUG</code>日志可能非常长。在日志记录打开的情况下尝试这些程序，看看它们能做什么。</p><h2 id="--9">死锁</h2><p>在继续探索之前，应该先看看使用锁时的一个常见问题。如你所见，如果已经获取了<code>Lock</code>，则对<code>.acquire()</code>的二次调用将等到持有<code>Lock</code>的线程调用<code>.release()</code>。运行此代码时，你认为会发生什么情况？</p><pre><code>import threading

l = threading.Lock()
print("before first acquire")
l.acquire()
print("before second acquire")
l.acquire()
print("acquired lock twice")</code></pre><p>当程序第二次调用<code>l.acquire()</code>时，该函数将挂起，等待<code>Lock</code>的释放。在本例中，可以通过删除第二次调用来修复死锁，但死锁通常发生在以下两个微妙的事情之一：</p><ol><li>未正确释放<code>Lock</code>的错误。</li><li>设计问题，其中一个函数需要由某些函数调用，这些函数可能具有或可能不具有<code>Lock</code>。</li></ol><p>第一种情况有时会发生，但使用<code>Lock</code>作为上下文管理器会大大减少错误出现的频率。建议尽可能使用上下文管理器编写代码，因为它们有助于避免异常跳过<code>.release()</code>调用的情况。</p><p>在某些语言中，设计问题可能要复杂一些。值得庆幸的是，Python线程的又一个对象<code>RLock</code>就是为这种情况而设计的。它允许线程在调用<code>.release()</code>之前多次通过<code>.acquire()</code>实现<code>RLock</code>。该线程中调用<code>.release()</code>的次数与调用<code>.acquire()</code>的次数相同。</p><p><code>Lock</code>和<code>RLock</code>是线程中用来防止竞态条件的两个基本工具，还有一些其他工具以不同的方式发挥作用。在你查看它们之前，让我们转到一个稍微不同的问题上。</p><h2 id="--10"><strong>生产者-消费者线程</strong></h2><p>生产者-消费者问题（Producer-Consumer Problem，以下简称：PCP）是计算机科学中研究线程或进程同步的代表性问题，下面要通过它的一个变体来了解Python中threading模块提供的各种方法。</p><p>对于本例，你将想象一个程序需要从网络读取信息并将其写入磁盘。程序会确定是否要请求信息。它必须监听并接受信息，这些信息不会以正常的速度传入，而是会以突发的方式传入。程序的这一部分叫做生产者。</p><p>另一方面，一旦收到信息，你就需要将其写入数据库。数据库访问速度很慢，但这个速度足以跟上信息传输的平均速度。当一大堆信息进来时，访问速度还不够快。这部分是消费者。</p><p>在生产者和消费者之间，创建一个<code>Pipeline</code>，它将随着你对不同的同步对象的了解而变化。</p><p>这是基本的布局。让我们看看使用<code>Lock</code>的解决方案。它并不完美，但它使用的工具是你已经知道的，所以这是一个很好的开始。</p><h3 id="-pcp"><strong>使用锁的PCP</strong></h3><p>因为这是一篇关于Python的<code>threading</code>模块的文章，而且你刚刚阅读了<code>Lock</code>的使用方法，，所以让我们尝试用一两个使用<code>Lock</code>的线程来解决这个问题。</p><p>一般的设计是，有一个<code>producer</code>线程从模拟网络读取消息并将信息放入<code>Pipeline</code>：</p><pre><code>import random

SENTINEL = object()

def producer(pipeline):
    """Pretend we're getting a message from the network."""
    for index in range(10):
        message = random.randint(1, 101)
        logging.info("Producer got message: %s", message)
        pipeline.set_message(message, "Producer")

    # Send a sentinel message to tell consumer we're done
    pipeline.set_message(SENTINEL, "Producer")</code></pre><p>要生成模拟信息，<code>producer</code>中会生成一个介于1和101（不含101）之间的随机整数，然后调用<code>pipeline</code>的<code>.set_message()</code>，将其发送到<code>consumer</code>。</p><p><code>producer</code>还使用<code>SENTINEL</code>值作为标记，当向<code>consumer</code>发送了10个值，就停止发送。这有点尴尬，但不要担心，在完成这个示例之后，你将看到消除这个<code>SENTINEL</code>值的方法。</p><p>在<code>pipeline</code>的另一边是消费者：</p><pre><code>def consumer(pipeline):
    """Pretend we're saving a number in the database."""
    message = 0
    while message is not SENTINEL:
        message = pipeline.get_message("Consumer")
        if message is not SENTINEL:
            logging.info("Consumer storing message: %s", message)</code></pre><p><code>consumer</code>从<code>pipeline</code>中读取一条信息并将其写入一个虚拟数据库，在本例中，只是将信息打印到显示器上。如果它得到<code>SENTINEL</code>值，就结束函数执行过程，该函数将终止线程。</p><p>在看真正有趣<code>Pipeline</code>部分之前，这里是<code>__main__</code>的代码，它产生了以下线程：</p><pre><code>if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    # logging.getLogger().setLevel(logging.DEBUG)

    pipeline = Pipeline()
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
        executor.submit(producer, pipeline)
        executor.submit(consumer, pipeline)</code></pre><p>这看起来应该相当熟悉，因为它接近前面示例中的<code>__main__</code>代码。</p><p>请记住，你可以通过取消注释行打开<code>DEBUG</code>日志记录，以查看所有日志记录消息：</p><pre><code># logging.getLogger().setLevel(logging.DEBUG)</code></pre><p>通过<code>DEBUG</code>日志信息来查看每个线程获取和释放锁的确切位置是值得的。</p><p>现在让我们看看将信息从<code>producer</code>传递给消费者的管道：</p><pre><code>class Pipeline:
    """
    Class to allow a single element pipeline between producer and consumer.
    """
    def __init__(self):
        self.message = 0
        self.producer_lock = threading.Lock()
        self.consumer_lock = threading.Lock()
        self.consumer_lock.acquire()

    def get_message(self, name):
        logging.debug("%s:about to acquire getlock", name)
        self.consumer_lock.acquire()
        logging.debug("%s:have getlock", name)
        message = self.message
        logging.debug("%s:about to release setlock", name)
        self.producer_lock.release()
        logging.debug("%s:setlock released", name)
        return message

    def set_message(self, message, name):
        logging.debug("%s:about to acquire setlock", name)
        self.producer_lock.acquire()
        logging.debug("%s:have setlock", name)
        self.message = message
        logging.debug("%s:about to release getlock", name)
        self.consumer_lock.release()
        logging.debug("%s:getlock released", name)</code></pre><p>哇！这么多代码。其中相当大的一部分只是日志语句，以便在运行代码时更容易看到发生了什么。下面是删除所有日志记录语句后的相同代码：</p><pre><code>class Pipeline:
    """
    Class to allow a single element pipeline between producer and consumer.
    """
    def __init__(self):
        self.message = 0
        self.producer_lock = threading.Lock()
        self.consumer_lock = threading.Lock()
        self.consumer_lock.acquire()

    def get_message(self, name):
        self.consumer_lock.acquire()
        message = self.message
        self.producer_lock.release()
        return message

    def set_message(self, message, name):
        self.producer_lock.acquire()
        self.message = message
        self.consumer_lock.release()</code></pre><p>这似乎更容易处理。此版本代码中的<code>Pipeline</code>有三个成员：</p><ul><li><code>.message</code>存储要传递的信息。</li><li><code>.producer_lock</code>是<code>threading.Lock</code>实例对象，在<code>producer</code>线程中，用它控制对信息的访问</li><li><code>.consumer_lock</code>也是<code>threading.Lock</code>实例对象，它在<code>consumer</code>线程控制对信息的访问。</li></ul><p><code>__init__()</code>初始化这三个成员，然后调用<code>.consumer_lock</code>上的<code>.acquire()</code>。这是你想开始的状态。允许<code>producer</code>添加新信息，但<code>consumer</code>需要等待信息出现。</p><p><code>.get_message()</code> 和 <code>.set_messages()</code>几乎相反。<code>.get_message()</code>调用<code>consumer_lock</code>上的<code>.acquire()</code>，它让<code>consumer</code>等待信息准备就绪。</p><p>一旦<code>consumer</code>获得了<code>.consumer_lock</code>，它就会复制出<code>.message</code>中的值，然后调用<code>.producer_lock</code>上的<code>.release()</code>，释放锁，允许<code>producer</code>将下一条信息插入到<code>pipeline</code>中。</p><p>在运行<code>.set_message()</code>之前，要注意<code>.get_message()</code>中的一个细节，通常以<code>return self.message</code>结束方法，但是此处不这样做，看看你能否弄清楚原因。</p><p>答案在此。一旦<code>consumer</code>调用<code>.producer_lock.release()</code>，它就会与<code>producer</code>交换位置，<code>producer</code>开始运行，这种情况可能在<code>.release()</code>返回之前发生！这意味着，当函数<code>returns self.message</code>时，有比较小的概率会生成下一条信息，因此你将丢失第一条信息。这是另一个竞态的例子。</p><p>转到<code>.set_message()</code>，可以看到事务的另一面，<code>producer</code>会用一条信息来调用它，获取<code>.producer_lock</code>，设置<code>.message</code>，然后调用<code>consumer_lock</code>上的<code>.release()</code>。这样就使得用户可以读取该值。</p><p>将日志设置为<code>WARNING</code>并执行代码，看看它是什么样子的：</p><pre><code>$ ./prodcom_lock.py
Producer got data 43
Producer got data 45
Consumer storing data: 43
Producer got data 86
Consumer storing data: 45
Producer got data 40
Consumer storing data: 86
Producer got data 62
Consumer storing data: 40
Producer got data 15
Consumer storing data: 62
Producer got data 16
Consumer storing data: 15
Producer got data 61
Consumer storing data: 16
Producer got data 73
Consumer storing data: 61
Producer got data 22
Consumer storing data: 73
Consumer storing data: 22</code></pre><p>一开始，你可能会发现奇怪的是，<code>producer</code>在<code>consumer</code>运行之前就收到两条信息。如果回顾一下<code>producer</code>和<code>.set_message()</code>，你会注意到，当<code>producer</code>视图将信息发送到<code>pipeline</code>时，会等待<code>Lock</code>。这是在<code>producer</code>收到信息和日志之后完成的。</p><p>当<code>producer</code>尝试发送第二条信息时，它将第二次调用<code>.set_message()</code>，并且它将被锁定。</p><p>操作系统可以在任何时候交换线程，但它通常会让每个线程在交换之前有一个合理的运行时间。这就是为什么<code>producer</code>通常运行到它在第二次调用<code>.set_message()</code>时被锁定为止。</p><p>但是，一旦某个线程被锁定，操作系统就会将其交换出去，并找到另一个要运行的线程，此时的另一个线程就是<code>consumer</code>。</p><p><code>consumer</code>调用<code>.get_message()</code>，该函数读取信息并调用<code>.producer_lock</code>上的<code>.release()</code>，从而允许<code>producer</code>在下次交换线程时再次运行。</p><p>注意，第一条消息是43，这正是<code>consumer</code>读的内容，尽管 <code>producer</code>已经生成了45这条信息。</p><p>以上是有限的测试，并没有很好地解决PCP，因为它一次只允许管道中的有一个值。当<code>producer</code>收到大量信息时，它将无处安放这些信息。</p><p>让我们使用<code>Queue</code>寻找一个更好的方法来解决这个问题。</p><p>原文链接：https://realpython.com/intro-to-python-threading/，译者：老齐</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 详解 Python 算法：快速排序 ]]>
                </title>
                <description>
                    <![CDATA[ 快速排序是一种流行的排序算法，经常与归并排序一起使用。快速排序是一个高效的排序算法，平均复杂度为 O(nlogn)。它受欢迎的部分原因还在于易于实施。 在本文中，我们将先使用整数集合演示快速排序算法，然后会用自定义对象深入探讨这种算法的实现方式。 在分治法、原地排序和非稳定排序中，都有快速排序算法。 - 在分治法中，对大数组使用递归进行排序之前，使用快速排序算法将数组拆分为更小的数组，直到最后得到一个空数组或只有一个元素的数组。 - 在原地排序中，快速排序不创建数组的任何副本，它在内存中完成所有的操作。 - 稳定排序指相同的值在数组中的顺序也是相同的，非稳定的排序算法不能保证这一点，只能说这样的顺序当然有可能出现，但不能保证。这在针对对象排序而不是对基本类型排序时变得很重要。例如，假设有几个自定义的 Person 类实例具有相同的 age，即 21 岁的 Dave 和 21 岁的 Mike。如果要对包含 Dave 和 Mike 的集合使用快速排序法按年龄排序，则无法保证每次运行算法时 Dave 都会出现在 Mike 之前，反之亦然。 快速排序 快速排序算法的执行流程如下：  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/sorting-in-python/</link>
                <guid isPermaLink="false">5e566c8aca1efa04e196b6c0</guid>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Wed, 26 Feb 2020 13:23:07 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/02/1582723261732.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>快速排序是一种流行的排序算法，经常与归并排序一起使用。快速排序是一个高效的排序算法，平均复杂度为 O(nlogn)。它受欢迎的部分原因还在于易于实施。</p><p>在本文中，我们将先使用整数集合演示快速排序算法，然后会用自定义对象深入探讨这种算法的实现方式。</p><p>在分治法、原地排序和非稳定排序中，都有快速排序算法。</p><p>- 在分治法中，对大数组使用递归进行排序之前，使用快速排序算法将数组拆分为更小的数组，直到最后得到一个空数组或只有一个元素的数组。</p><p>- 在原地排序中，快速排序不创建数组的任何副本，它在内存中完成所有的操作。</p><p>- 稳定排序指相同的值在数组中的顺序也是相同的，非稳定的排序算法不能保证这一点，只能说这样的顺序当然有可能出现，但不能保证。这在针对对象排序而不是对基本类型排序时变得很重要。例如，假设有几个自定义的 Person 类实例具有相同的 age，即 21 岁的 Dave 和 21 岁的 Mike。如果要对包含 Dave 和 Mike 的集合使用快速排序法按年龄排序，则无法保证每次运行算法时 Dave 都会出现在 Mike 之前，反之亦然。</p><h2 id="-">快速排序</h2><p>快速排序算法的执行流程如下：</p><p>- 将集合分成两个（大致）相等的部分，取一个伪随机元素并将其用作主元（注：“主元”，原文中 pivot，在多数介绍快速排序算法的中文资料中，并不对齐单独命名，只简单说成是“分割点”，即划分集合的元素，但本文作者使用了 pivot 这个词来表示“分割点”，译者认为很便于理解和表述，并将其翻译为“主元”）。</p><p>- 小于主元的元素将移动到其左侧，大于主元的元素将移动到其的右侧。</p><p>- 分别对于主元左侧和右侧的元素，重复此上述，直到对整个数组完成排序。</p><p>当我们将某个元素描述为比另一个元素“大”或“小”时，并不一定意味着这些元素是整数，我们可以自定义对象，并根据选择的任何属性进行排序。</p><p>假设有一个自定义类 Person，每个实例都有 name 和 age 属性，我们可以按姓名（词典顺序）或按年龄（升序或降序）进行排序。</p><h2 id="--1">快速排序算法的原理</h2><p>对于快速排序算法，很多时候，数组是不能等分的，这是因为整个过程取决于如何选择主元。我们需要选择一个主元，使其比一半的元素大，比另一半的元素小。尽管这个过程看起来很直观，但很难做到。</p><p>想一想：如何为数组选择合适的主元？关于这个问题的解决方法，在快速排序算法的发展史上有过好多种。如果随机选择，是行不通的，因为这不能保障主元是合适的，随机选择的代价会非常“昂贵”。此外，还曾经有人提出从中间选择一个元元素，或者选择第一个、或者后半部分中间的，设置用更复杂的递归公式选择，等等。</p><p>最直接的最简单的方法是选择第一个（或最后一个）元素作为主元，具有讽刺意味的是，这会导致快速排序法对已经排序（或几乎排序了）的数组执行效果非常糟糕。</p><p>但是，大多数人还是用这种方法来实现快速排序算法，因为它很简单，而且这种选择主元的方式是一种非常有效的操作（我们需要重复执行），这也正是我们下面要做的。</p><p>既然我们选择了一个主元，接下来该用它做些什么？同样，有几种方法可以处理各个分区。我们要设置三个指针，有一个指向主元，另外两个分别指向“较小”元素和“较大”元素。</p><p>然后移动元素，使所有小于主元的元素都在左侧，而所有较大的元素都在右侧。更小或更大的元素不一定会被排序，我们只希望它们位于主元的适当的一侧。然后，用递归方法遍历主元的左侧和右侧。</p><p>一步一步来看看我们计划要做的事情，从而理解以上所说的快速排序算法的执行过程。使用下面显示的数组，我们选择了第一个元素作为主元（29），low 表示指向较小元素的指针，最右边的 high 表示指向较大元素的指针。</p><p>- 29 是第一主元，low 指向 99，high 指向 44</p><p>29 | 99 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21,44 (high)</p><p>- high 从右向左移动，直到找到一个小于主元的值</p><p>29 | 99 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21 (high),44</p><p>- 现在 high 指向了 21，它是一个比主元小的元素，我们想在数组的开头附近找到一个值，使之可以用于与 21 交换。用一个比主元还小的值交换是没有意义的，所以如果 low 指向一个更小的元素，我们试着找到一个更大的元素。</p><p>- 将 low 变量向右移动，直到找到一个大于主元的元素。幸运的是， low 已经定位在 99，它就比主元大</p><p>- 交换 high 和 low 指向的元素：</p><p>29 | 21 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,99 (high),44</p><p>- 在我们这样做之后，就将 high 继续向左移动，将 low 向右边移动（21 和 99 现在所处的位置是正确的）。</p><p>- 再次，将 high 向左移动，下一个数字就是小于主元的 12。</p><p>- 现在我们通过将 low 向右移动来找一个大于主元的值，就是 41 了，它是第一个这样的值</p><p>不断重复上述过程，直到 low 指针和 high 指针最终在某个元素相遇：</p><p>29 | 21,27,12,19,28 (low/high),44,78,87,66,31,76,58,88,83,97,41,99,44</p><p>- 不再使用 29 作为主元了，所以剩下唯一要做的就是交换主元和 high 所指元素 28，然后我们就完成了这个递归步骤：</p><p>28,21,27,12,19,29,44,78,87,66,31,76,58,88,83,97,41,99,44</p><p>如你所见，我们已经让小于 29 的所有值现在都在 29 的左侧，大于 29 的所有值都在右侧。</p><p>然后，算法对 28、21、27、12、19（左侧）集合和44、78、87、66、31、76、58、88、83、97、41、99、44（右侧）集合执行相同的操作。</p><h2 id="--2">实现</h2><h3 id="--3">对数组排序</h3><p>快速排序是一种自然递归算法，将输入数组分成较小的数组，将元素移动到主元的适当一侧，然后重复。</p><p>让我们来看看几个递归调用：</p><p>- 当第一次调用算法时，我们考虑所有的元素——从索引 0 到 n-1，其中 n 是数组中的元素数量。</p><p>- 如果主元最终位于位置 k，那么我们将对从 0 到 k-1 和从 k+1 到 n-1 的元素重复该过程。</p><p>- 在将元素从 k+1 排到 n-1 时，当前主元将在某个位置 p 结束。然后我们将元素从 k+1 序到 p-1，从 p+1 排序到 n-1，依此类推。</p><p>也就是说，我们将使用两个函数：partition() 和 quick_sort()。quick_sort() 函数将首先用 partition() 函数对集合分组，然后在每组上递归调用自己。</p><p>下面从 partition() 函数开始：</p><pre><code>def partition(array, start, end):
    pivot = array[start]
    low = start + 1
    high = end

    while True:
        # If the current value we're looking at is larger than the pivot
        # it's in the right place (right side of pivot) and we can move left,
        # to the next element.
        # We also need to make sure we haven't surpassed the low pointer, since that
        # indicates we have already moved all the elements to their correct side of the pivot
        while low &lt;= high and array[high] &gt;= pivot:
            high = high - 1

        # Opposite process of the one above
        while low &lt;= high and array[low] &lt;= pivot:
            low = low + 1

        # We either found a value for both high and low that is out of order
        # or low is higher than high, in which case we exit the loop
        if low &lt;= high:
            array[low], array[high] = array[high], array[low]
            # The loop continues
        else:
            # We exit out of the loop
            break

    array[start], array[high] = array[high], array[start]

    return high</code></pre><p>最后，让我们实现 quick_sort() 函数：</p><pre><code>def quick_sort(array, start, end):
    if start &gt;= end:
        return

    p = partition(array, start, end)
    quick_sort(array, start, p-1)
    quick_sort(array, p+1, end)</code></pre><p>有了这两个函数，就可以对一个简单的数组执行 quick_sort()：</p><pre><code>array = [29,99,27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21,44]

quick_sort(array, 0, len(array) - 1)
print(array)</code></pre><p>输出：</p><pre><code>[12, 19, 21, 27, 28, 29, 31, 41, 44, 44, 58, 66, 76, 78, 83, 87, 88, 97, 99]</code></pre><p>由于此算法是非稳定的，所以不能保证这两个 44 的顺序总是这样的，也许会交换 —— 虽然这在整数数组中意义不大。</p><h3 id="--4">对自定义对象进行排序</h3><p>有几种方法可以重写此算法，以便在 Python 中对自定义对象进行排序。一种非常典型的 Python 方法是实现给定类的比较运算符，这意味着我们实际上不需要更改算法实现，因为 &gt;、=、&lt;= 等也可以应用于类对象。</p><p>另一个选择是以参数的方式给函数传入一个比较函数，用这个方法来执行对象的实际比较。用这种方式重写算法函数以用于自定义对象是相当简单的。但是请记住，算法并不稳定。</p><p>让我们从 Person 类开始：</p><pre><code>class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return self.name</code></pre><p>这是一个非常基本的类，只有两个属性，name 和 age。我们希望使用 age 作为排序键，这将通过为排序算法提供自定义 lambda 函数来实现。</p><p>但首先，让我们看看如何在函数中实现算法。我们没有直接使用 &lt;= 或 &gt;= 运算符进行比较，而是在函数中来判断哪个 Person 实例的年龄更高：</p><pre><code>def partition(array, start, end, compare_func):
    pivot = array[start]
    low = start + 1
    high = end

    while True:
        while low &lt;= high and compare_fun(array[high], pivot):
            high = high - 1

        while low &lt;= high and not compare_fun(array[low], pivot):
            low = low + 1

        if low &lt;= high:
            array[low], array[high] = array[high], array[low]
        else:
            break

    array[start], array[high] = array[high], array[start]

    return high
def quick_sort(array, start, end, compare_func):
    if start &gt;= end:
        return

    p = partition(array, start, end, compare_func)
    quick_sort(array, start, p-1, compare_func)
    quick_sort(array, p+1, end, compare_func)</code></pre><p>现在，让我们对这些实例对象的集合进行排序。你可以看到，在 quick_sort 函数的参数中，有 lambda 函数，它会对 age 属性的值进行实际的比较:</p><pre><code>p1 = Person("Dave", 21)
p2 = Person("Jane", 58)
p3 = Person("Matthew", 43)
p4 = Person("Mike", 21)
p5 = Person("Tim", 10)

array = [p1,p2,p3,p4,p5]

quick_sort(array, 0, len(array) - 1, lambda x, y: x.age &lt; y.age)
for person in array:
    print(person)</code></pre><p>输出是：</p><pre><code class="language-python">Tim
Dave
Mike
Matthew
Jane</code></pre><p>通过这种方式实现算法，只要提供适当的比较函数，它就可以用于我们选择的任何自定义对象。</p><h2 id="--5">优化快速排序算法</h2><p>考虑到快速排序独立地对给定数组的“一半”进行排序，那么它就非常便于实现并行计算。我们可以用一个单独的线程对数组的每个“一半”进行排序，理想情况下，可以将排序所需的时间减半。</p><p>但是，如果我们在选择主元时特别不走运，快速排序法可能会递归太深，此时即使用并行计算，其效率也不如归并排序。</p><p>对小数组进行排序时，建议使用非递归算法。即使是像插入排序这样的简单操作，在小数组上也比快速排序更有效。因此，理想情况下，我们可以检查排序对象是否只有少量元素（大多数建议是 10 个或更少），如果是这样，就用插入排序代替。</p><p>快速排序的一个流行变体是多主元快速排序，它使用 n-1 个主元将原始数组分解为 n 个较小的数组。然而，大多数情况下只使用两个主元，而不是更多。</p><p>有趣的事实：Java 7 的排序实现中使用了双主元快速排序，针对较小数组的则是插入排序。</p><h2 id="--6">结论</h2><p>正如我们前面提到的，快速排序的效率很大程度上取决于主元的选择——它可以成就或突破算法时间（和堆栈空间）的复杂度。在使用自定义对象时，算法的非稳定性也是一个潜在的问题。</p><p>然而，尽管如此，快速排序的平均时间复杂度 O(nlogn) 和相对低的内存使用、简单的实现方法，使它成为一种非常有效和流行的算法。</p><p>原文链接：https://stackabuse.com/quicksort-in-python/，作者：Marcus Sanatan</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 通俗易懂的 HTTPS（三） ]]>
                </title>
                <description>
                    <![CDATA[ 考虑到所有这些关于加密的信息，让我们把范围缩小一点，讨论一下 Python HTTPS 应用在真实的项目中的实际方式，加密只是事情的一半，访问安全网站时，需要两个主要组件：  * 加密：将明文转换为密文并返回。  * 身份认证：验证某人或事物是否名副其实。 你已经了解了关于加密的工作原理，但是如何身份认证？要了解真实项目中的身份认证，需要了解公钥基础结构（PKI）。PKI在安全生态系统中引入了另一个重要概念：证书。 证书就是互联网上的护照，和计算机世界中的大多数东西一样，它们只是含有数据的文件中。一般来说，证书包括以下信息：  * 颁发给：标识证书的所有者  * 颁发者：标识颁发证书的人  * 有效期：标识证书有效的时间范围 就像护照一样，证书只有在由权威机构生成和认可的情况下才真正有用。你的浏览器不可能知道你在互联网上访问的每个站点的每个证书，相反，PKI依赖于一个称为证书颁发机构（CA）的概念。 证书颁发机构负责颁发证书。在PKI中，它们被认为是可信的第三方（TTP）。本质上，这些实体充当证书的有效权限。假设你想去另一个国家，你有一本护照，上面有你所有的信息。在外国的移 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-is-https-part-three/</link>
                <guid isPermaLink="false">5e453085ca1efa04e196b5be</guid>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Thu, 13 Feb 2020 11:37:11 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/02/photo-1580920462192-3d40a3bb7bc2-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>考虑到所有这些关于加密的信息，让我们把范围缩小一点，讨论一下 Python HTTPS 应用在真实的项目中的实际方式，加密只是事情的一半，访问安全网站时，需要两个主要组件：</p><ul><li>加密：将明文转换为密文并返回。</li><li>身份认证：验证某人或事物是否名副其实。</li></ul><p>你已经了解了关于加密的工作原理，但是如何身份认证？要了解真实项目中的身份认证，需要了解公钥基础结构（PKI）。PKI在安全生态系统中引入了另一个重要概念：证书。</p><p>证书就是互联网上的护照，和计算机世界中的大多数东西一样，它们只是含有数据的文件中。一般来说，证书包括以下信息：</p><ul><li>颁发给：标识证书的所有者</li><li>颁发者：标识颁发证书的人</li><li>有效期：标识证书有效的时间范围</li></ul><p>就像护照一样，证书只有在由权威机构生成和认可的情况下才真正有用。你的浏览器不可能知道你在互联网上访问的每个站点的每个证书，相反，PKI依赖于一个称为证书颁发机构（CA）的概念。</p><p>证书颁发机构负责颁发证书。在PKI中，它们被认为是可信的第三方（TTP）。本质上，这些实体充当证书的有效权限。假设你想去另一个国家，你有一本护照，上面有你所有的信息。在外国的移民官员怎么知道你的护照上包含有效的信息？</p><p>如果你要自己填写所有信息并签字，那么你想访问的每个国家的每个移民官都需要亲自了解你，并且能够证明那里的信息确实正确。</p><p>处理此问题的另一种方法是将所有信息发送到可信的第三方（TTP）。TTP会对你提供的资料进行彻底调查，核实你的要求，然后签署你的护照。事实证明，这更为实际，因为移民局官员只需要了解可信的第三方。</p><p>TTP是如何在实践中处理证书的？过程如下：</p><ul><li>创建证书签名请求（CSR）：这就像填写签证信息一样。</li><li>将CSR发送给可信的第三方（TTP）：这就像将你的信息发送到签证申请办公室。</li><li>验证你的信息：不管怎样，TTP需要验证你提供的信息。作为一个例子，请看Amazon如何验证所有权。</li><li>生成一个公钥：TTP签署你的CSR。这相当于TTP签署你的签证。</li><li>签发已验证的公钥：这相当于你在邮件中收到签证。</li></ul><p>请注意，CSR以加密方式绑定到你的私钥。因此，信息公钥、私钥和证书颁发机构的所有三个部分都以某种方式相关。这将创建所谓的信任链，因此你现在拥有一个有效的证书，可以用来核实你的身份。</p><p>大多数情况下，这是网站所有者的责任，网站所有者将遵循所有这些步骤。在这个过程结束时，他们的证书上写着：</p><pre><code>根据Y，从时间A和时间B期间，我是X</code></pre><p>这句话就是证书真正告诉你的。变量的填写方法如下：</p><ul><li>A是有效的开始日期和时间。</li><li>B是有效的结束日期和时间。</li><li>X是服务器的名称。</li><li>Y是证书颁发机构的名称。</li></ul><p>基本上，这都是证书描述的。换句话说，有证书并不一定意味着你就是你所说的那个人，只是你让Y同意 你就是你所说的那个人。这就是可信的第三方的“可信”部分。</p><p>TTP需要在客户端和服务器之间共享，以便每个人都对HTTPS握手感到满意。你的浏览器会自动安装许多证书，要查看它们，请执行以下步骤：</p><ul><li>Chrome：进入设置&gt;高级&gt;隐私和安全&gt;管理证书&gt;权限。</li><li>Firefox：进入设置&gt;首选项&gt;隐私和安全&gt;查看证书&gt;权限。</li></ul><p>这涵盖了在真实项目中创建Python HTTPS应用所需的基础知识，接下来，把这些概念应用到自己的代码中，调试一个常见的示例，并成为你自己的秘密松鼠证书颁发机构！</p><h2 id="python-https-">Python HTTPS 应用</h2><p>你已经了解了制作Python HTTPS应用所需的基本知识，现在是将所有知识逐一绑定到你的应用的时候了，这将让服务器和客户端之间的通信更安全。</p><p>可以在自己的机器上设置整个PKI基础设施，这正是本节中要做的。没有听起来那么难，所以别担心！成为一个真正的证书颁发机构要比采取以下步骤困难得多，但你将要读到的大体上是你运行自己的CA（证书颁发机构）所需的全部内容。</p><h3 id="-"><strong>成为证书颁发机构</strong></h3><p>证书颁发机构只不过是一对非常重要的公钥和私钥。要成为CA（证书颁发机构），只需要生成一个公钥和私钥对。</p><p>注意：成为公众使用的CA是一个非常艰难的过程，尽管有很多公司遵循了这个过程。但是，到本文时，你也不会是这些公司中的一员！</p><p>你的初始公钥和私钥对将是自签名证书。如果你真的要成为一个CA（证书颁发机构），那么这个私钥的安全是非常重要的。如果有人可以访问CA的公钥和私钥对，他也可以生成一个完全有效的证书，并且除了停止信任你的CA之外，你无法检测该问题。</p><p>解除警告后，你可以立即生成证书。首先，生成一个私钥。将以下内容粘贴到名为<code>pki_helpers.py</code>的文件中：</p><pre><code># pki_helpers.py

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa


def generate_private_key(filename: str, passphrase: str):
    private_key = rsa.generate_private_key(
        public_exponent=65537, key_size=2048, backend=default_backend()
    )

    utf8_pass = passphrase.encode("utf-8")
    algorithm = serialization.BestAvailableEncryption(utf8_pass)

    with open(filename, "wb") as keyfile:
        keyfile.write(
            private_key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.TraditionalOpenSSL,
                encryption_algorithm=algorithm,
            )
        )

    return private_key</code></pre><p><code>generate_private_key()</code>使用RSA生成私钥。下面是代码的分解：</p><ul><li>第2行到第4行导入运行该函数所需的库。</li><li>第7行到第9行使用RSA生成私钥。神奇的数字65537和2048只是两个可能的值。你可以阅读更多关于这些数字有用的原因，或只是简单相信这些数字是有用的。</li><li>第11到12行设置用于私钥的加密算法。</li><li>第14至21行按指定的文件名将私钥写入磁盘。此文件使用提供的密码来加密。</li></ul><p>成为你自己的CA的下一步是生成自签名公钥。你可以绕过证书签名请求（CSR）并立即生成公钥。将以下内容粘贴到<code>pki_helpers.py</code>中：</p><pre><code># pki_helpers.py

from datetime import datetime, timedelta
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes

def generate_public_key(private_key, filename, **kwargs):
    subject = x509.Name(
        [
            x509.NameAttribute(NameOID.COUNTRY_NAME, kwargs["country"]),
            x509.NameAttribute(
                NameOID.STATE_OR_PROVINCE_NAME, kwargs["state"]
            ),
            x509.NameAttribute(NameOID.LOCALITY_NAME, kwargs["locality"]),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, kwargs["org"]),
            x509.NameAttribute(NameOID.COMMON_NAME, kwargs["hostname"]),
        ]
    )

    # Because this is self signed, the issuer is always the subject
    issuer = subject

    # This certificate is valid from now until 30 days
    valid_from = datetime.utcnow()
    valid_to = valid_from + timedelta(days=30)

    # Used to build the certificate
    builder = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(valid_from)
        .not_valid_after(valid_to)
    )

    # Sign the certificate with the private key
    public_key = builder.sign(
        private_key, hashes.SHA256(), default_backend()
    )

    with open(filename, "wb") as certfile:
        certfile.write(public_key.public_bytes(serialization.Encoding.PEM))

    return public_key</code></pre><p>这里有一个新的函数<code>generate_public_key()</code>，它将生成一个自签名的公钥。下面是这段代码的工作原理：</p><ul><li>第2行到第5行是运行该函数所需的导入。</li><li>第8行到第18行建立了有关证书主题的信息。</li><li>第21行使用相同的颁发者和使用者，因为这是自签名证书。</li><li>第24至25行指示此公钥有效的时间范围。在这个示例中，有效期是30天。</li><li>第28到36行将所有必需的信息添加到公钥生成器对象中，该对象需要进行签名。</li><li>第38至41行用私钥签署公钥。</li><li>第43到44行将公钥写入文件名。</li></ul><p>使用这两个函数，你可以在Python中快速生成私钥和公钥对：</p><pre><code>&gt;&gt;&gt; from pki_helpers import generate_private_key, generate_public_key
&gt;&gt;&gt; private_key = generate_private_key("ca-private-key.pem", "secret_password")
&gt;&gt;&gt; private_key
&lt;cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey object at 0x7ffbb292bf90&gt;
&gt;&gt;&gt; generate_public_key(
...   private_key,
...   filename="ca-public-key.pem",
...   country="US",
...   state="Maryland",
...   locality="Baltimore",
...   org="My CA Company",
...   hostname="my-ca.com",
... )
&lt;Certificate(subject=&lt;Name(C=US,ST=Maryland,L=Baltimore,O=My CA Company,CN=logan-ca.com)&gt;, ...)&gt;</code></pre><p>从<code>pki_helpers</code>导入函数后，首先生成私钥并将其保存到文件<code>ca-private-key.pem</code>。然后将该私钥传递到<code>generate_public_key()</code>以生成公钥。在你的目录中，现在应该有两个文件：</p><pre><code>$ ls ca*
ca-private-key.pem ca-public-key.pem</code></pre><p>祝贺你！你现在有能力成为证书颁发机构了。</p><h3 id="--1"><strong>信任你的服务器</strong></h3><p>要使服务器变得可信，第一步是生成证书签名请求（CSR）。在现实世界中，CSR将被发送到实际的证书颁发机构，如Verisign或Let's Encrypt。在本例中，你将使用刚刚创建的CA。</p><p>将生成CSR的代码从上面粘贴到<code>pki_helpers.py</code>文件中:：</p><pre><code># pki_helpers.py

def generate_csr(private_key, filename, **kwargs):
    subject = x509.Name(
        [
            x509.NameAttribute(NameOID.COUNTRY_NAME, kwargs["country"]),
            x509.NameAttribute(
                NameOID.STATE_OR_PROVINCE_NAME, kwargs["state"]
            ),
            x509.NameAttribute(NameOID.LOCALITY_NAME, kwargs["locality"]),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, kwargs["org"]),
            x509.NameAttribute(NameOID.COMMON_NAME, kwargs["hostname"]),
        ]
    )

    # Generate any alternative dns names
    alt_names = []
    for name in kwargs.get("alt_names", []):
        alt_names.append(x509.DNSName(name))
    san = x509.SubjectAlternativeName(alt_names)

    builder = (
        x509.CertificateSigningRequestBuilder()
        .subject_name(subject)
        .add_extension(san, critical=False)
    )

    csr = builder.sign(private_key, hashes.SHA256(), default_backend())
    with open(filename, "wb") as csrfile:
        csrfile.write(csr.public_bytes(serialization.Encoding.PEM))

    return csr</code></pre><p>在大多数情况下，此代码与生成原始公钥的方式相同。主要区别概述如下：</p><ul><li>第16至19行设置备用DNS名称，该名称对你的证书有效。</li><li>第21行到第25行生成不同的生成器对象，但同样的基本原则与以前一样适用。你正在为CSR构建所有必需的属性。</li><li>第27行用私钥签署CSR。</li><li>第29至30行将CSR以PEM格式写入磁盘。</li></ul><p>你会注意到，为了创建CSR，首先需要一个私钥。幸运的是，你可以在创建CA的私钥时使用相同的<code>generate_private_key()</code> 。使用上面的函数和前面定义的方法，可以执行以下操作：</p><pre><code>&gt;&gt;&gt; from pki_helpers import generate_csr, generate_private_key
&gt;&gt;&gt; server_private_key = generate_private_key(
...   "server-private-key.pem", "serverpassword"
... )
&gt;&gt;&gt; server_private_key
&lt;cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey object at 0x7f6adafa3050&gt;
&gt;&gt;&gt; generate_csr(
...   server_private_key,
...   filename="server-csr.pem",
...   country="US",
...   state="Maryland",
...   locality="Baltimore",
...   org="My Company",
...   alt_names=["localhost"],
...   hostname="my-site.com",
... )
&lt;cryptography.hazmat.backends.openssl.x509._CertificateSigningRequest object at 0x7f6ad5372210&gt;</code></pre><p>在控制台中运行这些步骤后，你应该得到两个新文件：</p><ul><li>server-private-key.pem：服务器的私钥</li><li>server-csr.pem：服务器的CSR</li></ul><p>你可以从控制台查看新的CSR和私钥：</p><pre><code>$ ls server*.pem
server-csr.pem  server-private-key.pem</code></pre><p>有了这两个文档，现在可以开始对密钥进行签名。通常，在这一步中会进行大量的验证。在实际项目中，CA会确保你拥有my-site.com，并要求你以各种方式证明它。</p><p>既然你是本例中的CA，就可以避免这些麻烦的证明，创建你自己的已验证的公钥。为此，你将在<code>pki_helpers.py</code>文件中添加另一个函数：</p><pre><code># pki_helpers.py
def sign_csr(csr, ca_public_key, ca_private_key, new_filename):
    valid_from = datetime.utcnow()
    valid_until = valid_from + timedelta(days=30)

    builder = (
        x509.CertificateBuilder()
        .subject_name(csr.subject)
        .issuer_name(ca_public_key.subject)
        .public_key(csr.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(valid_from)
        .not_valid_after(valid_until)
    )

    for extension in csr.extensions:
        builder = builder.add_extension(extension.value, extension.critical)

    public_key = builder.sign(
        private_key=ca_private_key,
        algorithm=hashes.SHA256(),
        backend=default_backend(),
    )

    with open(new_filename, "wb") as keyfile:
        keyfile.write(public_key.public_bytes(serialization.Encoding.PEM))</code></pre><p>这段代码看起来非常类似于<code>generate_ca.py</code>文件中的<code>generate_public_key()</code>。事实上，它们几乎是一样的。主要区别如下：</p><ul><li>第8行到第9行将使用者名称基于CSR，而颁发者基于证书颁发机构（CA）。</li><li>第10行这次从CSR获取公钥。</li><li>第16至17行复制CSR上设置的所有扩展名。</li><li>第20行用CA的私钥签署公钥。</li></ul><p>下一步是启动Python交互模式，并使用<code>sign_csr()</code>，需要加载CSR和CA的私钥和公钥，从加载CSR开始：</p><pre><code>&gt;&gt;&gt; from cryptography import x509
&gt;&gt;&gt; from cryptography.hazmat.backends import default_backend
&gt;&gt;&gt; csr_file = open("server-csr.pem", "rb")
&gt;&gt;&gt; csr = x509.load_pem_x509_csr(csr_file.read(), default_backend())
&gt;&gt;&gt; csr
&lt;cryptography.hazmat.backends.openssl.x509._CertificateSigningRequest object at 0x7f68ae289150&gt;</code></pre><p>在本节代码中，你将打开server-csr.pem文件，并使用<code>x509.load_pem_x509_csr()</code>创建csr对象。接下来，你需要加载CA的公钥：</p><pre><code>&gt;&gt;&gt; ca_public_key_file = open("ca-public-key.pem", "rb")
&gt;&gt;&gt; ca_public_key = x509.load_pem_x509_certificate(
...   ca_public_key_file.read(), default_backend()
... )
&gt;&gt;&gt; ca_public_key
&lt;Certificate(subject=&lt;Name(C=US,ST=Maryland,L=Baltimore,O=My CA Company,CN=logan-ca.com)&gt;, ...)&gt;</code></pre><p>再次，你创建了一个<code>ca_public_key</code>对象，它可以被<code>sign_csr()</code>使用。x509模块有一个便利的<code>load-pem-x509-u certificate()</code>来帮助你。最后一步是加载CA的私钥：</p><pre><code>&gt;&gt;&gt; from getpass import getpass
&gt;&gt;&gt; from cryptography.hazmat.primitives import serialization
&gt;&gt;&gt; ca_private_key_file = open("ca-private-key.pem", "rb")
&gt;&gt;&gt; ca_private_key = serialization.load_pem_private_key(
...   ca_private_key_file.read(),
...   getpass().encode("utf-8"),
...   default_backend(),
... )
Password:
&gt;&gt;&gt; private_key
&lt;cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey object at 0x7f68a85ade50&gt;</code></pre><p>此代码将加载你的私钥。回想一下，你的私钥是使用你指定的密码加密的。使用这三个组件，你现在可以签署CSR并生成已验证的公钥：</p><pre><code>&gt;&gt;&gt; from pki_helpers import sign_csr
&gt;&gt;&gt; sign_csr(csr, ca_public_key, ca_private_key, "server-public-key.pem")</code></pre><p>运行此命令后，目录中应该有三个服务器密钥文件：</p><pre><code>$ ls server*.pem
server-csr.pem  server-private-key.pem  server-public-key.pem</code></pre><p>这里的工作量相当大。好消息是，既然有了私钥和公钥对，你不必更改任何服务器代码就可以开始使用它了。</p><p>使用以前的server.py文件，运行以下命令启动全新的Python HTTPS应用：</p><pre><code>$ uwsgi \
    --master \
    --https localhost:5683,\
            logan-site.com-public-key.pem,\
            logan-site.com-private-key.pem \
    --mount /=server:app</code></pre><p>祝贺！你现在有了一个支持Python HTTPS的服务器，它运行着你自己的私钥-公钥对，私钥-公钥对是由你自己的证书颁发机构签署的!</p><p>现在，剩下要做的就是查询服务器。首先，需要对<code>client.py</code>代码进行一些更改：</p><pre><code># client.py
import os
import requests

def get_secret_message():
    response = requests.get("https://localhost:5683")
    print(f"The secret message is {response.text}")

if __name__ == "__main__":
    get_secret_message()</code></pre><p>与前面的代码相比，惟一的变化是从http改为https。如果尝试运行此代码，则会遇到错误：</p><pre><code>$ python client.py
...
requests.exceptions.SSLError: \
    HTTPSConnectionPool(host='localhost', port=5683): \
    Max retries exceeded with url: / (Caused by \
    SSLError(SSLCertVerificationError(1, \
    '[SSL: CERTIFICATE_VERIFY_FAILED] \
    certificate verify failed: unable to get local issuer \
    certificate (_ssl.c:1076)')))</code></pre><p>这是一个非常糟糕的错误信息！这里的重要部分是信息证书验证失败：无法获取本地颁发者。你现在应该更熟悉这些词了。从本质上讲，它是这样说的：</p><pre><code>`localhost:5683` gave me a certificate. I checked the issuer of the certificate it gave me, and according to all the Certificate Authorities I know about, that issuer is not one of them.</code></pre><p>如果尝试使用浏览器打开你的网站，则会收到类似信息：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/02/image-24.png" class="kg-image" alt="image-24" width="705" height="526" loading="lazy"></figure><p>如果要避免此信息，你必须返回有关你的证书颁发机构！只需将请求指向你先前生成的<code>ca-public-key.pem</code>文件：</p><pre><code># client.py
def get_secret_message():
    response = requests.get("http://localhost:5683", verify="ca-public-key.pem")
    print(f"The secret message is {response.text}")</code></pre><p>完成此操作后，你应该能够成功运行以下代码：</p><pre><code>$ python client.py
The secret message is fluffy tail</code></pre><p>很好！已经创建了一个功能完善的Python HTTPS服务器并成功实现了查询功能。现在，你和秘密松鼠之间可以愉快和安全地交换信息！</p><h2 id="--2"><strong>结论</strong></h2><p>在本问中，你学习了当前Internet上安全通信的一些核心基础，现在已经了解了这些构建模块，你将成为一个更好、更安全的开发人员。</p><p>如果你对这些信息感兴趣，那你就走运了！你仅仅蜻蜓点水式地触及了每一层中所有的细微差别。安全世界不断发展，新的技术和漏洞也不断被发现。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python中的 !=与is not不同 ]]>
                </title>
                <description>
                    <![CDATA[ Python中的is和==是不一样的。使用is可以比较数字，代码也正常运行。也有人说is比== 要更快，或者你可能觉得它看起来更像Python。然而，重要的是要记住这些运算符的行为并不完全相同。 ==用于比较两个对象的值是否相等，而is检查两个变量是否指向内存中的同一个对象。在大多数情况下，这意味着你应该使用==和！=，除非与None进行比较。 在本文中，你将学习：  * 对象相等和同一性的区别是什么  * 何时使用==和is比较对象  * 这些Python运算符的原理是什么  * 为什么使用is和is not比较值会导致意外  * 如何编写自定义的__eq__()类方法来定义相等运算符行为 介绍is 和is not的应用 is 和is not用来比较两个对象。在CPython中，比较的是对象的内存地址。Python中的一切都是对象，每个对象都存储在特定的内存位置， is和is not'检查两个变量是否引用内存中的同一个对象。 注意: 记住，具有相同值的对象可能存储在不同的内存地址中。 你可以使用id() 来检查一个对象的内存地址: >>> help(id) Help o ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/python-is-operator/</link>
                <guid isPermaLink="false">5e4367ffca1efa04e196b51f</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ qiwsir ]]>
                </dc:creator>
                <pubDate>Wed, 12 Feb 2020 03:02:23 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1581294881880-86400a60aa1a.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Python中的<code>is</code>和<code>==</code>是不一样的。使用<code>is</code>可以比较数字，代码也正常运行。也有人说<code>is</code>比<code>==</code>要更快，或者你可能觉得它看起来更像Python。然而，重要的是要记住这些运算符的行为并不完全相同。</p><p><code>==</code>用于比较两个对象的值是否相等，而<code>is</code>检查两个变量是否指向内存中的同一个对象。在大多数情况下，这意味着你应该使用<code>==</code>和<code>！=</code>，除非与<code>None</code>进行比较。</p><p>在本文中，你将学习：</p><ul><li>对象相等和同一性的区别是什么</li><li>何时使用<code>==</code>和<code>is</code>比较对象</li><li>这些Python运算符的原理是什么</li><li>为什么使用<code>is</code>和<code>is not</code>比较值会导致意外</li><li>如何编写自定义的<code>__eq__()</code>类方法来定义相等运算符行为</li></ul><h2 id="-is-is-not-"><strong>介绍<code>is</code> 和<code>is not</code>的应用</strong></h2><p><code>is</code> 和<code>is not</code>用来比较两个对象。在CPython中，比较的是对象的内存地址。Python中的一切都是对象，每个对象都存储在特定的内存位置， <code>is</code>和<code>is not</code>'检查两个变量是否引用内存中的同一个对象。</p><p>注意: 记住，具有相同值的对象可能存储在不同的内存地址中。</p><p>你可以使用<code>id()</code> 来检查一个对象的内存地址:</p><pre><code>&gt;&gt;&gt; help(id)
Help on built-in function id in module builtins:

id(obj, /)
    Return the identity of an object.

    This is guaranteed to be unique among simultaneously existing objects.
    (CPython uses the object's memory address.)

&gt;&gt;&gt; id(id)
2570892442576</code></pre><p>最后一行显示存储内置函数<code>id</code>本身的内存地址。</p><p>通常，具有相同值的对象在默认情况下具有相同的id。例如，数字-5到256在CPython中被保存，每个数字都存储在内存中单一且固定的位置，这为常用整数节省了内存。</p><p>你可以使用<code>sys.intern()</code>来保存字符串以提高性能，此函数允许你比较它们的内存地址，而不是对字符串里的字符进行逐个比较：</p><pre><code>&gt;&gt;&gt; from sys import intern
&gt;&gt;&gt; a = 'hello world'
&gt;&gt;&gt; b = 'hello world'
&gt;&gt;&gt; a is b
False
&gt;&gt;&gt; id(a)
1603648396784
&gt;&gt;&gt; id(b)
1603648426160

&gt;&gt;&gt; a = intern(a)
&gt;&gt;&gt; b = intern(b)
&gt;&gt;&gt; a is b
True
&gt;&gt;&gt; id(a)
1603648396784
&gt;&gt;&gt; id(b)
1603648396784</code></pre><p>变量<code>a</code>和<code>b</code>最初指向内存中的两个不同对象，如它们的不同id所示。使用<code>intern</code>后，<code>a</code>和<code>b</code>则指向内存中的同一对象。在原来的操作中，两个<code>'hello world'</code>分别在新的内存位置创建对象，但是，对同样的字符串执行<code>intern</code>后，后面所创建的字符串所指向的内存地址与第一个<code>'hello world'</code>的内存地址相同。</p><p>注意：即使对象的内存地址在任何给定的时间都是唯一的，但这个内存地址在同一代码的不同运行过程中是不同的，并且取决于CPython的版本和运行代码的计算机。</p><p>默认情况下，具有<code>intern</code>效果的对象是<code>None</code>、<code>True</code>、<code>False</code>和简单字符串。请记住，大多数情况下，具有相同值的不同对象将存储在不同的内存地址中，这意味着你不应该使用<code>is</code>来比较值。</p><h3 id="-"><strong>存储整数</strong></h3><p>Python将常用的值（例如，整数-5到256）默认保存在内存中，从而节省内存开支。下面的代码向你展示了为什么只有一些整数具有固定的内存地址：</p><pre><code>&gt;&gt;&gt; a = 256
&gt;&gt;&gt; b = 256
&gt;&gt;&gt; a is b
True
&gt;&gt;&gt; id(a)
1638894624
&gt;&gt;&gt; id(b)
1638894624

&gt;&gt;&gt; a = 257
&gt;&gt;&gt; b = 257
&gt;&gt;&gt; a is b
False

&gt;&gt;&gt; id(a)
2570926051952
&gt;&gt;&gt; id(b)
2570926051984</code></pre><p>最初，<code>a</code>和<code>b</code>引用内存中的同一个存储对象，但当它们的值超出常用整数的范围（从-5到256）时，它们就存储在不同的内存地址中。</p><h3 id="--1"><strong>当多个变量引用同一对象时</strong></h3><p>用赋值运算符（<code>=</code>）使一个变量等于另一个变量时，可以使这些变量指向内存中的同一对象。这可能会导致可变对象出现意外行为：</p><pre><code>&gt;&gt;&gt; a = [1, 2, 3]
&gt;&gt;&gt; b = a
&gt;&gt;&gt; a
[1, 2, 3]
&gt;&gt;&gt; b
[1, 2, 3]

&gt;&gt;&gt; a.append(4)
&gt;&gt;&gt; a
[1, 2, 3, 4]
&gt;&gt;&gt; b
[1, 2, 3, 4]

&gt;&gt;&gt; id(a)
2570926056520
&gt;&gt;&gt; id(b)
2570926056520</code></pre><p>刚才发生了什么? 你向<code>a</code> 添加了一个新元素，但是现在<code>b</code>也包含了这个元素! 在<code>b = a</code>这一行，设置b指向与a相同的内存地址，这样两个变量就都引用相同的对象。</p><p>如果你独立地定义这些列表，那么它们就被存储在不同的内存地址中，并独立地运行:</p><pre><code>&gt;&gt;&gt; a = [1, 2, 3]
&gt;&gt;&gt; b = [1, 2, 3]
&gt;&gt;&gt; a is b
False
&gt;&gt;&gt; id(a)
2356388925576
&gt;&gt;&gt; id(b)
2356388952648</code></pre><p>因为<code>a</code>和<code>b</code>现在引用内存中的不同对象，所以更改一个对象不会影响另一个对象。</p><h2 id="--2"><strong>用<code>==</code>和<code>!=</code>比较对象</strong></h2><p>回想一下，具有相同值的对象通常存储在不同的内存地址中。如果要检查两个对象是否具有相同的值，而不管它们存储在内存中的位置，使用运算符<code>=</code>和<code>！=</code>。在绝大多数情况下，这就是你想做的。</p><h3 id="--3"><strong>当对象副本相等但不相同时</strong></h3><p>在下面的示例中，<code>b</code>是<code>a</code>的副本（<code>a</code>是可变对象，如列表或字典）。两个变量都有相同的值，但它们将各自存储在不同的内存地址：</p><pre><code>&gt;&gt;&gt; a = [1, 2, 3]
&gt;&gt;&gt; b = a.copy()
&gt;&gt;&gt; a
[1, 2, 3]
&gt;&gt;&gt; b
[1, 2, 3]

&gt;&gt;&gt; a == b
True
&gt;&gt;&gt; a is b
False

&gt;&gt;&gt; id(a)
2570926058312
&gt;&gt;&gt; id(b)
2570926057736</code></pre><p><code>a</code> 和 <code>b</code> 现在存储在不同的内存地址，因此<code>a is b</code>不再返回True。但是，<code>a==b</code>返回True，因为两个对象具有相同的值。</p><h3 id="--4"><strong>相等比较如何起作用</strong></h3><p><code>==</code>的魔力体现在该符号左边对象所具有的<code>__eq__()</code>方法中。</p><p>这是一个神奇的类方法，每当这个类的一个实例与另一个对象进行比较时都会调用它。如果未实现此方法，则默认情况下<code>==</code>比较两个对象的内存地址。</p><p>作为练习，创建一个继承<code>str</code>的<code>SillyString</code>类并实现<code>__eq__()</code> ，以比较此字符串的长度是否与另一个对象的长度相同：</p><pre><code>class SillyString(str):
    # This method gets called when using == on the object
    def __eq__(self, other):
        print(f'comparing {self} to {other}')
        # Return True if self and other have the same length
        return len(self) == len(other)</code></pre><p>现在，用<code>'hello world'</code>创建的SillyString实例应该等于用<code>'world hello'</code>创建的实例，甚至等于长度相同的任何其他对象：</p><pre><code>&gt;&gt;&gt; # Compare two strings
&gt;&gt;&gt; 'hello world' == 'world hello'
False

&gt;&gt;&gt; # Compare a string with a SillyString
&gt;&gt;&gt; 'hello world' == SillyString('world hello')
comparing world hello to hello world
True

&gt;&gt;&gt; # Compare a SillyString with a list
&gt;&gt;&gt; SillyString('hello world') == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
comparing hello world to [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
True</code></pre><p>当然，对于一个字符串形式的对象来说，这是愚蠢的行为，但它确实说明了当你使用<code>==</code>比较两个对象时会发生什么。对于<code>!=</code>，则是通过实现特定的<code>__ne__()</code>方法，给出逆响应。</p><p>上面的示例还清楚地向你展示了为什么更好的做法是用 <code>is</code>来比较<code>None</code>，而不是使用<code>==</code>运算符。<code>is</code>比较的是内存地址，所以它不仅更快，而且更安全，因为它不依赖于任何<code>__eq__()</code>方法的逻辑。</p><h2 id="--5"><strong>比较的比较</strong></h2><p>根据经验，你应该常用<code>==</code> 和<code>！=</code>，除非与<code>None</code>进行比较：</p><ul><li>使用<code>==</code>和<code>！=</code>比较对象的相等性。这里，你通常比较两个对象的值。如果要比较两个对象是否具有相同的内容，而不关心它们存储在内存中的位置，则需要下面的做法。</li><li>如果要比较对象的唯一标识，请使用<code>is</code>和<code>is not</code>。这里，你要比较两个变量是否指向内存中的同一个对象。这些运算符的主要用例是与None进行比较。与使用类方法相比，按内存地址与None进行比较更快、更安全。</li></ul><p>具有相同值的变量通常存储在不同的内存地址中，这意味着你应该使用<code>==</code> 和<code>！=</code>来比较他们的值。只有当你想检查两个变量是否指向同一个内存地址时，才使用<code>is</code>和<code>is not</code>。</p><h2 id="--6"><strong>结论</strong></h2><p>在本文中，你了解了<code>==</code> 和<code>！=</code>比较两个对象的值，而<code>is</code>和<code>is not</code>比较两个变量是否引用内存中的同一个对象。如果你牢记这一区别，那么应该能够防止代码中出现意外行为。</p><p>你还可以看看如何使用<code>sys.intern()</code>来优化字符串的内存使用和比较，尽管Python可能已经在幕后自动为你处理了这一问题。</p><p>现在你已经了解了两种比较的幕后操作，可以尝试编写自己的<code>__eq__()</code>方法，这些方法定义了在使用<code>==</code>运算符时如何比较该类的实例。去应用关于Python比较运算符的这些新知识吧！</p><p>原文链接：https://realpython.com/python-is-identity-vs-equality/</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
