《高阶Perl》——1.4 层次化数据

1.4 层次化数据

已经介绍的例子显示了递归过程的风采,但是它们遗漏了重要的一点。在介绍汉诺塔问题时,我说过当你想解决的问题可以简化成同样问题的更简形式时,递归是很有用的。但是这样的问题是普遍的这一点可能不清楚。

多数递归的函数是为处理递归的数据结构的建立。一个递归的数据结构就像一个列表、一棵树或者一个堆,它们根据相同数据结构的更小的实例定义而成。最常见的例子可能是文件系统目录结构。文件可以是:

文件可以是一个目录,含有一列文件,其中的一些可以是目录,再含有另一些文件,以此类推。处理这样的结构最有效的办法是递归过程。从概念上,每次调用这样一个过程就处理一个单一文件。这个文件可以是普通文件,也可以是目录,程序通过递归调用自身处理这个目录所含有的子文件。如果子文件也是目录,程序将继续递归调用。

下面的例子是一个函数,以目录名作为参数计算它包含的所有文件的总大小,以及它的子目录、子目录的子目录等。

### Code Library: total-size-broken
sub total_size {
  my ($top) = @_;
  my $total = -s $top;

初次调用此函数时,它有一个参数$top,是要检查的文件或目录的名字。函数首先用Perl的-s操作符得到这个文件或目录本身的大小。此操作符产生以字节为单位的文件大小。如果文件是一个目录,此操作符得到目录本身占用的磁盘空间,而不是指目录所含文件所占用的。请记住,目录本身是文件的一个列表,这个列表也占用一些空间。如果顶层文件确实是个目录,此函数会把它的内容的大小累计到总和$total中。

  return $total if -f $top;
  unless (opendir DIR, $top) {
    warn "Couldn't open directory $top: $!; skipping.\n";
    return $total;
  }

-f操作符检查参数是否是普通文件,如果是,那么函数可以直接返回总和。否则,它假设此顶层文件确实是个目录,并尝试用opendir()打开它。如果无法打开目录,函数发出一条警告消息并返回到目前的总值,它包括目录本身的大小,但不包括它所含内容的大小。

  my $file;
  while ($file = readdir DIR) {
    next if $file eq '.' || $file eq '..';
    $total += total_size("$top/$file");
  }

接下来的代码块,while循环,是函数的核心。它每次从目录中读取一个文件名,对后者递归调用自身,把结果累计到总和中。

  closedir DIR;
  return $total;
}

循环结束后,函数关闭目录并返回总和。

在循环中,函数略过了文件名如.和..,这些分别是目录本身和它的父目录的别名,如果函数不这样做,它将永不会结束,因为它将尝试计算许许多多的文件,如././././././fred和dir/../dir/../dir/../dir/fred。

这个函数有个大bug,事实上它根本不能工作。问题在目录句柄,即DIR,是全局的,因此函数是不可重入的。函数会失效,本质上和缺少my的factorial()失效一样。第一次调用进行顺利,但是如果total_size()递归调用自身,即第二次执行将打开同一个目录句柄DIR。最后,第二次调用将到达它的目录的终点,关闭DIR,然后返回,从这以后,第一次调用将尝试继续,发现DIR已经关闭了,那就退出while循环,还未从顶层目录读取所有的文件名。第二次执行递归调用自身时,也将有同样的问题。

结果就是写成的函数只遍历了目录树的第一个分支。如果目录层次有一个这样的结构,

那函数将沿着top-a-d路径,看到文件j和k,然后报告top+a+d+j+k的总大小,而没有注意到b、c、e、f、g、h、i或l。

为了修正这个错误,需要使目录句柄DIR成为一个私有变量,像$top和$total一样。这里没有用opendir DIR, $top,而是使用opendir $dir, $top,其中$dir是私有变量。当opendir的第一个参数是一个未定义的变量,opendir会产生一个新的、匿名的目录句柄并把它保存到$dir。

与其这样做:

opendir DIR, $somedir;
print (readdir DIR);
closedir DIR;

不如这样做:

my $dir;
opendir $dir, $somedir;
print (readdir $dir);
closedir $dir;

达到同样的效果。这有很大的区别:DIR是个全局目录句柄,能被程序的任何其他部分读取或关闭;$dir中的目录句柄是私有的,仅能被生成它的函数读取或关闭,或者被一些明确地传递了$dir的值的函数读取或关闭。

有了这个新的技巧,就可以重写total_size()函数以使其正确运行:

### Code Library: total-size
sub total_size {
  my ($top) = @_;
  my $total = -s $top;
  my $DIR;

  return $total if -f $top;
  unless (opendir $DIR, $top) {
    warn "Couldn't open directory $top: $!; skipping.\n";
    return $total;
  }

  my $file;
  while ($file = readdir $DIR) {
    next if $file eq '.' || $file eq '..';
    $total += total_size("$top/$file");
  }
  closedir $DIR;
  return $total;
}

实际上,此处的closedir不是必需的,因为此种方法产生的目录句柄会在含有它的变量离开作用域时自动关闭。当total_size()返回时,它的私有变量销毁了,包括$DIR,它包含了打开的目录句柄对象的最后一个引用。Perl接着销毁目录句柄对象,在此过程中,关闭目录句柄。未来将省略明确的closedir。

这个函数仍然有些问题:它没有正确处理符号链接,如果一个文件在同一目录中有两个名字,它会统计两次。此外,在Unix系统里,一个文件在磁盘上实际所占的空间通常与-s报告的长度不同,因为磁盘空间是每次按1024字节的块分配的。但是函数足够有用了,因此可以期望它很好地应用到一些别的任务中。如果要修正这些问题,将只需在这一处修正它们,而不用在50个不同的应用中50个略微不同的目录遍历函数中修正同样的问题。

时间: 2024-10-30 04:30:13

《高阶Perl》——1.4 层次化数据的相关文章

《高阶Perl》——导读

前 言 在编程圈子里有一句著名的俗语,一个优秀的Fortran程序员可以用任何语言写Fortran程序.然而,让人悲哀的是,不管他们是否愿意,Fortran程序员用任何语言写Fortran程序.类似地,作为Perl程序员,我们也在用Perl写C程序,不管我们是否愿意.这让人羞愧,因为Perl是一门比C更富有表现力的语言.我们本可以做得更好,以C程序员梦想不到的方式使用Perl,但是我们没有那样做. 怎么会这样呢?Perl的设计初衷是一方面作为C的替代品,另一方面作为UNIX脚本语言(如Bourn

《高阶Perl》——3.5 MEMOIZE模块

3.5 MEMOIZE模块 本书不是关于Perl模块的内部细节的,而是关于一些Memoize模块内部使用的并和稍后要做的事情有直接联系的技术,所以现在简短地介绍一下. Memoize得到一个函数名(或引用)作为它的参数.它制造一个新的函数,后者维护一个缓存并在其中查找它的参数.如果新的函数在缓存里找到了参数,就返回缓存的值:如果没找到,就调用原始函数,把返回的值保存入缓存,并把它返回给原始的主调者. 制造完这个新的函数,Memoize就把它装入Perl的符号表以代替原始的函数,那样当你认为你在调

《高阶Perl》——3.10 可供选择的记忆术

3.10 可供选择的记忆术 大多数纯函数提供一个缓存的机会.尽管乍一看纯函数很少,它们只以一定频率出现.纯函数特别普遍的地方是在排序中用做比较器函数. Perl内置的sort操作符是通用的,它可以把一列任何种类的数据以程序要求的任何次序排序.默认状态下,它把一列字符串以字母表次序排序,但是程序员可以任意提供一个比较器函数(comparator function),告诉Perl怎样重排sort的参数列表.比较器函数被反复调用,每次带有待排序列表中的两个不同元素,如果两个元素次序正确,就必须返回一个

《高阶Perl》——3.8 对象方法里的缓存

3.8 对象方法里的缓存 对象方法,它经常不理解地把缓存的值保存在独立的散列里.考虑一个投资银行写的程序里的Investor对象.该对象表现了银行的一个客户: package Investor; # Compute total amount currently invested sub total { my $self = shift; # ... complex computation performed here ... return $total; } 如果$total不会改变,就可以缓存

《高阶Perl》——1.7 HTML

1.7 HTML 我曾承诺递归对操作层次化定义的数据结构有用,我还用了文件系统作为一个例子.但它是个稍微特殊的数据结构的例子,因为通常认为数据结构是在内存里,而不是在磁盘上. 文件系统的树型结构让人联想到目录,每个目录都含有一列其他文件.任何拥有一些包含其他条目列表的领域都将含有树型结构.一个极好的例子是HTML数据. HTML数据是一系列元素和普通文本.每个元素含有一些内容,它们又是一系列更多的元素和更多的普通文本.这是个递归的描述,类似文件系统的描述,HTML文件的结构也类似于文件系统的结构

《高阶Perl》——3.7 键的生成

3.7 键的生成 先前的记忆器至少有一个严重的问题.它需要把函数的参数转变成一个散列键,它的做法是使用join: my $key = join ',', @_; 这对只带一个参数的函数有效,也对参数不含逗号的函数有效,包括所有的参数是数字的函数.但是如果函数的参数可能包含逗号,它可能失效,因为以下两个调用却计算出了相同的键: func("x,", "y"); func("x", ",y"); 第一次调用,返回的值将以键&quo

《高阶Perl》——3.12 速度的好处

3.12 速度的好处 此刻有个说法比较诱人,那就是记忆术只比手动缓存技术改善了一点,因为它做了同样的事情,仅有的额外的好处是你可以快速启动或者关闭它.但这不完全正确.当工具之间的速度和便利的差别够大,它会改变你思考和使用工具的方式.自动地使一个函数带记忆仅需要消耗手动书写代码的1/100的时间.这和飞机与牛车的速度差别一样.说飞机只是更快的牛车就忽略了本质:量变如此巨大以致它也变成了实质的质变. 例如,有了自动的记忆术,就有可能为函数增加缓存行为而不必预先仔细考虑性能细节.记忆术这么简单以至于可

《高阶Perl》——3.9 持续的缓存

3.9 持续的缓存 在离开自动的记忆术的主题之前,将浏览一些外围的技术.将看到一个函数如何被带记忆的版本替代,后者在缓存里存储了返回值,缓存就仅是一个散列变量. 在Perl里,可以使用tie操作符把一个散列变量关联到一个磁盘上的数据库,那么存储在散列的数据会自动写到磁盘上,从散列取回数据实际上是从磁盘取回.把这个功能增加到memoize函数是简单的: use DB_File; sub memoize { my ($func, $keygen, $file) = @_; my %cache; if

《高阶Perl》——3.2 内联缓存

3.2 内联缓存 给一个函数添加缓存的最直接的方式就是给函数一个私有的散列.在这个例子里,可以使用一个数组代替散列,因为fib()的参数总是一个非负整数.但是一般需要使用一个散列,那么将会看到: ### Code Library: fib-cached # Compute the number of pairs of rabbits alive in month n { my %cache; sub fib { my ($month) = @_; unless (exists $cache{$m