如何在Python中实现goto语句的方法


Posted in Python onMay 18, 2019

Python 默认是没有 goto 语句的,但是有一个第三方库支持在 Python 里面实现类似于

goto 的功能:https://github.com/snoack/python-goto.。比如在下面这个例子里,

from goto import with_goto

@with_goto
def func():
  for i in range(2):
    for j in range(2):
      goto .end
  label .end
  return (i, j, k)

func() 在执行第一遍循环时,就会从最内层的 for j in range(2) 跳到函数的return 语句前面。

按理说本文到此就该完了,但是这个库有一个限制,如果嵌套的循环层次太深,就无法工作。比如下面这几行代码:

@with_goto
def func():
  for i in range(2):
    for j in range(2):
      for k in range(2):
        for m in range(2):
          for n in range(2):
            goto .end
  label .end
  return (i, j, k, m, n)

会让它抛出 SyntaxError

本文接下来的内容,就是如何打破这个限制。

python-goto 是如何工作的

python-goto 这个库,通过 decorator 的方式修改了传进来的函数 func__code__ 属性,把插入的字节码暗桩替换成相关的 JMP 语句。具体的琐碎实现细节,可以参考该项目下 goto.py 这个文件,一共也就不到两百行。

本文开头的例子中,func 函数的字节码可以用

import dis
dis.dis(func)

打印出来。

下面贴出不带 @with_goto 时的输出(# 号后面的内容是我加的):实际上

# for i in range(2):
# 7 是源代码行号(跟示例不太对得上,不要太在意细节XD)
# 0/2/4 这些是 offset,在这里每条字节码长度都是 2。
# >> 表示会跳到这里。
 7      0 SETUP_LOOP       40 (to 42)
       2 LOAD_GLOBAL       0 (range)
       4 LOAD_CONST        1 (2)
       6 CALL_FUNCTION      1
       8 GET_ITER
    >>  10 FOR_ITER        28 (to 40)
       12 STORE_FAST        0 (i)

# for j in range(2):
 8     14 SETUP_LOOP       22 (to 38)
       16 LOAD_GLOBAL       0 (range)
       18 LOAD_CONST        1 (2)
       20 CALL_FUNCTION      1
       22 GET_ITER
    >>  24 FOR_ITER        10 (to 36)
       26 STORE_FAST        1 (j)

# goto .end
 9     28 LOAD_GLOBAL       1 (goto)
       30 LOAD_ATTR        2 (end)
       32 POP_TOP
# 结束循环 j
       34 JUMP_ABSOLUTE      24
    >>  36 POP_BLOCK
# 结束循环 i
    >>  38 JUMP_ABSOLUTE      10
    >>  40 POP_BLOCK

# label .end
 10   >>  42 LOAD_GLOBAL       3 (label)
       44 LOAD_ATTR        2 (end)
       46 POP_TOP

# return (i, j, k)
 11     48 LOAD_FAST        0 (i)
       50 LOAD_FAST        1 (j)
       52 LOAD_GLOBAL       4 (k)
       54 BUILD_TUPLE       3

跟带 @with_goto 时的输出比较,只有这两点差别:

# goto .end
- 9     28 LOAD_GLOBAL       1 (goto)
-       30 LOAD_ATTR        2 (end)
-       32 POP_TOP
+ 9     28 POP_BLOCK
+       30 POP_BLOCK
+       32 JUMP_FORWARD      14 (to 48)
# label .end
- 10   >>  42 LOAD_GLOBAL       3 (label)
-       44 LOAD_ATTR        2 (end)
-       46 POP_TOP
+ 10   >>  42 NOP
+       44 NOP
+       46 NOP

- 11     48 LOAD_FAST        0 (i)
+ 11   >>  48 LOAD_FAST        0 (i)

在没有引入 @with_goto 时,goto .end 在 Python 解释器的眼里,其实就是goto.end,即访问某个叫 goto 的全局域里的对象的 end 属性。该语句会被编译成三条语句:LOAD_GLOBALLOAD_ATTRPOP_TOP。这就是插入在字节码里的暗桩。

在引入 @with_goto 之后,这三条语句会被替换成一条 JMP 语句外加若干条辅助的语句。这样在执行到这些字节码时,就会跳到指定的地方了,比如在上面例子中跳到 offset 48,也即原来 label .end 的下一条字节码。

(关于 Python 字节码的官方文档并不显眼,藏在 dis 这个模块下。注意它不是按字母表顺序介绍每个字节码的,所以要想查特定的字节码,需要 Ctrl+F 一下。)

JMP 语句只需要一条,如果要向前跳,就用 JUMP_FORWARD;向后跳,就用JUMP_ABSOLUTE。但是辅助的语句可能不止一条,比如要想从一个 for loop 或者 try block 跳出来,需要加 POP_BLOCK 语句。有多少层循环就需要加多少条 POP_BLOCK,比如前面的示例里是两层循环,就是两条 POP_BLOCK

另外,由于 Python 字节码的长度固定为两个 byte,一个 byte 用于表示字节码的类型,另一个用于表示参数。如果要想放下超过字节码预留的空位的参数,需要用 EXTENDED_ARG语句。比如

EXTENDED_ARG       7
EXTENDED_ARG     2046
OP            x

那么语句 OP 的参数就是 7 << 16 + 2046 << 8 + x。

对于 JUMP_FORWARD,它的参数是 offset。所以当目标地址离当前位置的 offset 超过256 时,需要额外生成 EXTENDED_ARGJUMP_ABSOLUTE 也是同样的道理,只是该语句的参数是绝对地址。

所以对于深层嵌套内、需要跳到很远的 goto 语句,就要加不少辅助语句。而python-goto 这个库,在替换暗桩时,并不会额外增加语句。如果所需的语句超过暗桩的大小,会抛出 SyntaxError。

在 Python 3.6 之前,不带参数的语句只需要 1 个字节,同样 6 个字节的地方,可以容纳 1 条必需的 JMP 语句和 4 条 POP_BLOCK。除非你是在一个五层循环里用 goto,不太会碰到这个限制。但是 Python 3.6 之后,POP_BLOCK 也要用 2 个字节了,顿时连三层循环都 hold 不住了,这个问题就显得尖锐起来。上面还没考虑到需要加EXTENDED_ARG 的情况。

如何绕过字节码大小的限制

那么一个显而易见的解决方案就浮出水面了:为何不试试在修改字节码的时候,动态改变字节码的大小,让它有足够的位置容纳新增的辅助语句?这样一来,就能彻底地解决问题了。

这个就是开头说到的,打破限制的方法。

Python 本身是允许动态增大/缩小 __code__ 属性里的字节码的。但是有个问题,Python里许多字节码依赖特定的位置或者偏移。如果我们挪动了涉及的字节码,需要同步修改这些语句的参数。(包括我们新生成的 goto 语句里面的 JUMP_ABSOLUTEJUMP_FORWARD

这个听起来简单,似乎只要把参数 patch 成实际修改后的值就好了。然而 Python 是通过在字节码前面插入 EXTENDED_ARG 来实现定长字节码里支持不定长参数的功能。修改参数的值可能需要动态调整 EXTENDED_ARG 语句的数量;而调整 EXTENDED_ARG 又反过来影响到各个语句的参数…… 所以这里需要一个 while True 循环,直到某一次调整不会触发 EXTENDED_ARG 语句的变化为止。

好在如果我们只单方面增大字节码,就只需要增加 EXTENDED_ARG 语句。而每在一个地方增加完 EXTENDED_ARG 语句,就意味着对应的 OP 语句参数能缩小 256。后面无论怎么调整,都不太可能需要再增加多一个 EXTENDED_ARG 语句。这么一来,调整的次数就不会多。

虽然说起来好像就那么两三段话的事,但是开发难度会很大。因为需要 patch 的字节码类型很多,大约十来种吧。而且逻辑上较为复杂,牵连的地方很多。实际上我没有实现前述的方案,只是设计了下而已。如果你要实现它,请在编码时保持内心的平静,另外多写测试用例,不然很容易出问题。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
浅析Python中的多进程与多线程的使用
Apr 07 Python
详解python如何调用C/C++底层库与互相传值
Aug 10 Python
Python使用迭代器捕获Generator返回值的方法
Apr 05 Python
opencv改变imshow窗口大小,窗口位置的方法
Apr 02 Python
python 实现12bit灰度图像映射到8bit显示的方法
Jul 08 Python
django 通过url实现简单的权限控制的例子
Aug 16 Python
pytorch实现保证每次运行使用的随机数都相同
Feb 20 Python
Python 定义只读属性的实现方式
Mar 05 Python
Python pandas对excel的操作实现示例
Jul 21 Python
scrapy结合selenium解析动态页面的实现
Sep 28 Python
python switch 实现多分支选择功能
Dec 21 Python
Python 中Operator模块的使用
Jan 30 Python
OpenCV搞定腾讯滑块验证码的实现代码
May 18 #Python
Python3匿名函数lambda介绍与使用示例
May 18 #Python
python中数组和矩阵乘法及使用总结(推荐)
May 18 #Python
Python实现二叉树前序、中序、后序及层次遍历示例代码
May 18 #Python
python的内存管理和垃圾回收机制详解
May 18 #Python
Django处理多用户类型的方法介绍
May 18 #Python
Django 配置多站点多域名的实现步骤
May 17 #Python
You might like
第十节 抽象方法和抽象类 [10]
2006/10/09 PHP
用PHP制作的意见反馈表源码
2007/03/11 PHP
js下函数般调用正则的方法附代码
2008/06/22 PHP
php数组对百万数据进行排除重复数据的实现代码
2010/06/08 PHP
自定义Laravel (monolog)日志位置,并增加请求ID的实现
2019/10/17 PHP
实现png图片和png背景透明(支持多浏览器)的方法
2009/09/08 Javascript
JavaScript中的noscript元素属性位置及作用介绍
2013/04/11 Javascript
解析DHTML,JavaScript,DOM,BOM以及WEB标准的描述
2013/06/19 Javascript
js实现新年倒计时效果
2015/12/10 Javascript
javascript实现简单加载随机色方块
2015/12/25 Javascript
javascript下使用Promise封装FileReader
2016/02/19 Javascript
JavaScript触发onScroll事件的函数节流详解
2016/12/14 Javascript
详解Angularjs 如何自定义Img的ng-load 事件
2017/02/15 Javascript
three.js利用卷积法如何实现物体描边效果
2019/11/27 Javascript
[03:36]2014DOTA2 TI小组赛综述 八强诞生进军钥匙球馆
2014/07/15 DOTA
Python正则捕获操作示例
2017/08/19 Python
分析python动态规划的递归、非递归实现
2018/03/04 Python
用 Python 连接 MySQL 的几种方式详解
2018/04/04 Python
pandas中去除指定字符的实例
2018/05/18 Python
python实现的自动发送消息功能详解
2019/08/15 Python
python selenium实现发送带附件的邮件代码实例
2019/12/10 Python
Python3 实现爬取网站下所有URL方式
2020/01/16 Python
python 获取当前目录下的文件目录和文件名实例代码详解
2020/03/10 Python
使用python matploblib库绘制准确率,损失率折线图
2020/06/16 Python
python中pdb模块实例用法
2021/01/15 Python
html5拖曳操作 HTML5实现网页元素的拖放操作
2013/01/02 HTML / CSS
戴尔英国官网:Dell英国
2017/05/27 全球购物
德国柯吉澳趣味家居:Koziol
2017/08/24 全球购物
丝芙兰波兰:Sephora.pl
2018/03/25 全球购物
法律专业实习鉴定
2013/12/22 职场文书
卫生标语大全
2014/06/21 职场文书
出国签证在职证明
2014/09/20 职场文书
个人租房协议书(范本)
2014/10/14 职场文书
《正比例》教学反思
2016/02/23 职场文书
2016年暑期社会实践活动总结报告
2016/04/06 职场文书
中秋节作文(五年级)之关于月亮
2019/09/11 职场文书