乐趣区

关于python:深入理解-python-虚拟机字节码教程1原来装饰器是这样实现的

深刻了解 python 虚拟机:字节码教程 (1)——原来装璜器是这样实现的

在本篇文章当中次要给大家介绍在 cpython 当中一些比拟常见的字节码,从根本上了解 python 程序的执行。在本文当中次要介绍一些 python 基本操作的字节码,并且将从字节码的角度剖析函数装璜器的原理!

Python 常见字节码

LOAD_CONST

这个指令用于将一个常量加载到栈中。常量能够是数字、字符串、元组、列表、字典等对象。例如:

>>> dis.dis(lambda: 42)
  1           0 LOAD_CONST               1 (42)
              2 RETURN_VALUE

LOAD_NAME

这个指令用于将一个变量加载到栈中。例如:

>>> dis.dis(lambda: x)
  1           0 LOAD_GLOBAL              0 (x)
              2 RETURN_VALUE
>>>

STORE_NAME

这个指令用于将栈顶的值存储到一个变量中。例如:

>>> dis.dis("x=42")
  1           0 LOAD_CONST               0 (42)
              2 STORE_NAME               0 (x)
              4 LOAD_CONST               1 (None)
              6 RETURN_VALUE

BINARY_ADD

这个指令用于对栈顶的两个值进行加法运算并将后果推送到栈中。

>>> dis.dis(lambda: x + y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE

BINARY_SUBTRACT

这个指令用于对栈顶的两个值进行减法运算并将后果推送到栈中。

>>> dis.dis(lambda: x - y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_SUBTRACT
              6 RETURN_VALUE

同样的加减乘除取余数的字节码如下所示:

>>> dis.dis(lambda: x + y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE
>>> dis.dis(lambda: x - y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_SUBTRACT
              6 RETURN_VALUE
>>> dis.dis(lambda: x * y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_MULTIPLY
              6 RETURN_VALUE
>>> dis.dis(lambda: x / y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_TRUE_DIVIDE
              6 RETURN_VALUE
>>> dis.dis(lambda: x // y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_FLOOR_DIVIDE
              6 RETURN_VALUE
>>> dis.dis(lambda: x % y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_MODULO
              6 RETURN_VALUE

COMPARE_OP

这个指令用于比拟栈顶的两个值,并且将比拟失去的后果压入栈中,这个字节码前面后一个字节的参数,示意小于大于不等于等等比拟符号。例如:

>>> dis.dis(lambda: x - y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_SUBTRACT
              6 RETURN_VALUE
>>> dis.dis(lambda: x > y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               4 (>)
              6 RETURN_VALUE
>>> dis.dis(lambda: x < y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               0 (<)
              6 RETURN_VALUE
>>> dis.dis(lambda: x != y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               3 (!=)
              6 RETURN_VALUE
>>> dis.dis(lambda: x <= y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               1 (<=)
              6 RETURN_VALUE
>>> dis.dis(lambda: x >= y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               5 (>=)
              6 RETURN_VALUE
>>> dis.dis(lambda: x == y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               2 (==)
              6 RETURN_VALUE

RETURN_VALUE

将栈顶元素弹出作为返回值。

BUILD_LIST

这个指令用于创立一个列表。例如:

>>> dis.dis(lambda: [a, b, c, e])
  1           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 LOAD_GLOBAL              2 (c)
              6 LOAD_GLOBAL              3 (e)
              8 BUILD_LIST               4
             10 RETURN_VALUE

这条字节码指令有一个参数示意栈空间当中列表元素的个数,在下面的例子当中这个参数是 4。

BUILD_TUPLE

这个指令用于创立一个元组。例如:

>>> dis.dis(lambda: (a, b, c))
  1           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 LOAD_GLOBAL              2 (c)
              6 BUILD_TUPLE              3
              8 RETURN_VALUE

同样的这个字节码也有一个参数,示意创立元组的元素个数。

BUILD_MAP

这个指令用于创立一个字典。例如:

BUILD_SET

和 list 和 tuple 一样,这条指令是用于创立一个汇合对象,同样的这条指令也有一个参数示意用于创立汇合的元素的个数。

>>> dis.dis(lambda: {a, b, c, d})
  1           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 LOAD_GLOBAL              2 (c)
              6 LOAD_GLOBAL              3 (d)
              8 BUILD_SET                4
             10 RETURN_VALUE

BUILD_CONST_KEY_MAP

这条指令是用于创立一个字典对象,同样的这条指令也有一个参数,示意字典当中元素的个数。

>>> dis.dis(lambda: {1:2, 3:4})
  1           0 LOAD_CONST               1 (2)
              2 LOAD_CONST               2 (4)
              4 LOAD_CONST               3 ((1, 3))
              6 BUILD_CONST_KEY_MAP      2
              8 RETURN_VALUE

从字节码角度剖析装璜器的原理

如果你是一个 pythoner 那么你必定或多或少据说过装璜器,这是一个 python 的语法糖咱们能够用它来做很多乏味的事件,比方在不批改源代码的根底之上给函数附加一些性能,比如说计算工夫。

import time

def eval_time(func):
    
    def cal_time(*args, **kwargs):
        start = time.time()
        r = func(*args, **kwargs)
        end = time.time()
        return r, end - start
    return cal_time


@eval_time
def fib(n):
    a = 0
    b = 1
    while n > 0:
        n -= 1
        a, b = b, a + b
    return a

在下面的代码当中咱们实现了一个计算斐波拉契数列的函数,除此之外还写了一个 eval_time 函数用于计算函数执行的工夫,当初调用函数 fib(10),程序的输入如下所示:

>>>fib(10)
(55, 5.9604644775390625e-06)

能够看到实现了咱们想要的成果。

当初咱们应用一个更加简略的例子来模仿下面的代码构造,不便咱们对下面函数执行的过程进行剖析:

s = """
def decorator(func):
    print("Hello")
    return func

@decorator
def fib(n):
    pass
"""
dis.dis(s)

下面的 dis 函数的输入对应代码的字节码如下所示:

  2           0 LOAD_CONST               0 (<code object decorator at 0x108068d40, file "<dis>", line 2>)
              2 LOAD_CONST               1 ('decorator')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (decorator)

  6           8 LOAD_NAME                0 (decorator)

  7          10 LOAD_CONST               2 (<code object fib at 0x1075c1710, file "<dis>", line 6>)
             12 LOAD_CONST               3 ('fib')
             14 MAKE_FUNCTION            0
             16 CALL_FUNCTION            1
             18 STORE_NAME               1 (fib)
             20 LOAD_CONST               4 (None)
             22 RETURN_VALUE

Disassembly of <code object decorator at 0x108068d40, file "<dis>", line 2>:
  3           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Hello')
              4 CALL_FUNCTION            1
              6 POP_TOP

  4           8 LOAD_FAST                0 (func)
             10 RETURN_VALUE

Disassembly of <code object fib at 0x1075c1710, file "<dis>", line 6>:
  8           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
  • 执行第一条指令 LOAD_CONST,这条指令次要是加载一个 code object 对象,这个对象外面次要是蕴含函数 decorator 的字节码,次要是下面字节码的第二块内容。在执行完这条字节码之后栈空间如下所示:
  • 执行完第二条指令 LOAD_CONST 之后,会将字符串 decorator 加载进入栈空间当中。
  • 执行第三条指令 MAKE_FUNCTION,这条字节码的作用是在虚拟机外部创立一个函数,函数的名称为 decorator,函数对应的字节码则是在先前压入栈空间当中的 code object 对象,这条指令还会将创立好的函数对象压入栈中。
  • STORE_NAME,条字节码会将栈顶的元素弹出,并且将 co_names[oparg] 指向这个对象,在下面的字节码当中 co_names[oparg] 就是 decorator。
  • LOAD_NAME,这条字节码就是将 co_names[oparg] 对应的名字指向的对象从新加载进入栈空间当中,也就是下面的 decorator 函数退出进行栈空间当中。
  • 接下来的三条字节码 LOAD_CONST,LOAD_CONST 和 MAKE_FUNCTION,在执行这三条字节码之后,栈空间如下所示:
  • 接下来的一条指令十分重要,这条指令便是装璜器的外围原理,CALL_FUNCTION 这条指令有一个参数 i,在下面的字节码当中为 1,也就是说从栈顶开始的前 i 个元素都是函数参数,调用的函数在栈空间的地位为 i + 1(从栈顶往下数),那么在下面的状况下就是说调用 decorator 函数,并且将 fib 函数作为 decorator 函数的参数,decorator 函数的返回值再压入栈顶。在下面的代码当中 decorator 函数返回值也是一个函数,也就是 decorator 函数的参数,即 fib 函数。
  • 接下来便是 STORE_NAME 字节码,这条字节码的含意咱们在后面曾经说过了,就是将栈顶元素弹出,保留到 co_names[oparg] 指向的对象当中,在下面的代码当中也就是将栈顶的对象保留到 fib 当中。栈顶元素 fib 函数是调用函数 decorator 的返回值。

看到这里就可能了解了原来装璜器的最基本的原理不就是函数调用嘛,比方咱们最后面的用于计算函数执行工夫的装璜器的原理就是:

fib = eval_time(fib)

将 fib 函数作为 eval_time 函数的参数,再将这个函数的返回值保留到 fib 当中,当然这个对象必须是可调用的,不然前面应用 fib() 就会保留,咱们能够应用上面的代码来验证这个成果。

def decorator(func):
    return func()


@decorator
def demo():
    return "function demo return string : Demo"

print(demo)

执行下面的程序后果为:

function demo return string : Demo

能够看到 demo 曾经变成了一个字符串对象而不再是一个函数了,因为 demo = decorator(demo),而在函数 decorator 当中返回值是 demo 函数本人的返回值,因而才打印了字符串。

总结

在本篇文章当中次要给大家介绍了 python 当中一些根底的字节码对应的含意以及示例代码,本篇文章最重要的便是从字节码的角度解释了装璜器的实质原理,这对咱们当前应用装璜器十分有帮忙,能够灵便的管制和理解装璜器其中产生的故事。


本篇文章是深刻了解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

退出移动版