编程常见问题 — Python 文档
编程常见问题
内容
- 编程常见问题
- 一般的问题
- 核心语言
- 当变量有值时,为什么我会收到 UnboundLocalError?
- Python中局部变量和全局变量的规则是什么?
- 为什么在具有不同值的循环中定义的 lambda 都返回相同的结果?
- 如何跨模块共享全局变量?
- 在模块中使用 import 的“最佳实践”是什么?
- 为什么对象之间共享默认值?
- 如何将可选参数或关键字参数从一个函数传递到另一个函数?
- 参数和参数之间有什么区别?
- 为什么更改列表“y”也会更改列表“x”?
- 如何编写带有输出参数的函数(按引用调用)?
- 你如何在 Python 中创建一个高阶函数?
- 如何在 Python 中复制对象?
- 如何找到对象的方法或属性?
- 我的代码如何发现对象的名称?
- 逗号运算符的优先级是怎么回事?
- 是否有等效于 C 的“?:”三元运算符?
- 是否可以在 Python 中编写混淆的单行代码?
- 数字和字符串
- 表现
- 序列(元组/列表)
- 字典
- 对象
- 模块
一般的问题
是否有带有断点、单步等的源代码级调试器?
是的。
pdb 模块是一个简单但足够的 Python 控制台模式调试器。 它是标准 Python 库的一部分,在库参考手册 中有 的记录。 您还可以使用 pdb 的代码作为示例编写自己的调试器。
IDLE 交互式开发环境是标准 Python 发行版(通常以 Tools/scripts/idle 形式提供)的一部分,包括一个图形调试器。
PythonWin 是一个 Python IDE,它包含一个基于 pdb 的 GUI 调试器。 Pythonwin 调试器为断点着色,并具有许多很酷的功能,例如调试非 Pythonwin 程序。 Pythonwin 作为 Python for Windows Extensions 项目的一部分和 ActivePython 发行版的一部分提供(参见 https://www.activestate.com/activepython)。
Boa Constructor 是一个使用 wxWidgets 的 IDE 和 GUI 构建器。 它提供可视化框架创建和操作、对象检查器、许多源视图,如对象浏览器、继承层次结构、文档字符串生成的 html 文档、高级调试器、集成帮助和 Zope 支持。
Eric 是一个基于 PyQt 和 Scintilla 编辑组件构建的 IDE。
Pydb 是标准 Python 调试器 pdb 的一个版本,经过修改以用于 DDD(数据显示调试器),这是一种流行的图形调试器前端。 Pydb 可以在 http://bashdb.sourceforge.net/pydb/ 找到,DDD 可以在 https://www.gnu.org/software/ddd 找到。
有许多包含图形调试器的商业 Python IDE。 它们包括:
- Wing IDE (https://wingware.com/)
- Komodo IDE (https://komodoide.com/)
- PyCharm (https://www.jetbrains.com/pycharm/)
是否有帮助查找错误或执行静态分析的工具?
是的。
PyChecker 是一个静态分析工具,可以发现 Python 源代码中的错误,并对代码的复杂性和风格发出警告。 您可以从 http://pychecker.sourceforge.net/ 获取 PyChecker。
Pylint 是另一个工具,可以检查模块是否满足编码标准,并且还可以编写插件以添加自定义功能。 除了 PyChecker 执行的错误检查之外,Pylint 还提供了一些附加功能,例如检查行长度、变量名称是否符合您的编码标准、声明的接口是否完全实现等。 https://docs.pylint.org/ 提供了 Pylint 功能的完整列表。
如何从 Python 脚本创建独立的二进制文件?
如果您只需要一个独立的程序,用户无需先安装 Python 发行版即可下载和运行该程序,则不需要将 Python 编译为 C 代码的能力。 有许多工具可以确定程序所需的模块集,并将这些模块与 Python 二进制文件绑定在一起以生成单个可执行文件。
一种是使用freeze工具,它作为Tools/freeze
包含在Python源代码树中。 它将 Python 字节码转换为 C 数组; 一个 C 编译器,您可以将所有模块嵌入到一个新程序中,然后与标准 Python 模块链接。
它的工作原理是递归扫描您的源代码以查找导入语句(两种形式),并在标准 Python 路径和源目录(对于内置模块)中查找模块。 然后它将用 Python 编写的模块的字节码转换为 C 代码(可以使用 marshal 模块转换为代码对象的数组初始值设定项)并创建一个定制的配置文件,该文件只包含那些实际使用的内置模块程序。 然后它编译生成的 C 代码并将其与 Python 解释器的其余部分链接以形成一个独立的二进制文件,它的作用与您的脚本完全相同。
显然,freeze 需要一个 C 编译器。 还有其他几个实用程序没有。 一个是 Thomas Heller 的 py2exe(仅适用于 Windows),位于
另一个工具是 Anthony Tuininga 的 cx_Freeze。
核心语言
当变量有值时,为什么我会收到 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 循环来定义几个不同的 lambdas(甚至是普通函数),例如:
>>> squares = []
>>> for x in range(5):
... squares.append(lambda: x**2)
这为您提供了一个列表,其中包含 5 个计算 x**2
的 lambda。 您可能期望,当被调用时,它们将分别返回 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
,在第二个中是 1
,在第三个中是 2
,依此类推。 因此,每个 lambda 现在将返回正确的结果:
>>> squares[2]()
4
>>> squares[4]()
16
请注意,此行为并非 lambda 所特有,也适用于常规函数。
在模块中使用 import 的“最佳实践”是什么?
一般来说,不要使用from modulename import *
。 这样做会使导入器的命名空间变得混乱,并使 linter 更难检测未定义的名称。
在文件顶部导入模块。 这样做可以明确您的代码需要哪些其他模块,并避免模块名称是否在范围内的问题。 每行使用一个导入可以轻松添加和删除模块导入,但每行使用多个导入会占用更少的屏幕空间。
如果您按以下顺序导入模块,这是一种很好的做法:
- 标准库模块——例如
sys
、os
、getopt
、re
- 第三方库模块(安装在 Python 的 site-packages 目录中的任何东西)——例如 mx.DateTime、ZODB、PIL.Image 等
- 本地开发的模块
有时需要将导入移动到函数或类以避免循环导入出现问题。 戈登麦克米兰 说:
循环导入很好,其中两个模块都使用“导入 ”的进口形式。 当第二个模块想要从第一个模块中获取一个名称(“来自模块导入名称”)并且导入位于顶层时,它们会失败。 那是因为第一个模块中的名称尚不可用,因为第一个模块正忙于导入第二个模块。
在这种情况下,如果第二个模块仅在一个函数中使用,则可以轻松地将导入移到该函数中。 到调用导入时,第一个模块将完成初始化,第二个模块可以进行导入。
如果某些模块是特定于平台的,则可能还需要将导入移出顶级代码。 在这种情况下,甚至可能无法导入文件顶部的所有模块。 在这种情况下,在相应的特定于平台的代码中导入正确的模块是一个不错的选择。
仅当需要解决诸如避免循环导入或试图减少模块的初始化时间等问题时,才将导入移动到局部范围内,例如在函数定义中。 如果根据程序的执行方式,许多导入是不必要的,则此技术特别有用。 如果模块仅在该函数中使用过,您可能还想将导入移动到该函数中。 请注意,由于模块的一次初始化,第一次加载模块可能很昂贵,但多次加载模块实际上是免费的,只需要几次字典查找。 即使模块名称超出范围,该模块也可能在 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
时,我们不会通过增加它的值来改变 int 5
; 相反,我们正在创建一个新对象(int 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)
和 [ X143X] 创建新对象)。
换句话说:
- 如果我们有一个可变对象(list、dict、set等),我们可以使用一些特定的操作来改变它和所有引用的变量它会看到变化。
- 如果我们有一个不可变对象(str、int、tuple 等),所有引用它的变量将始终看到相同的值,但是将该值转换为新值的操作总是返回一个新对象。
如果你想知道两个变量是否指向同一个对象,你可以使用 is 运算符,或者内置函数 id()。
如何编写带有输出参数的函数(按引用调用)?
请记住,参数在 Python 中是通过赋值传递的。 由于赋值只是创建对对象的引用,因此调用方和被调用方中的参数名称之间没有别名,因此本身没有按引用调用。 您可以通过多种方式实现所需的效果。
通过返回结果元组:
def func2(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 x, y = func2(x, y) print(x, y) # output: new-value 100
这几乎总是最清晰的解决方案。
通过使用全局变量。 这不是线程安全的,不推荐使用。
通过传递一个可变(就地可变)对象:
def func1(a): a[0] = 'new-value' # 'a' references a mutable list a[1] = a[1] + 1 # changes a shared object args = ['old-value', 99] func1(args) print(args[0], args[1]) # output: 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) print(args['a'], args['b'])
或者将值捆绑在一个类实例中:
class callByRef: 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 callByRef args.b = args.b + 1 # change object in-place args = callByRef(a='old-value', b=99) func4(args) print(args.a, args.b)
几乎从来没有一个很好的理由把这个复杂化。
您最好的选择是返回一个包含多个结果的元组。
你如何在 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"
不是:
"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+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
不要在家里尝试这个,孩子们!
数字和字符串
如何指定十六进制和八进制整数?
要指定八进制数字,请在八进制值前面加上零,然后是小写或大写的“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
是一个等待咬人的bug。
如何将字符串转换为数字?
对于整数,使用内置的 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()。 对于花哨的格式,请参阅 Formatted string literals 和 Format String Syntax 部分,例如 "{: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()
-
def myFunc(): print("hello") fname = "myFunc" f = locals()[fname] f() f = eval(fname) f()
注意:使用 eval() 既缓慢又危险。 如果您对字符串的内容没有绝对的控制权,有人可能会传递一个导致执行任意函数的字符串。
是否有等效于 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()
这种方式效果很好。
表现
我的程序太慢了。 我如何加快速度?
总的来说,这是一个艰难的过程。 首先,以下是在进一步潜水之前要记住的事项清单:
- 性能特征因 Python 实现而异。 此常见问题解答侧重于 CPython。
- 行为可能因操作系统而异,尤其是在谈论 I/O 或多线程时。
- 在尝试优化任何代码之前,您应该始终在程序 中找到热点 (请参阅 profile 模块)。
- 编写基准脚本将允许您在搜索改进时快速迭代(请参阅 timeit 模块)。
- 强烈建议在潜在地引入隐藏在复杂优化中的回归之前具有良好的代码覆盖率(通过单元测试或任何其他技术)。
话虽如此,有很多技巧可以加速 Python 代码。 以下是一些有助于达到可接受的性能水平的一般原则:
- 让你的算法更快(或改变为更快的算法)比试图在你的代码中散布微优化技巧可以产生更大的好处。
- 使用正确的数据结构。 内置类型和集合模块的研究文档。
- 当标准库提供用于做某事的原语时,它可能(虽然不能保证)比您可能想出的任何替代方案都快。 对于用 C 编写的原语(例如内置函数和一些扩展类型)来说,这是双重的。 例如,请务必使用 list.sort() 内置方法或相关的 sorted() 函数进行排序(请参阅 Sorting HOW TO 中高级用法示例)。
- 抽象倾向于创建间接性并迫使解释器更多地工作。 如果间接级别超过已完成的有用工作量,您的程序将会变慢。 你应该避免过度的抽象,尤其是在微小的函数或方法的形式下(这也往往不利于可读性)。
如果您已经达到了纯 Python 所能允许的极限,那么有一些工具可以带您走得更远。 例如,Cython 可以将稍微修改过的 Python 代码版本编译成 C 扩展,并且可以在许多不同的平台上使用。 Cython 可以利用编译(和可选的类型注释)使您的代码比解释时更快。 如果你对自己的C编程能力有信心,还可以【X64X】自己写一个C扩展模块【X94X】。
将多个字符串连接在一起的最有效方法是什么?
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]
是除最后一个字符之外的所有字符串,这对于从字符串中删除尾部换行符很有用。
如何以相反的顺序迭代序列?
使用 Python 2.4 中新增的 reversed() 内置函数:
for x in reversed(sequence):
... # do something with x ...
这不会触及您的原始序列,而是以相反的顺序构建一个新副本以进行迭代。
在 Python 2.3 中,您可以使用扩展的切片语法:
for x in sequence[::-1]:
... # 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]
如果列表的所有元素都可以用作设置键(即 它们都是 hashable) 这通常更快
mylist = list(set(mylist))
这会将列表转换为集合,从而删除重复项,然后返回到列表。
你如何在 Python 中创建一个数组?
使用列表:
["this", 1, "is", "an", "array"]
列表在时间复杂度上等同于 C 或 Pascal 数组; 主要区别在于 Python 列表可以包含许多不同类型的对象。
array
模块还提供了创建具有紧凑表示的固定类型数组的方法,但它们的索引速度比列表慢。 另请注意,数字扩展和其他扩展也定义了具有各种特征的类数组结构。
要获得 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
创建一个列表,其中包含 3 个对长度为 2 的相同列表的引用。 对一行的更改将显示在所有行中,这几乎肯定不是您想要的。
建议的方法是先创建一个所需长度的列表,然后用新创建的列表填充每个元素:
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]
为什么 a_tuple[i] += ['item'] 在加法起作用时引发异常?
这是因为增强赋值运算符是 assignment 运算符这一事实的组合,以及 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']
最后一步的替代方法是:
>>> result = []
>>> for p in pairs: result.append(p[1])
如果你觉得这更清晰,你可能更喜欢使用它而不是最终的列表理解。 但是,对于长列表,它的速度几乎是其两倍。 为什么? 首先,append()
操作必须重新分配内存,虽然它使用了一些技巧来避免每次都这样做,但它仍然必须偶尔这样做,而且成本相当高。 其次,表达式“result.append”需要额外的属性查找,第三,由于必须进行所有这些函数调用,因此速度有所降低。
对象
什么是班级?
类是通过执行类语句创建的特定对象类型。 类对象用作创建实例对象的模板,实例对象包含特定于数据类型的数据(属性)和代码(方法)。
一个类可以基于一个或多个其他类,称为其基类。 然后它继承其基类的属性和方法。 这允许通过继承来连续细化对象模型。 您可能有一个通用的 Mailbox
类,它提供邮箱的基本访问器方法,以及诸如 MboxMailbox
、MaildirMailbox
、OutlookMailbox
等处理各种特定邮箱的子类格式。
什么是方法?
方法是某个对象 x
上的函数,通常称为 x.name(arguments...)
。 方法被定义为类定义中的函数:
class C:
def meth(self, arg):
return arg * 2 + self.attribute
什么是自我?
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()。 如果您自己开发类,更合适的面向对象风格是在封装特定行为的类上定义方法,而不是检查对象的类并根据它是什么类做不同的事情。 例如,如果您有一个函数可以执行某些操作:
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__
来为自身存储本地状态,而不会导致无限递归。
如何从覆盖它的派生类调用在基类中定义的方法?
使用内置的 super() 函数:
class Derived(Base):
def meth(self):
super(Derived, self).meth()
对于 3.0 之前的版本,您可能正在使用经典类:对于诸如 class Derived(Base): ...
之类的类定义,您可以调用 Base
中定义的方法 meth()
(或 Base
的基类)作为 Base.meth(self, arguments...)
。 这里,Base.meth
是一个未绑定的方法,因此您需要提供 self
参数。
如何组织我的代码以更轻松地更改基类?
您可以为基类定义别名,在类定义之前为其分配真正的基类,并在整个类中使用别名。 然后,您只需更改分配给别名的值。 顺便说一句,如果您想动态决定(例如 取决于资源的可用性)使用哪个基类。 例子:
BaseAlias = <real base class>
class Derived(BaseAlias):
def meth(self):
BaseAlias.meth(self)
...
如何创建静态类数据和静态类方法?
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
也指任何 c
的 C.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
模块
如何创建 .pyc 文件?
当模块第一次被导入时(或者当源文件自当前编译文件被创建后发生变化时)一个包含编译代码的 .pyc
文件应该在 __pycache__
子目录中创建包含 .py
文件的目录。 .pyc
文件的文件名以与 .py
文件相同的名称开头,以 .pyc
结尾,中间部分取决于特定的 python
创建它的二进制文件。 (有关详细信息,请参阅 PEP 3147。)
可能无法创建 .pyc
文件的原因之一是包含源文件的目录存在权限问题,这意味着无法创建 __pycache__
子目录。 例如,如果您以一个用户身份开发但以另一个用户身份运行,例如您正在使用 Web 服务器进行测试,则可能会发生这种情况。
除非设置了 PYTHONDONTWRITEBYTECODE 环境变量,否则如果您正在导入模块并且 Python 有能力(权限、可用空间等),则会自动创建 .pyc 文件创建一个 __pycache__
子目录并将编译后的模块写入该子目录。
在顶级脚本上运行 Python 不被视为导入,也不会创建 .pyc
。 例如,如果您有一个顶级模块 foo.py
导入另一个模块 xyz.py
,那么当您运行 foo
(通过键入 python foo.py
作为 shell 命令),会为xyz
,因为xyz
被导入,但不会为foo
文件,因为[ X289X] 没有被导入。
如果您需要为 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
酒吧.py:
from foo import foo_var
bar_var = 2
问题是解释器将执行以下步骤:
- 主要进口 foo
- 为 foo 创建了空的全局变量
- foo 被编译并开始执行
- foo 进口酒吧
- 为 bar 创建了空的全局变量
- bar 被编译并开始执行
- bar 导入 foo (这是一个空操作,因为已经有一个名为 foo 的模块)
- 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__('xyz') 返回 ; 我怎么得到z?
考虑使用 importlib 中的便捷函数 import_module():
z = importlib.import_module('x.y.z')
当我编辑导入的模块并重新导入它时,更改不会显示。 为什么会发生这种情况?
出于效率和一致性的考虑,Python 仅在第一次导入模块时读取模块文件。 如果没有,在由许多模块组成的程序中,每个模块都导入相同的基本模块,基本模块将被多次解析和重新解析。 要强制重新读取已更改的模块,请执行以下操作:
import importlib
import modname
importlib.reload(modname)
警告:此技术不是 100% f 防工具。 特别是,包含以下语句的模块
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'