如何在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计算最大优先级队列实例
Dec 18 Python
利用python画一颗心的方法示例
Jan 31 Python
python安装教程 Pycharm安装详细教程
May 02 Python
Python基于更相减损术实现求解最大公约数的方法
Apr 04 Python
python2.7实现爬虫网页数据
May 25 Python
Python递归函数 二分查找算法实现解析
Aug 12 Python
Python jieba库用法及实例解析
Nov 04 Python
Keras构建神经网络踩坑(解决model.predict预测值全为0.0的问题)
Jul 07 Python
Python在字符串中处理html和xml的方法
Jul 31 Python
python中_del_还原数据的方法
Dec 09 Python
教你用python控制安卓手机
May 13 Python
如何使用PyCharm及常用配置详解
Jun 03 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下载远程图片函数 可伪造来路
2013/06/25 PHP
解析php入库和出库
2013/06/25 PHP
php处理复杂xml数据示例
2016/07/11 PHP
JavaScript动态插入script的基本思路及实现函数
2013/11/11 Javascript
JavaScript版的TwoQueues缓存模型
2014/12/29 Javascript
jQuery中用dom操作替代正则表达式
2014/12/29 Javascript
jquery $(document).ready()和window.onload的区别浅析
2015/02/04 Javascript
javascript设置和获取cookie的方法实例详解
2016/01/05 Javascript
JavaScript模拟鼠标右键菜单效果
2020/12/08 Javascript
js前端实现多图图片上传预览的两个方法(推荐)
2016/11/18 Javascript
Canvas 制作动态进度加载水球详解及实例代码
2016/12/09 Javascript
node.js实现回调的方法示例
2017/03/01 Javascript
JavaScript基于activexobject连接远程数据库SQL Server 2014的方法
2017/07/12 Javascript
Vue.js项目模板搭建图文教程
2017/09/20 Javascript
基于jQuery中ajax的相关方法汇总(必看篇)
2017/11/08 jQuery
详解使用vue-admin-template的优化历程
2018/05/20 Javascript
JS实现将二维数组转为json格式字符串操作示例
2018/07/12 Javascript
微信实现自动跳转到用其他浏览器打开指定APP下载
2019/02/15 Javascript
微信小程序如何修改本地缓存key中单个数据的详解
2019/04/26 Javascript
vue动态循环出的多个select出现过的变为disabled(实例代码)
2019/11/10 Javascript
如何在vue中使用jointjs过程解析
2020/05/29 Javascript
Javascript基于OOP实实现探测器功能代码实例
2020/08/26 Javascript
[05:29]2014DOTA2国际邀请赛 赛后专访:LGDNewbee顺利过关
2014/07/13 DOTA
python中hasattr()、getattr()、setattr()函数的使用
2019/08/16 Python
Pytorch根据layers的name冻结训练方式
2020/01/06 Python
解析pip安装第三方库但PyCharm中却无法识别的问题及PyCharm安装第三方库的方法教程
2020/03/10 Python
Python3自动生成MySQL数据字典的markdown文本的实现
2020/05/07 Python
梅西酒窖:Macy’s Wine Cellar
2018/01/07 全球购物
印度领先的眼镜电子商务网站:Lenskart
2019/12/16 全球购物
小学评语大全
2014/04/22 职场文书
岗位工作说明书
2014/07/29 职场文书
党在我心中的演讲稿
2014/09/13 职场文书
高中开学感言
2015/08/01 职场文书
优秀乡村医生事迹材料(2016精选版)
2016/02/29 职场文书
Golang 1.18 多模块Multi-Module工作区模式的新特性
2022/04/11 Golang
提高系统的吞吐量解决数据库重复写入问题
2022/04/23 MySQL