编程常见问题¶
一般性问题¶
是否有带断点、单步执行等的源代码级调试器?¶
有。
下面描述了几种 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 的局部变量,并在定义 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
本地开发的模块
有时,有必要将导入移动到函数或类中,以避免循环导入的问题。Gordon McMillan 说:
当两个模块都使用 “import <模块>” 形式的导入时,循环导入是可行的。当第二个模块想要从第一个模块中获取一个名称(“from module import name”)并且导入位于顶层时,循环导入就会失败。这是因为第一个模块中的名称尚不可用,因为第一个模块正在忙于导入第二个模块。
在这种情况下,如果第二个模块仅在一个函数中使用,则可以轻松地将导入移动到该函数中。当调用导入时,第一个模块将完成初始化,并且第二个模块可以进行导入。
如果某些模块是特定于平台的,则可能还需要将导入移出代码的顶层。在这种情况下,甚至可能无法在文件的顶部导入所有模块。在这种情况下,在相应的特定于平台的代码中导入正确的模块是一个不错的选择。
仅当有必要解决诸如避免循环导入或试图减少模块的初始化时间之类的问题时,才将导入移动到局部作用域(例如在函数定义内)。如果许多导入取决于程序的执行方式而变得不必要,则此技术特别有用。如果模块仅在该函数中使用,则你可能还需要将导入移动到函数中。请注意,由于模块的一次性初始化,第一次加载模块可能很耗时,但是多次加载模块几乎是免费的,仅需几次字典查找。即使模块名称已超出范围,该模块也可能在 sys.modules
中可用。
如何将可选或关键字参数从一个函数传递到另一个函数?¶
在函数的参数列表中使用 *
和 **
说明符来收集参数;这将为你提供一个包含位置参数的元组和一个包含关键字参数的字典。然后,你可以使用 *
和 **
在调用另一个函数时传递这些参数。
def f(x, *args, **kwargs):
...
kwargs['width'] = '14.3c'
...
g(x, *args, **kwargs)
实参和形参有什么区别?¶
形参(Parameters) 是在函数定义中出现的名称,而 实参(arguments) 是在调用函数时实际传递给函数的值。形参定义了函数可以接受的 实参类型。例如,给定函数定义:
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
都引用它。列表是可变的(mutable),这意味着你可以更改它们的内容。
在调用 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
不再相等。这是因为整数是不可变的(immutable),当我们执行 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 中编写带有输出参数(按引用调用)的函数?¶
请记住,在 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)
,它返回一个函数 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[:]
如何查找对象的方法或属性?¶
对于用户自定义类的实例 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"
不是
"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
是一个等待被咬的错误。
如何获取 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')
引发 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
更强大,并且更适合该任务。
“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 实现中有所不同。此 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 单元*。
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 中进行施瓦茨变换吗?¶
该技术归因于 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
。要测试“真正的继承”,请扫描该类的 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__()
实现调用 object.__setattr__()
来设置 self 上的属性,而不会导致无限递归。
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
自己的 dict 中创建一个新的、不相关的名为“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
。
但是,只有在确保对象身份时,才能将身份测试替代为相等性测试。通常,在三种情况下可以保证身份:
1) 赋值会创建新名称,但不会更改对象身份。在赋值 new = old
之后,可以保证 new is old
。
2) 将对象放入存储对象引用的容器中不会更改对象身份。在列表赋值 s[0] = x
之后,可以保证 s[0] is x
。
3) 如果对象是单例,则表示该对象只能存在一个实例。在赋值 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
在标准库代码中,你会看到几个正确使用身份测试的常见模式
1) 如 PEP 8 所建议的那样,身份测试是检查 None
的首选方法。这在代码中看起来像纯英文,并且避免与其他可能具有布尔值且计算结果为 false 的对象混淆。
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 方法将无法工作,因为它无法检测到属性的更改。
为了使 lru_cache 方法在 station_id 可变时也能工作,类需要定义 __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_compile
和 compileall
模块。
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 <模块> import ...
,并将所有代码放在函数内部。全局变量和类变量的初始化应仅使用常量或内置函数。这意味着从导入的模块中的所有内容都引用为 <模块>.<名称>
。
Jim Roskind 建议在每个模块中按以下顺序执行步骤
导出(全局变量、函数和不需要导入基类的类)
import
语句活动代码(包括从导入的值初始化的全局变量)。
Van Rossum 不太喜欢这种方法,因为导入出现在一个奇怪的地方,但它确实有效。
Matthias Urlichs 建议重构你的代码,以便首先不需要递归导入。
这些解决方案并非互斥。
__import__(‘x.y.z’) 返回 <模块 ‘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'