美文网首页Ruby程序员我们就爱程序媛
Ruby动态删除方法中的打印语句

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

作者: lanzhiheng | 来源:发表于2017-08-24 10:51 被阅读93次

    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 !!!

    相关文章

      网友评论

      • 0a97638bc5f3:打发单身时光还有个选择,同城活动
        lanzhiheng:@黎炽新 老司机要带带我吗。

      本文标题:Ruby动态删除方法中的打印语句

      本文链接:https://www.haomeiwen.com/subject/flusdxtx.html