编程常见问题 — Python 文档
编程常见问题
内容
- 编程常见问题
- 一般的问题
- 核心语言
- 当变量有值时,为什么我会收到 UnboundLocalError?
- Python中局部变量和全局变量的规则是什么?
- 为什么在具有不同值的循环中定义的 lambda 都返回相同的结果?
- 如何跨模块共享全局变量?
- 在模块中使用 import 的“最佳实践”是什么?
- 为什么对象之间共享默认值?
- 如何将可选参数或关键字参数从一个函数传递到另一个函数?
- 参数和参数之间有什么区别?
- 为什么更改列表“y”也会更改列表“x”?
- 如何编写带有输出参数的函数(按引用调用)?
- 你如何在 Python 中创建一个高阶函数?
- 如何在 Python 中复制对象?
- 如何找到对象的方法或属性?
- 我的代码如何发现对象的名称?
- 逗号运算符的优先级是怎么回事?
- 是否有等效于 C 的“?:”三元运算符?
- 是否可以在 Python 中编写混淆的单行代码?
- 函数参数列表中的斜杠(/)是什么意思?
- 数字和字符串
- 表现
- 序列(元组/列表)
- 对象
- 什么是班级?
- 什么是方法?
- 什么是自我?
- 如何检查对象是给定类的实例还是它的子类的实例?
- 什么是委托?
- 如何从扩展它的派生类调用在基类中定义的方法?
- 如何组织我的代码以更轻松地更改基类?
- 如何创建静态类数据和静态类方法?
- 如何在 Python 中重载构造函数(或方法)?
- 我尝试使用 __spam,但收到关于 _SomeClassName__spam 的错误。
- 我的班级定义了 __del__ 但当我删除对象时它没有被调用。
- 如何获取给定类的所有实例的列表?
- 为什么
id()
的结果看起来不唯一? - 我什么时候可以使用 is 运算符进行身份测试?
- 子类如何控制不可变实例中存储的数据?
- 如何缓存方法调用?
- 模块
一般的问题
是否有带有断点、单步等的源代码级调试器?
是的。
下面描述了几个 Python 调试器,内置函数 breakpoint() 允许您进入其中任何一个。
pdb 模块是一个简单但足够的 Python 控制台模式调试器。 它是标准 Python 库的一部分,在库参考手册 中有 的记录。 您还可以使用 pdb 的代码作为示例编写自己的调试器。
IDLE 交互式开发环境是标准 Python 发行版(通常以 Tools/scripts/idle 形式提供)的一部分,包括一个图形调试器。
PythonWin 是一个 Python IDE,包含一个基于 pdb 的 GUI 调试器。 PythonWin 调试器为断点着色,并具有许多很酷的功能,例如调试非 PythonWin 程序。 PythonWin 可作为 pywin32 项目的一部分和 ActivePython 发行版的一部分。
Eric 是一个基于 PyQt 和 Scintilla 编辑组件构建的 IDE。
trepan3k 是一个类似 gdb 的调试器。
Visual Studio Code 是一个带有调试工具的 IDE,它与版本控制软件集成。
有许多包含图形调试器的商业 Python IDE。 它们包括:
是否有工具可以帮助查找错误或执行静态分析?
是的。
Pylint 和 Pyflakes 进行基本检查,帮助您更快地发现错误。
静态类型检查器如 Mypy、Pyre 和 Pytype 可以检查 Python 源代码中的类型提示。
如何从 Python 脚本创建独立的二进制文件?
如果您只需要一个独立的程序,用户无需先安装 Python 发行版即可下载和运行该程序,则不需要将 Python 编译为 C 代码的能力。 有许多工具可以确定程序所需的模块集,并将这些模块与 Python 二进制文件绑定在一起以生成单个可执行文件。
一种是使用freeze工具,它作为Tools/freeze
包含在Python源代码树中。 它将 Python 字节码转换为 C 数组; 一个 C 编译器,您可以将所有模块嵌入到一个新程序中,然后与标准 Python 模块链接。
它的工作原理是递归扫描您的源代码以查找导入语句(两种形式),并在标准 Python 路径和源目录(对于内置模块)中查找模块。 然后它将用 Python 编写的模块的字节码转换为 C 代码(可以使用 marshal 模块转换为代码对象的数组初始值设定项)并创建一个定制的配置文件,该文件只包含那些实际使用的内置模块程序。 然后它编译生成的 C 代码并将其与 Python 解释器的其余部分链接以形成一个独立的二进制文件,它的作用与您的脚本完全相同。
以下软件包可以帮助创建控制台和 GUI 可执行文件:
- Nuitka(跨平台)
- PyInstaller(跨平台)
- PyOxidizer(跨平台)
- cx_Freeze(跨平台)
- py2app(仅限 macOS)
- py2exe(仅限 Windows)
核心语言
当变量有值时,为什么我会收到 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 func1(a, b): ... a = 'new-value' # a and b are local names ... b = b + 1 # assigned to new objects ... return a, b # return new values ... >>> x, y = 'old-value', 99 >>> func1(x, y) ('new-value', 100)
这几乎总是最清晰的解决方案。
通过使用全局变量。 这不是线程安全的,不推荐使用。
通过传递一个可变(就地可变)对象:
>>> def func2(a): ... a[0] = 'new-value' # 'a' references a mutable list ... a[1] = a[1] + 1 # changes a shared object ... >>> args = ['old-value', 99] >>> func2(args) >>> args ['new-value', 100]
通过传入一个发生变异的字典:
>>> def func3(args): ... args['a'] = 'new-value' # args is a mutable dictionary ... args['b'] = args['b'] + 1 # change it in-place ... >>> args = {'a': 'old-value', 'b': 99} >>> func3(args) >>> args {'a': 'new-value', 'b': 100}
或者将值捆绑在一个类实例中:
>>> class Namespace: ... def __init__(self, /, **args): ... for key, value in args.items(): ... setattr(self, key, value) ... >>> def func4(args): ... args.a = 'new-value' # args is a mutable Namespace ... args.b = args.b + 1 # change object in-place ... >>> args = Namespace(a='old-value', b=99) >>> func4(args) >>> vars(args) {'a': 'new-value', 'b': 100}
几乎从来没有一个很好的理由把这个复杂化。
您最好的选择是返回一个包含多个结果的元组。
你如何在 Python 中创建一个高阶函数?
您有两种选择:可以使用嵌套作用域,也可以使用可调用对象。 例如,假设您想定义 linear(a,b)
,它返回一个计算值 a*x+b
的函数 f(x)
。 使用嵌套范围:
def linear(a, b):
def result(x):
return a * x + b
return result
或者使用可调用对象:
class linear:
def __init__(self, a, b):
self.a, self.b = a, b
def __call__(self, x):
return self.a * x + self.b
在这两种情况下,
taxes = linear(0.3, 2)
给出一个可调用对象,其中 taxes(10e6) == 0.3 * 10e6 + 2
。
可调用对象方法的缺点是它有点慢并且导致代码稍长。 但是,请注意,一组可调用对象可以通过继承共享它们的签名:
class exponential(linear):
# __init__ inherited
def __call__(self, x):
return self.a * (x ** self.b)
对象可以封装几种方法的状态:
class counter:
value = 0
def set(self, x):
self.value = x
def up(self):
self.value = self.value + 1
def down(self):
self.value = self.value - 1
count = counter()
inc, dec, reset = count.up, count.down, count.set
这里的 inc()
、dec()
和 reset()
就像共享相同计数变量的函数。
如何在 Python 中复制对象?
一般来说,在一般情况下尝试 copy.copy() 或 copy.deepcopy()。 并非所有对象都可以复制,但大多数可以。
有些对象可以更容易地复制。 字典有一个 copy() 方法:
newdict = olddict.copy()
可以通过切片复制序列:
new_l = l[:]
如何找到对象的方法或属性?
对于用户定义类的实例 x,dir(x)
返回按字母顺序排列的名称列表,其中包含实例属性和方法以及由其类定义的属性。
我的代码如何发现对象的名称?
一般来说,它不能,因为对象并没有真正的名称。 本质上,赋值总是将名称绑定到值; def
和 class
语句也是如此,但在这种情况下,该值是可调用的。 考虑以下代码:
>>> class A:
... pass
...
>>> B = A
>>> a = B()
>>> b = a
>>> print(b)
<__main__.A object at 0x16D07CC>
>>> print(a)
<__main__.A object at 0x16D07CC>
可以说该类有一个名称:即使它绑定到两个名称并通过名称 B 调用,创建的实例仍然报告为类 A 的实例。 但是,无法确定实例的名称是 a 还是 b,因为这两个名称都绑定到相同的值。
一般来说,您的代码不需要“知道特定值的名称”。 除非您有意编写内省程序,否则这通常表明改变方法可能是有益的。
在 comp.lang.python 中,Fredrik Lundh 曾经给出了一个很好的类比来回答这个问题:
就像你得到你在门廊上找到的那只猫的名字一样:猫(物体)本身不能告诉你它的名字,它并不真正关心——所以找出它叫什么的唯一方法是问你所有的邻居(命名空间),如果是他们的猫(对象)...…
....如果您发现它有很多名字,或者根本没有名字,请不要感到惊讶!
逗号运算符的优先级是怎么回事?
逗号不是 Python 中的运算符。 考虑这个会话:
>>> "a" in "b", "a"
(False, 'a')
由于逗号不是运算符,而是表达式之间的分隔符,因此上面的计算就像您输入的一样:
("a" in "b"), "a"
不是:
"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
不要在家里尝试这个,孩子们!
函数参数列表中的斜杠(/)是什么意思?
函数参数列表中的斜杠表示它之前的参数是仅位置参数。 仅位置参数是没有外部可用名称的参数。 在调用仅接受位置参数的函数时,参数将仅根据其位置映射到参数。 例如,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
是一个等待咬人的bug。
如何获得 int 文字属性而不是 SyntaxError?
尝试以正常方式查找 int
文字属性会出现语法错误,因为句点被视为小数点:
>>> 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()。 对于花哨的格式,请参阅 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()
使用 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()
这种方式效果很好。
表现
我的程序太慢了。 我如何加快速度?
总的来说,这是一个艰难的过程。 首先,这里列出了在进一步潜水之前要记住的事情:
- 性能特征因 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]
是除最后一个字符之外的所有字符串,这对于从字符串中删除尾部换行符很有用。
如何以相反的顺序迭代序列?
使用 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]
如果列表的所有元素都可以用作设置键(即 它们都是 hashable) 这通常更快
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
创建一个列表,其中包含 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']
对象
什么是班级?
类是通过执行类语句创建的特定对象类型。 类对象用作创建实例对象的模板,实例对象包含特定于数据类型的数据(属性)和代码(方法)。
一个类可以基于一个或多个其他类,称为其基类。 然后它继承其基类的属性和方法。 这允许通过继承来连续细化对象模型。 您可能有一个通用的 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() 还会检查来自 抽象基类 的虚拟继承。 因此,即使没有直接或间接继承自已注册的类,测试也会返回 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__()
实现必须修改 self.__dict__
来为自身存储本地状态,而不会导致无限递归。
如何从扩展它的派生类调用在基类中定义的方法?
使用内置的 super() 函数:
class Derived(Base):
def meth(self):
super().meth() # calls Base.meth
在示例中, super() 将自动确定调用它的实例(self
值),查找 方法解析顺序 (MRO) 与type(self).__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
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
我什么时候可以使用 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 方法起作用,但该类需要定义 __eq__ 和 __hash__ 方法,以便缓存可以检测相关属性更新:
class Weather:
"Example with a mutable station identifier"
def __init__(self, station_id):
self.station_id = station_id
def change_station(self, station_id):
self.station_id = station_id
def __eq__(self, other):
return self.station_id == other.station_id
def __hash__(self):
return hash(self.station_id)
@lru_cache(maxsize=20)
def historic_rainfall(self, date, units='cm'):
'Rainfall on a given date'
# Depends on the station_id, date, and units.
模块
如何创建 .pyc 文件?
当一个模块第一次被导入时(或者当源文件自当前编译文件被创建后发生了变化)一个包含编译代码的 .pyc
文件应该被创建在 __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
创建一个.pyc
,但是不会为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
bar.py
:
from foo import foo_var
bar_var = 2
问题是解释器将执行以下步骤:
- 主要进口
foo
- 创建
foo
的空全局变量 foo
编译并开始执行foo
进口bar
- 创建
bar
的空全局变量 bar
编译并开始执行bar
导入foo
(这是一个空操作,因为已经有一个名为foo
的模块)- 导入机制尝试从
foo
全局变量中读取foo_var
,以设置bar.foo_var = foo.foo_var
最后一步失败了,因为 Python 还没有完成解释 foo
并且 foo
的全局符号字典仍然是空的。
当你使用import foo
,然后在全局代码中尝试访问foo.foo_var
时,也会发生同样的事情。
此问题有(至少)三种可能的解决方法。
Guido van Rossum 建议避免使用 from <module> import ...
,并将所有代码放在函数中。 全局变量和类变量的初始化应该只使用常量或内置函数。 这意味着来自导入模块的所有内容都被引用为 <module>.<name>
。
Jim Roskind 建议在每个模块中按以下顺序执行步骤:
- 导出(不需要导入基类的全局变量、函数和类)
import
语句- 活动代码(包括从导入值初始化的全局变量)。
Van Rossum doesn’t like this approach much because the imports appear in a strange place, but it does work.
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'