Skip to main content

编程常见问题

Contents

一般的问题

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

是。

pdb模块是一个简单但足够的用于Python的控制台模式调试器。它是标准Python库的一部分,是 documented in the Library Reference Manual。您还可以使用pdb的代码作为示例编写自己的调试器。

IDLE交互式开发环境是标准Python分发包(通常可用作Tools/scripts/idle)的一部分,包括一个图形调试器。

PythonWin是一个包含基于pdb的GUI调试器的Python IDE。 Pythonwin调试器颜色断点,并有很多很酷的功能,如调试非Pythonwin程序。 Pythonwin可作为 Python的Windows扩展 项目的一部分,并作为ActivePython发行版的一部分(参见 https://www.activestate.com/activepython )。

Boa构造函数 是一个使用wxWidgets的IDE和GUI构建器。它提供了可视化框架创建和操作,对象检查器,源代码上的许多视图,如对象浏览器,继承层次结构,doc字符串生成的html文档,高级调试器,集成帮助和Zope支持。

埃里克 是一个基于PyQt和Scintilla编辑组件的IDE。

Pydb是标准Python调试器pdb的一个版本,修改为与DDD(数据显示调试器)配合使用,DDD是一个流行的图形调试器前端。 Pydb可以在 http://bashdb.sourceforge.net/pydb/ 找到,DDD可以在 https://www.gnu.org/software/ddd 找到。

有许多商业Python IDE包括图形调试器。他们包括:

有没有工具来帮助找到错误或执行静态分析?

是。

PyChecker是一个静态分析工具,它在Python源代码中发现错误,并警告代码复杂性和风格。你可以从 http://pychecker.sourceforge.net/ 获得PyChecker。

Pylint 是另一个工具,用于检查模块是否满足编码标准,并且还可以编写插件以添加自定义功能。除了PyChecker执行的错误检查外,Pylint还提供了一些额外的功能,例如检查行长度,变量名是否根据编码标准格式良好,是否完全实现了声明接口等。 https://docs.pylint.org/ 提供了Pylint的完整列表。

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

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

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

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

显然,冻结需要一个C编译器。还有几个其他的实用程序没有。一个是Thomas Heller的py2exe(仅限Windows)

另一个工具是Anthony Tuininga的 cx_Freeze

有没有编码标准或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) 尝试打印未初始化的局部变量并导致错误时。

在上面的示例中,您可以通过声明外部变量global:

>>> 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 声明中用于识别副作用的有用性。

为什么在具有不同值的循环中定义的lambdas都返回相同的结果?

假设你使用for循环来定义一些不同的lambda(或者甚至是plain函数),例如。:

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

这给了一个列表,其中包含5个计算 x**2 的lambdas。你可能期望,当被调用时,它们将分别返回 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 为lambda创建一个新的本地变量 n,并在定义lambda时计算,使得它具有与 x 在循环中该点相同的值。这意味着 n 的值将在第一个lambda中为 0,在第二个中为 1,在第三个中为 2,以此类推。因此,每个lambda现在将返回正确的结果:

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

注意,这种行为不是lambdas特有的,但也适用于常规函数。

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

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

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)

注意,使用模块也是实现Singleton设计模式的基础,同样的原因。

在模块中使用导入的“最佳做法”是什么?

一般来说,不要使用 from modulename import *。这样做会干扰进口商的命名空间,并且使得linters更难以检测未定义的名称。

导入文件顶部的模块。这样做清楚了你的代码需要什么其他模块,避免模块名称是否在范围内的问题。每行使用一个导入可以轻松添加和删除模块导入,但每行使用多个导入会占用较少的屏幕空间。

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

  1. 标准库模块 sysosgetoptre

  2. 第三方库模块(任何安装在Python site-packages目录中)。 mx.DateTime,ZODB,PIL.Image等。

  3. 本地开发的模块

有时需要将导入移动到函数或类,以避免循环导入的问题。 Gordon McMillan说:

循环导入是正确的,其中两个模块使用“import <module>”形式的导入。当第二个模块想要从第一个模块中获取一个名称(“从模块导入名称”)并且导入在顶层时,它们失败。这是因为第一个名称还不可用,因为第一个模块正在忙于导入第二个。

在这种情况下,如果第二个模块仅用于一个函数,则导入可以轻松地移动到该函数中。在调用导入时,第一个模块将完成初始化,第二个模块可以进行导入。

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

只有当需要解决诸如避免循环导入或试图减少模块的初始化时间等问题时,才将导入移动到本地作用域(例如函数定义内部)。如果许多导入是不必要的,这种技术尤其有用,这取决于程序的执行方式。如果模块只在该函数中使用,您可能还需要将导入移动到函数中。注意,第一次加载模块可能是昂贵的,因为模块的一次性初始化,但是多次加载模块几乎是免费的,仅花费几个字典查找。即使模块名称超出范围,该模块可能在 sys.modules 中可用。

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

这种类型的bug通常咬住新手程序员。考虑这个函数:

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 will never provide a third parameter for this function.
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. 列表是 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

我们可以看到,在这种情况下,xy 不再相等。这是因为整数是 immutable,当我们做 x = x + 1 时,我们不是通过增加它的值来改变int 5;相反,我们创建一个新对象(int 6)并将其分配给 x (即,改变 x 所指的对象)。在这个赋值之后,我们有两个对象(ints 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 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
    

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

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

  3. 通过传递一个可变的(可变的就地)对象:

    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
    
  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)
    print(args['a'], args['b'])
    
  5. 或者捆绑类实例中的值:

    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) 返回包含实例属性以及由其类定义的方法和属性的名称的字母表列表。

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

一般来说,它不能,因为对象不真的有名字。基本上,赋值总是将一个名字绑定到一个值; 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 有一个假的布尔值。因此,使用 ... 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 % jj 的标志相同的愿望。如果你想要,也想要:

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

那么整数除法必须返回floor。 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') == 144int('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()。有关花式格式,请参阅 格式化字符串文字格式字符串语法 部分,例如 "{: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()eval() 来解析函数名称:

    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() 这种方式很好。

有scanf()或sscanf()等效吗?

不是这样的。

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

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

性能

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

这是一个艰难的一般,。首先,下面是潜水前要记住的事项:

  • Python实现的性能特性不同。本FAQ主要针对 CPython

  • 不同操作系统的行为可能不同,尤其是在谈论I/O或多线程时。

  • 您应该总是在您的程序 before 中找到热点,尝试优化任何代码(参见 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() 内置函数,这是Python 2.4中的新功能:

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 模块还提供了创建具有紧凑表示的固定类型数组的方法,但它们的索引比列表慢。还要注意,Numeric扩展和其他定义了具有各种特性的数组类结构。

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

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

如果需要可变性,可以使用列表而不是元组。这里lisp车的类似物是 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

这将生成一个包含长度为2的3个不同列表的列表。您还可以使用列表推导:

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

异常是有点更令人惊讶,更令人惊讶的是,即使有一个错误,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']

最后一步的替代方法是:

>>> result = []
>>> for p in pairs: result.append(p[1])

如果你觉得这更清晰,你可能更喜欢使用这个而不是最终的列表解析。然而,对于长列表,它几乎是慢两倍。为什么?首先,append() 操作必须重新分配内存,虽然它使用一些技巧来避免每次都这样做,但它仍然必须偶尔做到,并且成本相当高。第二,表达式“result.append”需要额外的属性查找,第三,由于必须进行所有这些函数调用,速度降低。

对象

什么是类?

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

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

什么是方法?

方法是一些对象 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 (或 Base 的基类之一)中定义的方法 meth() 称为 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 自己的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 是当前类名,带有任何前导下划线。

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

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

这有几个可能的原因。

del语句不一定调用 __del__() - 它只是减少对象的引用计数,如果这到达零 __del__() 被调用。

如果您的数据结构包含圆形链接(例如,每个子代都有父引用,每个父代都有子代列表的树),引用计数将永远不会回到零。一旦Python运行一个算法来检测这样的循环,但是垃圾收集器可能在最后一次对数据结构的引用消失后运行一段时间,因此您的 __del__() 方法可能在不方便和随机的时间被调用。如果你想重现一个问题,这是不方便的。更糟的是,对象的 __del__() 方法执行的顺序是任意的。你可以运行 gc.collect() 强制收集,但有 are 病理情况下,对象将永远不会收集。

尽管循环收集器,仍然是一个好主意,定义一个显式的 close() 方法对象被调用,当你完成他们。然后 close() 方法可以删除引用subobjecs的属性。不要直接调用 __del__() - __del__() 应该调用 close()close() 应该确保对同一个对象可以多次调用它。

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

最后,如果您的 __del__() 方法引发异常,则会向 sys.stderr 输出警告消息。

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

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

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

id() 内置函数返回一个整数,该整数在对象的生命周期内保证是唯一的。因为在CPython中,这是对象的内存地址,它频繁发生,在对象从内存中删除后,下一个新创建的对象被分配在内存中的相同位置。这通过这个例子说明:

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

两个ids属于先前创建的不同的整数对象,并在执行 id() 调用后立即删除。要确保要检查其id的对象仍然存在,请创建对对象的另一个引用:

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

模块

如何创建.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 已导入,但不会有 .pyc 文件为 foo 创建,因为 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

问题是解释器将执行以下步骤:

  • 主要进口foo

  • 创建foo的空全局变量

  • foo被编译并开始执行

  • foo导入栏

  • 创建空的全局变量

  • bar被编译并开始执行

  • bar import 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建议在每个模块中按以下顺序执行步骤:

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

  • import 语句

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

van Rossum不喜欢这种方法,因为进口出现在一个奇怪的地方,但它的工作。

Matthias Urlichs建议重构您的代码,以便递归导入不是必要的。

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

__import __(’x.y.z’)返回<module’x’>;我如何得到z?

考虑使用来自 importlib 的方便函数 import_module():

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

当我编辑导入的模块并重新导入时,更改不会显示。为什么会这样?

出于效率和一致性的考虑,Python只在第一次导入模块时读取模块文件。如果没有,在由许多模块组成的程序中,每个模块导入相同的基本模块,基本模块将被解析和重新解析多次。要强制重新读取已更改的模块,请执行此操作:

import importlib
import modname
importlib.reload(modname)

警告:这种技术不是100%防呆。特别是,包含语句的模块

from modname import some_objects

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

>>> 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'