如何在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用fork来创建子进程注意事项
Jul 03 Python
在Python中使用M2Crypto模块实现AES加密的教程
Apr 08 Python
Python使用bs4获取58同城城市分类的方法
Jul 08 Python
Python循环语句中else的用法总结
Sep 11 Python
python的文件操作方法汇总
Nov 10 Python
Python3之文件读写操作的实例讲解
Jan 23 Python
Python基础之条件控制操作示例【if语句】
Mar 23 Python
python读取并定位excel数据坐标系详解
Jun 26 Python
matplotlib命令与格式之tick坐标轴日期格式(设置日期主副刻度)
Aug 06 Python
Django 项目重命名的实现步骤解析
Aug 14 Python
python3 requests库文件上传与下载实现详解
Aug 22 Python
python网络编程之五子棋游戏
May 14 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
php生成文件
2007/01/15 PHP
跟我学Laravel之请求与输入
2014/10/15 PHP
php中字符查找函数strpos、strrchr与strpbrk用法
2014/11/18 PHP
无需数据库在线投票调查php代码
2016/07/20 PHP
yii2利用自带UploadedFile实现上传图片的示例
2017/02/16 PHP
laravel Validator ajax返回错误信息的方法
2019/09/29 PHP
Jquery Change与bind事件代码
2011/09/29 Javascript
JS+JSP checkBox 全选具体实现
2014/01/02 Javascript
浅谈Unicode与JavaScript的发展史
2015/01/19 Javascript
Kendo Grid editing 自定义验证报错提示的解决方法
2016/11/18 Javascript
js 模仿锚点定位的实现方法
2016/11/19 Javascript
jquery实现自适应banner焦点图
2017/02/16 Javascript
webpack打包后直接访问页面图片路径错误的解决方法
2017/06/17 Javascript
Bootstrap弹出框之自定义悬停框标题、内容和样式示例代码
2017/07/11 Javascript
React-native桥接Android原生开发详解
2018/01/17 Javascript
学习使用ExpressJS 4.0中的新Router的用法
2018/11/06 Javascript
微信小程序如何利用getCurrentPages进行页面传值
2019/07/01 Javascript
基于layPage插件实现两种分页方式浅析
2019/07/27 Javascript
vue 二维码长按保存和复制内容操作
2020/09/22 Javascript
如何实现小程序与小程序之间的跳转
2020/11/04 Javascript
python多进程提取处理大量文本的关键词方法
2018/06/05 Python
Python3中bytes类型转换为str类型
2018/09/27 Python
Python使用pandas对数据进行差分运算的方法
2018/12/22 Python
Django给admin添加Action的步骤详解
2019/05/01 Python
使用WingPro 7 设置Python路径的方法
2019/07/24 Python
Python列表list操作相关知识小结
2020/01/29 Python
Html5原生拖拽相关事件简介以及基础实现
2020/11/19 HTML / CSS
Furla官网:意大利著名的皮革品牌
2019/08/06 全球购物
Linux内核的同步机制是什么?主要有哪几种内核锁
2016/07/11 面试题
成人高等教育毕业生自我鉴定
2013/10/22 职场文书
班班通校本培训方案
2014/03/12 职场文书
我与祖国共奋进演讲稿
2014/09/13 职场文书
父亲婚礼答谢词
2015/01/04 职场文书
国庆节慰问信
2015/02/15 职场文书
暑假安全保证书
2015/02/28 职场文书
党委工作总结2015
2015/04/27 职场文书