Ruby动态删除方法中的打印语句

0. 前言及需求

运行时动态编程也算是Ruby的卖点之一,心血来潮想要尝试一下我是否能够在运行时动态对Ruby原有的方法进行调整?可能生产线上用不上这些黑科技,不过对于个人折腾来说这还是蛮好的,可以打发一下单身的时光。

我们设定一个简单的方法,做的事情很简单,只是输出了对应的方法名,并且前后有形如p xxx的打印语句

def method_with_print
  p "begin method"
  puts "This is '#{self.method(:method_with_print).name}' method"
  p "end method"
end


if __FILE__ == $0
  method_with_print
end

运行结果如下

>> ruby xx.rb
"begin method"
This is 'method_with_print' method
"end method"

我们有什么办法把上述方法中的p语句去掉,我能想到的方式是

  1. 获取方法的字符串版本。
  2. 使用正则表达式匹配并替换p语句,得到一个新的定义方法的字符串。
  3. 在恰当的上下文运行新的字符串,重新定义方法。
  4. 以Ruby的方式包装一个方法,去改造并调用原来的方法。

看起来好像也不难,一步步来试试看。

1. 获取方法定义的字符串版本

如果我们是用的JavaScript,想要获取对应方法的字符串版本只需要调用方法对象的toString便可

> function a() {
... return "lan"
... }
undefined
> a.toString()
'function a() {\nreturn "lan"\n}'

但Ruby似乎并没有这么直接的方式获取定义方法的字符串,估计是社区觉得这种需求其实用处不大吧,从网上搜索了一下有一个叫做method_source的包可以做到这一点,他是以补丁的方式来增强Ruby原有的模块方法,进而为每一个非绑定方法添加一个source的属性,我们通过这个属性就可以获取到方法的源代码了。不过这里有一个问题,这个方法的实现机制是通过调用目标方法原有的source_location方法获取到定义该方法的具体位置,然后访问对应的文件,截取出指定方法对应的定义字符串。换句话说如果我们的方法是在REPL 里面定义的话就不能获取到对应方法的字符串了。目前我们只能在获取在脚本里(xx.rb)定义对应方法的字符串了。

上面所说的这个库,其实已经包含在我们平时用得比较多的pry工具里面了,这是一个比较常用的REPL工具,现在我只需要在原有代码的可执行部分里面添加

if __FILE__ == $0
  # method_with_print
  require 'pry'
  puts Object.instance_method(:method_with_print).source
end

运行对应脚本 xx.rb就能得到目标方法的定义字符串了

>> ruby xx.rb

def method_with_print
  p "begin method"
  puts "This is '#{self.method(:method_with_print).name}' method"
  p "end method"
end

可以看到以上的做法相比JavaScript来说有点绕,因为Ruby的方法是不需要添加括号就可以调用的,直接写方法名的话就是调用方法。这里想要操作对应方法名的方法对象,并且是非绑定版本的。可以通过Object.instance_method来获取。

2. 正则匹配且替换

我们已经获取了对应的字符串版本了,那么接下来要做的就是匹配并且替换掉原来的p xxxx语句了。

我就写一个比较简单的正则匹配就好了,毕竟如果要匹配ruby所有的打印语句的话,会占用比较多的时间以及篇幅。

根据上面的思路我得到了这样一个程序版本

if __FILE__ == $0
  # method_with_print
  require 'pry'
  REG_CONSOLE = /\s+p\s+.+/
  method_string = Object.instance_method(:method_w\
ith_print).source
  method_string.gsub!(REG_CONSOLE, '')

  method_string
end

运行看看

>> ruby xx.rb

def method_with_print
  puts "This is '#{self.method(:method_with_print).name}' method"
end

可见,对应的p xxx语句已经从字符串中删除了,我们已经得到了改版之后的方法定义字符串了。

3. 重新定义方法

如何重新定义方法? 我们应该都听过JavaScript有名为eval的方法,可以动态执行字符串。类似的的Ruby也有Kernel#eval。而且它在类层面还提供了Class#class_eval,在对象层面提供了Object#instance_eval方法,让你可以操作不同的上下文。这里讲一下比较直观的Kernel#eval, 我们要直接执行Ruby代码可以像这样执行

>> eval("p 'I love ruby'")

"I love ruby"
 => "I love ruby"

那之前定义的方法是不是也能以字符串的形式,通过Kernel#eval方法来重新定义?我把代码写成这样

if __FILE__ == $0
  # method_with_print
  require 'pry'
  REG_CONSOLE = /\s+p\s+.+/
  method_string = Object.instance_method(:method_with_print).source
  method_string.gsub!(REG_CONSOLE, '')

  eval(method_string)

  method_with_print
end

运行看看结果是否符合预期

>> ruby xx.rb

This is 'method_with_print' method

Awesome, 已经满足了我们这次的需求了,我们可以在运行时删除方法的p xxx语句,并且重新定义了原有的方法。最后我试试用Ruby的方式来处理一下个问题,肯定不是最优雅的,不过这是我目前能想到的足够折腾的处理方式。

help

4. Ruby的处理方式

Ruby是“真”面向对象的编程语言,因为他真的能够做到一切都是对象,比如

[1] pry(main)> 1.to_s
=> "1"
[2] pry(main)> '2'.to_i
=> 2

平时我们定义的函数,其实也是方法

[3] pry(main)> def m
[3] pry(main)*   'lan'
[3] pry(main)* end
=> :m
[4] pry(main)> self.m
=> "lan"
[5] pry(main)> self
=> main

m其实是挂在main这个对象上的方法。用面向对象的方式来解决上面的问题,我们是否可以给方法添加一个属性,通过这个属性来调用原有方法的删除了p xxxx语句之后的版本呢?我们首先来看看方法对象的继承链条

[6] pry(main)> m_method = Object.instance_method(:m)
[10] pry(main)> m_method.class.ancestors
=> [UnboundMethod,
 MethodSource::MethodExtensions,
 MethodSource::SourceLocation::UnboundMethodExtensions,
 Object,
 PP::ObjectMixin,
 Kernel,
 BasicObject]

可见方法对象所属类的祖先链如下

[UnboundMethod, MethodSource::MethodExtensions, MethodSource::SourceLocation::UnboundMethodExtensions, Object, PP::ObjectMixin, Kernel, BasicObject]

祖先链有这一大堆的东西,那要不我们就斗胆一点扩展一下MethodSource::MethodExtensions这个模块吧。 你怎么知道他是一个模块而不是类?

[13] pry(main)> MethodSource::MethodExtensions.class
=> Module

我尝试在模块里面添加MethodSource::MethodExtensions#remove_p_statement方法

require 'pry'

module MethodSource::MethodExtensions
  REG_CONSOLE = /\s+p\s+.+/

  def remove_p_statement(*params)
    method_string = self.source.gsub(REG_CONSOLE, '')
    method_owner = self.owner
    new_method = method_owner.instance_eval(method_string)
    method_owner.send(new_method, *params)
  end
end

它是方法的方法,只需要在方法的后面调用它。它会在对象的上下文Object#instance_eval重新定义这个方法,然后在内部自动发派这个方法,并附带上一个可变参数。最后我把执行脚本的主体内容改为

if __FILE__ == $0
  puts "=========="
  puts "new method result:\n"
  Object.instance_method(:method_with_print).remove_p_statement()
  puts "=========="

  puts "=========="
  puts "old method result:\n"
  method_with_print()
  puts "=========="
end

PS: 由于method_with_print方法定义的时候没有参数,我们这里括号里面的内容都是空。

最后的结果如下

==========
new method result:
This is 'method_with_print' method
==========

==========
old method result:
"begin method"
This is 'method_with_print' method
"end method"
==========

可见,我们的方法调用了 MethodSource::MethodExtensions#remove_p_statement 这个方法之后得到了一个新的方法并执行,但是却不会影响到执行脚本上下文中最初定义的原始方法的行为。

5. 再见

以上代码有什么用? .........其实还真没什么卵用,纯属瞎折腾。

play

Happy Coding and Writing !!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,226评论 0 4
  • 可用性工程阅读笔记 Nielsen 看这本书和写下这些笔记总共用了快两个星期的时间,这段时间事情真的有点多。。 从...
    Witty_28阅读 1,949评论 1 12
  • 很直接的题目 当妈至今也快4个年头了,有了艾马似乎是放弃了许多平时的爱好,一没时间,二没精力,但,收获了宝贵...
    马番薯阅读 201评论 0 1
  • 初夏,我和他相遇。 其实,也不算相遇,只是一次擦肩而过而已。但,就是因为这小小的一次擦肩而过,改变了我整个的初中生...
    病瘾大大阅读 229评论 0 1