【问题标题】:How can I monkey-patch an instance method in Perl?如何在 Perl 中对实例方法进行猴子修补?
【发布时间】:2010-10-01 18:30:27
【问题描述】:

我正在尝试修补 (duck-punch :-) LWP::UserAgent 实例,如下所示:

sub _user_agent_get_basic_credentials_patch {
  return ($username, $password);
}

my $agent = LWP::UserAgent->new();
$agent->get_basic_credentials = _user_agent_get_basic_credentials_patch;

这不是正确的语法——它会产生:

不能修改非左值子程序 在 [module] 行 [lineno] 调用。

我记得(来自 Programming Perl),调度查找是根据祝福包动态执行的(ref($agent),我相信),所以我不确定实例猴子补丁会如何在不影响祝福包的情况下工作。

我知道我可以继承 UserAgent,但我更喜欢更简洁的猴子补丁方法。同意的成年人和你有什么。 ;-)

【问题讨论】:

    标签: perl monkeypatching


    【解决方案1】:

    John Siracusa's answer 的基础上,我发现我仍然想要对原始函数的引用。所以我这样做了:

    MONKEY_PATCH_INSTANCE:
    {
      my $counter = 1; # could use a state var in perl 5.10
    
      sub monkey_patch_instance
      {
        my($instance, $method, $code) = @_;
        my $package = ref($instance) . '::MonkeyPatch' . $counter++;
        no strict 'refs';
        my $oldFunction = \&{ref($instance).'::'.$method};
        @{$package . '::ISA'} = (ref($instance));
        *{$package . '::' . $method} = sub {
            my ($self, @args) = @_;
            $code->($self, $oldFunction, @args);
        };
        bless $_[0], $package; # sneaky re-bless of aliased argument
      }
    }
    
    # let's say you have a database handle, $dbh
    # but you want to add code before and after $dbh->prepare("SELECT 1");
    
    monkey_patch_instance($dbh, prepare => sub {
        my ($self, $oldFunction, @args) = @_;
    
        print "Monkey patch (before)\n";
        my $output = $oldFunction->(($self, @args));
        print "Monkey patch (after)\n";
    
        return $output;
        });
    

    和原来的答案一样,只是我传递了一些参数$self$oldFunction

    这让我们可以像往常一样调用$self$oldFunction,但在其周围添加额外的代码。

    【讨论】:

      【解决方案2】:

      编辑:这是我为后代保留的解决方案的错误尝试。查看赞成/接受的答案。 :-)

      啊,我才发现语法需要稍微调整一下:

      $agent->{get_basic_credentials} = _user_agent_get_basic_credentials_patch;
      

      如果没有 {} 分隔符,它看起来像一个方法调用(这不是一个有效的左值)。

      我仍然想知道如何通过这种语法绑定/查找实例方法。蒂亚!

      【讨论】:

      • 其实这个方法好像被调用了,但是这样没有任何参数。
      • 是否试图获得 Peer Pressure 徽章?
      【解决方案3】:

      如果动态范围(使用local)不令人满意,您可以自动化自定义包重新分配技术:

      MONKEY_PATCH_INSTANCE:
      {
        my $counter = 1; # could use a state var in perl 5.10
      
        sub monkey_patch_instance
        {
          my($instance, $method, $code) = @_;
          my $package = ref($instance) . '::MonkeyPatch' . $counter++;
          no strict 'refs';
          @{$package . '::ISA'} = (ref($instance));
          *{$package . '::' . $method} = $code;
          bless $_[0], $package; # sneaky re-bless of aliased argument
        }
      }
      

      示例用法:

      package Dog;
      sub new { bless {}, shift }
      sub speak { print "woof!\n" }
      
      ...
      
      package main;
      
      my $dog1 = Dog->new;
      my $dog2 = Dog->new;
      
      monkey_patch_instance($dog2, speak => sub { print "yap!\n" });
      
      $dog1->speak; # woof!
      $dog2->speak; # yap!
      

      【讨论】:

      • 看来重新祝福会导致猴子补丁实例丢失原始类提供的所有其他方法,除非您调整@ISA:@{$package. '::ISA'} = ref($instance);
      • 那么,您何时将 MonkeyPatch 上传到 CPAN? :) 我经常认为这对用户来说应该更容易。
      • brian:我实际上认为另一个答案中显示的“通过 refaddr 偷偷覆盖跟踪”技术比 reblessing 技术更 CPANable,但我想这完全取决于您对命名空间污染的容忍度。
      • 很好......这更符合我正在寻找的对象级修补程序。
      【解决方案4】:

      本着 Perl 的“让困难的事情成为可能”的精神,这里有一个示例,说明如何在不破坏继承的情况下进行单实例猴子修补。

      建议您在任何其他人必须支持、调试或依赖的代码中实际执行此操作(就像您说的,同意成年人):

      #!/usr/bin/perl
      
      use strict;
      use warnings;
      {
      
          package Monkey;
      
          sub new { return bless {}, shift }
          sub bar { return 'you called ' . __PACKAGE__ . '::bar' }
      }
      
      use Scalar::Util qw(refaddr);
      
      my $f = Monkey->new;
      my $g = Monkey->new;
      my $h = Monkey->new;
      
      print $f->bar, "\n";    # prints "you called Monkey::bar"
      
      monkey_patch( $f, 'bar', sub { "you, sir, are an ape" } );
      monkey_patch( $g, 'bar', sub { "you, also, are an ape" } );
      
      print $f->bar, "\n";    # prints "you, sir, are an ape"
      print $g->bar, "\n";    # prints "you, also, are an ape"
      print $h->bar, "\n";    # prints "you called Monkey::bar"
      
      my %originals;
      my %monkeys;
      
      sub monkey_patch {
          my ( $obj, $method, $new ) = @_;
          my $package = ref($obj);
          $originals{$method} ||= $obj->can($method) or die "no method $method in $package";
          no strict 'refs';
          no warnings 'redefine';
          $monkeys{ refaddr($obj) }->{$method} = $new;
          *{ $package . '::' . $method } = sub {
              if ( my $monkey_patch = $monkeys{ refaddr( $_[0] ) }->{$method} ) {
                  return $monkey_patch->(@_);
              } else {
                  return $originals{$method}->(@_);
              }
          };
      }
      

      【讨论】:

        【解决方案5】:

        Fayland Lam 回答,正确的语法是:

            local *LWP::UserAgent::get_basic_credentials = sub {
                return ( $username, $password );
            };
        

        但这是修补(动态范围)整个类,而不仅仅是实例。在您的情况下,您可能可以摆脱这种情况。

        如果您真的只想影响实例,请使用您描述的子类化。这可以像这样“即时”完成:

        {
           package My::LWP::UserAgent;
           our @ISA = qw/LWP::UserAgent/;
           sub get_basic_credentials {
              return ( $username, $password );
           };
        
           # ... and rebless $agent into current package
           $agent = bless $agent;
        }
        

        【讨论】:

        • 而不是:我们的@ISA ...你不能使用base 'LWP::UserAgent'来代替吗?对我来说看起来更具可读性。
        • 不错的解决方案 blixtor。简单易读,准确解决问题。
        • 同意——所以我从中得出的结论是,在 Perl 中没有真正的方法来修补 instance 的 实例方法,但您可以创建一个匿名的覆盖方法的包或修改包绑定方法。
        • @j_random_hacker:我的意思是你不能在不知道它的关联包的情况下获取一个受祝福的对象并将一个方法注入到它的解析顺序中。不过,您也许可以使用匿名包和 AUTOLOAD 做一些额外的技巧来委托给关联的包。
        • 实际上,John Siracusa 在他的回答中做了类似的事情。
        【解决方案6】:
        sub _user_agent_get_basic_credentials_patch {
          return ($username, $password);
        }
        
        my $agent = LWP::UserAgent->new();
        $agent->get_basic_credentials = _user_agent_get_basic_credentials_patch;
        

        你这里不是1个,而是2个问题,因为这就是你正在做的:

        ( $agent->get_basic_credentials() ) = _user_agent_get_basic_credentials_patch(); 
        

        在这两种情况下,您都在调用 subs 而不是简单地引用它们。

        assign the result of 
                      '_user_agent_get_basic_credentials_patch' 
        to the value that was returned from
                      'get_basic_credentials';
        

        等价逻辑:

        {
           package FooBar; 
           sub foo(){ 
                 return 5; 
           }
           1;
        }
        my $x =  bless( {}, "FooBar" ); 
        sub baz(){ 
              return 1; 
        }
        $x->foo() = baz(); 
        #   5 = 1;  
        

        难怪它会抱怨。

        您的答案中的“固定”代码也是错误的,出于同样的原因,还有另一个您可能没有意识到的问题:

         $agent->{get_basic_credentials} = _user_agent_get_basic_credentials_patch;
        

        这是一种相当有缺陷的逻辑,认为它的工作方式与您想象的一样。

        它真正在做的是:

        1. Dereference $agent, which is a HashRef
        2. Set the hash-key 'get_basic_credentials' to the result from _user_agent_get_basic_credentials_patch
        

        您根本没有分配任何功能。

        {
        package FooBar; 
        sub foo(){ 
             return 5; 
        } 
        1;
        }
        my $x =  bless( {}, "FooBar" ); 
        sub baz(){ 
          return 1; 
        }
        $x->{foo} = baz(); 
        #  $x is now  = ( bless{ foo => 1 }, "FooBar" ); 
        #  $x->foo(); # still returns 5
        #  $x->{foo}; # returns 1; 
        

        猴子补丁当然是相当邪恶的,我自己还没有看到如何在类似的单一实例上覆盖方法。

        但是,你可以这样做:

          {
             no strict 'refs'; 
             *{'LWP::UserAgent::get_basic_credentials'} = sub { 
                 # code here 
        
             }; 
          }
        

        这将全局替换 get_basic_credentials 代码部分的行为(我可能有些错误,有人纠正我)

        如果您真的需要在每个实例的基础上执行此操作,您可能可以做一些类继承,然后构建一个派生类,和/或动态创建新包。

        【讨论】:

        • 在像我这样的孤立案例中,猴子补丁并不是真正的邪恶——我有一个特殊的案例,即使用 HTTP auth 的一次性用户代理。当你开始到处做它而不记录它时,它就变得很糟糕。除此之外,感谢您的帮助!不错的分析。
        【解决方案7】:

        【讨论】:

        • 很好的参考——我应该更经常地记住 Google 代码搜索。
        • Google 代码已不复存在。这就是为什么网站政策是不要在其他地方发布答案的链接。
        【解决方案8】:

        Perl 认为您正在尝试调用赋值左侧的子例程,这就是它抱怨的原因。我认为你可以直接敲击 Perl 符号表(使用 *LWP::UserAgent::get_basic_credentials 或其他东西),但我缺乏 Perl-fu 来正确地做出那个咒语。

        【讨论】:

          猜你喜欢
          • 2015-03-23
          • 2022-11-24
          • 2011-03-20
          • 1970-01-01
          • 2016-11-27
          • 2017-02-12
          • 1970-01-01
          • 2015-11-10
          • 2021-03-10
          相关资源
          最近更新 更多