如何在 Perl 中对实例方法进行猴子修补?

发布于 2024-07-11 18:49:51 字数 579 浏览 5 评论 0原文

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

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 进行子类化,但我更喜欢更简洁的猴子修补方法。 同意成年人,你有什么。 ;-)

I'm trying to monkey-patch (duck-punch :-) a LWP::UserAgent instance, like so:

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;

This isn't the right syntax -- it yields:

Can't modify non-lvalue subroutine
call at [module] line [lineno].

As I recall (from Programming Perl), dispatch lookup is performed dynamically based on the blessed package (ref($agent), I believe), so I'm not sure how instance monkey patching would even work without affecting the blessed package.

I know that I can subclass the UserAgent, but I would prefer the more concise monkey-patched approach. Consenting adults and what have you. ;-)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

不顾 2024-07-18 18:49:51

正如 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;
}

As answered by Fayland Lam, the correct syntax is:

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

But this is patching (dynamically scoped) the whole class and not just the instance. You can probably get away with this in your case.

If you really want to affect just the instance, use the subclassing you described. This can be done 'on the fly' like this:

{
   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;
}
属性 2024-07-18 18:49:51

如果动态范围(使用 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!

If dynamic scope (using local) isn't satisfactory, you can automate the custom package reblessing technique:

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

Example usage:

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!
初与友歌 2024-07-18 18:49:51

本着 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}->(@_);
        }
    };
}

In the spirit of Perl's "making hard things possible", here's an example of how to do single-instance monkey patching without mucking with the inheritance.

I DO NOT recommend you actually doing this in any code that anyone else will have to support, debug or depend on (like you said, consenting adults):

#!/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}->(@_);
        }
    };
}
七禾 2024-07-18 18:49:51
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 代码部分的行为(我可能有点错,有人纠正我)

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

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;

You have not 1, but 2 problems here, because this is what you are doing:

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

on both sides cases, you're calling the subs instead of simply referring to them.

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

Equivalent logic :

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

So its no wonder its complaining.

Your "fixed" code in your answer is also wrong, for the same reason, with another problem you may not realise:

 $agent->{get_basic_credentials} = _user_agent_get_basic_credentials_patch;

This is rather flawed logic thinking it works like you think it does.

What it is really doing, is:

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

You didn't assign any function at all.

{
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; 

Monkey patching is rather evil of course, and I have not myself seen how to override a method on a singular instance of something like that.

However, what you can do is this:

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

     }; 
  }

Which will globally replace the get_basic_credentials code sections behaviour ( I might be wrong somewhat, somebody correct me )

If you really need to do it on a per-instance basis you could probably do a bit of class inheritance and just build a derived class instead, and/or dynamically create new packages.

浅笑依然 2024-07-18 18:49:51

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

Perl thinks you're trying to call the subroutine on the left of the assignment, which is why it's complaining. I think you may be able to whack the Perl symbol table directly (using *LWP::UserAgent::get_basic_credentials or something), but I lack the Perl-fu to correctly make that incantation.

盛装女皇 2024-07-18 18:49:51

基于 John Siracusa 的回答......我发现我仍然想要对原始函数的引用。 所以我这样做了:

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,但在它周围装饰额外的代码。

Building upon John Siracusa's answer… I found that I still wanted a reference to the original function. So I did this:

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;
    });

It's the same as in the original answer, except I pass through some parameters $self and $oldFunction.

This lets us invoke $self's $oldFunction as usual, but decorate additional code around it.

余生共白头 2024-07-18 18:49:51

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

啊,我刚刚意识到语法需要一点调整:

$agent->{get_basic_credentials} = _user_agent_get_basic_credentials_patch;

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

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

Edit: This was an incorrect attempt at a solution that I'm keeping for posterity. Look at the upvoted/accepted answers. :-)

Ah, I just realized that the syntax needs a little bit of adjustment:

$agent->{get_basic_credentials} = _user_agent_get_basic_credentials_patch;

Without the {} delimiters it looks like a method invocation (which would not be a valid l-value).

I'd still like to know how the instance method gets bound/looked up via this syntax. TIA!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文