Linux 编程提示

作者:Jim Shapiro

如果您像许多 Linux 用户一样,可能听说过 Perl,但一直不愿学习另一种语言。几个月前,我也处在这种境地。一位朋友建议我试用一下 Perl。由于我已经了解 C 语言,所以 Perl 很容易上手。我很快发现自己可以使用 Perl 进行各种文本重新格式化。我的朋友和同事对此印象深刻,但持怀疑态度。Perl 是否能处理需要读取、处理和写入大量数据的大型文件?

然而,当我编写了一个 Perl 程序(我更喜欢称它们为程序而不是脚本,只是为了将它们与 shell 脚本区分开来)来加载超过 350 万行的美国邮政数据时,他们的怀疑消除了。现在我实际上在教授 Perl(但我跑题了)。

在本文中,我想建议一种经常被忽视的 Perl 用途 - Perl 作为原型设计工具。大多数 C 程序员花费大量时间管理内存,我也不例外。内存管理是一项必要的功能,特别是如果您希望保持 C 程序的紧凑性(不使用超过必要的内存)和良好的行为(不会因产生的核心转储而崩溃)。自己管理内存的问题在于,它可能会分散您对程序目的的注意力,而程序的目的是通常运行一个算法。

使用 Perl,您不仅可以获得可靠的内存处理例程,还可以在解释/编译的环境中获得它们,并且感谢自由软件基金会,它们不会花费您一分钱。事实上,如果您有 Linux,您可能已经拥有 Perl。让我用两个例子来说明如何将 Perl 用作原型设计工具:一个简单的蒙特卡洛计算和一个更重要的几何问题。

π 的蒙特卡洛估计

蒙特卡洛技术使用概率方法进行估计。通常,将一个或多个随机数代入函数,并测试结果值的有效性。该程序跟踪满意测试的数量和测试的总数。结果是满意测试与总测试的比率,并且随着测试次数的增加,会监控该比率。

#!/usr/bin/perl
# Monte Carlo calculation of pi
srand(time | $$);
for($i = 1, $inside = 0.0; $i <= 1000000; $i++)
   {
   $x = rand;
   $y = rand;
   $inside++ if $x * $x + $y * $y < 1.0;
   printf "After %7d points pi ~= %9.7lf\n", $i, 4.0 * $inside / $i if
      ($i % 10000) == 0;
   }

图 1. π 的蒙特卡洛计算

让我们用一个简短的 Perl 程序来入门。图 1 是一个使用蒙特卡洛计算估计 pi 的程序。考虑一个内接于边长为 2 且中心位于原点的正方形内的圆。正方形的面积为 4,并且由于圆的半径为 1,因此其面积为 pi。因此,圆的面积与正方形面积的比率为 pi / 4,这也是正方形内的随机点也位于圆内的概率。该程序重复一个循环一百万次,每次都调用 Perl 的 rand 函数两次,一次用于 x,一次用于 y。计算 xy 对与原点的距离,如果小于 1,则将其计为成功测试。(从技术上讲,为了简单起见,此程序仅使用圆和正方形的第一象限部分,但通过对称性,面积比与使用两个图形的整体时相同。)

Linux 提示 - 如果您为 Perl 程序提供唯一的扩展名(例如 .pl),则很容易使它们在 ls 类型的列表中脱颖而出。将以下行添加到文件 /etc/DIR_COLORS

.pl 01;33 # perl programs (yellow)

将使列表中所有带有 .pl 扩展名的文件的名称以黄色显示。有关详细信息,请参阅 ls 的手册页和文件 /etc/DIR_COLORS。

首先,请注意 Perl 程序有多短。另外,请注意它与 C 程序有多么相似,尤其是 for 循环和 printf 函数。但是,存在重要差异。当需要时使用变量(例如 $i$x 等),无需进行特定声明。即使对于普通的观察者来说,变量的类型也不清楚。这些在它们控制的语句之后的 if 语句是什么意思?rand 像一个函数一样使用,但是没有括号 - 也许它是一个关键字。

所有这些差异都是 Perl 的特性。Perl 会为您跟踪变量。变量实际上在内部是字符串,但在需要时(例如,当计算随机点与原点的距离并将其与 1 进行比较时)会被转换为双精度浮点数。但是,您无需担心任何这些细节。行尾的 if 测试是 C 的 if 块的便捷等效项(C 样式在 Perl 中也可以),但更短。一旦掌握了它,您会发现自己一直在使用 Perl 的 if 样式。Perl 的 if 有三个亲戚(unlesswhileuntil),它们也可以在块之前或一系列逗号分隔的语句的末尾使用。最后,rand 确实是一个函数 - 在这种情况下,括号是可选的。许多函数都是这种情况,包括 for 循环底部的 printf

如果您运行图 1 中的程序,您将在每次 10,000 次测试后获得 pi 的估计值。我将该程序称为 m_c.pl,并通过键入其名称来运行它。第一行是我系统上 Perl 程序的路径。如果您的路径不同,请更改此路径。您还可以使用 -c 命令行开关测试程序是否存在语法错误,即

perl -c m_c.pl

Perl 还会提供警告,例如,当您分配给一个变量并且不再使用该变量时。使用 -w 命令行开关打开警告。这是一种方便的方法来发现拼写错误,拼写错误很容易在没有显式变量声明的环境中出现。我通常在开发过程中同时使用这两个测试,即

perl -cw m_c.pl
多边形中的点

接下来,让我们考虑一个更困难的问题。我们如何测试一个点是否在多边形内部?这个问题并不像最初看起来那么简单,尤其是在您考虑到特殊情况时 - 例如当该点位于多边形的边界上时。那是在内部还是在外部?或者您想将其算作一个单独的情况 - 在边界上?让我们分解问题,首先编写一个 Perl 子程序来测试一个点是否在一条线上。

一种效率不高但易于编码的例程的工作方式如下。如果一个点在一条线上,则该点到每个线端点的距离之和与端点之间的距离相同,即线的长度。您可能需要重读前面的句子并做一个小草图,以使自己确信确实如此。我们首先需要一个例程(在 Perl 中,它们被称为子程序)来查找两点之间的距离。C 有一个内置函数 hypot,但图 2 中的单行代码是 Perl 等效代码。sub 关键字表示子程序,子程序的名称紧随其后。子程序的定义中没有参数列表。这些是在图 2 中子程序名称后面的注释。我们将在调用子程序时提供参数列表,正如您很快就会看到的那样。

sub hypot # (x, y) returns sqrt(x * x + y * y)
{
sqrt $_[0] * $_[0] + $_[1] * $_[1];
}

图 2. 用于查找点之间距离的 Perl 子程序

当您在 Perl 中调用子程序时,所有参数都会自动放入数组 @_ 中(@ 表示标准数组)。在这种情况下,该数组有两个元素,即两点的 xy 坐标的差异。请注意,这些元素是通过地址引用的,因此我们需要小心。对此类变量的任何修改都会更改此子程序之外的值。在 Perl 中,您还可以在子程序中或任何块内创建局部变量。通常我会这样做,但由于这个子程序很短并且可能会被多次调用,所以我避免了局部变量的开销。再次注意,Perl 函数 sqrt 被调用时没有任何括号。子程序的返回值是最后计算的东西(或者您可以使用特定的返回值来覆盖此行为)。在这里,毕达哥拉斯结果是最后计算的值,因此不需要显式的 return。您可能会发现,正如我所发现的那样,很少需要显式的 return 语句。

现在我们需要告诉我们的程序点和区域是什么。在 C 中,我们似乎拥有结构体的优势。我们可能会设置类似于图 3 中的 typedef 和声明的内容。

typedef struct
   {
   double x,
          y;
   } POINT;
POINT *polygon_p;

图 3. Typedef 和声明

一个点有 xy 坐标,一个多边形有三个或更多个点。每次我们需要一个多边形时,我们可以 malloc 为它分配空间并用点填充它 - 并在我们完成它时释放空间。这就是 Perl 的闪光点。

Perl 既没有显式的内存分配也没有结构体。好消息是,我们不必担心内存 - 当我们需要它时它就在那里。坏消息是我们必须提出一些像 C 结构体一样的系统。这证明是微不足道的。让我们把一切都放入字符串中。Perl 会处理任何长度的字符串,从而使我们摆脱了计算字符的麻烦。您甚至不必为终止空字符添加一个 - Perl 不使用终止符。

我们的点将只是一个包含两个由逗号连接的双精度浮点数的字符串;我们的线将是一个包含两个由冒号连接的点的字符串;我们的多边形将是一个包含三个或更多个由冒号连接的点的字符串,如下所示

$point = "1.0,2.0";
$line = "0.0,0.0:0.0,1.0";
$polygon = "1.0,2.0:6.0,5.0:0.0,3.1";

由于 Perl 的 join 和 split 函数,这些“结构”在视觉上易于扫描,并且非常易于管理。图 4 是我使用上述方案开发的 point_on_line 子例程。

$epsilon = 1.0e-10;
sub point_on_line #(point, line) returns 0 or 1
{ # if on - sum of distance to ends should be distance between ends
local($point, $line) = @_;
local($p_x, $p_y) = split(",", $point);
local($l_b, $l_e) = split(":", $line);
local($l_b_x, $l_b_y) = split(",", $l_b);
local($l_e_x, $l_e_y) = split(",", $l_e);
&fabs(&hypot($p_x - $l_b_x, $p_y - $l_b_y) +
      &hypot($p_x - $l_e_x, $p_y - $l_e_y) -
      &hypot($l_e_x - $l_b_x, $l_e_y - $l_b_y)) < $epsilon;
}
sub fabs # (x) returns absolute value of x
{
local($rv) = @_;
$rv = -$rv if $rv < 0.0;
$rv;
}

图 4. point-on-line 子例程

这个子例程用两个字符串调用,即:

&point_on_line($point, $line);

如果点在线上,则返回 1,否则返回 0。请注意,在这种情况下,我将调用参数放入局部数组中以确保安全性和易于维护——与 $_[0]$_[1] 相比,$point$line 更容易在下周记住。 split 函数的用法应该很明显。 point 字符串通过逗号分隔成两个坐标字符串,line 字符串通过冒号分隔成两个点字符串。然后,直线起点和终点的坐标 $l_b$l_e 被分解为它们各自的坐标。最后,坐标被传递给我们的 hypot 函数。请注意,如何使用 & 符号作为子例程调用的前缀。 Perl 没有与 C 的 fabs 等效的函数,所以我快速地在图 4 中创建了自己的函数。我使用 $epsilon 变量来避免浮点舍入问题。

我们一直在自下而上地构建这个东西。到目前为止,我们有一个例程来测试一个点是否在一条线上。我们的目标是测试一个点是否在多边形内的例程。让我给你概述一下我们将如何解决这个问题,为你提供另外两个 Perl 例程(你应该能够读懂它们),然后向你展示外部的“主力”例程,它将进行最终的内部/在线/外部的确定。

我们的多边形是一个闭合图形,每个点要么在内部,要么在外部,要么在边界上(实际上有一个被称为约旦曲线定理的数学陈述证明了这一点!)。如果我们能确定一个保证在我们多边形之外的点,我们可以通过将这个测试点与我们的“外部”点连接成一条直线,并计算这条线穿过多边形的次数来测试任何点的“内部性”。显然,如果这条线从不穿过多边形,则该点在外部;如果它穿过一次,则该点在内部;一般来说,如果该线穿过多边形的边界奇数次,则该点在内部。偶数次穿过意味着该点在外部。换句话说,每次该线穿过多边形时,它都会从内部变为外部或反之亦然。从外部点开始,我们的第一次穿过(如果有)将我们带入内部;下一次穿过(如果有)将我们带回外部;等等,奇数次穿过-内部,偶数次穿过-外部。所以,看起来我们需要一个例程来测试两条线是否相交。

一种测试线是否相交的方法是确保每条线的两个端点都在另一条线的相对两侧。(像以前一样,重读并绘制一些图片。)这导致了我们的最终例程。现在我们将测试,当我们沿着一条线从起点走到终点,然后转向直接走到另一个点时,我们是逆时针还是顺时针旋转。这个讨论和由此产生的例程是模仿 Sedgewick 在“C 算法”中的内容,第 349-354 页(Robert Sedgewick, Addison Wesley, 1990)。让我们从逆时针子例程开始,图 5 中的 ccw

sub ccw # (three points) return -1, 0, or 1
{
local(@points) = @_;
local($rv) = 0;
local($dx1, $dx2, $dy1, $dy2, $p0x, $p0y, $p1x, $p1y, $p2x, $p2y);
($p0x, $p0y) = split(",", $points[0]);
($p1x, $p1y) = split(",", $points[1]);
($p2x, $p2y) = split(",", $points[2]);
$dx1 = $p1x - $p0x;
$dy1 = $p1y - $p0y;
$dx2 = $p2x - $p0x;
$dy2 = $p2y - $p0y;
switch:
   {
   $rv =  1, last if $dx1 * $dy2 > $dy1 * $dx2;
   $rv = -1, last if $dx1 * $dy2 < $dy1 * $dx2;
   $rv = -1, last if ($dx1 * $dx2 < 0.0) || ($dy1 * $dy2 < 0.0);
   $rv =  1, last if ($dx1 * $dx1 + $dy1 * $dy1) < ($dx2 * $dx2 + $dy2 * $dy2);
   }
$rv;
}

图 5. 逆时针 (ccw 子例程)

ccw 例程接受三个点,并将从第二个点到第三个点的线的斜率与从第一个点到第二个点的线的斜率进行比较。它经过精心构建,可以处理垂直线,甚至共线点的情况。请注意,如何将输入点收集到局部数组 @points 中,避免副作用并使程序易于维护和理解。这些点分别被分割成它们各自的坐标,并在标记为 switch 的块中进行测试(原因很明显)。到目前为止,你可能已经猜到 Perl 允许标签,就像 C 一样。然而,Perl 中没有特定的 switch 结构。上面 ccw 中的块是 Perl 构建 switch 的方式。 last 关键字立即退出该块,有时 Perl 解释器足够聪明,可以在内部将该块转换为 C switch 语句(尽管在这种情况下不是)。

sub intersect # (two lines) returns 0 or 1
{
local($l1, $l2) = @_;
local($l1_b, $l1_e) = split(":", $l1);
local($l2_b, $l2_e) = split(":", $l2);
&ccw($l1_b, $l1_e, $l2_b) * &ccw($l1_b, $l1_e, $l2_e) <= 0 &&
&ccw($l2_b, $l2_e, $l1_b) * &ccw($l2_b, $l2_e, $l1_e) <= 0;
}

图 6. 相交子例程

图 6 是相交例程。它非常简单。这些线被分割成端点,并测试端点的“侧面”。

现在我们拥有了构建“内部”子例程的所有构建块。首先,我们通过一条直线将测试点与一个“外部”点连接起来。然后,我们沿着多边形走动,依次测试每个多边形的边,累积这些边与测试线的交点。如果交点的数量是奇数,则该点在内部,反之亦然。

$big_float = 1.0e7;
sub lower_left_index # (polygon) returns index of lower left corner
{
local($polygon) = @_;
local($index) = 0;
local(@vertices) = split(":", $polygon);
local($x_min, $y_min) = split(",", $vertices[0]);
local($i, $x, $y);
for($i = 1; $i <= $#vertices; $i++)
   {
   ($x, $y) = split(",", $vertices[$i]);
   if(($y < $y_min) || (($y == $y_min) && ($x < $x_min)))
      {
      $x_min = $x;
      $y_min = $y;
      $index = $i;
      }
   }
$index;
}
sub inside # (point, polygon) returns 0 or 1
{
local($point, $polygon) = @_;
local(@vertices) = split(":", $polygon);
local($index) = &lower_left_index($polygon);
local($last_index) = $index ? $index - 1 : $#vertices;
local($count, $holding_point) = (0, 0);
local($i, $lp, $lt, $vertex, $x, $y, $big_x_point);
local($check_index) = $index; # true if index is not zero
OUTER: for(;;)
   { # one pass loop
   for($i = 0, $vertex = $vertices[$#vertices]; $i <= $#vertices; $i++)
      {
      $lp = join(":", $vertex, $vertices[$i]);
      $vertex = $vertices[$i];
      if(&point_on_line_2($point, $lp))
         {
         $count = 1;
         print "Point on boundary\n" if defined $verbose;
         last OUTER;
         }
      }
   ($x, $y) = split(",", $point);
   $big_x_point = join(",", $big_float, $y);
   $lt = join(":", $point, $big_x_point);
   for($i = 0; $i <= $#vertices; $i++)
      {
      if(&point_on_line_2($vertices[$index], $lt))
         {
         $holding_point = 1;
         }
      else
         {
         if(!$holding_point)
            {
            $lp = join(":", $vertices[$last_index], $vertices[$index]);
            $count++ if &intersect($lp, $lt);
            }
         elsif($holding_point &&
               (&ccw($point, $big_x_point, $vertices[$index]) !=
                &ccw($point, $big_x_point, $vertices[$last_index])))
            {
            $count++;
            }
         $last_index = $index;
         $holding_point = 0;
         }
      $index++;
      if($check_index && ($index == @vertices))
         {
         $check_index = 0;
         $index = 0;
         }
      }
   last;
   } # one pass "loop"
$count & 1;
}

图 7. lower-left-index 和 inside 子例程

图 7 中的一对子例程是 Sedgewick 在“包含在多边形中”(第 353-355 页)部分中建议的函数的 Perl 实现,尽管更完整。 lower_left_index 子例程返回在具有最小 y 坐标的所有点中,具有最小 x 坐标的多边形点的索引。这对于解释多边形顶点位于测试线上的特殊情况是必要的。请注意,在块内的第三行中,通过使用冒号分割 $polygon 字符串自动构建 @vertices 数组。 @vertices 数组的每个元素是由逗号分隔的一对坐标,每个顶点一个。每当我们需要单独的 x、y 对时,就会调用 split 函数,就像我们之前看到的那样。这发生在 for 循环之前,用于初始化 $x_min$y_min,以及在循环内部生成一个新的测试对 $x, $y。循环变量 $i 中的上限是 $#vertices,它是 @vertices 的最后一个成员的索引。 Perl 自动为每个数组保留这些变量之一。该例程中的最后一个语句 $index; 简单地确定返回值。

inside 子例程是我们的“主力”函数。不可否认的是,它相当复杂,但如果你读到这里,你应该能够理解它的逻辑。这里有一些帮助。变量 $index 用于从 lower_left_index 返回的索引开始,从一个顶点到另一个顶点绕多边形行走。如果此索引不是零,则需要在遇到具有最大索引的顶点后重置它。变量 $check_index 跟踪是否需要此重置,如果需要,是否已经完成。变量 $last_index 是不在测试线上的最后一个顶点的索引。通常,这是“在” $index “后面”的顶点的索引。

OUTER 标签利用了 Perl 最方便的功能之一,即可以毫不费力地从嵌套循环中退出。如果你喜欢,你可以在 C 中使用 goto 来做到这一点。在本例中,多边形的边是通过使用冒号作为分隔符的 join 函数创建的。这些边存储在 $lp 中。如果测试点位于多边形边上,则无需进一步测试,因此会立即退出 OUTER 循环。请注意,在这种情况下,将 $count 设置为 1 相当于将边界计为多边形内部,因为 1 是一个奇数。通过修改包含语句的 if 块,将边界计为外部,甚至作为特殊情况,都是很简单的:last OUTER;

如果该点不在边上,则构造一条从测试点到 x 坐标等于 $big_float 的外部点的水平线,并将其存储在 $lt 中。此函数的其余部分测试线是否相交或“侧面”,具体取决于前一个顶点是否在测试线上,并根据需要递增 $count。如果该点在内部,则 inside 函数的返回值为 1,如果该点在外部,则返回值为 0。

#!/usr/local/bin/perl
while(<DATA>)
   {
   chop;
   $polygon .= $_ . ":";
   }
chop $polygon;
for(;;)
   {
   print "Enter x and y separated by a comma (q to quit): ";
   chop($point = <STDIN>);
   last if $point =~ /^[qQ]/;
   print("No comma!  Try again.\n"), redo if $point !~ /,/;
   $point =~ s/ +//g;
   print "Checking point: $point\n";
   printf "%s\n", &inside($point, $polygon) ? "inside" : "outside";
   }
__END__
5.0,4.0
0.0,0.0
10.0,5.0
0.0,10.0

图 8. Inside 子例程的驱动例程

图 8 中提供了一个简单的 inside 子例程的驱动例程。此例程从 perl 程序本身的末尾读取其数据。以下行的任何内容

__END__

被视为数据,并通过(自动打开的)DATA 文件句柄访问。此驱动程序中引入的两个新运算符是“.”,它连接两个字符串,“.=”,它将一个字符串附加到另一个字符串。也就是说,“.=”之于“.”就像“+=”之于“+”。 chop 函数从其参数列表的每个元素中删除最后一个字符。请注意该行如何

chop $polygon;

从多边形字符串中删除最后的冒号,使其成为合法的多边形。如果你想运行此驱动程序,请用你自己的数据替换我的数据,但请务必在 x 和 y 坐标之间放置一个逗号。

总结

你已经看到了两个关于如何使用 Perl 进行原型设计的示例。我希望从这些例子中,你已经获得了对 Perl 语法的理解。更重要的是,我希望你已经看到使用 Perl 如何让你从专注于编程特定的细节(如内存分配)中解放出来。相反,你可以将精力集中在启动和运行你的算法上。我发现,在许多情况下,Perl 原型足以满足我的目的,从而节省了我用 C 或 C++ 编写程序的时间!

当我将原型算法从 Perl 重写为另一种语言时,我发现很容易改变方向。逻辑在我身后,让我可以专注于 C 特性、内存分配/释放、输入/输出、错误报告等。

我对读者的建议是在 Perl 中编写一个简单的应用程序,亲眼看看这种非常优雅和强大的语言是如何工作的。你可能在前一两个程序中节省不了多少时间,但不久之后 Perl 的好处就会显现出来。如果你觉得有雄心壮志,请尝试编写一个例程来替换我的 point_on_line。我之前提到过,我用于测试一个点是否在一条线上的算法不是很有效。另一种更有效的方案是首先检查该点的 x 坐标是否在直线的 x 范围内,如果是,则检查该点的 y 坐标是否满足直线的方程。垂直线是特殊情况。

我在 Perl 中原型设计的众多算法包括 LZW 数据压缩(与 UNIX compress 实用程序中使用的相同)、RSA 加密、许多矩阵运算(包括特征值/特征向量确定)以及从数据库输出 C 代码的代码生成器。我甚至有一个名为“perls”的小程序,它可以读取 perl 编程技巧数据库,并在屏幕上打印一个随机技巧。[我可以将此程序提供给 The Linux Journal 和/或通过互联网提供给它的观众。如果你有兴趣,请告诉我。]

[是的,我们有兴趣。我们想把它放在我们的网站上,甚至可能放在 cgi 脚本中。]

Jim Shapiro是一位专门从事数学算法编程的顾问。他目前正在为一家电信公司开发一个GIS系统。当他不在Linux系统上用C或Perl编程时,经常可以在壁球场上找到他。Jim是LUGOR(落基山Linux用户组)的创始成员之一。

Perl资源

Programming Perl,作者:Larry Wall 和 Randal L. Schwartz,O'Reilly & Associates, Inc.,1992年。如果你想认真学习Perl,这是必读之书。它包含所有内容,包括一些非常复杂的例子。但是,不推荐给初学者。

Learning Perl,作者:Randal L. Schwartz,O'Reilly & Associates, Inc.,1993年。一本教程,分成按课程大小划分的章节。

Teach Yourself Perl in 21 Days,作者:David Till,SAMS publishing,1995年。我个人最喜欢的。看起来比实际更令人望而却步(841页)。我太兴奋了,以至于我在七天内就读完了。读完这本,再读“Programming perl”,你很快就会成为专家。

“man”页面。如果你想了解这门语言的特色,这还不错,但我的看起来已经过时了。

加载Disqus评论