Skip to content

Latest commit

 

History

History
1358 lines (945 loc) · 50.6 KB

File metadata and controls

1358 lines (945 loc) · 50.6 KB

二、Python 语法、常见陷阱和风格指南

Python 编程语言的设计和开发一直掌握在其原始作者 Guido van Rossum 手中,在许多情况下,他被亲切地称为仁慈的终身独裁者BDFL。尽管 van Rossum 被认为拥有一台时间机器(他反复以“我昨晚刚刚实现了它”来回答功能请求):http://www.catb.org/jargon/html/G/Guido.html ),他仍然只是一个人,需要帮助维护和开发 Python。为此,开发了Python 增强方案PEP流程)。该流程允许任何人提交一份 PEP,其中包含该功能的技术规范和捍卫其有用性的理由。在讨论 Python 邮件列表和可能的一些改进之后,BDFL 将决定接受或拒绝该提案。

Python 风格指南(PEP 8https://www.python.org/dev/peps/pep-0008/ 曾作为其中一个政治公众人物提交,并自那时起定期接受和改进。它有许多伟大的、被广泛接受的公约以及一些有争议的公约。特别是,79 个字符的最大行长是许多讨论的主题。但是,将一行限制为 79 个字符确实有一些优点。除此之外,虽然只是样式指南本身并没有将代码变成 Pythonic,例如“Python 的禅”(PEP 20)https://www.python.org/dev/peps/pep-0020/ 优雅地说:“美胜于丑。”PEP 8定义了代码应该如何以精确的方式格式化,PEP 20更多的是一种哲学和心态。

常见的陷阱是一系列常见的错误,从初级错误到高级错误。它们的范围从作为参数传递列表或字典(它们是可变的)到闭包中的后期绑定问题。一个更重要的问题是如何以干净的方式解决循环进口问题。

本章示例中使用的一些技术对于如此早期的一章来说可能有点过于先进,但请不要担心。本章是关于风格和常见陷阱的。所用技术的内部工作原理将在后面的章节中介绍。

本章将介绍以下主题:

  • 代码样式(PEP 8pyflakesflake8等)
  • 常见缺陷(列为函数参数、按值传递与按引用传递以及继承行为)

python 密码的定义是非常主观的,主要反映了作者的观点。在处理项目时,与 Python 或本书给出的编码准则相比,保持与该项目的编码风格一致更为重要。

代码风格——或者什么是 Python 代码?

Pythonic code-当您第一次听说它时,您可能会认为它是一种编程范式,类似于面向对象或函数式编程。虽然其中一些可以被认为是这样的,但它实际上更多的是一种设计理念。Python 让您可以自由选择以面向对象、过程、函数、面向方面甚至面向逻辑的方式进行编程。这些自由使 Python 成为一种很好的编写语言,但与往常一样,自由的缺点是需要大量的规则来保持代码的干净性和可读性。PEP8标准告诉我们如何格式化代码,但 Pythonic 代码不仅仅是语法。这就是蟒蛇哲学(PEP20的全部内容,代码是:

  • 清洁的
  • 易于理解的
  • 美丽的
  • 明确的
  • 可读的

其中大多数听起来像是常识,我认为它们应该是常识。然而,在有些情况下,没有一种明显的方法可以做到这一点(当然,除非你是荷兰人,正如你将在本章后面阅读的那样)。这是本章的目标,目的是了解什么是美丽的代码,以及为什么在 Python 风格指南中做出了某些决定。

一些程序员曾经问 GuidovanRossum Python 是否会支持大括号。从那天起,牙套就可以通过__future__进口获得:

>>> from __future__ import braces
 File "<stdin>", line 1
SyntaxError: not a chance

格式化字符串–printf 样式还是 str.format?

Python 已经支持printf-style%str.format很长一段时间了,所以您很可能已经熟悉了这两种语言。

在本书中,将使用printf-style格式,原因如下:

  • 最重要的原因是,它对我来说是自然而然的。我已经在许多不同的编程语言中使用printf大约 20 年了。
  • 大多数编程语言都支持printf语法,这让很多人都很熟悉。
  • 虽然只与本书中的示例相关,但它占用的空间稍小,所需的格式更改也较少。与显示器不同的是,这些年来,书籍并没有变得更广泛。

一般来说,现在大多数人都推荐str.format,但这主要取决于偏好。printf-style更简单,而str.format方法更强大。

如果您想了解更多关于如何将printf-style格式替换为str.format(当然也可以是格式),那么我建议您访问上的 PyFormat 站点 https://pyformat.info/

PEP20,巨蟒之禅

大部分的蟒蛇哲学可以通过 PEP20 来解释。巨蟒有一个漂亮的小复活节彩蛋,总是让你想起PEP20。只需在 Python 控制台中键入import this,您将得到PEP20行。引用PEP20

“长期的 Pythoneer 蒂姆·彼得斯(Tim Peters)将 BDFL 关于 Python 设计的指导原则简洁地归纳为 20 条格言,其中只有 19 条被写下来。”

接下来的几段将解释这 19 行的意图。

PEP20 部分中的示例在工作中不一定完全相同,但它们的用途相同。这里的许多例子都是虚构的,除了解释这一段的基本原理之外,没有其他用途。

为了让更清晰,在开始之前,让我们看看import this的输出:

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

美胜于丑

虽然美观是相当主观的,但有一些 Python 风格的规则需要遵守:限制行长度、将语句保留在单独的行上、在单独的行上拆分导入,等等。

简言之,不是像这样有点复杂的函数:

 def filter_modulo(items, modulo):
    output_items = []
    for i in range(len(items)):
        if items[i] % modulo:
            output_items.append(items[i])
    return output_items

或者这个:

filter_modulo = lambda i, m: [i[j] for i in range(len(i))
                              if i[j] % m]

只需执行以下操作:

def filter_modulo(items, modulo):
    for item in items:
        if item % modulo:
            yield item

更简单,更容易阅读,更漂亮一点!

这些例子的结果并不相同。前两个返回列表,而最后一个返回生成器。生成器将在第 6 章生成器和协程——无限,一次一步中进行更深入的讨论。

显性优于隐性

导入、参数和变量名只是许多情况中的一部分,在这些情况下,显式代码更容易读取,但编写代码时需要付出更多的努力和/或冗长。

以下是一个例子:

from spam import *
from eggs import *

some_function()

虽然这样可以节省一些输入,但是很难看到某个函数是在哪里定义的。它是在鸡蛋里定义的吗?垃圾邮件?也许在这两个模块中?这里有一些具有高级内省功能的编辑器可以帮助您,但是为什么不保持它的明确性,以便每个人(即使只是在线查看代码)都可以看到它在做什么呢?

import spam
import eggs

spam.some_function()
eggs.some_function()

额外的好处是,我们可以在这里从spameggs显式调用函数,每个人都会对代码的功能有更好的了解。

使用*args**kwargs的函数也是如此。它们有时非常有用,但它们的缺点是不太清楚哪些参数对函数有效:

def spam(egg, *args, **kwargs):
    processed_egg = process_egg(egg, *args, **kwargs)
    return Spam(processed_egg)

文档显然对此类情况有帮助,我并不反对通常使用*args**kwargs,但至少保持最常见的参数明确无疑是一个好主意。即使当它要求您重复父类的参数时,它也会使代码更加清晰。将来重构父类时,您将知道是否有子类仍然使用某些参数。

简单胜于复杂

“简单胜于复杂。复杂胜于复杂。”

开始一个新项目时,最重要的问题是:它需要有多复杂?

例如,假设我们已经编写了一个小程序,现在我们需要存储一些数据。我们有什么选择?

  • 完整的数据库服务器,如 PostgreSQL 或 MySQL
  • 一个简单的文件系统数据库,如 SQLite 或 AnyDBM
  • 平面文件存储,如 CSV 和 TSV
  • 结构化存储,如 JSON、YAML 或 XML
  • 序列化 Python,如 Pickle 或 Marshal

所有这些选项都有自己的用例以及优缺点,具体取决于用例:

  • 您是否存储了大量数据?那么,完整的数据库服务器和平面文件存储通常是最方便的选择。
  • 它是否可以在不安装任何软件包的情况下轻松移植到不同的系统?这使得除了完整的数据库服务器之外的任何东西都成为方便的选项。
  • 我们需要搜索数据吗?使用其中一个数据库系统,包括文件系统和完整服务器,这会容易得多。
  • 是否有其他应用需要能够编辑数据?这使得平面文件存储和结构化存储等通用格式成为方便的选项,但不包括序列化 Python。

很多问题!但最重要的一点是:它需要有多复杂?将数据存储在pickle文件中可以分三行完成,而连接到数据库(即使使用 SQLite)将更加复杂,并且在许多情况下不需要:

import pickle  # Or json/yaml
With open('data.pickle', 'wb') as fh:
    pickle.dump(data, fh, pickle.HIGHEST_PROTOCOL)

与:

import sqlite3
connection = sqlite3.connect('database.sqlite')
cursor = connection.cursor()
cursor.execute('CREATE TABLE data (key text, value text)')
cursor.execute('''INSERT INTO data VALUES ('key', 'value')''')
connection.commit()
connection.close()

当然,这些示例远非完全相同,因为一个存储完整的数据对象,而另一个仅在 SQLite 数据库中存储一些键/值对。然而,这不是重点。关键是,代码要复杂得多,但在许多情况下,它实际上没有那么通用。使用适当的库,这可以简化,但基本前提保持不变。简单比复杂好,如果不需要复杂,最好避免复杂。

平的比嵌套的好

嵌套代码很快变得不可读且难以理解。这里没有严格的规则,但通常当您有三个级别的嵌套循环时,是时候重构了。

看看下面的例子,它打印了一个二维矩阵列表。虽然这里没有什么特别的错误,但将其拆分为几个函数可能会更容易理解其用途,也更容易测试:

def print_matrices():
    for matrix in matrices:
        print('Matrix:')
        for row in matrix:
            for col in row:
                print(col, end='')
            print()
        print()

略为平淡的版本如下:

def print_row(row):
    for col in row:
        print(col, end='')

def print_matrix(matrix):
    for row in matrix:
        print_row(row)
        print()

def print_matrices(matrices):
    for matrix in matrices:
        print('Matrix:')
        print_matrix(matrix)
        print()

这个例子可能有点复杂,但想法是正确的。具有深度嵌套的代码很容易变得非常不可读。

稀疏比密集好

空白通常是一件好事。是的,它会使你的文件更长,你的代码会占用更多的空间,但如果你按逻辑拆分代码,它会对可读性有很大帮助:

>>> def make_eggs(a,b):'while',['technically'];print('correct');\
...     {'this':'is','highly':'unreadable'};print(1-a+b**4/2**2)
...
>>> make_eggs(1,2)
correct
4.0

虽然在技术上是正确的,但这并不是所有的可读性。我确信,我需要付出一些努力来找出代码的实际用途,以及在不尝试的情况下打印的数字:

>>> def make_eggs(a, b):
...     'while', ['technically']
...     print('correct')
...     {'this': 'is', 'highly': 'unreadable'}
...     print(1 - a + ((b ** 4) / (2 ** 2)))
...
>>> make_eggs(1, 2)
correct
4.0

尽管如此,这并不是最好的代码,但至少代码中发生的事情更为明显。

可读性计数

更短并不总是意味着更容易阅读:

fib=lambda n:reduce(lambda x,y:(x[0]+x[1],x[0]),[(1,1)]*(n-2))[0]

虽然简短的版本在简洁性上有一定的美感,但我个人认为以下内容更美:

def fib(n):
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

实用战胜纯洁

“特殊情况不足以打破规则。尽管实用性胜过纯洁性。”

违反规则有时是很诱人的,但这往往是一个滑坡。当然,这适用于所有规则。如果您的快速修复将打破规则,您应该立即尝试重构它。很可能你以后没有时间修复它,并且会后悔。

不过没必要过火。如果解决方案足够好,并且重构将需要更多的工作,那么选择工作方法可能会更好。尽管所有这些例子都与进口有关,但本指南几乎适用于所有情况。

为了防止出现长行,可以使用几种方法缩短导入,如添加反斜杠、添加括号或缩短导入:

from spam.eggs.foo.bar import spam, eggs, extra_spam, extra_eggs, extra_stuff  from spam.eggs.foo.bar import spam, eggs, extra_spam, extra_eggs

只需遵循PEP8(每行一次导入)即可轻松避免这种情况:

from spam.eggs.foo.bar import spam from spam.eggs.foo.bar import eggs from spam.eggs.foo.bar import extra_spam from spam.eggs.foo.bar import extra_eggs from spam.eggs.foo.bar import extra_stuff  from spam.eggs.foo.bar import spam
from spam.eggs.foo.bar import eggs
from spam.eggs.foo.bar import extra_spam
from spam.eggs.foo.bar import extra_eggs

但真正的长期进口呢?

from spam_eggs_and_some_extra_spam_stuff import my_spam_and_eggs_stuff_which_is_too_long_for_a_line

是的……尽管通常不建议为导入添加反斜杠,但在某些情况下,它仍然是最佳选项:

from spam_eggs_and_some_extra_spam_stuff \
    import my_spam_and_eggs_stuff_which_is_too_long_for_a_line

错误永远不应该悄无声息地过去

“错误不应悄无声息地通过。除非明确地沉默。”

套用 Jamie Zawinsky 的话:有些人在遇到错误时会认为“我知道,我会使用try/except/pass块。”现在他们有两个问题。

简单或过于广泛的异常捕获已经不是一个好主意。不把它们传下去会让你(或其他从事代码工作的人)想知道发生了什么:

try:
    value = int(user_input)
except:
    pass

如果您真的需要捕获所有错误,请非常明确地说明:

try:
    value = int(user_input)
except Exception as e:
    logging.warn('Uncaught exception %r', e)

或者更好的是,专门捕捉它并添加一个正常的默认值:

try:
    value = int(user_input)
except ValueError:
    value = 0

这个问题实际上更加复杂。那么依赖于异常中发生的任何事情的代码块呢?例如,考虑下面的代码块:

try:
    value = int(user_input)
    value = do_some_processing(value)
    value = do_some_other_processing(value)
except ValueError:
    value = 0

如果ValueError升高,是哪一行引起的?是int(user_input)do_some_processing(value)还是do_some_other_processing(value)?由于无提示地捕获错误,无法知道何时定期执行代码,这可能非常危险。如果由于某种原因,其他函数的处理发生了变化,那么以这种方式处理异常就成了一个问题。因此,除非它真的打算这样做,否则请改用这个:

try:
    value = int(user_input)
except ValueError:
    value = 0
else:
    value = do_some_processing(value)
    value = do_some_other_processing(value)

面对歧义,拒绝猜测的诱惑

虽然猜测在很多情况下是有效的,但如果你不小心,它们会咬你。正如“显式优于隐式”一段中所述,当有几个from ... import *时,您无法始终确定哪个模块为您提供了预期的变量。

歧义通常应该避免,因此可以避免猜测。清晰明确的代码生成的 bug 更少。函数调用是可能出现歧义的一种有用的情况。以以下两个函数调用为例:

spam(1, 2, 3, 4, 5)
spam(spam=1, eggs=2, a=3, b=4, c=5)

它们可能是相同的,但也可能不是。如果没有看到函数,就不可能说。如果按以下方式实现该功能,则两者的结果将大不相同:

def spam(a=0, b=0, c=0, d=0, e=0, spam=1, eggs=2):
    pass

我并不是说在所有情况下都应该使用关键字参数,但是如果涉及到许多参数和/或难以识别参数(如数字),这将是一个好主意。您也可以选择逻辑变量名来传递参数,而不是使用关键字参数,只要代码清楚地传达了含义。

例如,下面是一个类似的调用,它使用自定义变量名来传达意图:

a = 3
b = 4
c = 5
spam(a, b, c)

一个显而易见的方法

“应该有一种,最好只有一种明显的方法。尽管这种方法一开始可能并不明显,除非你是荷兰人。”

一般来说,在思考一个难题一段时间后,你会发现有一种解决方案显然比其他方案更可取。然而,在有些情况下,情况并非如此,在这种情况下,如果你是荷兰人,它可能会很有用。这里的笑话是,BDFL 和 Python 的原始作者 Guido van Rossum 是荷兰人(就像你真正的荷兰人一样)。

现在总比没有好

“现在总比没有好。尽管从来都比现在好。”

与其把问题推到未来,不如现在解决问题。然而,在某些情况下,立即修复它不是一种选择。在这些情况下,一个好的替代方法是将函数标记为已弃用,这样就不会意外忘记问题:

import warnings
warnings.warn('Something deprecated', DeprecationWarning)

难解释,容易解释

“如果实现很难解释,那是个坏主意。如果实现很容易解释,那可能是个好主意。”

和往常一样,尽量让事情简单。虽然复杂的代码可以很好地测试,但它更容易出现 bug。保存东西越简单越好。

名称空间是一个非常好的想法

“名称空间是一个非常好的主意,让我们做更多的工作吧!”

名称空间可以使代码使用起来更加清晰。正确地命名它们会让它变得更好。例如,下面这行代码的作用是什么?

load(fh)

不太清楚,对吧?

有名称空间的版本如何?

pickle.load(fh)

现在我们明白了。

为了给出一个名称空间的示例,它的完整长度使其无法使用,我们将看看 Django 中的User类。在 Django 框架中,User类存储在django.contrib.auth.models.User中。许多项目以以下方式使用对象:

from django.contrib.auth.models import User
# Use it as: User

虽然这相当清楚,但可能会让人认为User类是当前类的本地类。相反,执行会让人们知道它位于不同的模块中:

from django.contrib.auth import models
# Use it as: models.User

不过,这很快就会与其他车型的进口产品发生冲突,因此我个人推荐以下产品:

from django.contrib.auth import models as auth_models
# Use it as auth_models.User

以下是另一种选择:

import django.contrib.auth.models as auth_models
# Use it as auth_models.User

结论

现在我们应该对 python 思想有一些概念。创建以下代码:

  • 美丽的
  • 可读的
  • 毫不含糊的
  • 足够明确
  • 并非完全没有空格

因此,让我们继续看一些关于如何使用 Python 风格指南创建漂亮、可读且简单的代码的示例。

解释 PEP8

前面的段落已经展示了许多使用PEP20作为参考的示例,但是还有一些其他重要的指南需要注意。PEP8 风格指南规定了标准 Python 编码约定。不过,简单地遵循 PEP8 标准并不能使您的代码成为 Pythonic,但这无疑是一个良好的开端。你使用哪种风格其实并不重要,只要你是始终如一的。唯一比不使用适当的样式指南更糟糕的是与它不一致。

鸭型

Duck 类型是一种通过行为处理变量的方法。引用 Alex Martelli(我的 Python 英雄之一,也被许多人戏称为 MartelliBot):

“不要检查它是否是鸭子:检查它是否像鸭子一样呱呱叫,走路像鸭子一样,等等,这取决于你需要玩语言游戏的鸭子行为的子集。如果论点没有通过这个特定的鸭子子集测试,那么你可以耸耸肩,问“为什么是鸭子?”

在许多情况下,当人们进行if spam != '':之类的比较时,他们实际上只是在寻找任何被认为是真实价值的东西。虽然您可以将该值与字符串值''进行比较,但通常不必使其如此具体。在许多情况下,简单地做if spam:就足够了,实际上功能更好。

例如,以下代码行使用timestamp的值生成文件名:

filename = '%s.csv' % timestamp

因为它被命名为timestamp,人们可能会试图检查它实际上是date还是datetime对象,如下所示:

import datetime
if isinstance(timestamp, (datetime.date, datetime.datetime)):
    filename = '%s.csv' % timestamp
else:
    raise TypeError(
        'Timestamp %r should be date(time) object, got %s'
        % (timestamp, type(timestamp))) 

虽然这并不是天生的错误,但在 Python 中比较类型被认为是一种不好的做法,因为通常不需要这样做。在 Python 中,首选 duck 类型。试着把它转换成一个字符串,而不管它实际上是什么。要说明这对最终结果的影响有多小,请参见以下代码:

import datetime
timestamp = datetime.date(2000, 10, 5)
filename = '%s.csv' % timestamp
print('Filename from date: %s' % filename)

timestamp = '2000-10-05'
filename = '%s.csv' % timestamp
print('Filename from str: %s' % filename)

正如您所料,结果是相同的:

Filename from date: 2000-10-05.csv
Filename from str: 2000-10-05.csv

将数字转换为浮点数或整数也是如此;只需要特定的特性,而不是强制执行特定的类型。需要可以作为数字传递的东西吗?试着转换成intfloat。需要一个file对象吗?为什么不直接检查hasattr是否有read方法?

所以,不要这样做:

if isinstance(value, int):

相反,只需使用以下内容:

value = int(value)

而不是这个:

import io

if isinstance(fh, io.IOBase):

只需使用以下行:

if hasattr(fh, 'read'):

价值观与身份比较的差异

Python 中有几种比较对象的方法,标准大于和小于、等于和不等于。但实际上还有一些,其中一个有点特别。这是身份比较运算符:您使用的不是if spam == eggs,而是if spam is eggs。最大的区别在于一个比较价值,另一个比较身份。这听起来有点模糊,但实际上相当简单。至少在 CPython 实现中,内存地址正在被比较,这意味着它是可以得到的最轻松的查找之一。值需要确保类型具有可比性,并且可能需要检查子值,而标识检查只检查唯一标识符是否相同。

如果您曾经编写过 Java,那么您应该熟悉这个原则。在 Java 中,常规字符串比较(spam == eggs)将使用标识而不是值。要比较该值,您需要使用spam.equals(eggs)来获得正确的结果。

看看这个例子:

a = 200 + 56
b = 256
c = 200 + 57
d = 257

print('%r == %r: %r' % (a, b, a == b))
print('%r is %r: %r' % (a, b, a is b))
print('%r == %r: %r' % (c, d, c == d))
print('%r is %r: %r' % (c, d, c is d))

虽然值相同,但标识不同。该代码的实际结果如下所示:

256 == 256: True
256 is 256: True
257 == 257: True
257 is 257: False

问题在于 Python 为所有介于-5256之间的整数保留了一个内部整数对象数组;这就是为什么它适用于256,但不适用于257

你可能想知道为什么有人会想用is而不是==。有多个有效答案;根据具体情况,一个是正确的,另一个不是。但性能也是一个非常重要的考虑因素。基本指导原则是,在比较 Python 单例(如TrueFalseNone)时,始终使用is进行比较。

关于性能考虑,请考虑以下示例:

spam = range(1000000)
eggs = range(1000000)

当执行spam == eggs时,这将对两个列表中的每个项目进行相互比较,因此有效地在内部执行 1000000 次比较。与使用spam is eggs时仅进行一次简单的身份检查相比。

要了解 Python 在使用is操作符时实际上在内部做什么,可以使用id函数。当执行if spam is eggs时,Python 实际上会在内部执行if id(spam) == id(eggs)

回路

来自其他语言的人可能会尝试使用for循环,甚至while循环来处理listtuplestr等项目。虽然有效,但它比需要的复杂得多。例如,考虑这个代码:

i = 0
while i < len(my_list):
    item = my_list[i]
    i += 1
    do_something(i, item)

相反,您可以执行以下操作:

for i, item in enumerate(my_list):
    do_something(i, item)

虽然可以写得更短,但通常不建议这样做,因为这样做不会提高可读性:

[do_something(i, item) for i, item in enumerate(my_list)]

最后一个选项可能对某些人来说很清楚,但并非所有人都清楚。就我个人而言,我更喜欢在实际存储结果时限制列表理解、dict 理解以及 map 和 filter 语句的使用。

例如:

spam_items = [x for x in items if x.startswith('spam_')]

但前提是它不会影响代码的可读性。

考虑一下这段代码:

eggs = [is_egg(item) or create_egg(item) for item in list_of_items if egg and hasattr(egg, 'egg_property') and isinstance(egg, Egg)]  eggs = [is_egg(item) or create_egg(item) for item in list_of_items
        if egg and hasattr(egg, 'egg_property')
        and isinstance(egg, Egg)]

与其把所有的东西都放在列表中,为什么不把它分成几个函数呢?

def to_egg(item):
    return is_egg(item) or create_egg(item)

def can_be_egg(item):
    has_egg_property = hasattr(egg, 'egg_property')
    is_egg_instance = isinstance(egg, Egg)
    return egg and has_egg_property and is_egg_instance

eggs = [to_egg(item) for item in list_of_items if can_be_egg(item)]  eggs = [to_egg(item) for item in list_of_items if
        can_be_egg(item)]

虽然这段代码有点长,但我个人认为这样更容易阅读。

最大线路长度

许多 Python 程序员认为 79 个字符太过紧凑,只会使行变长。虽然我不打算具体讨论 79 个字符,但设置一个较低且固定的限制,如 79 或 99 是一个好主意。虽然显示器越来越宽,但限制行数仍有助于提高可读性,并允许将多个文件放在一起。我经常看到四个 Python 文件挨着打开。如果行宽超过 79 个字符,那就不合适了。

PEP8 指南告诉我们在线条过长的情况下使用反斜杠。虽然我同意反斜杠比长线更可取,但我仍然认为如果可能的话应该避免反斜杠。以下是 PEP8 中的一个示例:

with open('/path/to/some/file/you/want/to/read') as file_1, \
        open('/path/to/some/file/being/written', 'w') as file_2:
    file_2.write(file_1.read())

我不使用反斜杠,而是将其重新格式化如下:

filename_1 = '/path/to/some/file/you/want/to/read'
filename_2 = '/path/to/some/file/being/written'
with open(filename_1) as file_1, open(filename_2, 'w') as file_2:
    file_2.write(file_1.read())

或者可能是以下内容:

filename_1 = '/path/to/some/file/you/want/to/read'
filename_2 = '/path/to/some/file/being/written'
with open(filename_1) as file_1:
    with open(filename_2, 'w') as file_2:
        file_2.write(file_1.read())

当然,并不总是一个选项,但保持代码简短易读是一个很好的考虑。它实际上为代码添加了更多信息。如果您使用的名称不是filename_1,而是传达文件名目标的名称,那么您要做的事情会立即变得更清楚。

验证代码质量、pep8、pyflakes 等

Python 中有许多用于检查代码质量的工具。最简单的错误,例如pep8,只验证几个简单的PEP8错误。更高级的,如pylint,会进行高级内省,以检测其他工作代码中的潜在错误。对于许多项目来说,pylint提供的大部分内容都有点过头了,但仍然很有趣。

第 8 页

flake8工具结合了 pep8、pyflakes 和 McCabe,为代码建立了质量标准。flake8工具是在我的包中维护代码质量的最重要的包之一。我维护的所有软件包都有 100%flake8合规性要求。它不保证代码可读,但至少需要一定程度的一致性,这在与多个程序员一起编写项目时非常重要。

Pep8

用来检查 Python 代码质量的最简单工具之一是pep8包。它并没有检查 PEP8 标准中的所有内容,但它还有很长的路要走,并且仍然定期更新以添加新的检查。pep8检查的一些最重要的事情是如下:

  • 缩进,虽然 Python 不会检查缩进所使用的空格数,但它对代码的可读性没有帮助
  • 缺少空格,如spam=123
  • 空白太多,如def eggs(spam = 123):
  • 空行太多或太少
  • 队伍太长了
  • 语法和缩进错误
  • 不正确和/或多余的比较(not inis notif spam is True以及没有isinstance的类型比较)

结论是,pep8工具在测试空白和一些更常见的样式问题方面帮助很大,但它仍然相当有限。

鱼鳞

这就是 pyflakes 的用武之地。pyflakes 比pep8聪明一点,会提醒你一些风格问题,例如:

  • 未使用的进口
  • 通配符导入(from module import *
  • 不正确的__future__导入(在其他导入之后)

但更重要的是,它警告潜在的错误,例如:

  • 已导入名称的重新定义
  • 未定义变量的使用
  • 赋值前引用变量
  • 重复的参数名称
  • 未使用的局部变量

PEP8 的最后一位包含在 PEP8 命名包中。它确保您的命名接近 PEP8 规定的标准:

  • 类名为大写字母
  • 函数、变量和参数名称均为小写
  • 常量为全大写且被视为常量
  • 实例方法和类方法的第一个参数分别为selfcls

麦卡贝

最后,还有麦卡比复杂性。它通过查看抽象语法树AST来检查代码的复杂性。它会找出代码中有多少行、级别和语句,并在代码复杂度超过预先配置的阈值时发出警告。通常,您将通过flake8使用 McCabe,但也可以手动调用。使用以下代码:

def spam():
    pass

def eggs(matrix):
    for x in matrix:
        for y in x:
            for z in y:
                print(z, end='')
            print()
        print()

McCabe 将向我们提供以下输出:

# pip install mccabe
...
# python -m mccabe cabe_test.py 1:1: 'spam' 1
5:1: 'eggs' 4

当然,您的最大阈值是可配置的,但默认值是 10。McCabe 测试返回一个受参数(如函数大小、嵌套深度等)影响的数字。如果您的函数达到 10,则可能是重构代码的时候了。

第 8 页

所有这些组合在一起就是flake8,一个组合这些工具并输出单个报告的工具。flake8生成的一些警告可能不符合您的口味,因此可以禁用每个检查,包括每个文件以及整个项目(如果需要)。例如,我个人禁用了我所有项目的W391,这会警告文件末尾有空行。这是我在编写代码时发现的有用的东西,这样我就可以轻松地跳到文件的末尾并开始编写代码,而不必先附加几行。

通常,在提交代码和/或将其联机之前,只需从源目录运行flake8,以递归方式检查所有内容。

下面是一些格式不正确的代码的演示:

def spam(a,b,c):
    print(a,b+c)

def eggs():
    pass

其结果是如下:

# pip install flake8
...
# flake8 flake8_test.py
flake8_test.py:1:11: E231 missing whitespace after ','
flake8_test.py:1:13: E231 missing whitespace after ','
flake8_test.py:2:12: E231 missing whitespace after ','
flake8_test.py:2:14: E226 missing whitespace around arithmetic operator
flake8_test.py:4:1: E302 expected 2 blank lines, found 1

派林

pylint是一个更高级的,在某些情况下更好的代码质量检查器。然而,pylint的力量确实有一些缺点。而flake8是一种非常快速、轻松、安全的质量检查,pylint具有更高级的自省能力,因此速度要慢得多。此外,pylint很可能会给你大量的警告,这些警告是不相关的,甚至是错误的。这可能被视为pylint中的一个缺陷,但实际上更多的是被动代码分析的限制。像pychecker这样的工具实际上加载并执行您的代码。在许多情况下,这是安全的,但也有一些情况下是不安全的。想想在执行删除文件的命令时会发生什么。

虽然我并不反对pylint,但总的来说,我发现大多数重要的问题都是由flake8处理的,其他问题可以通过一些适当的编码标准轻松避免。如果配置正确,它可能是一个非常有用的工具,但如果没有配置,它将非常冗长。

常见陷阱

Python 是一种语言,旨在清晰易读,没有任何歧义和意外行为。不幸的是,这些目标并不是在所有情况下都可以实现的,这就是为什么 Python 在某些情况下可能会做一些与您期望的不同的事情。

本节将向您展示在编写 Python 代码时可能遇到的一些问题。

范围很重要!

在 Python 中,有几个案例可能没有使用您实际期望的作用域。有些示例是在声明类时使用函数参数。

函数参数

以下示例显示了由于在默认参数中选择不小心而中断的情况:

def spam(key, value, list_=[], dict_={}):
    list_.append(value)
    dict_[key] = value

    print('List: %r' % list_)
    print('Dict: %r' % dict_)

spam('key 1', 'value 1')
spam('key 2', 'value 2')

您可能会期望以下输出:

List: ['value 1']
Dict: {'key 1': 'value 1'}
List: ['value 2']
Dict: {'key 2': 'value 2'}

但实际上是这样的:

List: ['value 1']
Dict: {'key 1': 'value 1'}
List: ['value 1', 'value 2']
Dict: {'key 1': 'value 1', 'key 2': 'value 2'}

原因是list_dict_实际上是在多个调用之间共享的。如果您正在做一些不正常的事情,那么这实际上是有用的,因此请避免在函数中使用可变对象作为默认参数。

同一示例的安全替代方案如下所示:

def spam(key, value, list_=None, dict_=None):
    if list_ is None:
        list_ = []

    if dict_ is None:
        dict_ = {}

    list_.append(value)
    dict_[key] = value

类属性

定义类时也会出现问题。混合类属性和实例属性非常容易。尤其是来自其他语言(如 C#)时,这可能会令人困惑。让我们举例说明一下:

class Spam(object):
    list_ = []
    dict_ = {}

    def __init__(self, key, value):
        self.list_.append(value)
        self.dict_[key] = value

        print('List: %r' % self.list_)
        print('Dict: %r' % self.dict_)

Spam('key 1', 'value 1')
Spam('key 2', 'value 2')

与函数参数一样,列表和字典是共享的。因此,输出如下:

List: ['value 1']
Dict: {'key 1': 'value 1'}
List: ['value 1', 'value 2']
Dict: {'key 1': 'value 1', 'key 2': 'value 2'}

更好的选择是在类的__init__方法中初始化可变对象。这样,它们就不会在实例之间共享:

class Spam(object):
    def __init__(self, key, value):
        self.list_ = [key]
        self.dict_ = {key: value}

        print('List: %r' % self.list_)
        print('Dict: %r' % self.dict_)

处理类时要注意的另一个重要事项是类属性将被继承,这就是可能会导致混淆的地方。继承时,原始属性将保留(除非被覆盖),即使在子类中:

 >>> class A(object):
...     spam = 1

>>> class B(A):
...     pass

Regular inheritance, the spam attribute of both A and B are 1 as
you would expect.
>>> A.spam
1
>>> B.spam
1

Assigning 2 to A.spam now modifies B.spam as well
>>> A.spam = 2

>>> A.spam
2
>>> B.spam
2

虽然这是由于继承而产生的,但使用该类的其他人可能不会怀疑该变量在此期间会发生变化。毕竟,我们修改了A.spam,而不是B.spam

有两种简单的方法可以防止这种情况。显然,可以简单地分别为每个类设置spam。但更好的解决方案是永远不要修改类属性。很容易忘记属性将在多个位置更改,如果无论如何都必须对其进行修改,通常最好将其放在实例变量中。

修改全局范围内的变量

当从全局范围访问变量时,一个常见的问题是设置变量会使其成为局部变量,即使在访问全局变量时也是如此。

这项工作:

 >>> def eggs():
...     print('Spam: %r' % spam)

>>> eggs()
Spam: 1

但以下情况并非如此:

 >>> spam = 1

>>> def eggs():
...     spam += 1
...     print('Spam: %r' % spam)

>>> eggs()
Traceback (most recent call last):
 ...
UnboundLocalError: local variable 'spam' referenced before assignment

问题是spam += 1实际上转化为spam = spam + 1,任何包含spam =的内容都会使变量在您的作用域中成为局部变量。由于局部变量是在该点赋值的,所以它还没有值,您正在尝试使用它。对于这些情况,有一个global声明,尽管我真的建议您完全避免使用 globals。

覆盖和/或创建额外内置

虽然它在某些情况下可能有用,但通常您希望避免覆盖全局函数。与内置的语句、函数和变量类似,命名函数的PEP8约定是使用尾随的下划线。

因此,不要使用此选项:

list = [1, 2, 3]

相反,请使用以下命令:

list_ = [1, 2, 3]

对于列表等,这只是一个很好的惯例。对于fromimportwith等语句,这是一个要求。忘记这一点可能会导致非常混乱的错误:

>>> list = list((1, 2, 3))
>>> list
[1, 2, 3]

>>> list((4, 5, 6))
Traceback (most recent call last):
 ...
TypeError: 'list' object is not callable

>>> import = 'Some import'
Traceback (most recent call last):
 ...
SyntaxError: invalid syntax

如果你真的想要定义一个在任何地方都可用的内置函数,那么是可能的。出于调试目的,我知道在开发时会将此代码添加到项目中:

import builtins
import inspect
import pprint
import re

def pp(*args, **kwargs):
    '''PrettyPrint function that prints the variable name when
    available and pprints the data'''
    name = None
    # Fetch the current frame from the stack
    frame = inspect.currentframe().f_back
    # Prepare the frame info
    frame_info = inspect.getframeinfo(frame)

    # Walk through the lines of the function
    for line in frame_info[3]:
        # Search for the pp() function call with a fancy regexp
        m = re.search(r'\bpp\s*\(\s*([^)]*)\s*\)', line)
        if m:
            print('# %s:' % m.group(1), end=' ')
            break

    pprint.pprint(*args, **kwargs)

builtins.pf = pprint.pformat
builtins.pp = pp

对于生产代码来说,它太粗糙了,但在处理需要打印语句进行调试的大型项目时,它仍然很有用。可在第 11 章调试-解决 bug中找到替代(更好)调试解决方案。

用法非常简单:

x = 10
pp(x)

以下是输出:

# x: 10

迭代时修改

在某种程度上,您会遇到这样的问题:在遍历列表、dict 或 set 等可变对象时,您无法修改它们。所有这些都会导致RuntimeError告诉您在迭代过程中无法修改对象:

dict_ = {'spam': 'eggs'}
list_ = ['spam']
set_ = {'spam', 'eggs'}

for key in dict_:
    del dict_[key]

for item in list_:
    list_.remove(item)

for item in set_:
    set_.remove(item)

这可以通过复制对象来避免。最方便的选择是使用list功能:

dict_ = {'spam': 'eggs'}
list_ = ['spam']
set_ = {'spam', 'eggs'}

for key in list(dict_):
    del dict_[key]

for item in list(list_):
    list_.remove(item)

for item in list(set_):
    set_.remove(item)

捕捉异常–Python 2 和 Python 3 之间的差异

在 Python 3 中,as语句使捕捉异常并存储异常变得更加明显。问题是许多人仍然习惯于except Exception, variable语法,这种语法已经不起作用了。幸运的是,Python 3 语法已经后端口到 Python 2,因此现在您可以在任何地方使用以下语法:

try:
    ... # do something here
except (ValueError, TypeError) as e:
    print('Exception: %r' % e)

另一个重要的区别是 Python3 使这个变量成为异常范围的局部变量。结果是如果您想以后使用try/except块,需要在try/except块之前声明异常变量:

def spam(value):
    try:
        value = int(value)
    except ValueError as exception:
        print('We caught an exception: %r' % exception)

    return exception

spam('a')

你可能会想到,既然我们在这里得到了一个例外,这是可行的;但事实上,它不存在,因为exceptionreturn语句中并不存在。

实际输出如下:

We caught an exception: ValueError("invalid literal for int() with base 10: 'a'",)
Traceback (most recent call last):
  File "test.py", line 14, in <module>
    spam('a')
  File "test.py", line 11, in spam
    return exception
UnboundLocalError: local variable 'exception' referenced before assignment

就我个人而言,我认为前面的代码在任何情况下都是被破坏的:如果没有异常怎么办?这也会引起同样的错误。幸运的是,解决方法很简单;只需将值写入范围外的变量。这里需要注意的一点是,您需要显式地将变量保存到父范围。此代码也不起作用:

def spam(value):
    exception = None
    try:
        value = int(value)
    except ValueError as exception:
        print('We caught an exception: %r' % exception)

    return exception

我们确实需要显式地保存它,因为 Python 3 会自动删除在except语句末尾使用as variable保存的任何内容。原因是 Python3 中的异常包含一个__traceback__属性。具有此属性会使垃圾收集器更难处理,因为它引入了递归自引用循环(异常->回溯->异常->回溯…)。为了解决这个问题,Python 基本上做了以下工作:

exception = None
try:
    value = int(value)
except ValueError as exception:
    try:
        print('We caught an exception: %r' % exception)
    finally:
        del exception

幸运的是,解决方案非常简单,但您应该记住,这可能会导致程序内存泄漏。Python 垃圾收集器足够聪明,可以理解变量不再可见,并最终将删除它,但它可能需要更多的时间。垃圾收集的实际工作原理见第 12 章性能–跟踪并减少内存和 CPU 使用。以下是代码的工作版本:

def spam(value):
    exception = None
    try:
        value = int(value)
    except ValueError as e:
        exception = e
        print('We caught an exception: %r' % exception)

    return exception

后期装订–注意封口

闭包是在代码中实现局部作用域的一种方法。它们可以在本地定义变量,而不覆盖父(或全局)范围内的变量,并在以后从外部范围隐藏变量。Python 中闭包的问题是,出于性能原因,Python 试图尽可能晚地绑定其变量。虽然它通常很有用,但确实有一些意想不到的副作用:

eggs = [lambda a: i * a for i in range(3)]

for egg in eggs:
    print(egg(5))

预期结果如何?应该是这样的,对吧?

0
5
10

不,不幸的是没有。这类似于类继承如何处理属性。由于后期绑定,变量i在调用时从周围的作用域调用,而不是在实际定义时调用。

实际结果如下:

10
10
10

那么该怎么办呢?与前面提到的情况一样,需要将变量设置为局部变量。一种替代方法是通过使用partial来强制立即绑定函数:

import functools

eggs = [functools.partial(lambda i, a: i * a, i) for i in range(3)]

for egg in eggs:
    print(egg(5))

更好的解决方案是通过不引入使用外部变量的额外作用域(即lambda),完全避免绑定问题。如果将ia都指定为lambda的参数,则这不会是问题。

循环进口

即使 Python 对循环导入相当宽容,但在某些情况下,您也会遇到错误。

假设我们有两个文件。

eggs.py

from spam import spam

def eggs():
    print('This is eggs')
    spam()

spam.py

from eggs import eggs

def spam():
    print('This is spam')

if __name__ == '__main__':
    eggs()

运行spam.py将导致循环import错误:

Traceback (most recent call last):
  File "spam.py", line 1, in <module>
    from eggs import eggs
  File "eggs.py", line 1, in <module>
    from spam import spam
  File "spam.py", line 1, in <module>
    from eggs import eggs
ImportError: cannot import name 'eggs'

有几种方法可以解决这个问题。重构代码通常是最好的,但最好的解决方案取决于问题。在上述情况下,它可以很容易地解决。只需使用模块导入而不是函数导入(我建议不考虑循环导入)。

eggs.py

import spam

def eggs():
    print('This is eggs')
    spam.spam()

spam.py

import eggs

def spam():
    print('This is spam')

if __name__ == '__main__':
    eggs.eggs()

另一种解决方案是在函数中移动导入,以便它们在运行时发生。这不是最漂亮的解决方案,但在许多情况下,它确实起到了作用。

eggs.py

def eggs():
    from spam import spam
    print('This is eggs')
    spam()

spam.py

def spam():
    from eggs import eggs
    print('This is spam')

if __name__ == '__main__':
    eggs()

最后还有将导入移到实际使用它们的代码之下的解决方案。通常不建议这样做,因为它会使导入的位置变得不明显,但我仍然觉得在函数调用中使用import更可取。

eggs.py

def eggs():
    print('This is eggs')
    spam()

from spam import spam

spam.py

def spam():
    print('This is spam')

from eggs import eggs

if __name__ == '__main__':
    eggs()

是的,还有其他解决方案,如动态导入。DjangoForeignKey字段如何支持字符串而不是实际类就是一个例子。但是,这些通常是一个非常糟糕的使用方法,因为它们只会在运行时被检查。因此,bug 只有在执行任何使用它的代码时才会出现,而不是在修改代码时。因此,请尽可能避免这些错误,或者确保添加适当的自动测试以防止意外错误。特别是当它们在内部导致循环导入时,它们成为调试的巨大痛苦。

进口碰撞

可能非常令人困惑的一个问题是冲突导入多个同名的包/模块。我的包上有不止几个 bug 报告,例如,有人试图使用我的numpy-stl项目,该项目位于名为stl.py的测试文件中名为stl的包中。结果是:它正在导入自身而不是stl包。虽然这种情况很难避免,至少在包内是如此,但相对导入通常是更好的选择。这是因为它还告诉其他程序员,导入来自本地范围,而不是另一个包。因此,与其写import spam,不如写from . import spam。这样,代码将始终从当前包加载,而不是从碰巧具有相同名称的任何全局包加载。

除此之外,还有包之间不兼容的问题。多个包可能使用通用名称,因此在安装这些包时要小心。如果有疑问,请创建一个新的虚拟环境,然后重试。这样做可以节省大量调试。

总结

本章向我们展示了 Python 哲学的全部内容,并向我们解释了 Python 禅宗的全部内容。虽然代码风格非常个性化,但 Python 有一些非常有用的指导原则,至少可以让人们保持基本相同的页面和风格。最后,我们都是同意的成年人;每个人都有权编写他/她认为合适的代码。但我请求你。请通读样式指南,并尝试遵守它们,除非你有很好的理由不这样做。

权力带来了巨大的责任,也带来了一些陷阱,尽管陷阱并不多。有些很狡猾,经常欺骗我,我已经写 Python 很长时间了!不过 Python 一直在改进。自 Python2 以来,已经注意到了许多陷阱,但有些陷阱将一直存在。例如,在大多数支持递归导入和定义的语言中,递归导入和定义可以轻松地满足您的需求,但这并不意味着我们将停止改进 Python。

多年来 Python 改进的一个很好的例子是 collections 模块。它包含许多有用的集合,这些集合是用户根据需要添加的。它们中的大多数实际上是用纯 Python 实现的,正因为如此,任何人都可以很容易地阅读它们。理解可能需要更多的努力,但我真的相信,如果你能把这本书读完,你就不会有问题理解这些藏品的作用。但我不能保证完全理解内部工作原理;其中的一些部分更倾向于通用计算机科学,而不是掌握 Python。

下一章将向您展示 Python 中可用的一些集合,以及它们是如何在内部构造的。尽管您无疑熟悉列表和字典等集合,但您可能不知道某些操作所涉及的性能特征。如果本章中的一些示例不太清楚,您不必担心。下一章将至少回顾其中的一些,更多内容将在后面的章节中介绍。