【问题标题】:Pimp my Perl code皮条客我的 Perl 代码
【发布时间】:2010-01-19 12:53:02
【问题描述】:

我是一位经验丰富的开发人员,但不是 Perl。我通常学习 Perl 来破解脚本,然后我又忘记了,直到下一次。因此,我正在寻求专业人士的建议。

这一次我正在构建一系列数据分析脚本。粗略简化,程序结构是这样的:

01 my $config_var = 999;

03 my $result_var = 0;

05 foreach my $file (@files) {
06   open(my $fh, $file);
07   while (<$fh>) {
08     &analyzeLine($_);
09   }
10 }

12 print "$result_var\n";

14 sub analyzeLine ($) {
15   my $line = shift(@_);
16   $result_var = $result_var + calculatedStuff;
17 }

在现实生活中,config_vars 和 result_vars 最多有六种不同的类型。

这些脚本的主要区别在于分配给config_vars 的值。主循环在每种情况下都是相同的,analyzeLine() 基本相同,但可能会有一些小的变化。

我可以通过复制这段代码来实现我的目的,在这里和那里做一些小的改动;但这严重违反了各种良好设计的规则。理想情况下,我想编写一系列脚本,其中只包含一组配置变量初始化,然后是

do theCommonStuff;

请注意,config_var(及其同级)必须可用于公共代码,result_var 及其类似代码也必须如此,analyzeLine() 对此进行一些计算。

我应该将我的“通用”代码打包到一个模块中吗?创建一个班级?使用全局变量?

虽然不完全编写高尔夫代码,但我正在寻找一种简单、紧凑的解决方案,让我可以干涸并仅为差异编写代码。我想我宁愿不要将代码从包含所有配置的巨大表中删除,当然也不会将其调整为使用数据库。

期待您的建议,谢谢!


更新

既然有人问,这里是真实的analyzeLine:

# Update stats with time and call data in one line.
sub processLine ($) {
  my $line = shift(@_);
  return unless $line =~ m/$log_match/;
  # print "$1 $2\n";
  my ($minute, $function) = ($1, $2);
  $startMinute = $minute if not $startMinute;
  $endMinute = $minute;
  if ($minute eq $currentMinute) {
    $minuteCount = $minuteCount + 1;
  } else {
    if ($minuteCount > $topMinuteCount) {
      $topMinute = $currentMinute;
      $topMinuteCount = $minuteCount;
      printf ("%40s %s : %d\n", '', $topMinute, $topMinuteCount);
    }
    $totalMinutes = $totalMinutes + 1;
    $totalCount = $totalCount + $minuteCount;
    $currentMinute = $minute;
    $minuteCount = 1;
  }
}

由于这些变量在很大程度上是相互依赖的,我认为单独计算的函数式解决方案并不实用。我为误导人们道歉。

【问题讨论】:

  • 你能给我们一个实际的例子来说明你想用这个实现什么吗?这看起来是使用函数式编程的好机会。

标签: perl refactoring coding-style


【解决方案1】:

两个 cmets:首先,不要发布行号,因为它们会使复制、粘贴和编辑变得比必要的困难。其次,不要使用&amp;func() 来调用子程序。见perldoc perlsub:

可以使用显式的&amp; 前缀调用子例程。 &amp; 在现代 Perl 中是可选的,... &amp; 表单不仅使参数列表成为可选,它还禁用对您提供的参数的任何原型检查。

简而言之,除非您知道自己在做什么以及为什么这样做,否则使用&amp; 可能会令人惊讶。

另外,don't use prototypes 在 Perl 中。它们与其他语言中的原型不同,同样,除非您知道自己在做什么,否则它们会产生非常令人惊讶的效果。

不要忘记检查open等系统调用的返回值。将autodie 与现代perls 一起使用。

针对您的具体问题,将所有配置变量收集到一个散列中。将该哈希传递给analyzeLine。

#!/usr/bin/perl

use warnings; use strict;
use autodie;

my %config = (
    frobnicate => 'yes',
    machinate  => 'no',
);

my $result;
$result += analyze_file(\%config, $_) for @ARGV;

print "Result = $result\n";

sub analyze_file {
    my ($config, $file) = @_;

    my $result;

    open my $fh, '<', $file;
    while ( my $line = <$fh> ) {
        $result += analyze_line($config, $line);
    }

    close $fh;

    return $result;
}

sub analyze_line {
    my ($line) = @_;
    return length $line;
}

当然,您会注意到 $config 被到处传递,这意味着您可能希望将其转化为 OO 解决方案:

#!/usr/bin/perl

package My::Analyzer;

use strict; use warnings;

use base 'Class::Accessor::Faster';

__PACKAGE__->follow_best_practice;
__PACKAGE__->mk_accessors( qw( analyzer frobnicate machinate ) );

sub analyze_file {
    my $self = shift;
    my ($file) = @_;

    my $result;

    open my $fh, '<', $file;
    while ( my $line = <$fh> ) {
        $result += $self->analyze_line($line);
    }

    close $fh;

    return $result;
}

sub analyze_line {
    my $self = shift;
    my ($line) = @_;
    return $self->get_analyzer->($line);
}

package main;

use warnings; use strict;
use autodie;

my $x = My::Analyzer->new;

$x->set_analyzer(sub {
        my $length; $length += length $_ for @_; return $length;
});
$x->set_frobnicate('yes');
$x->set_machinate('no');


my $result;
$result += $x->analyze_file($_) for @ARGV;

print "Result = $result\n";

【讨论】:

  • 感谢众多体贴的cmets!我添加了行号以便更容易地讨论代码,因为我期望进行更抽象的讨论。
  • 我认为原型是允许参数检查的好主意;另外,我使用 & 来表明我正在调用自定义函数。但我当然可以两者都没有。
  • 我确实在我的open 上使用or die,也在opendir 上使用,谢谢。再次,抱歉我的例子过于简单化了。 Nix 在autodie 上,我的 perl 环境范围从 5.005 到 5.10。
  • 您的 OO 解决方案看起来相当先进,可能正在使用我的旧 Perl 中没有的功能;另外,我不能轻易安装新的 CPAN 软件包。我可能最终会贬低我在这里得到的建议。
  • 也感谢您的原型链接。最有启发性,因为我一直很想问“为什么?”现在下面的引用:My first thought any time I see code with prototypes is that the person who wrote it doesn't know Perl very well. 为使用原型提供了一个非常好的理由:我真的不太了解 Perl,我打算宣传这个事实! :)
【解决方案2】:

继续创建一个类层次结构。您的任务是 OOP 编程风格的理想场所。 这是一个例子:

package Common;
sub new{
  my $class=shift;
  my $this=bless{},$class;
  $this->init();
  return $this;
}
sub init{}
sub theCommonStuff(){ 
  my $this=shift;
  for(1..10){ $this->analyzeLine($_); }
}
sub analyzeLine(){
  my($this,$line)=@_;
  $this->{'result'}.=$line;
}

package Special1;
our @ISA=qw/Common/;
sub init{
  my $this=shift;
  $this->{'sep'}=',';   # special param: separator
}
sub analyzeLine(){      # modified logic
  my($this,$line)=@_;
  $this->{'result'}.=$line.$this->{'sep'};
}

package main;
my $c = new Common;
my $s = new Special1;
$c->theCommonStuff;
$s->theCommonStuff;
print $c->{'result'}."\n";
print $s->{'result'}."\n";

【讨论】:

  • 听起来不错,谢谢!我可以将这些包放在单独的文件中,每个文件都像包一样命名,对吧?
  • 请不要使用也不推荐使用间接对象语法,但表面上吸引人的是能够写new Common而不是Common-&gt;new。
  • @Carl Smotricz:你问了这个问题,所以不要因为选择一个答案而不是另一个而感到难过。请注意,@catwalk 访问对象的内部(违反封装)而不是使用 getter 和 setter,我使用 Class::Accessor::Faster 的唯一原因是避免编写访问器;-)
  • 哦,当然,允许模块的用户指定一个 sub 来对每行进行实际的“分析”,这为您提供了灵活性以及脚手架和实际工作之间的分离。
  • 要获得出色的 OOP 优势和更少的样板,请查看 Moose。 moose.perl.org
【解决方案3】:

如果所有公共代码都在一个函数中,那么一个函数将您的配置变量作为参数,并返回结果变量(作为返回值,或作为输入/输出参数),就可以了。否则,创建一个类(“包”)也是一个好主意。

sub common_func {
    my ($config, $result) = @_;
    # ...
    $result->{foo} += do_stuff($config->{bar});
    # ...
}

请注意,上面的配置和结果都是哈希(实际上是对其的引用)。您可以使用您认为适合您目标的任何其他数据结构。

【讨论】:

  • 我的原型是出于习惯,但是是的,它是可选的。
  • 我不知道 -1 是否来自您,以及它是否超出了原型,但如果是这样,将不胜感激撤回反对票。 (如果是为了别的,我很想听听。):-)
  • @Chris Jester-Young:不,这只是原型问题。我刚刚离开 SO 有一段时间了。
  • @Sinan:谢谢,非常感谢!
【解决方案4】:

一些想法:

  • 如果有多个$result_vars,我建议创建一个单独的子程序来计算每个子程序。
  • 如果子例程依赖于该函数之外的信息,则应将其作为参数传递给该子例程,而不是依赖于全局状态。
  • 或者将整个事物包装在一个类中,将$result_var 作为类的属性。

实际上,有几种方法可以实现:

(1) 让您的&amp;analyzeLine 函数返回calculatedStuff,并在函数外的循环中将其添加到&amp;result_var:

  $result_var = 0;
  foreach my $file (@files) {
      open(my $fh, $file);
          while (<$fh>) {
              $result_var += analyzeLine($_);
          }
      }
  }

  sub analyzeLine ($) {
      my $line = shift(@_);
      return calculatedStuff;
  }

(2) 将$result_var 显式传递给analyzeLine,并返回更改后的$result_var。

  $result_var = 0;
  foreach my $file (@files) {
      open(my $fh, $file);
          while (<$fh>) {
              $result_var = addLineToResult($result_var, $_);
          }
      }
  }

  sub addLineToResult ($$) {
      my $running_total = shift(@_);
      my $line = shift(@_);
      return $running_total + calculatedStuff;
  }

重要的部分是,如果您为几个 $result_var 中的每一个分离出函数,您将更容易编写干净的代码。不用担心优化。当您的代码证明自己很慢时,这可能会在以后出现。改进后的设计将使优化在时机成熟时更容易。

【讨论】:

  • 几个单独的结果是不实用的,很抱歉没有提供足够的信息。我曾考虑通过参数传递所有内容,但最终可能会使用类。感谢您的意见!
  • 如果你只是想稍后通过使用&amp; 绕过原型,那么对你的潜艇进行原型设计是没有意义的。
  • @Carl——不用担心。这就是为什么有多种设计模式的原因。对于以后阅读本文的人可能仍然有用。 @Ether--谢谢。我现在已经解决了。我不是 Perl 大师,只是喜欢干净的代码。
【解决方案5】:

为什么不创建一个函数并使用 $config_var 和 $result_var 作为参数?

【讨论】:

  • 每个都有几个,我必须将引用传递给result_vars。另外,如果函数在单独的文件中,我如何从“主”脚本访问它?
猜你喜欢
  • 2011-05-27
  • 1970-01-01
  • 1970-01-01
  • 2013-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多