编程常见问题¶
一般问题¶
有带断点、单步执行等的源代码级调试器吗?¶
是的。
下面介绍了一些 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 包含图形调试器。它们包括
有什么工具可以帮助查找错误或执行静态分析吗?¶
是的。
如何从 Python 脚本创建独立二进制文件?¶
如果您只想要一个用户可以下载和运行而无需先安装 Python 发行版的独立程序,则不需要将 Python 编译为 C 代码的能力。有许多工具可以确定程序所需模块的集合,并将这些模块与 Python 二进制文件绑定在一起以生成单个可执行文件。
其中之一是使用 freeze 工具,它包含在 Python 源代码树中作为 Tools/freeze。它将 Python 字节码转换为 C 数组;使用 C 编译器,您可以将所有模块嵌入到新程序中,然后将其与标准 Python 模块链接。
它的工作原理是递归扫描您的源文件以查找 import 语句(两种形式),并在标准 Python 路径以及源目录(用于内置模块)中查找模块。然后,它将用 Python 编写的模块的字节码转换为 C 代码(可以使用 marshal 模块转换为代码对象的数组初始化器),并创建一个只包含程序中实际使用的内置模块的自定义配置文件。然后,它编译生成的 C 代码并将其与 Python 解释器的其余部分链接,形成一个自包含的二进制文件,其行为与您的脚本完全相同。
以下软件包可以帮助创建控制台和 GUI 可执行文件
Nuitka(跨平台)
PyInstaller(跨平台)
PyOxidizer(跨平台)
cx_Freeze(跨平台)
py2app(仅限 macOS)
py2exe(仅限 Windows)
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 的列表,这些 lambda 计算 x**2
。您可能期望,当调用它们时,它们将分别返回 0
、1
、4
、9
和 16
。然而,当您实际尝试时,您会看到它们都返回 16
>>> squares[2]()
16
>>> squares[4]()
16
发生这种情况是因为 x
不是 lambda 的局部变量,而是在外部作用域中定义的,并且在调用 lambda 时(而不是定义时)访问它。在循环结束时,x
的值为 4
,因此所有函数现在都返回 4**2
,即 16
。您还可以通过更改 x
的值来验证这一点,并查看 lambda 的结果如何变化
>>> x = 8
>>> squares[2]()
64
为了避免这种情况,您需要将值保存在 lambda 的局部变量中,这样它们就不依赖于全局 x
的值
>>> squares = []
>>> for x in range(5):
... squares.append(lambda n=x: n**2)
这里,n=x
创建了一个新的局部变量 n
,该变量在定义 lambda 时计算,因此它与 x
在循环中的该点所具有的值相同。这意味着 n
的值在第一个 lambda 中将是 0
,在第二个 lambda 中是 1
,在第三个 lambda 中是 2
,依此类推。因此,每个 lambda 现在都将返回正确的结果
>>> squares[2]()
4
>>> squares[4]()
16
请注意,此行为并非 lambda 特有,也适用于普通函数。
在模块中使用 import 的“最佳实践”是什么?¶
一般来说,不要使用 from modulename import *
。这样做会弄乱导入器的命名空间,并使 linters 更难检测未定义的名称。
在文件顶部导入模块。这样做可以清楚地表明您的代码需要哪些其他模块,并避免模块名称是否在作用域中的问题。每行使用一个 import 可以轻松添加和删除模块导入,但每行使用多个 import 可以节省屏幕空间。
按照以下顺序导入模块是好的做法
第三方库模块(安装在 Python 的 site-packages 目录中的任何模块)——例如
dateutil
、requests
、PIL.Image
本地开发模块
有时需要将 import 移动到函数或类中,以避免循环导入问题。Gordon McMillan 说:
当两个模块都使用“import <module>”形式的 import 时,循环导入是允许的。当第二个模块想从第一个模块中获取一个名称(“from module import name”)并且 import 在顶层时,它们会失败。这是因为第一个模块中的名称尚未可用,因为第一个模块正忙于导入第二个模块。
在这种情况下,如果第二个模块仅在一个函数中使用,那么 import 可以很容易地移动到该函数中。当调用 import 时,第一个模块将完成初始化,第二个模块可以执行其 import。
如果某些模块是平台特定的,可能还需要将 import 移出代码的顶层。在这种情况下,甚至可能无法在文件顶部导入所有模块。在这种情况下,在相应的平台特定代码中导入正确的模块是一个不错的选择。
仅在需要解决循环导入等问题或尝试减少模块初始化时间时,才将 import 移动到局部作用域(例如函数定义内部)。如果许多 import 根据程序执行方式是不必要的,这种技术特别有用。如果模块仅在该函数中使用,您可能还希望将 import 移动到函数中。请注意,首次加载模块可能很昂贵,因为模块会进行一次性初始化,但多次加载模块实际上是免费的,只需进行几次字典查找。即使模块名称已超出作用域,该模块也可能在 sys.modules
中可用。
如何将可选参数或关键字参数从一个函数传递到另一个函数?¶
使用函数参数列表中的 *
和 **
说明符收集参数;这将为您提供作为元组的位置参数和作为字典的关键字参数。然后,您可以在调用另一个函数时使用 *
和 **
传递这些参数
def f(x, *args, **kwargs):
...
kwargs['width'] = '14.3c'
...
g(x, *args, **kwargs)
参数和形参之间有什么区别?¶
形参 由函数定义中出现的名称定义,而 实参 是调用函数时实际传递给函数的值。形参定义了函数可以接受何种实参。例如,给定函数定义
def func(foo, bar=None, **kwargs):
pass
foo、bar 和 kwargs 是 func
的形参。但是,当调用 func
时,例如
func(42, bar=314, extra=somevar)
值 42
、314
和 somevar
是实参。
为什么改变列表“y”也改变了列表“x”?¶
如果你写了这样的代码
>>> x = []
>>> y = x
>>> y.append(10)
>>> y
[10]
>>> x
[10]
你可能想知道为什么向 y
追加一个元素也会改变 x
。
导致此结果的有两个因素
变量只是引用对象的名称。
y = x
并不会创建列表的副本——它会创建一个新的变量y
,该变量引用x
引用的同一个对象。这意味着只有一个对象(列表),并且x
和y
都引用它。列表是可变的,这意味着您可以更改其内容。
调用 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
我们可以看到在这种情况下 x
和 y
不再相等。这是因为整数是不可变的,当我们执行 x = x + 1
时,我们不是通过增加其值来修改整数 5
;相反,我们是创建一个新对象(整数 6
)并将其赋值给 x
(即,更改 x
引用的对象)。在此赋值之后,我们有两个对象(整数 6
和 5
)和两个引用它们的变量(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
会创建新对象)。
换句话说
如何编写具有输出参数的函数(按引用调用)?¶
请记住,在 Python 中,参数是通过赋值传递的。由于赋值只创建对对象的引用,因此调用者和被调用者中的参数名称之间没有别名,因此本身没有按引用调用。您可以通过多种方式实现所需的效果。
通过返回结果的元组
>>> 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)
这几乎总是最清晰的解决方案。
通过使用全局变量。这不是线程安全的,不推荐。
通过传递一个可变(原地可更改)对象
>>> 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]
通过传递一个被修改的字典
>>> 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}
或者将值捆绑在一个类实例中
>>> 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)
,它返回一个计算值 a*x+b
的函数 f(x)
。使用嵌套作用域
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[:]
如何查找对象的方法或属性?¶
对于用户定义类的实例 x
,dir(x)
返回一个按字母顺序排列的列表,其中包含实例属性和方法以及其类定义的属性。
我的代码如何发现对象的名称?¶
一般来说,它不能,因为对象实际上没有名称。本质上,赋值总是将名称绑定到值;def
和 class
语句也是如此,但在这种情况下,值是可调用的。考虑以下代码
>>> 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"
not
"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* 具有错误的布尔值时,它可能会给出错误的结果。因此,始终最好使用 ... 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 % j
具有与 j
相同的符号。如果您希望如此,并且还希望
i == (i // j) * j + (i % j)
那么整数除法必须返回向下取整的值。C 语言也要求该恒等式成立,然后截断 i // j
的编译器需要使 i % j
具有与 i
相同的符号。
当 j
为负数时,i % j
的实际用例很少。当 j
为正数时,有许多用例,并且在几乎所有这些用例中,i % j
为 >= 0
更实用。如果现在是 10 点,200 小时前是几点?-190 % 12 == 2
是有用的;-190 % 12 == -10
是一个潜在的错误。
如何获取整数文字属性而不是 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')
会引发 ValueError
。int(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('w', s)
>>> print(a)
array('w', 'Hello, world')
>>> a[0] = 'y'
>>> print(a)
array('w', '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
更强大,也更适合这项任务。
What does UnicodeDecodeError
or UnicodeEncodeError
error mean?¶
请参阅 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 实现的性能特征各不相同。本 FAQ 侧重于 CPython。
行为可能因操作系统而异,尤其是在涉及 I/O 或多线程时。
在尝试优化任何代码之前,您应该始终找到程序中的热点(请参阅
profile
模块)。编写基准测试脚本将使您在寻找改进时能够快速迭代(请参阅
timeit
模块)。强烈建议在可能引入隐藏在复杂优化中的回归之前,拥有良好的代码覆盖率(通过单元测试或任何其他技术)。
话虽如此,加速 Python 代码的方法有很多。以下是一些有助于达到可接受性能水平的通用原则
使您的算法更快(或更改为更快的算法)可以带来比尝试在您的代码中散布微优化技巧更大的好处。
使用正确的数据结构。查阅 内置类型 和
collections
模块的文档。当标准库提供一个执行某项操作的原语时,它可能(尽管不保证)比您可能想到的任何替代方案都快。对于用 C 编写的原语,例如内置函数和一些扩展类型,这一点尤其正确。例如,请务必使用
list.sort()
内置方法或相关的sorted()
函数进行排序(并参阅 排序技术 以获取中等高级用法的示例)。抽象往往会创建间接性并迫使解释器做更多的工作。如果间接性的级别超过了所做的有用工作量,您的程序就会变慢。您应该避免过度抽象,尤其是以微小函数或方法的形式(这通常也会损害可读性)。
如果您已达到纯 Python 所能提供的极限,则有一些工具可以帮助您走得更远。例如,Cython 可以将略微修改的 Python 代码编译成 C 扩展,并可在许多不同平台上使用。Cython 可以利用编译(和可选的类型注释)使您的代码比解释时显着更快。如果您对自己的 C 编程技能有信心,您还可以自己编写 C 扩展模块。
参见
专门讨论性能技巧的维基页面。
连接许多字符串最有效的方法是什么?¶
str
和 bytes
对象是不可变的,因此将许多字符串连接在一起效率低下,因为每次连接都会创建一个新对象。在一般情况下,总运行时间成本是总字符串长度的二次方。
要累积许多 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 cell。
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
创建一个包含 3 个对同一长度为二的列表的引用的列表。对一行的更改将显示在所有行中,这几乎肯定不是你想要的。
建议的方法是首先创建所需长度的列表,然后用新创建的列表填充每个元素:
A = [None] * 3
for i in range(3):
A[i] = [None] * 2
这会生成一个包含 3 个不同长度为二的列表的列表。你也可以使用列表推导式:
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
这个异常有点令人惊讶,更令人惊讶的是,即使出现错误,append 操作也成功了:
>>> 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
类,它提供邮箱的基本访问方法,以及 MboxMailbox
、MaildirMailbox
、OutlookMailbox
等子类,它们处理各种特定的邮箱格式。
什么是方法?¶
方法是某个对象 x
上的一个函数,你通常将其调用为 x.name(arguments...)
。方法在类定义内部定义为函数:
class C:
def meth(self, arg):
return arg * 2 + self.attribute
什么是 self?¶
Self 只是方法第一个参数的常规名称。定义为 meth(self, a, b, c)
的方法应该像 x.meth(a, b, c)
一样被调用,其中 x
是定义该方法的类的某个实例;被调用的方法会认为它被调用为 meth(x, a, b, c)
。
如何检查对象是否是给定类或其子类的实例?¶
使用内置函数 isinstance(obj, cls)
。你可以通过提供一个元组而不是单个类来检查对象是否是多个类中的任何一个的实例,例如 isinstance(obj, (class1, class2, ...))
,也可以检查对象是否是 Python 的内置类型之一,例如 isinstance(obj, str)
或 isinstance(obj, (int, float, complex))
。
请注意 isinstance()
还会检查来自抽象基类的虚拟继承。因此,即使注册类没有直接或间接继承它,测试也会返回 True
。要测试“真实继承”,请扫描类的方法解析顺序:
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__()
实现会调用 object.__setattr__()
来设置自身的属性,而不会导致无限递归:
class X:
def __setattr__(self, name, value):
# Custom logic here...
object.__setattr__(self, name, value)
或者,可以通过直接向 self.__dict__
插入条目来设置属性。
如何从扩展了基类的派生类中调用基类中定义的方法?¶
使用内置函数 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
对于任何满足 isinstance(c, C)
的 c
,c.count
也指向 C.count
,除非 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
是当前类名,并去掉了任何前导下划线。
标识符可以在类内部不变地使用,但要在类外部访问它,必须使用篡改后的名称:
class A:
def __one(self):
return 1
def two(self):
return 2 * self.__one()
class B(A):
def three(self):
return 3 * self._A__one()
four = 4 * A()._A__one()
特别地,这并不能保证隐私,因为外部用户仍然可以故意访问私有属性;许多 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
。身份测试通常比相等测试快。并且与相等测试不同,身份测试保证返回布尔值 True
或 False
。
然而,只有在对象身份得到保证时,身份测试才能替代相等测试。通常,有三种情况保证身份:
赋值创建新名称,但不会改变对象身份。在赋值
new = old
之后,保证new is old
。将对象放入存储对象引用的容器中不会改变对象身份。在列表赋值
s[0] = x
之后,保证s[0] is x
。如果一个对象是单例,这意味着该对象只能存在一个实例。在赋值
a = None
和b = None
之后,保证a is b
,因为None
是一个单例。
在大多数其他情况下,不建议进行身份测试,而首选相等测试。特别是,不应使用身份测试来检查像 int
和 str
这样的常量,它们不保证是单例的:
>>> 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
在标准库代码中,你会看到几种正确使用身份测试的常见模式:
正如 PEP 8 所建议的,身份测试是检查
None
的首选方式。这在代码中读起来就像纯英语,并避免了与其他可能具有布尔值但评估为假的对象混淆。当
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
容器实现有时需要使用身份测试来增强相等测试。这可以防止代码被诸如
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 文件?¶
当模块首次导入时(或自当前编译文件创建以来源文件已更改时),包含编译代码的 .pyc
文件应在包含 .py
文件的目录的 __pycache__
子目录中创建。.pyc
文件的文件名将以与 .py
文件相同的名称开头,并以 .pyc
结尾,中间部分取决于创建它的特定 python
二进制文件。(有关详细信息,请参阅 PEP 3147。)
一个可能导致不创建 .pyc
文件 的原因是源文件所在目录的权限问题,这意味着无法创建 __pycache__
子目录。例如,如果你以一个用户开发,但以另一个用户运行(比如你正在使用 Web 服务器进行测试),就可能发生这种情况。
除非设置了PYTHONDONTWRITEBYTECODE
环境变量,否则如果你导入模块且 Python 能够(权限、可用空间等)创建 __pycache__
子目录并将编译后的模块写入该子目录,则会自动创建 .pyc 文件。
在顶层脚本上运行 Python 不被视为导入,也不会创建 .pyc
文件。例如,如果你有一个顶层模块 foo.py
导入了另一个模块 xyz.py
,当你运行 foo
时(通过在 shell 命令中输入 python foo.py
),会为 xyz
创建一个 .pyc
文件,因为 xyz
被导入了,但不会为 foo
创建 .pyc
文件,因为 foo.py
没有被导入。
如果你需要为 foo
创建一个 .pyc
文件——也就是说,为一个未被导入的模块创建一个 .pyc
文件——你可以使用 py_compile
和 compileall
模块来完成。
py_compile
模块可以手动编译任何模块。一种方法是在该模块中交互式地使用 compile()
函数:
>>> import py_compile
>>> py_compile.compile('foo.py')
这会将 .pyc
文件写入到与 foo.py
相同位置的 __pycache__
子目录中(或者你可以使用可选参数 cfile
覆盖此设置)。
你还可以使用 compileall
模块自动编译一个或多个目录中的所有文件。你可以通过在 shell 提示符下运行 compileall.py
并提供包含要编译的 Python 文件的目录路径来完成此操作:
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 不太喜欢这种方法,因为 import 语句出现在一个奇怪的地方,但它确实有效。
Matthias Urlichs 建议重构你的代码,以便递归导入根本不必要。
这些解决方案并非互斥。
__import__('x.y.z') 返回 ;如何获取 z? ¶
考虑使用来自 importlib
的便捷函数 import_module()
:
z = importlib.import_module('x.y.z')
当我编辑导入的模块并重新导入时,更改没有显示。为什么会发生这种情况?¶
出于效率和一致性的原因,Python 只在模块第一次导入时读取模块文件。如果不是这样,在一个由许多模块组成,并且每个模块都导入相同基本模块的程序中,基本模块将被多次解析和重新解析。要强制重新读取已更改的模块,请执行以下操作:
import importlib
import modname
importlib.reload(modname)
警告:这种技术并非百分之百万无一失。特别是,包含以下语句的模块:
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'