浅谈 (x, y) = (y, x)

交换两个变量的值,大家最常见的写法是这样的:

>>> temp = x
>>> x = y
>>> y = temp

但其实更 Pythonic 的写法是这样的:

>>> x, y = y, x

大家有没有想过为什么在 Python 中可以这样交换两个变量的值?

Python 代码是先解释(这里的解释是相对编译而言的,Python 不同与 C/C++ 之类的编译型语言,是需要从源文件编译成机器指令)成 Python 字节码(byte code, .pyc文件主要是用来存储这些字节码的)之后,再由 Python 解释器来执行这些字节码的。一般来说,Python 语句会对应若干字节码指令,Python 的字节码类似于汇编指令的中间语言,但是一个字节码并不只是对应一个机器指定。

内置模块 dis 可以用来分析字节码。DOC

The dis module supports the analysis of CPython bytecode by disassembling it. The CPython bytecode which this module takes as an input is defined in the file Include/opcode.h and used by the compiler and the interpreter.

常用的 dis 模块方法: dis.dis([bytesource])

dis.dis([bytesource])
Disassemble the bytesource object. bytesource can denote either a module, a class, a method, a function, or a code object. For a module, it disassembles all functions. For a class, it disassembles all methods. For a single code sequence, it prints one line per bytecode instruction. If no object is provided, it disassembles the last traceback.

dis.dis 接收参数为一个代码块(可以是模块,类,方法,函数,或者是对象),可以得到这个代码块对应的字节码指令序列。

>>> import dis
>>> def test():
...     a = 1
...     
... 
>>> dis.dis(test)
  3           0 LOAD_CONST               1 (1)
              3 STORE_FAST               0 (a)
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE        

输出的格式分别是:行号,地址,指令,操作参数, 参数解释(识别变量名称,常量值等)

切入正题, 我们直接来看下第二种写法的字节码指令:

swap_2.py

x = 1
y = 3
x, y = y, x

python -m dis swap_2.py

  1           0 LOAD_CONST               0 (1)
              3 STORE_NAME               0 (x)

  2           6 LOAD_CONST               1 (3)
              9 STORE_NAME               1 (y)

  3          12 LOAD_NAME                1 (y)
             15 LOAD_NAME                0 (x)
             18 ROT_TWO
             19 STORE_NAME               0 (x)
             22 STORE_NAME               1 (y)
             25 LOAD_CONST               2 (None)
             28 RETURN_VALUE

部分字节码指令如下,具体的指令请移步官网:

LOAD_CONST(consti)
Pushes co_consts[consti] onto the stack.
STORE_NAME(namei)
Implements name = TOS. namei is the index of name in the attribute co_names of the code object. The compiler tries to use STORE_FAST or STORE_GLOBAL if possible.
LOAD_NAME(namei)
Pushes the value associated with co_names[namei] onto the stack.
ROT_TWO()
Swaps the two top-most stack items.

解释下上面的字节码指令:

第一行执行两个字节码指令, 分别是LOAD_CONSTSTORE_NAME,执行的动作是将 co_consts[0] 压栈(也就是常量表的第一个常量,整数1压入栈中),然后获取co_names[0]的变量名x(变量名表的第一个名字),栈顶元素(整数1)出栈和co_names[0]存储到f->f_locals。

第二行的执行方式如同第一行。

co_consts[0] = 1
co_names[0] = x
f->f_locals['x'] = 1

co_consts[1] = 3
co_names[1] = y
f->f_locals['y'] = 3

重点在第三行,前两行的计算顺序都是从友往左进行的(一般情况下, Python 表达式的计算顺序是从左到右,但是在表达式赋值的时候,表达式右边的操作数优先于左边),也就是说,第四行是这样执行的,先创建元组(y, x),执行的动作是两个 LOAD_NAME,会依次搜索local,global,builtin名字空间中的co_names[1](对应变量名y)和co_names[0](对应变量名x)并把相对应的值压栈。接下去执行的动作是交换ROT_TWO, 交换栈顶的两个元素位置。

从下一个执行指令就可以看出来,先获取co_names[0]的变量名x,栈顶元素(现在是原先y的值)出栈并储存,两次存储就实现了交换两个变量的值。

第二种方法不借助任何中间变量并且能够获得更好的性能。我们可以简单测试下:

>>> from timeit import Timer
>>> Timer('temp = x;x = y;y = temp', 'x=2;y=3').timeit()
0.030814170837402344
>>> Timer('x, y = y, x', 'x=2;y=3').timeit()
0.027340173721313477

为什么第二种方法消耗的时间更少呢?可以猜测一下,是中间变量赋值引起的耗时。具体验证可以分析下两种方法的字节码指令。

Life such short,be Pythonic .

Blog : JunNplus

你可能感兴趣的:(python)