【问题标题】:Using closures as Iterators使用闭包作为迭代器
【发布时间】:2026-02-23 16:30:01
【问题描述】:

我最近一直在玩马尔可夫链,试图从一个大型语料库中生成文本,只是为了看看我得到了什么(其中一些很有趣)。

构建文本生成所需的数据结构的很大一部分是创建n-grams。给定一个小示例文本:“今天是 3 月 6 日星期四”一个示例 n-gram,其中n = 3 将是:

Today is Thursday
is Thursday March 
Thursday March the
March the sixth
# skipped lines that have < 3 words because is isn't enough for a 3-gram

根据文本的大小,我的代码生成的 n-gram 列表可能非常大,在某些语言中存在 generator 的概念,其中包含用于制作自定义迭代器的 yield 语句,但 Perl不幸的是不是其中之一。

相反,在 Perl 中,我们可以在词法变量上使用闭包来创建 Iterators,但是我在理解使用它们时真正获得的东西时遇到了一些麻烦。

这是我创建的用于创建 n-gram 的迭代器(假设 n 保存在 $self->order 中):

sub _ngrams {
   my ($self, @words) = @_; 

   return sub {
      while(@words) {
         my @ngram = @words[0 .. $self->order]; # get $order + 1 words
         shift @words;                          # drop the first word

         return @ngram;
      }

      return; # nothing left to do
  };
}

我真的从这段代码效率方面获得了什么好处吗?单词列表仍然完全保存在@words 的内存中。是否有替代实现可以减少我的内存占用?

下面是使用迭代器生成字典的方式:

sub seed { 
   my $self = shift; 

   my $ngram_it = $self->_ngrams(split /\s+/, $self->text); 
GRAM:
   while (my @gram = $ngram_it->()) {
      next GRAM unless @gram == scalar grep { $_ } @gram;

      my $val = pop @gram; 
      my $key = join ' ', @gram; 

      if (exists $self->lexicon->{$key}) {
         push @{$self->lexicon->{$key}}, $val;
      }
      else {
         $self->lexicon->{$key} = [$val];
      }
   }
}

任何输入都会非常有帮助。

【问题讨论】:

  • 使用迭代器为您提供了灵活性。您可以轻松地交换一个从流中提供单词的迭代器。 (我不会有一个返回 n-gram 的迭代器,我会有一个返回单词的迭代器。)
  • @ikegami 但这在这种情况下可行吗?我需要在哪里获得 N + 1 个单词,然后只删除第一个?然后抓取接下来的 N + 1 个单词,现在包括前面的 N 个单词。
  • 只使用已有的逻辑,但将其移出迭代器。

标签: perl iterator closures


【解决方案1】:

首先,您的迭代器实现有在最后几个值中返回undef 项的不良倾向。我会改成

sub _ngrams {
   my ($self, @words) = @_; 
   my $order = $self->order;

   return sub {
      if (@words > $order) {
         my @ngram = @words[0 .. $order]; # get $order + 1 words
         shift @words;                          # drop the first word

         return @ngram;
      }

      return; # nothing left to do
  };
}

接下来,这个迭代器是一个很好的抽象。这并不意味着以任何方式提高性能,它只是使您的主要代码更简单有用。在这里,如果你不分离迭代,你的代码会更短(但不会更简单),并在你的主代码中完成。

但是,迭代器可以处理一些有趣的事情,例如惰性求值或无限流。为了让这个有用,我们必须完全切换到流:

# contract: an iterator returns a list of things
# or an empty list when depleted

sub _ngrams {
   my ($self, $source) = @_; 
   my $order = $self->order;

   my @ngram = (undef, map { $source->() } 1 .. $order);

   return sub {
      if (my ($next) = $source->()) {
          (undef, @ngram) = (@ngram, $next);  # or instead: shift/push
          return @ngram;
      }
      return;
  };
}

初始化如下

my $text = $self->text;
my $iter = $self->_ngrams(sub {
    return $1 if $text =~ /\G\s*(\S+)/gc;
    return;
});

这在这里有用吗?不,因为您立即从迭代器中获取所有元素。 最简单的解决方案不会使用花哨的抽象,而是这样:

sub seed { 
   my $self = shift; 

   my @words = split /\s+/, $self->text;
   my $order = $self->order;
   while (@words > $order) {
      my @gram = @words[0 .. $order];  # get the next n-gram
      shift @words;

      my $val = pop @gram; 
      push @{$self->lexicon->{join ' ', @gram}}, $val;
   }
}

我敢打赌它也是最(时间)性能最好的变体。

注意:没有必要测试exists,因为 Perl 会散列 autovivify。 (或者你在使用奇怪的扩展?)

【讨论】:

  • 谢谢,这真的很有用。您的最后一个示例确实比拆分过程要干净得多。一个小的不是,该值实际上是 n-gram 的最后一部分,这就是我将其弹出的原因。在写这篇文章之前,我也刚刚阅读了关于高阶 Perl 中的迭代器的章节,并且很想使用它们:)