以下介绍了一些 Python 的调试器,用内置函数
breakpoint()
即可切入这些调试器中。
pdb 模块是一个简单但是够用的控制台模式 Python 调试器。 它是标准 Python 库的一部分,并且
已收录于库参考手册
。 你也可以通过使用 pdb 代码作为样例来编写你自己的调试器。
作为标准 Python 发行版附带组件的 IDLE 交互式环境(通常位于 Tools/scripts/idle)中包含一个图形化的调试器。
PythonWin 是一种 Python IDE,其中包含了一个基于 pdb 的 GUI 调试器。PythonWin 的调试器会为断点着色,并提供了相当多的超酷特性,例如调试非 PythonWin 程序等。PythonWin 是 pywin32 项目的组成部分,也是 ActivePython 发行版的组成部分。
Eric 是一个基于PyQt和Scintilla编辑组件构建的IDE。
trepan3k 是一个类似 gdb 的调试器。
Visual Studio Code 是包含了调试工具的 IDE,并集成了版本控制软件。
有數個商業化Python整合化開發工具包含圖形除錯功能。這些包含:
Pylint 和 Pyflakes 可进行基本检查来帮助你尽早捕捉漏洞。
静态类型检查器,例如 Mypy 、 Pyre 和 Pytype 可以检查Python源代码中的类型提示。
如果只是想要一个独立的程序,以便用户不必预先安装 Python 即可下载和运行它,则不需要将 Python 编译成 C 代码。有许多工具可以检测程序所需的模块,并将这些模块与 Python 二进制程序捆绑在一起生成单个可执行文件。
一种方案是使用 freeze 工具,它以
Tools/freeze
的形式包含在 Python 源代码树中。它将 Python 的字节码转换为 C 语言的数组;用 C 语言编译器可以将你的所有模块嵌入到一个新的程序中,然后与标准的 Python 模块连接。
它的工作原理是递归扫描源代码,获取两种格式的 import 语句,并在标准 Python 路径和源码目录(用于内置模块)检索这些模块。然后,把这些模块的 Python 字节码转换为 C 代码(可以利用 marshal 模块转换为代码对象的数组初始化器),并创建一个定制的配置文件,该文件仅包含程序实际用到的内置模块。然后,编译生成的 C 代码并将其与 Python 解释器的其余部分链接,形成一个自给自足的二进制文件,其功能与 Python 脚本代码完全相同。
下列包可以用于帮助创建控制台和 GUI 的可执行文件:
Nuitka (跨平台)
PyInstaller (跨平台)
PyOxidizer (跨平台)
cx_Freeze (跨平台)
py2app (仅限 macOS)
py2exe (僅限 Windows)
有的。 标准库模块所要求的编码风格记录于 PEP 8 之中。
因为在函数内部某处添加了一条赋值语句,导致之前正常工作的代码报出 UnboundLocalError 错误,这可能是有点令人惊讶。
這段程式碼:
>>> x = 10
>>> def bar():
... print(x)
>>> bar()
可以執行,但是這段程式:
>>> 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()
与类和实例变量貌似但不一样,其实以上是在修改外部作用域的变量值,为了提示这一点,这里需要显式声明一下。
>>> print(x)
你可以使用 nonlocal 关键字在嵌套作用域中执行类似的操作:
>>> def foo():
... x = 10
... def bar():
... nonlocal x
... print(x)
... x += 1
... bar()
... print(x)
>>> foo()
Python 的區域變數和全域變數有什麼規則?¶
函数内部只作引用的 Python 变量隐式视为全局变量。如果在函数内部任何位置为变量赋值,则除非明确声明为全局变量,否则均将其视为局部变量。
起初尽管有点令人惊讶,不过考虑片刻即可释然。一方面,已分配的变量要求加上 global 可以防止意外的副作用发生。另一方面,如果所有全局引用都要加上 global ,那处处都得用上 global 了。那么每次对内置函数或导入模块中的组件进行引用时,都得声明为全局变量。这种杂乱会破坏 global 声明用于警示副作用的有效性。
为什么在循环中定义的参数各异的 lambda 都返回相同的结果?¶
假设用 for 循环来定义几个取值各异的 lambda(即便是普通函数也一样):
>>> squares = []
>>> for x in range(5):
... squares.append(lambda: x**2)
以上会得到一个包含5个 lambda 函数的列表,这些函数将计算 x**2。大家或许期望,调用这些函数会分别返回 0 、1 、 4 、 9 和 16。然而,真的试过就会发现,他们都会返回 16 :
>>> squares[2]()
>>> squares[4]()
这是因为 x 不是 lambda 函数的内部变量,而是定义于外部作用域中的,并且 x 是在调用 lambda 时访问的——而不是在定义时访问。循环结束时 x 的值是 4 ,所以此时所有的函数都将返回 4**2 ,即 16 。通过改变 x 的值并查看 lambda 的结果变化,也可以验证这一点。
>>> x = 8
>>> squares[2]()
为了避免发生上述情况,需要将值保存在 lambda 局部变量,以使其不依赖于全局 x 的值:
>>> squares = []
>>> for x in range(5):
... squares.append(lambda n=x: n**2)
以上 n=x 创建了一个新的 lambda 本地变量 n,并在定义 lambda 时计算其值,使其与循环当前时点的 x 值相同。这意味着 n 的值在第 1 个 lambda 中为 0 ,在第 2 个 lambda 中为 1 ,在第 3 个中为 2,依此类推。因此现在每个 lambda 都会返回正确结果:
>>> squares[2]()
>>> squares[4]()
请注意,上述表现并不是 lambda 所特有的,常规的函数也同样适用。
如何跨模块共享全局变量?¶
在单个程序中跨模块共享信息的规范方法是创建一个特殊模块(通常称为 config 或 cfg)。只需在应用程序的所有模块中导入该 config 模块;然后该模块就可当作全局名称使用了。因为每个模块只有一个实例,所以对该模块对象所做的任何更改将会在所有地方得以体现。 例如:
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)
请注意,出于同样的原因,采用模块也是实现单例设计模式的基础。
导入模块的“最佳实践”是什么?¶
通常请勿使用 from modulename import * 。因为这会扰乱 importer 的命名空间,且会造成未定义名称更难以被 Linter 检查出来。
请在代码文件的首部就导入模块。这样代码所需的模块就一目了然了,也不用考虑模块名是否在作用域内的问题。每行导入一个模块则增删起来会比较容易,每行导入多个模块则更节省屏幕空间。
按如下顺序导入模块就是一种好做法:
标准库模块——比如: sys 、 os 、 getopt 、 re 等。
第三方库模块(安装于 Python site-packages 目录中的内容)——如 mx.DateTime、ZODB、PIL.Image 等。
本地开发的模块
为了避免循环导入引发的问题,有时需要将模块导入语句移入函数或类的内部。Gordon McMillan 的说法如下:
当两个模块都采用 "import <module>" 的导入形式时,循环导入是没有问题的。但如果第 2 个模块想从第 1 个模块中取出一个名称("from module import name")并且导入处于代码的最顶层,那导入就会失败。原因是第 1 个模块中的名称还不可用,这时第 1 个模块正忙于导入第 2 个模块呢。
如果只是在一个函数中用到第 2 个模块,那这时将导入语句移入该函数内部即可。当调用到导入语句时,第 1 个模块将已经完成初始化,第 2 个模块就可以进行导入了。
如果某些模块是平台相关的,可能还需要把导入语句移出最顶级代码。这种情况下,甚至有可能无法导入文件首部的所有模块。于是在对应的平台相关代码中导入正确的模块,就是一种不错的选择。
只有为了避免循环导入问题,或有必要减少模块初始化时间时,才把导入语句移入类似函数定义内部的局部作用域。如果根据程序的执行方式,许多导入操作不是必需的,那么这种技术尤其有用。如果模块仅在某个函数中用到,可能还要将导入操作移入该函数内部。请注意,因为模块有一次初始化过程,所以第一次加载模块的代价可能会比较高,但多次加载几乎没有什么花费,代价只是进行几次字典检索而已。即使模块名超出了作用域,模块在 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
参数默认值的特性有时会很有用处。 如果有个函数的计算过程会比较耗时,有一种常见技巧是将每次函数调用的参数和结果缓存起来,并在同样的值被再次请求时返回缓存的值。这种技巧被称为“memoize”,实现代码可如下所示:
# 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)
形参和实参之间有什么区别?¶
形参 是指出现在函数定义中的名称,而 实参 则是在调用函数时实际传入的值。 形参定义了一个函数能接受何种类型的实参。 例如,对于以下函数定义:
def func(foo, bar=None, **kwargs):
foo 、 bar 和 kwargs 是 func 的形参。 不过在调用 func 时,例如:
func(42, bar=314, extra=somevar)
42 、 314 和 somevar 则是实参。
为什么修改列表 'y' 也会更改列表 'x'?¶
如果代码编写如下:
>>> x = []
>>> y = x
>>> y.append(10)
或许大家很想知道,为什么在 y 中添加一个元素时, x 也会改变。
产生这种结果有两个因素:
变量只是指向对象的一个名称。执行 y = x 并不会创建列表的副本——而只是创建了一个新变量 y,并指向 x 所指的同一对象。这就意味着只存在一个列表对象,x 和 y 都是对它的引用。
列表属于 mutable 对象,这意味着它的内容是可以修改的。
在调用 append() 之后,该可变对象的内容由 [] 变为 [10]。由于 x 和 y 这两个变量引用了同一对象,因此用其中任意一个名称所访问到的都是修改后的值 [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 和 y 就不再相等了。因为整数是 immutable 对象,在执行 x = x + 1 时,并不会修改整数对象 5,给它加上 1;而是创建了一个新的对象(整数对象 6 )并将其赋给 x (也就是改变了 x 所指向的对象)。在赋值完成后,就有了两个对象(整数对象 6 和 5 )和分别指向他俩的两个变量( x 现在指向 6 而 y 仍然指向 5 )。
某些操作(例如 y.append(10) 和 y.sort() )会直接修改原对象,而看上去相似的另一些操作(例如 y = y + [10] 和 sorted(y) )则会创建新的对象。通常在 Python 中(以及所有标准库),直接修改原对象的方法将会返回 None ,以助避免混淆这两种不同类型的操作。因此如果误用了 y.sort() 并期望返回 y 的有序副本,则结果只会是 None ,这可能就能让程序引发一条容易诊断的错误。
不过还存在一类操作,用不同的类型执行相同的操作有时会发生不同的行为:即增量赋值运算符。例如,+= 会修改列表,但不会修改元组或整数(a_list += [1, 2, 3] 与 a_list.extend([1, 2, 3]) 同样都会改变 a_list,而 some_tuple += (1, 2, 3) 和 some_int += 1 则会创建新的对象)。
换而言之:
对于一个可变对象( 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)
['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)
{'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[:]
如何用代码获取对象的名称?¶
一般而言这是无法实现的,因为对象并不存在真正的名称。赋值本质上是把某个名称绑定到某个值上;def 和 class 语句同样如此,只是值换成了某个可调用对象。比如以下代码:
>>> class A:
... pass
>>> B = A
>>> a = B()
>>> b = a
>>> print(b)
<__main__.A object at 0x16D07CC>
>>> print(a)
<__main__.A object at 0x16D07CC>
可以不太严谨地说,上述类具有一个名称:即便它绑定了两个名称并通过名称 B 发起调用,可是创建出来的实例仍被视为是类 A 的实例。但无法说出实例的名称是 a 还是 b,因为这两个名称都被绑定到同一个值上了。
代码一般没有必要去“知晓”某个值的名称。通常这种需求预示着还是改变方案为好,除非真的是要编写内审程序。
在 comp.lang.python 中,Fredrik Lundh 在回答这样的问题时曾经给出过一个绝佳的类比:
这就像要知道家门口的那只猫的名字一样:猫(对象)自己不会说出它的名字,它根本就不在乎自己叫什么——所以唯一方法就是问一遍你所有的邻居(命名空间),这是不是他们家的猫(对象)……
……并且如果你发现它有很多名字或根本没有名字,那也不必惊讶!
逗号运算符的优先级是什么?¶
逗号不是 Python 的运算符。 请看以下例子:
>>> "a" in "b", "a"
(False, 'a')
由于逗号不是运算符,而只是表达式之间的分隔符,因此上述代码就相当于:
("a" in "b"), "a"
"a" in ("b", "a")
对于各种赋值运算符( = 、 += 等)来说同样如此。他们并不是真正的运算符,而只是赋值语句中的语法分隔符。
是否提供等价于 C 语言 "?:" 三目运算符的东西?¶
有的。语法如下:
[on_true] if [expression] else [on_false]
x, y = 50, 25
small = x if x < y else y
在 Python 2.5 引入上述语法之前,通常的做法是使用逻辑运算符:
[expression] and [on_true] or [on_false]
然而这种做法并不保险,因为当 on_true 为布尔值“假”时,结果将会出错。所以肯定还是采用 ... if ... else ... 形式为妙。
是否可以用 Python 编写让人眼晕的单行程序?¶
可以。通常是在 lambda 中嵌套 lambda 来实现的。请参阅以下三个来自 Ulf Bartelt 的示例代码:
from functools import reduce
# Primes < 1000
print(list(filter(None,map(lambda y:y*reduce(lambda x,y:x*y!=0,
map(lambda x,y=y:y%x,range(2,int(pow(y,0.5)+1))),1),range(2,1000)))))
# First 10 Fibonacci numbers
print(list(map(lambda x,f=lambda x,f:(f(x-1,f)+f(x-2,f)) if x>1 else 1:
f(x,f), range(10))))
# Mandelbrot set
print((lambda Ru,Ro,Iu,Io,IM,Sx,Sy:reduce(lambda x,y:x+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
十六进制数也很简单。只要在十六进制数前面加上一个零和一个小写或大写的字母 "x"。十六进制数中的字母可以为大写或小写。比如在 Python 解释器中输入:
>>> a = 0xa5
>>> b = 0XB2
为什么 -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 字面属性而不是 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") ,这会把 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)
>>> sio.write("there!")
>>> 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'
是否有与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' 错误是什么意思?¶
我的程序太慢了。该如何加快速度?¶
总的来说,这是个棘手的问题。在进一步讨论之前,首先应该记住以下几件事:
不同的 Python 实现具有不同的性能特点。 本 FAQ 着重解答的是 CPython。
不同操作系统可能会有不同表现,尤其是涉及 I/O 和多线程时。
在尝试优化代码 之前 ,务必要先找出程序中的热点(请参阅 profile 模块)。
编写基准测试脚本,在寻求性能提升的过程中就能实现快速迭代(请参阅 timeit 模块)。
强烈建议首先要保证足够高的代码测试覆盖率(通过单元测试或其他技术),因为复杂的优化有可能会导致代码回退。
话虽如此,Python 代码的提速还是有很多技巧的。以下列出了一些普适性的原则,对于让性能达到可接受的水平会有很大帮助:
相较于试图对全部代码铺开做微观优化,优化算法(或换用更快的算法)可以产出更大的收益。
使用正确的数据结构。参考 內建型態 和 collections 模块的文档。
如果标准库已为某些操作提供了基础函数,则可能(当然不能保证)比所有自编的函数都要快。对于用 C 语言编写的基础函数则更是如此,比如内置函数和一些扩展类型。例如,一定要用内置方法 list.sort() 或 sorted() 函数进行排序(某些高级用法的示例请参阅 如何排序 )。
抽象往往会造成中间层,并会迫使解释器执行更多的操作。如果抽象出来的中间层级太多,工作量超过了要完成的有效任务,那么程序就会被拖慢。应该避免过度的抽象,而且往往也会对可读性产生不利影响,特别是当函数或方法比较小的时候。
如果你已经达到纯 Python 允许的限制,那么有一些工具可以让你走得更远。 例如, Cython 可以将稍微修改的 Python 代码版本编译为 C 扩展,并且可以在许多不同的平台上使用。 Cython 可以利用编译(和可选的类型注释)来使代码明显快于解释运行时的速度。 如果您对 C 编程技能有信心,也可以自己 编写 C 扩展模块 。
专门介绍 性能提示 的wiki页面。
将多个字符串连接在一起的最有效方法是什么?¶
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:
https://code.activestate.com/recipes/52560/
如果列表允许重新排序,不妨先对其排序,然后从列表末尾开始扫描,依次删除重复项:
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 元素: