如何在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 Mysql自动备份脚本
Jul 14 Python
跟老齐学Python之list和str比较
Sep 20 Python
Python随手笔记第一篇(2)之初识列表和元组
Jan 23 Python
Python_LDA实现方法详解
Oct 25 Python
Python实现计算圆周率π的值到任意位的方法示例
May 08 Python
python实现比较文件内容异同
Jun 22 Python
Python将文本去空格并保存到txt文件中的实例
Jul 24 Python
python使用PIL实现多张图片垂直合并
Jan 15 Python
基于OpenCV python3实现证件照换背景的方法
Mar 22 Python
Python3.7 基于 pycryptodome 的AES加密解密、RSA加密解密、加签验签
Dec 04 Python
Python Django中的STATIC_URL 设置和使用方式
Mar 27 Python
Python数据正态性检验实现过程
Apr 18 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数据库连接
2006/10/09 PHP
那些年一起学习的PHP(三)
2012/03/22 PHP
php计算数组不为空元素个数的方法
2014/01/27 PHP
PHP多维数组遍历方法(2种实现方法)
2015/12/10 PHP
windows下的WAMP环境搭建图文教程(推荐)
2017/07/27 PHP
Laravel关系模型指定条件查询方法
2019/10/10 PHP
CSDN轮换广告图片轮换效果
2007/03/27 Javascript
Javascript 模式实例 观察者模式
2009/10/24 Javascript
IE6下focus与blur错乱的解决方案
2011/07/31 Javascript
requireJS使用指南
2016/04/27 Javascript
bootstrap模态框跳转到当前模板页面 框消失了而背景存在问题的解决方法
2020/11/30 Javascript
聊聊JavaScript如何实现继承及特点
2017/04/07 Javascript
jQuery插件FusionCharts绘制的2D双面积图效果示例【附demo源码】
2017/04/11 jQuery
jQuery实现的弹幕效果完整实例
2017/09/06 jQuery
vue中echarts3.0自适应的方法
2018/02/26 Javascript
layui 表格的属性的显示转换方法
2018/08/14 Javascript
Vue如何使用混合Mixins和插件开发详解
2020/02/05 Javascript
vue - props 声明数组和对象操作
2020/07/30 Javascript
在vscode 中设置 vue模板内容的方法
2020/09/02 Javascript
Python的Flask框架的简介和安装方法
2015/11/13 Python
Python基于FTP模块实现ftp文件上传操作示例
2018/04/23 Python
对python中不同模块(函数、类、变量)的调用详解
2019/07/16 Python
python实现ftp文件传输系统(案例分析)
2020/03/20 Python
python实现梯度下降和逻辑回归
2020/03/24 Python
澳大利亚排名第一的狂热牛仔品牌:ONETEASPOON
2018/11/20 全球购物
美国糖果店:Sugarfina
2019/02/21 全球购物
美国领先的眼镜和太阳镜在线零售商:Glasses.com
2019/08/26 全球购物
法国一家多品牌成衣精品中/高档商店:Graduate Store
2019/08/28 全球购物
ASP.NET中的身份验证有那些
2012/07/13 面试题
工程造价管理专业大专生求职信
2013/10/06 职场文书
总经理助理的职责
2014/03/14 职场文书
大学专科求职信
2014/07/02 职场文书
低碳环保演讲稿
2014/08/28 职场文书
2014年专项整治工作总结
2014/11/17 职场文书
企业宣传语大全
2015/07/13 职场文书
2015年店长个人工作总结
2015/10/23 职场文书