编程常见问题解答

常规问题

是否有带有断点、单步执行等的源代码级调试器?

有。

下面介绍了几个 Python 调试器,内置函数 breakpoint() 允许你进入其中任何一个。

pdb 模块是一个简单但足够好的 Python 控制台模式调试器。它是 Python 标准库的一部分,并且 参考 手册 文档。你还可以使用 pdb 的代码作为示例编写自己的调试器。

IDLE 交互式开发环境是 Python 标准发行版的一部分(通常作为 Tools/scripts/idle3 提供),它包含一个图形调试器。

PythonWin 是一个 Python IDE,它包含基于 pdb 的 GUI 调试器。PythonWin 调试器对断点着色,并具有很多很酷的功能,例如调试非 PythonWin 程序。PythonWin 可作为 pywin32 项目的一部分以及 ActivePython 发行版的一部分。

Eric 是一个基于 PyQt 和 Scintilla 编辑组件构建的 IDE。

trepan3k 是一个类似 gdb 的调试器。

Visual Studio Code 是一个带有调试工具的 IDE,它与版本控制软件集成。

有很多商业 Python IDE 包含图形调试器。它们包括

是否有工具可以帮助查找错误或执行静态分析?

有。

PylintPyflakes 执行基本检查,这将帮助你尽早发现错误。

静态类型检查器,例如 MypyPyrePytype,可以检查 Python 源代码中的类型提示。

如何从 Python 脚本创建独立二进制文件?

如果你只需要一个独立程序,用户可以下载并运行,而无需先安装 Python 发行版,那么你不需要将 Python 编译成 C 代码的能力。有很多工具可以确定程序所需的模块集,并将这些模块与 Python 二进制文件绑定在一起以生成单个可执行文件。

一种方法是使用 freeze 工具,它包含在 Python 源代码树中,作为 Tools/freeze。它将 Python 字节码转换为 C 数组;使用 C 编译器,你可以将所有模块嵌入到一个新程序中,然后将其与标准 Python 模块链接。

它的工作原理是递归扫描你的源代码以查找 import 语句(两种形式),并在标准 Python 路径以及源代码目录(对于内置模块)中查找模块。然后,它将用 Python 编写的模块的字节码转换为 C 代码(可以使用 marshal 模块转换为代码对象的数组初始化器),并创建一个自定义配置文件,其中仅包含程序中实际使用的那些内置模块。然后,它编译生成的 C 代码并将其与 Python 解释器的其余部分链接,以形成一个自包含的二进制文件,其行为与你的脚本完全相同。

以下软件包可以帮助创建控制台和 GUI 可执行文件

Python 程序是否有编码标准或风格指南?

是的。标准库模块所需的编码风格记录为 PEP 8

核心语言

当变量具有值时,为什么我收到 UnboundLocalError?

当变量通过在函数体中某个位置添加赋值语句进行修改时,在以前有效的代码中收到 UnboundLocalError 可能令人惊讶。

此代码

>>> x = 10
>>> def bar():
...     print(x)
...
>>> bar()
10

有效,但此代码

>>> x = 10
>>> def foo():
...     print(x)
...     x += 1

导致 UnboundLocalError

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

这是因为当你在作用域中对变量进行赋值时,该变量将变为该作用域的局部变量,并隐藏外部作用域中任何同名变量。由于 foo 中的最后一条语句为 x 分配了一个新值,因此编译器将其识别为局部变量。因此,当早期的 print(x) 尝试打印未初始化的局部变量时,将导致错误。

在上面的示例中,你可以通过声明全局变量来访问外部作用域变量

>>> x = 10
>>> def foobar():
...     global x
...     print(x)
...     x += 1
...
>>> foobar()
10

需要此显式声明,以提醒你(与类和实例变量的表面类似情况不同),你实际上正在修改外部作用域中变量的值

>>> print(x)
11

你可以使用 nonlocal 关键字在嵌套作用域中执行类似的操作

>>> def foo():
...    x = 10
...    def bar():
...        nonlocal x
...        print(x)
...        x += 1
...    bar()
...    print(x)
...
>>> foo()
10
11

Python 中局部变量和全局变量的规则是什么?

在 Python 中,仅在函数内部引用的变量隐式为全局变量。如果在函数体中的任何位置为变量赋值,则假定该变量为局部变量,除非明确声明为全局变量。

虽然一开始有点令人惊讶,但稍作考虑就能解释这一点。一方面,为已赋值变量要求 global 提供了一个防止意外副作用的障碍。另一方面,如果所有全局引用都需要 global,那么你将始终使用 global。你必须将对内置函数或导入模块组件的所有引用声明为全局变量。这种混乱会破坏 global 声明用于识别副作用的用处。

为什么在循环中使用不同值定义的 lambda 都返回相同的结果?

假设你使用 for 循环定义一些不同的 lambda(甚至是普通函数),例如

>>> squares = []
>>> for x in range(5):
...     squares.append(lambda: x**2)

这会给你一个包含 5 个 lambda 的列表,用于计算 x**2。你可能希望在调用它们时,它们分别返回 014916。但是,当你实际尝试时,你会看到它们都返回 16

>>> squares[2]()
16
>>> squares[4]()
16

这发生是因为 x 不是 lambdas 的局部变量,而是在外部作用域中定义的,并且在调用 lambda 时访问它,而不是在定义它时访问它。在循环结束时,x 的值为 4,因此现在所有函数都返回 4**2,即 16。您还可以通过更改 x 的值并查看 lambdas 的结果如何变化来验证这一点

>>> x = 8
>>> squares[2]()
64

为避免这种情况,您需要将值保存在 lambdas 的局部变量中,以便它们不依赖于全局 x 的值

>>> squares = []
>>> for x in range(5):
...     squares.append(lambda n=x: n**2)

此处,n=x 创建了一个新的变量 n,该变量是 lambda 的局部变量,并在定义 lambda 时计算,以便它具有 x 在循环中该点处具有的相同值。这意味着 n 的值在第一个 lambda 中将为 0,在第二个 lambda 中为 1,在第三个 lambda 中为 2,依此类推。因此,每个 lambda 现在都将返回正确的结果

>>> squares[2]()
4
>>> squares[4]()
16

请注意,这种行为并非 lambdas 所特有,也适用于常规函数。

如何在模块间共享全局变量?

在单个程序中跨模块共享信息的规范方法是创建一个特殊模块(通常称为 config 或 cfg)。只需在应用程序的所有模块中导入 config 模块;然后该模块将作为全局名称提供。因为每个模块只有一个实例,所以对模块对象所做的任何更改都会在所有地方反映出来。例如

config.py

x = 0   # Default value of the 'x' configuration setting

mod.py

import config
config.x = 1

main.py

import config
import mod
print(config.x)

请注意,出于同样的原因,使用模块也是实现单例设计模式的基础。

在模块中使用 import 的“最佳实践”是什么?

一般来说,不要使用 from modulename import *。这样做会使导入程序的名称空间杂乱无章,并且会使 linter 难以检测未定义的名称。

在文件顶部导入模块。这样做可以清楚地表明您的代码需要哪些其他模块,并避免有关模块名称是否在作用域内的疑问。每行使用一个 import 可以轻松添加和删除模块 import,但每行使用多个 import 可以减少屏幕空间。

如果您按以下顺序导入模块,则是一种很好的做法

  1. 标准库模块——例如 sysosargparsere

  2. 第三方库模块(安装在 Python 的 site-packages 目录中的任何内容)——例如 dateutilrequestsPIL.Image

  3. 本地开发的模块

有时需要将 import 移动到函数或类中以避免循环 import 的问题。戈登·麦克米伦说

当两个模块都使用“import <module>”形式的导入时,循环导入是没问题的。当第二个模块想要从第一个模块中获取一个名称(“from module import name”)并且导入位于顶级时,它们就会失败。这是因为第一个模块中的名称尚未可用,因为第一个模块正忙于导入第二个模块。

在这种情况下,如果第二个模块只在一个函数中使用,那么可以轻松地将导入移到该函数中。当调用导入时,第一个模块将完成初始化,第二个模块可以执行其导入。

如果某些模块是特定于平台的,也可能需要将导入移出代码的顶级。在这种情况下,甚至不可能在文件顶部导入所有模块。在这种情况下,在相应的特定于平台的代码中导入正确的模块是一个不错的选择。

仅当需要解决诸如避免循环导入或尝试减少模块初始化时间等问题时,才将导入移入局部作用域(例如函数定义内)。如果许多导入根据程序的执行方式是不必要的,则此技术特别有用。如果模块只在该函数中使用,你可能还想将导入移到函数中。请注意,第一次加载模块可能很昂贵,因为模块是一次性初始化的,但多次加载模块实际上是免费的,只需进行两次字典查找。即使模块名称已超出范围,该模块可能仍可在 sys.modules 中找到。

为什么对象之间共享默认值?

这种类型的错误通常会困扰新手程序员。考虑此函数

def foo(mydict={}):  # Danger: shared reference to one dict for all calls
    ... compute something ...
    mydict[key] = value
    return mydict

第一次调用此函数时,mydict 包含一个项目。第二次,mydict 包含两个项目,因为当 foo() 开始执行时,mydict 已经包含一个项目。

通常期望函数调用为默认值创建新对象。这不是发生的事情。默认值在函数定义时只创建一次。如果该对象发生更改(如本例中的字典),则对函数的后续调用将引用此更改后的对象。

根据定义,不可变对象(例如数字、字符串、元组和 None)不受更改的影响。对可变对象(例如字典、列表和类实例)的更改可能导致混淆。

由于此特性,不使用可变对象作为默认值是一种良好的编程实践。相反,使用 None 作为默认值,并在函数内部检查参数是否为 None,如果是,则创建一个新的列表/字典/其他内容。例如,不要编写

def foo(mydict={}):
    ...

而是

def foo(mydict=None):
    if mydict is None:
        mydict = {}  # create a new dict for local namespace

此特性可能很有用。当你有计算耗时的函数时,一种常见技术是缓存函数每次调用的参数和结果值,并在再次请求相同值时返回缓存值。这称为“记忆”,可以这样实现

# Callers can only provide two parameters and optionally pass _cache by keyword
def expensive(arg1, arg2, *, _cache={}):
    if (arg1, arg2) in _cache:
        return _cache[(arg1, arg2)]

    # Calculate the value
    result = ... expensive computation ...
    _cache[(arg1, arg2)] = result           # Store result in the cache
    return result

你可以使用包含字典的全局变量而不是默认值;这取决于个人喜好。

如何将一个函数中的可选参数或关键字参数传递到另一个函数?

使用函数参数列表中的 *** 说明符收集参数;这会给你一个元组形式的位置参数和一个字典形式的关键字参数。然后,你可以使用 *** 在调用另一个函数时传递这些参数

def f(x, *args, **kwargs):
    ...
    kwargs['width'] = '14.3c'
    ...
    g(x, *args, **kwargs)

参数和变量有什么区别?

参数由函数定义中出现的名称定义,而参数是调用函数时实际传递给函数的值。参数定义函数可以接受的参数类型。例如,给定函数定义

def func(foo, bar=None, **kwargs):
    pass

foobarkwargsfunc的参数。但是,在调用func时,例如

func(42, bar=314, extra=somevar)

42314somevar是参数。

为什么更改列表“y”也会更改列表“x”?

如果你编写了类似的代码

>>> x = []
>>> y = x
>>> y.append(10)
>>> y
[10]
>>> x
[10]

你可能会疑惑为什么向y追加元素也会更改x

导致此结果的因素有两个

  1. 变量只是引用对象的名称。执行y = x不会创建列表的副本,而是创建一个新变量y,该变量引用x引用的同一对象。这意味着只有一个对象(列表),xy都引用它。

  2. 列表是可变的,这意味着你可以更改其内容。

调用append()后,可变对象的内容已从[]更改为[10]。由于这两个变量都引用同一对象,因此使用任一名称都可以访问修改后的值[10]

如果我们改为向x分配不可变对象

>>> x = 5  # ints are immutable
>>> y = x
>>> x = x + 1  # 5 can't be mutated, we are creating a new object here
>>> x
6
>>> y
5

我们可以看到在这种情况下,xy不再相等。这是因为整数是不可变的,当我们执行x = x + 1时,我们不会通过增加其值来改变整数5;相反,我们创建一个新对象(整数6)并将其分配给x(即,更改x引用的对象)。在此分配之后,我们有两个对象(整数65)和两个引用它们的变量(x现在引用6,但y仍然引用5)。

一些操作(例如 y.append(10)y.sort())会改变对象,而表面上相似的操作(例如 y = y + [10]sorted(y))会创建一个新对象。通常,在 Python 中(在标准库中所有情况下),改变对象的函数会返回 None,以帮助避免混淆两种类型的操作。因此,如果你错误地编写 y.sort(),认为它会给你一个 y 的已排序副本,你最终会得到 None,这可能会导致你的程序生成一个易于诊断的错误。

但是,有一类操作,其中相同的操作有时对不同的类型有不同的行为:增强赋值运算符。例如,+= 会改变列表,但不会改变元组或整数(a_list += [1, 2, 3] 等效于 a_list.extend([1, 2, 3]),并改变 a_list,而 some_tuple += (1, 2, 3)some_int += 1 创建新对象)。

换句话说

  • 如果我们有一个可变对象(listdictset 等),我们可以使用一些特定操作来改变它,所有引用它的变量都将看到更改。

  • 如果我们有一个不可变对象(strinttuple 等),所有引用它的变量都将始终看到相同的值,但将该值转换为新值的运算始终返回一个新对象。

如果你想知道两个变量是否引用同一个对象,你可以使用 is 运算符或内置函数 id()

如何编写具有输出参数的函数(按引用调用)?

请记住,在 Python 中,参数是通过赋值传递的。由于赋值只是创建对对象的引用,因此调用方和被调用方中的参数名之间没有别名,因此本身没有按引用调用。你可以通过多种方式实现所需的效果。

  1. 通过返回结果的元组

    >>> def func1(a, b):
    ...     a = 'new-value'        # a and b are local names
    ...     b = b + 1              # assigned to new objects
    ...     return a, b            # return new values
    ...
    >>> x, y = 'old-value', 99
    >>> func1(x, y)
    ('new-value', 100)
    

    这几乎总是最清晰的解决方案。

  2. 通过使用全局变量。这不是线程安全的,不推荐使用。

  3. 通过传递可变(可就地更改)对象

    >>> def func2(a):
    ...     a[0] = 'new-value'     # 'a' references a mutable list
    ...     a[1] = a[1] + 1        # changes a shared object
    ...
    >>> args = ['old-value', 99]
    >>> func2(args)
    >>> args
    ['new-value', 100]
    
  4. 通过传递一个会被改变的字典

    >>> def func3(args):
    ...     args['a'] = 'new-value'     # args is a mutable dictionary
    ...     args['b'] = args['b'] + 1   # change it in-place
    ...
    >>> args = {'a': 'old-value', 'b': 99}
    >>> func3(args)
    >>> args
    {'a': 'new-value', 'b': 100}
    
  5. 或将值捆绑到类实例中

    >>> class Namespace:
    ...     def __init__(self, /, **args):
    ...         for key, value in args.items():
    ...             setattr(self, key, value)
    ...
    >>> def func4(args):
    ...     args.a = 'new-value'        # args is a mutable Namespace
    ...     args.b = args.b + 1         # change object in-place
    ...
    >>> args = Namespace(a='old-value', b=99)
    >>> func4(args)
    >>> vars(args)
    {'a': 'new-value', 'b': 100}
    

    几乎没有理由让事情变得如此复杂。

你最好的选择是返回包含多个结果的元组。

如何在 Python 中制作高阶函数?

你有两种选择:你可以使用嵌套作用域或可调用对象。例如,假设你想定义 linear(a,b),它返回一个函数 f(x),该函数计算值 a*x+b。使用嵌套作用域

def linear(a, b):
    def result(x):
        return a * x + b
    return result

或使用可调用对象

class linear:

    def __init__(self, a, b):
        self.a, self.b = a, b

    def __call__(self, x):
        return self.a * x + self.b

在这两种情况下,

taxes = linear(0.3, 2)

给出一个可调用对象,其中 taxes(10e6) == 0.3 * 10e6 + 2

可调用对象方法的缺点是它有点慢,并且会导致代码稍长。但是,请注意,一组可调用对象可以通过继承共享它们的签名

class exponential(linear):
    # __init__ inherited
    def __call__(self, x):
        return self.a * (x ** self.b)

对象可以封装多个方法的状态

class counter:

    value = 0

    def set(self, x):
        self.value = x

    def up(self):
        self.value = self.value + 1

    def down(self):
        self.value = self.value - 1

count = counter()
inc, dec, reset = count.up, count.down, count.set

这里 inc()dec()reset() 充当函数,它们共享相同的计数变量。

如何在 Python 中复制一个对象?

一般来说,尝试 copy.copy()copy.deepcopy() 以获得一般情况。并非所有对象都可以复制,但大多数对象可以。

某些对象可以更轻松地复制。字典有一个 copy() 方法

newdict = olddict.copy()

序列可以通过切片复制

new_l = l[:]

如何查找对象的属性或方法?

对于用户定义类的实例 xdir(x) 返回一个按字母顺序排列的名称列表,其中包含实例属性和方法以及其类定义的属性。

我的代码如何发现一个对象的名称?

一般来说,它不能,因为对象实际上没有名称。从本质上讲,赋值始终将名称绑定到值;对于 defclass 语句也是如此,但在这种情况下,该值是一个可调用对象。考虑以下代码

>>> class A:
...     pass
...
>>> B = A
>>> a = B()
>>> b = a
>>> print(b)
<__main__.A object at 0x16D07CC>
>>> print(a)
<__main__.A object at 0x16D07CC>

可以说,该类有一个名称:即使它绑定到两个名称并通过名称 B 调用,创建的实例仍报告为类 A 的实例。但是,不可能说实例的名称是 a 还是 b,因为这两个名称都绑定到相同的值。

一般来说,你的代码不需要“知道特定值的名称”。除非你故意编写内省程序,否则这通常表明改变方法可能是有益的。

在 comp.lang.python 中,Fredrik Lundh 曾经针对这个问题给出了一个很好的类比

就像你获取在你门廊上发现的那只猫的名称一样:猫(对象)本身无法告诉你它的名字,它也不太在意——所以找出它叫什么的唯一方法就是问你所有的邻居(名称空间)是否它是他们的猫(对象)……

….并且不要惊讶,如果你发现它有很多名字,或者根本没有名字!

逗号运算符的优先级是怎么回事?

逗号不是 Python 中的运算符。考虑此会话

>>> "a" in "b", "a"
(False, 'a')

由于逗号不是运算符,而是表达式之间的分隔符,因此上面内容的评估方式就像你输入了

("a" in "b"), "a"

不是

"a" in ("b", "a")

各种赋值运算符(=+= 等)也是如此。它们不是真正的运算符,而是赋值语句中的语法分隔符。

是否有与 C 的“?:”三元运算符等效的运算符?

是的,有。语法如下

[on_true] if [expression] else [on_false]

x, y = 50, 25
small = x if x < y else y

在此语法在 Python 2.5 中引入之前,一个常见的惯例是使用逻辑运算符

[expression] and [on_true] or [on_false]

但是,此惯例不安全,因为当 on_true 具有 false 布尔值时,它可能会给出错误的结果。因此,最好始终使用 ... if ... else ... 形式。

是否可以在 Python 中编写混淆的一行代码?

可以。通常的做法是在 lambda 中嵌套 lambda 来实现。请看以下三个示例,稍作改编自 Ulf Bartelt

from functools import reduce

# Primes < 1000
print(list(filter(None,map(lambda y:y*reduce(lambda x,y:x*y!=0,
map(lambda x,y=y:y%x,range(2,int(pow(y,0.5)+1))),1),range(2,1000)))))

# First 10 Fibonacci numbers
print(list(map(lambda x,f=lambda x,f:(f(x-1,f)+f(x-2,f)) if x>1 else 1:
f(x,f), range(10))))

# Mandelbrot set
print((lambda Ru,Ro,Iu,Io,IM,Sx,Sy:reduce(lambda x,y:x+'\n'+y,map(lambda y,
Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,Sy=Sy,L=lambda yc,Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,i=IM,
Sx=Sx,Sy=Sy:reduce(lambda x,y:x+y,map(lambda x,xc=Ru,yc=yc,Ru=Ru,Ro=Ro,
i=i,Sx=Sx,F=lambda xc,yc,x,y,k,f=lambda xc,yc,x,y,k,f:(k<=0)or (x*x+y*y
>=4.0) or 1+f(xc,yc,x*x-y*y+xc,2.0*x*y+yc,k-1,f):f(xc,yc,x,y,k,f):chr(
64+F(Ru+x*(Ro-Ru)/Sx,yc,0,0,i)),range(Sx))):L(Iu+y*(Io-Iu)/Sy),range(Sy
))))(-2.1, 0.7, -1.2, 1.2, 30, 80, 24))
#    \___ ___/  \___ ___/  |   |   |__ lines on screen
#        V          V      |   |______ columns on screen
#        |          |      |__________ maximum of "iterations"
#        |          |_________________ range on y axis
#        |____________________________ range on x axis

孩子们,在家别尝试这个!

函数参数列表中的斜杠 (/) 意味着什么?

函数参数列表中的斜杠表示斜杠前面的参数仅限于位置。仅限于位置的参数是没有外部可用名称的参数。在调用接受仅限于位置的参数的函数时,参数将根据其位置映射到参数。例如,divmod() 是一个接受仅限于位置的参数的函数。它的文档如下所示

>>> help(divmod)
Help on built-in function divmod in module builtins:

divmod(x, y, /)
    Return the tuple (x//y, x%y).  Invariant: div*y + mod == x.

参数列表末尾的斜杠表示两个参数都仅限于位置。因此,使用关键字参数调用 divmod() 将导致错误

>>> divmod(x=3, y=4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: divmod() takes no keyword arguments

数字和字符串

如何指定十六进制和八进制整数?

要指定八进制数字,请在八进制值前面加上一个零,然后加上小写或大写字母“o”。例如,要将变量“a”设置为八进制值“10”(十进制为 8),请键入

>>> a = 0o10
>>> a
8

十六进制也很简单。只需在十六进制数字前面加上一个零,然后加上小写或大写字母“x”。十六进制数字可以用小写或大写字母指定。例如,在 Python 解释器中

>>> a = 0xa5
>>> a
165
>>> b = 0XB2
>>> b
178

为什么 -22 // 10 返回 -3?

这主要是出于希望 i % jj 具有相同的符号。如果你希望这样,并且还希望

i == (i // j) * j + (i % j)

那么整数除法必须返回向下取整。C 也要求该恒等式成立,然后截断 i // j 的编译器需要使 i % ji 具有相同的符号。

j 为负数时,i % j 的实际用例很少。当 j 为正数时,有许多用例,并且在几乎所有用例中,i % j>= 0 更为有用。如果时钟现在显示 10,那么 200 小时前它显示什么?-190 % 12 == 2 很有用;-190 % 12 == -10 是一个等待咬人的 bug。

如何获取 int 文字属性而不是 SyntaxError?

尝试以正常方式查找 int 文字属性会产生 SyntaxError,因为句点被视为小数点

>>> 1.__class__
  File "<stdin>", line 1
  1.__class__
   ^
SyntaxError: invalid decimal literal

解决方案是用空格或括号将文字与句点分开。

>>> 1 .__class__
<class 'int'>
>>> (1).__class__
<class 'int'>

如何将字符串转换为数字?

对于整数,请使用内置 int() 类型构造器,例如 int('144') == 144。类似地,float() 转换为浮点数,例如 float('144') == 144.0

默认情况下,这些将数字解释为十进制,因此 int('0144') == 144 成立,而 int('0x144') 会引发 ValueErrorint(string, base) 将要转换的基数作为第二个可选参数,因此 int( '0x144', 16) == 324。如果将基数指定为 0,则使用 Python 的规则来解释数字:前导“0o”表示八进制,“0x”表示十六进制数。

如果你只需要将字符串转换为数字,请不要使用内置函数 eval()eval() 的速度会明显慢一些,而且存在安全风险:有人可能会向你传递一个 Python 表达式,该表达式可能会产生不良副作用。例如,有人可能会传递 __import__('os').system("rm -rf $HOME"),这会擦除你的主目录。

eval() 还会将数字解释为 Python 表达式,因此例如 eval('09') 会产生语法错误,因为 Python 不允许在十进制数中使用前导“0”(“0”除外)。

如何将数字转换为字符串?

要转换,例如,数字 144 为字符串 '144',请使用内置类型构造函数 str()。如果你想要十六进制或八进制表示,请使用内置函数 hex()oct()。有关精细格式化的信息,请参阅 f 字符串格式字符串语法 部分,例如 "{:04d}".format(144) 产生 '0144',而 "{:.3f}".format(1.0/3.0) 产生 '0.333'

如何就地修改字符串?

你不能,因为字符串是不可变的。在大多数情况下,你应该简单地从想要组装它的各个部分构造一个新字符串。但是,如果你需要一个能够就地修改 Unicode 数据的对象,请尝试使用 io.StringIO 对象或 array 模块

>>> import io
>>> s = "Hello, world"
>>> sio = io.StringIO(s)
>>> sio.getvalue()
'Hello, world'
>>> sio.seek(7)
7
>>> sio.write("there!")
6
>>> sio.getvalue()
'Hello, there!'

>>> import array
>>> a = array.array('u', s)
>>> print(a)
array('u', 'Hello, world')
>>> a[0] = 'y'
>>> print(a)
array('u', 'yello, world')
>>> a.tounicode()
'yello, world'

如何使用字符串调用函数/方法?

有多种技术。

  • 最好的方法是使用将字符串映射到函数的字典。此技术的首要优点是字符串不必与函数的名称匹配。这也是用于模拟 case 结构的主要技术

    def a():
        pass
    
    def b():
        pass
    
    dispatch = {'go': a, 'stop': b}  # Note lack of parens for funcs
    
    dispatch[get_input()]()  # Note trailing parens to call function
    
  • 使用内置函数 getattr()

    import foo
    getattr(foo, 'bar')()
    

    请注意,getattr()适用于任何对象,包括类、类实例、模块等。

    标准库中有多处使用了此函数,如下所示

    class Foo:
        def do_foo(self):
            ...
    
        def do_bar(self):
            ...
    
    f = getattr(foo_instance, 'do_' + opname)
    f()
    
  • 使用 locals()解析函数名

    def myFunc():
        print("hello")
    
    fname = "myFunc"
    
    f = locals()[fname]
    f()
    

Perl 的 chomp() 有没有用于从字符串中移除尾部换行符的等效函数?

你可以使用 S.rstrip("\r\n") 从字符串 S 的末尾移除所有行终止符,而不移除其他尾部空白。如果字符串 S 表示多行,末尾有多个空行,则所有空行的行终止符都将被移除

>>> lines = ("line 1 \r\n"
...          "\r\n"
...          "\r\n")
>>> lines.rstrip("\n\r")
'line 1 '

由于通常只有在一次读取一行文本时才需要这样做,因此使用 S.rstrip() 这种方式效果很好。

有没有 scanf() 或 sscanf() 的等效函数?

没有类似的函数。

对于简单的输入解析,最简单的方法通常是使用字符串对象的 split() 方法将行拆分为以空格分隔的单词,然后使用 int()float() 将十进制字符串转换为数字值。 split() 支持一个可选的“sep”参数,如果行使用空格以外的内容作为分隔符,则此参数很有用。

对于更复杂的输入解析,正则表达式比 C 的 sscanf 更强大,更适合这项任务。

‘UnicodeDecodeError’或‘UnicodeEncodeError’错误是什么意思?

请参阅 Unicode HOWTO

我能否以奇数个反斜杠结束原始字符串?

以奇数个反斜杠结尾的原始字符串将转义字符串的引号

>>> r'C:\this\will\not\work\'
  File "<stdin>", line 1
    r'C:\this\will\not\work\'
         ^
SyntaxError: unterminated string literal (detected at line 1)

有几种解决方法。一种是使用常规字符串并加倍反斜杠

>>> 'C:\\this\\will\\work\\'
'C:\\this\\will\\work\\'

另一种方法是将包含转义反斜杠的常规字符串连接到原始字符串

>>> r'C:\this\will\work' '\\'
'C:\\this\\will\\work\\'

还可以使用 os.path.join() 在 Windows 上追加反斜杠

>>> os.path.join(r'C:\this\will\work', '')
'C:\\this\\will\\work\\'

请注意,虽然反斜杠会“转义”引号以确定原始字符串的结束位置,但在解释原始字符串的值时不会发生转义。也就是说,反斜杠仍然存在于原始字符串的值中

>>> r'backslash\'preserved'
"backslash\\'preserved"

另请参阅 语言参考 中的规范。

性能

我的程序太慢了。如何加快速度?

总的来说,这是一个难题。首先,在进一步深入研究之前,请记住以下事项

  • 性能特征因 Python 实现而异。本常见问题解答重点介绍 CPython

  • 行为因操作系统而异,尤其是在讨论 I/O 或多线程时。

  • 在尝试优化任何代码之前,你应该始终找到程序中的热点(请参阅 profile 模块)。

  • 编写基准脚本将允许你在搜索改进时快速迭代(请参阅 timeit 模块)。

  • 强烈建议在潜在引入隐藏在复杂优化中的回归之前拥有良好的代码覆盖率(通过单元测试或任何其他技术)。

话虽如此,有很多技巧可以加快 Python 代码的速度。以下是一些基本原则,这些原则在很大程度上有助于达到可接受的性能水平

  • 让你的算法更快(或更改为更快的算法)可以产生比尝试在整个代码中散布微优化技巧更大的好处。

  • 使用正确的数据结构。学习 内置类型collections 模块的文档。

  • 当标准库提供一个用于执行某项操作的基元时,它可能(尽管不能保证)比你可能提出的任何替代方案都快。对于用 C 编写的基元(例如内置函数和一些扩展类型)来说,这一点尤其如此。例如,务必使用 list.sort() 内置方法或相关的 sorted() 函数进行排序(并参阅 排序技术 以获取中等高级用法示例)。

  • 抽象往往会创建间接引用并迫使解释器做更多工作。如果间接引用的级别超过了完成的有用工作量,你的程序将变慢。你应该避免过度抽象,特别是以微小函数或方法的形式(这通常也有损于可读性)。

如果你已经达到纯 Python 所允许的极限,那么有一些工具可以让你更进一步。例如,Cython 可以将略微修改的 Python 代码版本编译成 C 扩展,并且可以在许多不同的平台上使用。Cython 可以利用编译(和可选类型注释)使你的代码比解释时显着更快。如果你对自己的 C 编程技能有信心,你还可以 编写一个 C 扩展模块

另请参阅

致力于 性能提示 的 wiki 页面。

将许多字符串连接在一起的最有效方法是什么?

strbytes 对象是不可变的,因此将许多字符串连接在一起是低效的,因为每次连接都会创建一个新对象。在一般情况下,总运行时间成本在总字符串长度中是二次方的。

要累积许多 str 对象,建议的做法是将它们放入列表中并在最后调用 str.join()

chunks = []
for s in my_strings:
    chunks.append(s)
result = ''.join(chunks)

(另一个相当有效的惯用做法是使用 io.StringIO

要累积多个 bytes 对象,推荐的方法是使用原位连接(+= 运算符)来扩展 bytearray 对象

result = bytearray()
for b in my_bytes_objects:
    result += b

序列(元组/列表)

如何转换元组和列表?

类型构造函数 tuple(seq) 将任何序列(实际上是任何可迭代对象)转换为一个元组,其中包含相同顺序的相同项。

例如,tuple([1, 2, 3]) 生成 (1, 2, 3)tuple('abc') 生成 ('a', 'b', 'c')。如果参数是元组,它不会进行复制,而是返回相同对象,因此在不确定对象是否已经是元组时调用 tuple() 的成本很低。

类型构造函数 list(seq) 将任何序列或可迭代对象转换为一个列表,其中包含相同顺序的相同项。例如,list((1, 2, 3)) 生成 [1, 2, 3]list('abc') 生成 ['a', 'b', 'c']。如果参数是列表,它会进行复制,就像 seq[:] 一样。

什么是负索引?

Python 序列使用正数和负数进行索引。对于正数,0 是第一个索引,1 是第二个索引,依此类推。对于负索引,-1 是最后一个索引,-2 是倒数第二个(倒数第二个)索引,依此类推。将 seq[-n] 视为与 seq[len(seq)-n] 相同。

使用负索引非常方便。例如,S[:-1] 是字符串的全部内容,除了最后一个字符,这对于从字符串中删除尾随换行符很有用。

如何反向迭代序列?

使用 reversed() 内置函数

for x in reversed(sequence):
    ...  # do something with x ...

这不会影响你的原始序列,而是构建一个反向排序的新副本进行迭代。

如何从列表中删除重复项?

请参阅 Python Cookbook,其中详细讨论了多种方法

如果你不介意重新排序列表,对其进行排序,然后从列表末尾开始扫描,在扫描过程中删除重复项

if mylist:
    mylist.sort()
    last = mylist[-1]
    for i in range(len(mylist)-2, -1, -1):
        if last == mylist[i]:
            del mylist[i]
        else:
            last = mylist[i]

如果列表的所有元素都可以用作集合键(即它们都是 可哈希的),这通常更快

mylist = list(set(mylist))

这会将列表转换为集合,从而删除重复项,然后将其转换回列表。

如何从列表中删除多个项

与删除重复项一样,使用删除条件反向显式迭代是一种可能性。但是,使用隐式或显式正向迭代进行切片替换更容易、更快速。这里有三种变体。

mylist[:] = filter(keep_function, mylist)
mylist[:] = (x for x in mylist if keep_condition)
mylist[:] = [x for x in mylist if keep_condition]

列表解析可能是最快的。

如何在 Python 中创建数组?

使用列表

["this", 1, "is", "an", "array"]

列表在时间复杂度上等同于 C 或 Pascal 数组;主要区别在于 Python 列表可以包含许多不同类型的对象。

array 模块还提供用于创建具有紧凑表示形式的固定类型数组的方法,但其索引速度比列表慢。另请注意,NumPy 和其他第三方包也定义了具有各种特性的类数组结构。

要获取 Lisp 风格的链表,可以使用元组模拟cons 单元格

lisp_list = ("like",  ("this",  ("example", None) ) )

如果需要可变性,可以使用列表而不是元组。此处 Lisp car 的类似物是 lisp_list[0]cdr 的类似物是 lisp_list[1]。仅在确定确实需要时执行此操作,因为它通常比使用 Python 列表慢得多。

如何创建多维列表?

你可能尝试像这样创建多维数组

>>> A = [[None] * 2] * 3

如果你打印它,它看起来是正确的

>>> A
[[None, None], [None, None], [None, None]]

但是当你分配一个值时,它会出现在多个地方

>>> A[0][0] = 5
>>> A
[[5, None], [5, None], [5, None]]

原因是使用 * 复制列表不会创建副本,它只会创建对现有对象的引用。 *3 创建一个列表,其中包含对长度为 2 的同一列表的 3 个引用。对一行进行的更改将显示在所有行中,这几乎肯定不是你想要的。

建议的做法是首先创建所需长度的列表,然后使用新创建的列表填充每个元素

A = [None] * 3
for i in range(3):
    A[i] = [None] * 2

这将生成一个包含 3 个不同长度为 2 的列表的列表。你还可以使用列表解析

w, h = 2, 3
A = [[None] * w for i in range(h)]

或者,你可以使用提供矩阵数据类型的扩展;NumPy 是最著名的。

如何将方法或函数应用于对象序列?

要调用方法或函数并累积返回值是一个列表,列表解析 是一个优雅的解决方案

result = [obj.method() for obj in mylist]

result = [function(obj) for obj in mylist]

要仅运行方法或函数而不保存返回值,一个简单的 for 循环就足够了

for obj in mylist:
    obj.method()

for obj in mylist:
    function(obj)

为什么 a_tuple[i] += [‘item’] 在加法运算有效时引发异常?

这是因为增强赋值运算符是赋值运算符以及 Python 中可变对象和不可变对象之间的差异共同造成的。

当增强赋值运算符应用于指向可变对象的元组元素时,通常适用此讨论,但我们将使用 list+= 作为我们的示例。

如果你编写

>>> a_tuple = (1, 2)
>>> a_tuple[0] += 1
Traceback (most recent call last):
   ...
TypeError: 'tuple' object does not support item assignment

异常的原因应该立即清楚:1 被添加到对象 a_tuple[0] 指向(1),生成结果对象 2,但是当我们尝试将计算结果 2 赋值给元组的元素 0 时,我们会收到一个错误,因为我们无法更改元组元素指向的内容。

在此增强赋值语句的底层,它执行的操作大约如下所示

>>> result = a_tuple[0] + 1
>>> a_tuple[0] = result
Traceback (most recent call last):
  ...
TypeError: 'tuple' object does not support item assignment

由于元组是不可变的,因此是操作的赋值部分产生了错误。

当你编写类似内容时

>>> a_tuple = (['foo'], 'bar')
>>> a_tuple[0] += ['item']
Traceback (most recent call last):
  ...
TypeError: 'tuple' object does not support item assignment

异常有点令人惊讶,更令人惊讶的是,即使出现错误,追加操作仍然有效

>>> a_tuple[0]
['foo', 'item']

要了解为何会发生这种情况,您需要知道 (a) 如果对象实现了 __iadd__() 魔术方法,则在执行 += 增强赋值时会调用该方法,其返回值将用于赋值语句中;(b) 对于列表,__iadd__() 等效于对列表调用 extend() 并返回列表。这就是我们说对于列表,+=list.extend() 的“简写”的原因

>>> a_list = []
>>> a_list += [1]
>>> a_list
[1]

这等效于

>>> result = a_list.__iadd__([1])
>>> a_list = result

a_list 指向的对象已发生突变,指向突变对象的指针被重新赋值给 a_list。赋值的最终结果是无操作,因为它指向 a_list 之前指向的同一对象的指针,但赋值仍然会发生。

因此,在我们的元组示例中,发生的情况等效于

>>> result = a_tuple[0].__iadd__(['item'])
>>> a_tuple[0] = result
Traceback (most recent call last):
  ...
TypeError: 'tuple' object does not support item assignment

__iadd__() 成功,因此列表已扩展,但即使 result 指向 a_tuple[0] 已指向的同一对象,该最终赋值仍会导致错误,因为元组是不可变的。

我想进行复杂的排序:您可以在 Python 中执行 Schwartzian 转换吗?

该技术归功于 Perl 社区的 Randal Schwartz,它通过将每个元素映射到其“排序值”的度量标准对列表的元素进行排序。在 Python 中,对 list.sort() 方法使用 key 参数

Isorted = L[:]
Isorted.sort(key=lambda s: int(s[10:15]))

如何根据另一个列表中的值对一个列表进行排序?

将它们合并到元组迭代器中,对结果列表进行排序,然后选出所需的元素。

>>> list1 = ["what", "I'm", "sorting", "by"]
>>> list2 = ["something", "else", "to", "sort"]
>>> pairs = zip(list1, list2)
>>> pairs = sorted(pairs)
>>> pairs
[("I'm", 'else'), ('by', 'sort'), ('sorting', 'to'), ('what', 'something')]
>>> result = [x[1] for x in pairs]
>>> result
['else', 'sort', 'to', 'something']

对象

什么是类?

类是通过执行类语句创建的特定对象类型。类对象用作模板来创建实例对象,实例对象体现了特定于数据类型的特定数据(属性)和代码(方法)。

一个类可以基于一个或多个其他类,称为其基类。然后,它将继承其基类的属性和方法。这允许通过继承对对象模型进行连续细化。您可能有一个通用的 Mailbox 类,它为邮箱提供基本的访问器方法,以及处理各种特定邮箱格式的子类,例如 MboxMailboxMaildirMailboxOutlookMailbox

什么是方法?

方法是对某个对象 x 的函数,您通常将其称为 x.name(arguments...)。方法在类定义中定义为函数

class C:
    def meth(self, arg):
        return arg * 2 + self.attribute

什么是 self?

Self 只是方法第一个参数的传统名称。定义为 meth(self, a, b, c) 的方法应在类中定义的某个实例 x 中称为 x.meth(a, b, c);被调用的方法会认为它被调用为 meth(x, a, b, c)

另请参见 为什么必须在方法定义和调用中显式使用“self”?

如何检查对象是否是给定类或其子类的实例?

使用内置函数 isinstance(obj, cls)。您可以通过提供元组而不是单个类来检查对象是否是多个类的实例,例如 isinstance(obj, (class1, class2, ...)),还可以检查对象是否是 Python 的内置类型,例如 isinstance(obj, str)isinstance(obj, (int, float, complex))

请注意,isinstance() 还会检查从 抽象基类 的虚拟继承。因此,即使没有直接或间接继承自注册类,测试也会返回 True。要测试“真正继承”,请扫描类的 MRO

from collections.abc import Mapping

class P:
     pass

class C(P):
    pass

Mapping.register(P)
>>> c = C()
>>> isinstance(c, C)        # direct
True
>>> isinstance(c, P)        # indirect
True
>>> isinstance(c, Mapping)  # virtual
True

# Actual inheritance chain
>>> type(c).__mro__
(<class 'C'>, <class 'P'>, <class 'object'>)

# Test for "true inheritance"
>>> Mapping in type(c).__mro__
False

请注意,大多数程序不会经常在用户定义的类上使用 isinstance()。如果您自己开发类,更恰当的面向对象风格是定义封装特定行为的类方法,而不是检查对象的类并根据其所属的类执行不同的操作。例如,如果您有一个执行某些操作的函数

def search(obj):
    if isinstance(obj, Mailbox):
        ...  # code to search a mailbox
    elif isinstance(obj, Document):
        ...  # code to search a document
    elif ...

更好的方法是在所有类上定义一个 search() 方法,然后只调用它

class Mailbox:
    def search(self):
        ...  # code to search a mailbox

class Document:
    def search(self):
        ...  # code to search a document

obj.search()

什么是委托?

委托是一种面向对象技术(也称为设计模式)。假设您有一个对象 x,并且只想更改其一个方法的行为。您可以创建一个新类,为想要更改的方法提供新的实现,并将所有其他方法委托给 x 的相应方法。

Python 程序员可以轻松实现委托。例如,以下类实现了一个类,其行为类似于文件,但会将所有写入的数据转换为大写

class UpperOut:

    def __init__(self, outfile):
        self._outfile = outfile

    def write(self, s):
        self._outfile.write(s.upper())

    def __getattr__(self, name):
        return getattr(self._outfile, name)

此处,UpperOut 类重新定义 write() 方法,以便在调用底层 self._outfile.write() 方法之前将参数字符串转换为大写。所有其他方法都委托给底层 self._outfile 对象。委托是通过 __getattr__() 方法完成的;有关控制属性访问的更多信息,请参阅 语言参考

请注意,对于更一般的情况,委托可能会变得更加棘手。当需要设置和检索属性时,类必须定义 __setattr__() 方法,并且必须谨慎地这样做。 __setattr__() 的基本实现大致相当于以下内容

class X:
    ...
    def __setattr__(self, name, value):
        self.__dict__[name] = value
    ...

大多数 __setattr__() 实现都必须修改 self.__dict__ 以存储 self 的本地状态,而不会导致无限递归。

如何从扩展它的派生类中调用在基类中定义的方法?

使用内置 super() 函数

class Derived(Base):
    def meth(self):
        super().meth()  # calls Base.meth

在示例中,super() 将自动确定调用它的实例(self 值),使用 type(self).__mro__ 查找方法解析顺序 (MRO),并在 MRO 中返回 Derived 之后的下一个:Base

如何组织我的代码,以便更轻松地更改基类?

您可以将基类分配给别名并从别名派生。然后,您只需更改分配给别名的值即可。顺便说一句,如果您想动态决定(例如,取决于资源的可用性)使用哪个基类,此技巧也很方便。示例

class Base:
    ...

BaseAlias = Base

class Derived(BaseAlias):
    ...

如何创建静态类数据和静态类方法?

Python 支持静态数据和静态方法(以 C++ 或 Java 的意义)。

对于静态数据,只需定义一个类属性。要为属性分配新值,您必须在赋值中显式使用类名

class C:
    count = 0   # number of times C.__init__ called

    def __init__(self):
        C.count = C.count + 1

    def getcount(self):
        return C.count  # or return self.count

c.count 还引用任何 cC.count,只要 isinstance(c, C) 成立,除非被 c 本身或从 c.__class__C 的基类搜索路径上的某个类覆盖。

注意:在 C 的方法中,像 self.count = 42 这样的赋值会在 self 自己的字典中创建一个名为“count”的新且不相关的实例。无论是否在方法内,重新绑定类静态数据名称都必须始终指定类

C.count = 314

静态方法是可能的

class C:
    @staticmethod
    def static(arg1, arg2, arg3):
        # No 'self' parameter!
        ...

然而,通过简单的模块级函数来获得静态方法的效果是一种更直接的方法

def getcount():
    return C.count

如果您的代码的结构是为每个模块定义一个类(或紧密相关的类层次结构),则这将提供所需的封装。

如何在 Python 中重载构造函数(或方法)?

此答案实际上适用于所有方法,但这个问题通常首先出现在构造函数的上下文中。

在 C++ 中,您会编写

class C {
    C() { cout << "No arguments\n"; }
    C(int i) { cout << "Argument is " << i << "\n"; }
}

在 Python 中,您必须编写一个使用默认参数捕获所有情况的单个构造函数。例如

class C:
    def __init__(self, i=None):
        if i is None:
            print("No arguments")
        else:
            print("Argument is", i)

这并不完全等效,但在实践中足够接近。

您还可以尝试一个可变长度的参数列表,例如

def __init__(self, *args):
    ...

相同的方法适用于所有方法定义。

我尝试使用 __spam,但收到了有关 _SomeClassName__spam 的错误。

具有双重前导下划线的变量名被“混淆”以提供一种简单但有效的方法来定义类私有变量。任何形式的标识符 __spam(至少两个前导下划线,最多一个尾随下划线)在文本上替换为 _classname__spam,其中 classname 是当前类名,任何前导下划线都被剥离。

这并不能保证私密性:外部用户仍然可以故意访问“_classname__spam”属性,并且私有值在对象的 __dict__ 中可见。许多 Python 程序员根本不费心使用私有变量名。

我的类定义了 __del__,但当我删除对象时它不会被调用。

造成这种情况的原因有几个。

del 语句不一定调用 __del__() – 它只是递减对象的引用计数,如果引用计数变为零,则调用 __del__()

如果你的数据结构包含循环链接(例如,每个子节点都有一个父节点引用,每个父节点都有一个子节点列表),引用计数永远不会变为零。Python 会时不时运行一个算法来检测此类循环,但垃圾回收器可能在你数据结构的最后一个引用消失后一段时间才运行,因此你的 __del__() 方法可能会在不方便且随机的时间被调用。如果你正在尝试重现一个问题,这会带来不便。更糟糕的是,执行对象 __del__() 方法的顺序是任意的。你可以运行 gc.collect() 来强制进行一次回收,但确实存在对象永远不会被回收的病态情况。

尽管有循环回收器,最好还是在对象上定义一个显式的 close() 方法,以便在完成使用对象时调用该方法。然后, close() 方法可以移除引用子对象的属性。不要直接调用 __del__()__del__() 应该调用 close(),而 close() 应该确保可以多次为同一个对象调用它。

避免循环引用的另一种方法是使用 weakref 模块,它允许你指向对象而不增加它们的引用计数。例如,树数据结构应该为它们的父级和同级引用(如果需要的话!)使用弱引用。

最后,如果你的 __del__() 方法引发了一个异常,一个警告消息将被打印到 sys.stderr

如何获取给定类的所有实例的列表?

Python 不会跟踪类(或内置类型)的所有实例。你可以对类的构造函数进行编程,通过保留对每个实例的弱引用的列表来跟踪所有实例。

为什么 id() 的结果似乎不是唯一的?

id() 内置函数返回一个整数,该整数在对象的整个生命周期中保证是唯一的。由于在 CPython 中,这是对象的内存地址,因此经常发生在对象从内存中删除后,下一个新创建的对象被分配在内存中的同一位置。以下示例对此进行了说明

>>> id(1000) 
13901272
>>> id(2000) 
13901272

这两个 id 属于不同的整数对象,它们在 id() 调用执行之前创建,并在执行之后立即删除。为了确保你想检查其 id 的对象仍然存活,请创建对该对象的另一个引用

>>> a = 1000; b = 2000
>>> id(a) 
13901272
>>> id(b) 
13891296

什么时候我可以依靠使用is运算符进行恒等性测试?

is运算符用于测试对象恒等性。测试a is b等同于id(a) == id(b)

恒等性测试最重要的属性是对象始终与其自身恒等,a is a始终返回True。恒等性测试通常比相等性测试更快。并且与相等性测试不同,恒等性测试保证返回布尔值TrueFalse

但是,只有在确保对象恒等性时,才能将恒等性测试替换为相等性测试。通常,有三种情况下可以保证恒等性

1) 赋值会创建新名称,但不会更改对象恒等性。在赋值new = old之后,可以保证new is old

2) 将对象放入存储对象引用的容器中不会更改对象恒等性。在列表赋值s[0] = x之后,可以保证s[0] is x

3) 如果一个对象是单例,这意味着该对象的实例只能存在一个。在赋值a = Noneb = None之后,可以保证a is b,因为None是一个单例。

在大多数其他情况下,不建议进行恒等性测试,而更倾向于进行相等性测试。特别是,不应使用恒等性测试来检查常量,例如intstr,因为不能保证它们是单例

>>> a = 1000
>>> b = 500
>>> c = b + 500
>>> a is c
False

>>> a = 'Python'
>>> b = 'Py'
>>> c = b + 'thon'
>>> a is c
False

同样,可变容器的新实例永远不会恒等

>>> a = []
>>> b = []
>>> a is b
False

在标准库代码中,您将看到几个正确使用恒等性测试的常见模式

1) 如PEP 8所推荐的,恒等性测试是检查None的首选方式。这在代码中读起来就像普通英语,并且避免了与其他可能具有求值为假布尔值的布尔值的混淆。

2) 当None是有效的输入值时,检测可选参数可能很棘手。在这些情况下,您可以创建一个单例哨兵对象,以确保它与其他对象不同。例如,以下是如何实现一个像dict.pop()一样工作的函数

_sentinel = object()

def pop(self, key, default=_sentinel):
    if key in self:
        value = self[key]
        del self[key]
        return value
    if default is _sentinel:
        raise KeyError(key)
    return default

3) 容器实现有时需要使用恒等性测试来增强相等性测试。这可以防止代码因诸如float('NaN')之类的对象而混淆,这些对象不等于自身。

例如,以下是collections.abc.Sequence.__contains__()的实现

def __contains__(self, value):
    for v in self:
        if v is value or v == value:
            return True
    return False

子类如何控制存储在不可变实例中的数据?

对不可变类型进行子类化时,重写 __new__() 方法,而不是 __init__() 方法。后者仅在创建实例之后运行,这对于更改不可变实例中的数据来说太晚了。

所有这些不可变类都具有与父类不同的签名

from datetime import date

class FirstOfMonthDate(date):
    "Always choose the first day of the month"
    def __new__(cls, year, month, day):
        return super().__new__(cls, year, month, 1)

class NamedInt(int):
    "Allow text names for some numbers"
    xlat = {'zero': 0, 'one': 1, 'ten': 10}
    def __new__(cls, value):
        value = cls.xlat.get(value, value)
        return super().__new__(cls, value)

class TitleStr(str):
    "Convert str to name suitable for a URL path"
    def __new__(cls, s):
        s = s.lower().replace(' ', '-')
        s = ''.join([c for c in s if c.isalnum() or c == '-'])
        return super().__new__(cls, s)

这些类可以像这样使用

>>> FirstOfMonthDate(2012, 2, 14)
FirstOfMonthDate(2012, 2, 1)
>>> NamedInt('ten')
10
>>> NamedInt(20)
20
>>> TitleStr('Blog: Why Python Rocks')
'blog-why-python-rocks'

如何缓存方法调用?

用于缓存方法的两个主要工具是 functools.cached_property()functools.lru_cache()。前者在实例级别存储结果,后者在类级别存储结果。

cached_property 方法仅适用于不接受任何参数的方法。它不会创建对实例的引用。缓存的方法结果将仅在实例存在时保留。

优点是,当不再使用实例时,缓存的方法结果将立即释放。缺点是,如果实例累积,累积的方法结果也会累积。它们可以无限增长。

lru_cache 方法适用于具有 可哈希 参数的方法。它会创建对实例的引用,除非做出特殊努力来传递弱引用。

最近最少使用算法的优点是,缓存受指定的maxsize 限制。缺点是,实例将保持存在,直到它们在缓存中过期或缓存被清除。

此示例显示了各种技术

class Weather:
    "Lookup weather information on a government website"

    def __init__(self, station_id):
        self._station_id = station_id
        # The _station_id is private and immutable

    def current_temperature(self):
        "Latest hourly observation"
        # Do not cache this because old results
        # can be out of date.

    @cached_property
    def location(self):
        "Return the longitude/latitude coordinates of the station"
        # Result only depends on the station_id

    @lru_cache(maxsize=20)
    def historic_rainfall(self, date, units='mm'):
        "Rainfall on a given date"
        # Depends on the station_id, date, and units.

以上示例假定 station_id 永远不会更改。如果相关的实例属性是可变的,则 cached_property 方法无法正常工作,因为它无法检测到属性的更改。

要在 station_id 可变时使 lru_cache 方法正常工作,该类需要定义 __eq__()__hash__() 方法,以便缓存可以检测到相关的属性更新

class Weather:
    "Example with a mutable station identifier"

    def __init__(self, station_id):
        self.station_id = station_id

    def change_station(self, station_id):
        self.station_id = station_id

    def __eq__(self, other):
        return self.station_id == other.station_id

    def __hash__(self):
        return hash(self.station_id)

    @lru_cache(maxsize=20)
    def historic_rainfall(self, date, units='cm'):
        'Rainfall on a given date'
        # Depends on the station_id, date, and units.

模块

如何创建 .pyc 文件?

首次导入模块(或源文件自创建当前已编译文件以来已更改)时,应在包含 .py 文件的目录的 __pycache__ 子目录中创建一个包含已编译代码的 .pyc 文件。 .pyc 文件的文件名将以与 .py 文件相同的名称开头,并以 .pyc 结尾,中间部分取决于创建它的特定 python 二进制文件。(请参阅 PEP 3147 了解详情。)

可能无法创建 .pyc 文件的原因之一是包含源文件的目录存在权限问题,这意味着无法创建 __pycache__ 子目录。例如,如果您作为某个用户进行开发,但以另一个用户身份运行,例如使用 Web 服务器进行测试,则可能会发生这种情况。

除非设置了 PYTHONDONTWRITEBYTECODE 环境变量,否则如果您正在导入模块并且 Python 有能力(权限、可用空间等)创建 __pycache__ 子目录并将已编译模块写入该子目录,则会自动创建 .pyc 文件。

在顶级脚本上运行 Python 不被视为导入,并且不会创建 .pyc。例如,如果您有一个顶级模块 foo.py,它导入另一个模块 xyz.py,当您运行 foo(通过键入 python foo.py 作为 shell 命令)时,将为 xyz 创建 .pyc,因为已导入 xyz,但不会为 foo 创建 .pyc 文件,因为 foo.py 尚未导入。

如果您需要为 foo 创建 .pyc 文件,即为未导入的模块创建 .pyc 文件,可以使用 py_compilecompileall 模块。

py_compile 模块可以手动编译任何模块。一种方法是在该模块中交互式地使用 compile() 函数

>>> import py_compile
>>> py_compile.compile('foo.py')                 

这会将 .pyc 写入与 foo.py 相同位置的 __pycache__ 子目录(或者您可以使用可选参数 cfile 覆盖它)。

您还可以使用 compileall 模块自动编译目录或目录中的所有文件。您可以通过运行 compileall.py 并提供包含要编译的 Python 文件的目录路径,从 shell 提示符中执行此操作

python -m compileall .

如何查找当前模块名称?

模块可以通过查看预定义的全局变量 __name__ 来找出自己的模块名称。如果其值为 '__main__',则该程序将作为脚本运行。许多通常通过导入来使用的模块还提供命令行界面或自检,并且仅在检查 __name__ 后才执行此代码

def main():
    print('Running test...')
    ...

if __name__ == '__main__':
    main()

如何拥有相互导入的模块?

假设您有以下模块

foo.py:

from bar import bar_var
foo_var = 1

bar.py:

from foo import foo_var
bar_var = 2

问题在于解释器将执行以下步骤

  • main 导入 foo

  • foo 创建空全局变量

  • foo 已编译并开始执行

  • foo 导入 bar

  • bar 创建空全局变量

  • bar 已编译并开始执行

  • bar 导入 foo(这是一个无操作,因为已经有一个名为 foo 的模块)

  • 导入机制尝试从 foo 全局变量中读取 foo_var,以设置 bar.foo_var = foo.foo_var

最后一步失败,因为 Python 尚未完成解释 foo,并且 foo 的全局符号字典仍然为空。

当你使用 import foo,然后尝试在全局代码中访问 foo.foo_var 时,也会发生同样的事情。

对于此问题,至少有三种可能的解决方法。

Guido van Rossum 建议避免使用 from <module> import ...,并将所有代码放在函数内。全局变量和类变量的初始化应仅使用常量或内置函数。这意味着从导入的模块中的所有内容都引用为 <module>.<name>

Jim Roskind 建议按以下顺序在每个模块中执行步骤

  • 导出(全局变量、函数和不需要导入基类的类)

  • import 语句

  • 活动代码(包括从导入的值初始化的全局变量)。

Van Rossum 不太喜欢这种方法,因为导入出现在一个奇怪的地方,但它确实有效。

Matthias Urlichs 建议重构你的代码,以便一开始就不需要递归导入。

这些解决方案并不相互排斥。

__import__(‘x.y.z’) 返回 <module ‘x’>;如何获取 z?

考虑使用 import_module() 便利函数,而不是 importlib

z = importlib.import_module('x.y.z')

当我编辑导入的模块并重新导入它时,更改不会显示。为什么会发生这种情况?

出于效率和一致性的原因,Python 仅在首次导入模块时读取模块文件。如果没有这样做,在一个由许多模块组成的程序中,每个模块都导入同一个基本模块,那么基本模块将被解析和重新解析多次。要强制重新读取已更改的模块,请执行以下操作

import importlib
import modname
importlib.reload(modname)

警告:此技术并非 100% 万无一失。特别是,包含以下语句的模块

from modname import some_objects

将继续使用导入对象的旧版本。如果模块包含类定义,则不会使用新类定义更新现有类实例。这可能导致以下自相矛盾的行为

>>> import importlib
>>> import cls
>>> c = cls.C()                # Create an instance of C
>>> importlib.reload(cls)
<module 'cls' from 'cls.py'>
>>> isinstance(c, cls.C)       # isinstance is false?!?
False

如果你打印出类对象的“标识”,问题的性质就会变得清晰

>>> hex(id(c.__class__))
'0x7352a0'
>>> hex(id(cls.C))
'0x4198d0'