编程常见问题

目录

一般性问题

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

对。

下面介绍了几种用于Python的调试器,以及内置函数 breakpoint() 允许你掉进其中任何一个。

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

作为标准python发行版(通常作为tools/scripts/idle提供)的一部分,idle交互式开发环境包括一个图形调试器。

pythonwin是一个包含基于pdb的GUI调试器的python ide。pythonwin调试器为断点着色,并具有许多很酷的功能,例如调试非pythonwin程序。pythonwin是 Python for Windows Extensions _项目和作为ActivePython分发的一部分(请参阅https://www.activestate.com/activepython)。

Eric 是一个基于PYQT和闪烁编辑组件的IDE。

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

有许多商业化的Python IDE,其中包括图形化的调试器。它们包括:

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

对。

PylintPyflakes 做一些基本的检查,这样可以帮助你更快地发现错误。

静态类型检查程序,例如 MypyPyrePytype 无法检查python源代码中的类型提示。

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

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

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

它的工作原理是:递归地扫描源代码中的import语句(以两种形式),并在标准的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

导致未绑定的本地错误:

>>> 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个计算 x**2 . 你可能会期望,当被调用时,他们会分别返回, 014916 .但是,当你真的尝试的时候,你会看到他们都回来了 16 ::

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

这是因为 x 不是lambda本地的,而是在外部作用域中定义的,并且在调用lambda时访问它---而不是在定义lambda时访问它。在循环结束时, x4 ,所有函数现在都返回 4**2 ,即 16 . 您还可以通过更改 x 看看羔羊的结果是如何变化的:

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

为了避免这种情况,需要保存lambda局部变量中的值,这样它们就不依赖于全局变量的值。 x ::

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

在这里, n=x 创建新变量 n 局部的lambda,并在定义lambdatimeit算,以便它具有与 x 就在那个时候。这意味着 n0 在第一个lambda中, 1 第二, 2 在第三个,等等。因此,每个lambda现在将返回正确的结果:

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

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

如何跨模块共享全局变量?

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

配置:

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

模型PY::

import config
config.x = 1

主.py::

import config
import mod
print(config.x)

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

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

一般来说,不要使用 from modulename import * .这样做会使导入程序的名称空间变得混乱,使linter更难检测未定义的名称。

导入文件顶部的模块。这样做可以明确您的代码需要什么其他模块,并避免模块名称是否在范围内的问题。每行使用一个导入可以很容易地添加和删除模块导入,但每行使用多个导入可以减少屏幕空间。

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

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

  2. 第三方库模块(任何安装在python站点包目录中的模块)——例如mx.datetime、zodb、pil.image等。

  3. 本地开发的模块

有时需要将导入移动到函数或类,以避免循环导入出现问题。戈登·麦克米兰说:

当两个模块都使用“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 can only provide two parameters and optionally pass _cache by keyword
def expensive(arg1, arg2, *, _cache={}):
    if (arg1, arg2) in _cache:
        return _cache[(arg1, arg2)]

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

您可以使用一个包含字典的全局变量,而不是默认值;这是一个品味问题。

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

使用 *** 函数参数列表中的说明符;这将为您提供作为元组的位置参数和作为字典的关键字参数。然后,在调用另一个函数时,可以使用 *** ::

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

参数和参数之间有什么区别?

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

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

foobar关键字参数 是的参数 func .但是,调用时 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 ,这可能会导致程序生成易于诊断的错误。

但是,有一类操作,同一个操作有时具有不同类型的不同行为:增广的赋值运算符。例如, += 改变列表,但不改变元组或int (a_list += [1, 2, 3] 等于 a_list.extend([1, 2, 3]) 变异 a_listsome_tuple += (1, 2, 3)some_int += 1 创建新对象)。

换言之:

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

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

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

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

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

  1. 通过返回结果的元组:

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

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

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

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

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

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

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

    几乎没有一个好的理由让事情复杂化。

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

如何在Python中生成高阶函数?

您有两个选择:可以使用嵌套范围,也可以使用可调用对象。例如,假设您想要定义 linear(a,b) 它返回一个函数 f(x) 计算值的 a*x+b . 使用嵌套范围::

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

或使用可调用对象:

class linear:

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

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

在这两种情况下:

taxes = linear(0.3, 2)

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

可调用对象方法的缺点是速度较慢,导致代码稍长。但是,请注意,可调用文件集合可以通过继承共享其签名:

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

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

class counter:

    value = 0

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

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

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

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

在这里 inc()dec()reset() 类似于共享相同计数变量的函数。

如何在Python中复制对象?

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

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

newdict = olddict.copy()

可以通过切片复制序列:

new_l = l[:]

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

对于用户定义类的实例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

在Python2.5中引入这种语法之前,一个常见的习惯用法是使用逻辑运算符::

[expression] and [on_true] or [on_false]

然而,这个成语是不安全的,因为当 on_true 有一个错误的布尔值。因此,最好使用 ... if ... else ... 形式。

是否可以在python中编写模糊的一行程序?

对。通常这是通过嵌套完成的 lambda 在内部 lambda . 见以下三个例子,由于ULF易货:

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

子项们,别在家里试这个!

函数参数列表中的斜线(/)是什么意思?

函数参数列表中的斜线表示它前面的参数只是位置参数。仅位置参数是没有外部可用名称的参数。调用只接受位置参数的函数时,参数仅根据其位置映射到参数。例如, 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() WITH关键字参数将导致错误::

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

很少有真正的用例用于 i % j 什么时候? j 是否定的。什么时候? j 是积极的,有很多,实际上在所有这些方面,它对 i % j 成为 >= 0 . 如果时钟现在显示10,那么200小时前它显示了什么? -190 % 12 == 2 是有用的; -190 % 12 == -10 是一只等着咬的虫子。

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

对于整数,使用内置的 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() . 有关花式格式,请参见 格式化字符串文本格式字符串语法 章节,例如 "{{: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'

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

有各种各样的技巧。

  • 最好是使用将字符串映射到函数的字典。这种技术的主要优点是字符串不需要匹配函数的名称。这也是用于模拟案例结构的主要技术:

    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 WHOTO .

性能

我的程序太慢了。我该怎么加速?

总的来说,这很难。首先,下面列出了潜水前需要记住的事项:

  • Python实现的性能特性各不相同。本常见问题解答的重点是 CPython .

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

  • 你应该总是在你的计划中找到热点 before 正在尝试优化任何代码(请参见 profile 模块)。

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

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

也就是说,有很多技巧可以加快Python代码的速度。以下是实现可接受性能水平的一些基本原则:

  • 使您的算法更快(或更改为更快的算法)可以产生比尝试在代码上撒上微优化技巧更大的好处。

  • 使用正确的数据结构。研究文件 内置类型 以及 collections 模块。

  • 当标准库为做某件事情提供了一个原语时,它很可能(尽管不能保证)比您可能想到的任何替代方法都要快。对于用C编写的原语(如内置函数和一些扩展类型),这是双重的。例如,确保使用 list.sort() 内置方法或相关 sorted() 进行排序的函数(请参见 如何排序 例如中等高级用法)。

  • 抽象倾向于创建间接的,并迫使解释器进行更多的工作。如果间接的级别超过了所做的有用工作的数量,那么您的程序将变慢。您应该避免过度抽象,特别是在小函数或方法的形式下(这通常对可读性有害)。

如果您已经达到纯Python所能允许的极限,那么有一些工具可以让您走得更远。例如, Cython 可以将稍微修改过的Python代码版本编译成C扩展,并且可以在许多不同的平台上使用。Cython可以利用编译(和可选的类型注释)使代码比解释时快得多。如果你对你的C编程技能有信心,你也可以 write a C extension module 你自己。

参见

wiki页面致力于 performance tips .

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

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) 将任何序列(实际上是任何iterable)转换为具有相同顺序的相同项的元组。

例如, tuple([1, 2, 3]) 产量 (1, 2, 3)tuple('abc') 产量 ('a', 'b', 'c') . 如果参数是一个元组,它不会生成副本,而是返回相同的对象,因此调用 tuple() 当您不确定对象已经是元组时。

类型构造函数 list(seq) 将任何序列或ITerable转换为具有相同顺序的相同项的列表。例如, 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秘诀》,以了解实现此目的的多种方法:

如果您不介意对列表重新排序,请对其排序,然后从列表的末尾进行扫描,同时删除重复项::

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

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

如何从列表中删除多个项目

与删除重复项一样,可以使用delete条件反向显式迭代。然而,使用切片替换和隐式或显式的正向迭代更容易和更快。以下是三种变体:

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汽车的类似物是 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]

为什么要做一个元组 [i] += [“项目”] 添加工作时引发异常?

这是因为增加的赋值运算符是 分配 以及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__ magic方法,当 += 执行增广赋值,其返回值是在赋值语句中使用的值;以及(b)对于列表, __iadd__ 等于调用 extend 并返回列表。这就是为什么我们在列表中这么说, += 是“速记” list.extend ::

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

这相当于:

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

由ou列表指向的对象已被变异,指向变异对象的指针被重新分配给 a_list . 赋值的最终结果是no op,因为它是指向同一对象的指针, 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中,使用 key 的参数 list.sort() 方法:

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

物体

什么是类?

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

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

什么是方法?

方法是某个对象上的函数 x 你通常称之为 x.name(arguments...) . 方法被定义为类定义中的函数:

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

什么是自我?

self只是方法第一个参数的常规名称。定义为 meth(self, a, b, c) 应称为 x.meth(a, b, c) 例如 x 定义所在的类;被调用的方法将认为它被调用为 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__ 方法;查阅 the language reference 有关控制属性访问的详细信息。

请注意,对于更一般的情况,授权可能会变得更加棘手。当必须设置和检索属性时,类必须定义 __setattr__() 方法也是如此,它必须如此小心。基本实施 __setattr__() 大致相当于:

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

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

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

使用内置 super() 功能:

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

在示例中, super() 将自动确定从中调用它的实例( self 值),查找 method resolution order (MRO)与 type(self).__mro__ ,并在之后返回下一行 Derived 在MRO中: Base .

如何组织代码以使更改基类更容易?

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

class Base:
    ...

BaseAlias = Base

class Derived(BaseAlias):
    ...

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

在Python中支持静态数据和静态方法(在C++或Java意义上)。

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

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

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

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

c.count 也指 C.count 对于任何 c 这样的话 isinstance(c, C) 保留,除非被重写 c 自身或通过基类搜索路径上的某个类 c.__class__ 回到 C .

注意:在c方法中,类似 self.count = 42 在中新建一个名为“count”的不相关实例 self 类静态数据名称的dict.rebinding必须始终指定类,无论是否在方法内::

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 (至少两个前导下划线,最多一个尾随下划线)文本替换为 _classname__spam 在哪里 classname 是当前类名,去掉任何前导下划线。

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

我的类定义 __del__ 但在删除对象时不调用它。

这有几个可能的原因。

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

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

尽管有循环收集器,但是定义一个显式的 close() 方法来调用对象。这个 close() 然后,方法可以移除引用子对象的属性。不要调用 __del__() 直接—— __del__() 应该调用 close()close() 应确保可以为同一对象多次调用它。

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

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

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

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

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

这个 id() builtin返回一个整数,该整数保证在对象的生存期内是唯一的。因为在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具有创建 __pycache__ 并将编译后的模块写入该子目录。

在顶级脚本上运行python不被视为导入和否 .pyc 将被创建。例如,如果您有顶级模块 foo.py 导入另一个模块 xyz.py 当你运行 foo (打字) python foo.py 作为shell命令),a .pyc 将为创建 xyz 因为 xyz 是输入的,但不是 .pyc 将为创建文件 foo 自从 foo.py 没有被导入。

如果需要创建 .pyc 文件供 foo --也就是说,创建一个 .pyc 未导入模块的文件--可以使用 py_compilecompileall 模块。

这个 py_compile 模块可以手动编译任何模块。一种方法是使用 compile() 在该模块中以交互方式工作:

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

这将写入 .pyc 到A __pycache__ 子目录与 foo.py (或者您可以用可选参数覆盖它 cfile

您还可以使用 compileall 模块。您可以通过运行 compileall.py 并提供包含要编译的python文件的目录路径:

python -m compileall .

如何查找当前模块名称?

通过查看预定义的全局变量,模块可以找到自己的模块名。 __name__ . 如果这个有价值 '__main__' ,程序作为脚本运行。通常通过导入使用的许多模块也提供命令行接口或自检,并且仅在检查后执行此代码。 __name__ ::

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

if __name__ == '__main__':
    main()

如何让模块相互导入?

假设您有以下模块:

Fo.Py::

from bar import bar_var
foo_var = 1

PY::

from foo import foo_var
bar_var = 2

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

  • 主要输入foo

  • 创建foo的空全局

  • 已编译foo并开始执行

  • 导入栏

  • 为条形图创建空全局

  • BAR已编译并开始执行

  • BAR导入foo(这是一个no op,因为已经有一个名为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 声明

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

范罗森不太类似于这种方法,因为输入产品出现在一个奇怪的地方,但它确实起作用。

MatthiasUrlichs建议重新构造代码,这样就不必首先进行递归导入。

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

_ import_uu('x.y.z')返回<module'x'>;如何获取z?

考虑使用便利功能 import_module()importlib 取而代之的是:

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

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

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

import importlib
import modname
importlib.reload(modname)

警告:这项技术不是100%的防伪。尤其是包含如下语句的模块:

from modname import some_objects

将继续使用导入对象的旧版本。如果模块包含类定义,现有的类实例将 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'