>>> 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 变量隐式视为全局变量。如果在函数内部任何位置为变量赋值,则除非明确声明为全局变量,否则均将其视为局部变量。
起初尽管有点令人惊讶,不过考虑片刻即可释然。一方面,已分配的变量要求加上 global
可以防止意外的副作用发生。另一方面,如果所有全局引用都要加上 global
,那处处都得用上 global
了。那么每次对内置函数或导入模块中的组件进行引用时,都得声明为全局变量。这种杂乱会破坏 global
声明用于警示副作用的有效性。
假设用 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)
请注意,出于同样的原因,使用模块也是实现Singleton设计模式的基础。
通常请勿使用 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
则是实参。
如果代码编写如下:
>>> 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}
没有什么理由要把问题搞得这么复杂。
最佳选择就是返回一个包含多个结果值的元组。
有两种选择:嵌套作用域、可调用对象。假定需要定义 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()
的表现,就如同共享了同一计数变量一样。
一般情况下,用 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")
对于各种赋值运算符( =
、 +=
等)来说同样如此。他们并不是真正的运算符,而只是赋值语句中的语法分隔符。
有的。语法如下:
[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 ...
形式为妙。
可以。通常是在 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
这主要是为了让 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")
,这会把 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'
可以使用 S.rstrip("\r\n")
从字符串 S
的末尾删除所有的换行符,而不删除其他尾随空格。如果字符串 S
表示多行,且末尾有几个空行,则将删除所有空行的换行符:
>>> lines = ("line 1 \r\n"
... "\r\n"
... "\r\n")
>>> lines.rstrip("\n\r")
'line 1 '
由于通常只在一次读取一行文本时才需要这样做,所以使用 S.rstrip()
这种方式工作得很好。
对于简单的输入解析,最方便的做法通常是使用字符串对象的 split()
方法将一行内容拆解为以空格分隔的单词,然后使用 int()
或 float()
将表示十进制数的字符串转换为数值。 split()
支持可选的 "sep" 形参,适用于内容行使用空格符以外的分隔符的情况。
以于更复杂的输入解析,正则表达式会比 C 的 sscanf()
更强大,也更适合此类任务。
见 Unicode 指南
总的来说,这是个棘手的问题。在进一步讨论之前,首先应该记住以下几件事:
不同的 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]
列表推导式可能是最快的。
["this", 1, "is", "an", "array"]
列表在时间复杂度方面相当于 C 或 Pascal 的数组;主要区别在于,Python 列表可以包含多种不同类型的对象。
array
模块还提供了创建具有紧凑表示的固定类型的数组的方法,但它的索引速度比列表慢。还要注意,数字扩展和其他扩展还定义了具有各种特性的类似数组的结构。
要获取Lisp样式的列表,可以使用元组模拟cons单元:
lisp_list = ("like", ("this", ("example", None) ) )
如果需要可变性,可以使用列表而不是元组。这里模拟lisp car的是 lisp_list[0]
,模拟cdr的是 lisp_list[1]
。只有在你确定真的需要的时候才这样做,因为它通常比使用Python列表慢得多。
多维数组或许会用以下方式建立:
>>> A = [[None] * 2] * 3
打印出来貌似没错:
[[None, None], [None, None], [None, None]]
但如果给某一项赋值,结果会同时在多个位置体现出来:
>>> A[0][0] = 5
[[5, None], [5, None], [5, None]]
原因在于用 *
对列表执行重复操作并不会创建副本,而只是创建现有对象的引用。 *3
创建的是包含 3 个引用的列表,每个引用指向的是同一个长度为 2 的列表。1 处改动会体现在所有地方,这一定不是应有的方案。
推荐做法是先创建一个所需长度的列表,然后将每个元素都填充为一个新建列表。