高级 Perl 编程:引用、复杂数据结构与命令行选项
1. 引用在子程序中的使用
在 Perl 编程里,我们常常需要将数组和哈希传递给子程序。不过,直接传递多个数组或哈希会有问题,因为它们在通过特殊变量
@_
传递时会受损。下面来看一个示例代码:
compare_two_arrays(@a, @b);
sub compare_two_arrays {
my (@array1, @array2) = @_;
# etc.
}
此代码的意图是通过
@_
把
@a
和
@b
数组的数据填充到子程序里的
@array1
和
@array2
中。但在列表上下文中,Perl 无法确定数组的大小。结果是
@array1
会获取
@a
和
@b
的所有数据,而
@array2
最终为空。这表明不能直接将多个数组传递给子程序。
解决这个问题的办法是传递数组的引用,示例代码如下:
compare_two_arrays(\@a, \@b);
sub compare_two_arrays {
my ($ar1, $ar2) = @_;
foreach my $e1 (@$ar1) {
foreach my $e2 (@$ar2) {
# something
}
}
}
现在,子程序会接收到两个可以赋值给一对变量的东西。这些变量(
$ar1
和
$ar2
)会成为每个数组的引用,只要在子程序中记得对它们进行解引用,就仍然可以访问原始的数组数据。同样的策略也适用于哈希。一般来说,始终通过引用传递数组和哈希。
另外,还可以传递标量引用以减少
@_
中的开销。例如:
my_function($scalar);
sub my_function {
my ($thing) = @_;
}
在这段代码中,
$scalar
的值会被复制到
@_
中,然后再复制到
$thing
中。要是
$scalar
包含像整个人类染色体的 DNA 序列这样庞大的数据,就会产生多个数据副本,这可能会在内存不足时降低计算机性能。为了减少通过
@_
复制的内存量,可以传递标量引用,示例代码如下:
print gc_content(\$chromosome), "\n";
sub gc_content {
my ($chr_ref) = @_;
my $Gs = $$chr_ref =~ tr/G/G/;
my $Cs = $$chr_ref =~ tr/C/C;
return ($Gs + $Cs) / length($$chr_ref);
}
2. 复杂数据结构
有些数据天生就很复杂,无法整齐地放入电子表格中。比如一本书,它包含标题、章节、小节和段落。我们可以从下往上构建其数据结构:
- 一个章节可以看作是段落的数组,例如:
my @chapter_4_1 = (
"paragraph 1 contents… ",
"paragraph 2 contents… ",
"paragraph n contents… ",
);
- 一个小节包含多个命名的章节,可将其存储为哈希,其中每个键是章节的名称,每个值是段落数组的引用,例如:
my %section_4 = (
"4.1 Hello World" => \@chapter_4_1,
"4.2 Scalar variables" => \@chapter_4_2,
"4.3 Use warnings" => \@chapter_4_3,
);
- 一本书包含标题和命名的小节,可表示为:
my %book = (
title => "Unix and Perl",
sections => {
Introduction => \%section_1,
Installation => \%section_2,
Essential_Unix => \%section_3,
Essential_Perl => \%section_4,
Advanced_Unix => \%section_5,
Advanced_Perl => \%section_6,
},
);
如果要访问特定的段落,可以这样做:
print $book{sections}{Essential_Perl}{"4.3 Use warnings"}[0];
当使用深度嵌套的数据结构时,处理数据会变得非常混乱。例如,一个典型的基因组序列及其包含的所有基因,在 Perl 中可以用多维哈希/数组结构来表示。要访问外显子的起始位置,可以这样写:
print $genome{$chrom}{$gene}[$transcript][$exon]{begin};
如果觉得这段代码难以理解,这是很正常的反应。虽然复杂的数据结构可以解决很多问题,但也会带来很多麻烦。如果要打印整个基因组的所有信息,需要使用很多嵌套的
foreach
循环:
foreach my $chrom (keys %genome) {
foreach my $gene (keys %{$genome{$chrom}}) {
foreach my $tx (@{$genome{$chrom}{$gene}}) {
foreach my $exon (@$tx) {
print "$exon->{begin} $exon->{end}\n";
}
}
}
}
在脚本中使用过多的嵌套循环通常不是个好主意。在复杂的程序中,很难看清哪些部分属于哪个循环,特别是当循环中包含很多其他代码时。建议不要嵌套超过两层循环,并且避免任何一组嵌套循环(或子程序)占用超过一屏可见的代码。
解决这个问题的方法是将嵌套循环拆分为子程序,这样可以使代码更易读。以下是一个更好的替代方案:
foreach my $chrom (keys %genome) {
foreach my $gene (keys %{$genome{$chrom}}) {
report_exons($genome{$chrom}{$gene});
}
}
sub report_exons {
my ($gene) = @_;
foreach my $transcript (@$gene) {
foreach my $exon (@$transcript) {
print "$exon->{begin} $exon->{end}\n";
}
}
}
有时候,我们想探索一个复杂的数据结构,但却忘记了它的结构。可以使用 Perl 的内置
Data::Dumper
模块来探索变量及其所有子结构。示例代码如下:
#!/usr/bin/perl
# dump.pl
use strict; use warnings;
use Data::Dumper;
my $thing = [0, 1, [2, 3, {hello => 'world'}], 6];
Dumper($thing);
运行这个脚本,会得到如下输出:
$VAR1 = [
0,
1,
[
2,
3,
{
'hello' => 'world'
}
],
6
];
Dumper()
函数会接收一个引用,并递归地遍历该引用所指向的数据结构的所有层级,然后打印出每一层级的数据。
不过,
Dumper()
的输出不太具有描述性。可以编写一个更美观的显示程序,将其命名为
display()
函数,并创建一个库。示例代码如下:
package Toolbox;
sub display {
my ($thing, $level) = @_;
no warnings;
my $tab = "\t" x $level;
print "$thing\n";
$level++;
use warnings;
if (ref($thing) eq 'ARRAY') {
for (my $i = 0; $i < @$thing; $i++) {
print "\t$tab [$i] = ";
display($thing->[$i], $level);
}
} elsif (ref($thing) eq 'HASH') {
foreach my $k (sort keys %$thing) {
print "\t$tab $k => ";
display($thing->{$k}, $level);
}
}
}
1;
下面是一个使用
Toolbox::display()
函数的测试脚本:
#!/usr/bin/perl
# dump.pl
use strict; use warnings;
use Toolbox;
my $thing = [0, 1, [2, 3, [4, 5]], 6];
Toolbox::display($thing);
输出如下:
ARRAY(0x100863120)
[0] = 0
[1] = 1
[2] = ARRAY(0x100800f00)
[0] = 2
[1] = 3
[2] = HASH(0x1008001f0)
hello => world
[3] = 6
与
Dumper()
的输出相比,新的
display()
函数增加了更多的缩进,有助于区分数据结构的不同层级,还会添加详细信息,提醒你在任何给定层级是在探索数组还是哈希。
3. 垃圾回收和引用计数
使用
my
关键字时,是在请求 Perl 分配一块内存。这块内存会在请求时创建,并在执行到同一作用域的结束花括号时返回给计算机。例如下面的无限循环会不断分配和释放内存:
while (1) {
my $variable = 'something';
}
在底层,
$variable
关联的内存创建时引用计数为 1,执行到结束花括号时,引用计数减 1 变为 0。引用计数为 0 的内存位置会被返回给计算机,释放未使用内存的操作称为垃圾回收。
再看一个问题,以下代码中
@array
每次循环都会重新创建,但其内容为何能存在于
@table
中呢?
#!/usr/bin/perl
# csv_reader.pl
use strict; use warnings;
my @table;
while (<>) {
my @array = split(/,/, $_);
push @table, \@array;
}
使用反斜杠操作符创建变量的引用时,其引用计数会加 1。所以
@array
关联的内存每次循环不会被销毁。
@array
在第 7 行的引用计数为 1,第 8 行由于反斜杠操作引用计数增加到 2,第 9 行
@array
超出作用域时引用计数减 1,但内存位置的引用计数仍为 1,所以不会被释放,
@table
会填充数组引用。
不过,引用计数垃圾回收在处理循环引用时会有问题。例如以下代码:
while (1) {
my $variable;
$variable = \$variable;
}
这段代码会给
$variable
分配一个指向自身的引用,其引用计数降为 1 且永远不会变为 0,
$variable
会不断分配内存而不释放,可能会导致计算机崩溃。
4. 添加命令行选项
大多数 Unix 命令行程序都有控制其行为的选项,Perl 程序也可以轻松实现相同的功能。有两个内置模块可用于处理命令行选项,分别是
Getopt::Std
和
Getopt::Long
。
4.1 Getopt::Std
Getopt::Std
用于单字符选项,其选项行为与其他 Unix 命令行程序类似。如果有多个选项,可以将它们连接在一起,例如
-l -t
可以写成
-lt
或
-tl
。对于需要额外参数的选项,如
-o
用于指定输出文件,选项和参数之间的空格可以省略,
-o file
和
-ofile
含义相同。以下是一个示例脚本:
#!/usr/bin/perl
# getopt_std.pl
use strict; use warnings;
use Getopt::Std;
my $usage = "usage: getopt.pl [options] <arguments…>
options:
-v version
-f flag
-p <some parameter>
";
die $usage unless @ARGV;
my %opt;
getopts('hvfp:', \%opt);
if ($opt{h}) {print $usage; exit}
if ($opt{v}) {print "version 1.0\n"; exit}
if ($opt{f}) {print "flag is turned on\n"}
if ($opt{p}) {print "Parameter is: $opt{p}\n"}
print "Other arguments were: @ARGV\n";
这个脚本的执行步骤如下:
1. 第 1 - 4 行是典型的头部信息,添加了
Getopt::Std
模块。
2. 第 6 - 11 行包含一个典型的使用说明,用于提醒用户程序支持的所有选项。
3. 第 12 行的
die()
函数会在程序没有在命令行中获得任何参数时打印使用信息。
4. 第 14 - 15 行将命令行中的选项提取出来并放入特殊的
%opt
变量中。字符串
'hvfp:'
指定了四个选项:
-h
、
-v
、
-f
和
-p
,
p
后面的冒号表示
-p
需要一个额外的参数,其他选项不需要参数。
5. 第 17 - 20 行根据存在的选项执行不同的操作。
6. 第 22 行打印命令行中指定的其他参数。
可以使用以下命令行选项进行实验:
| 命令 | 说明 |
| ---- | ---- |
|
$ getopt_std.pl
| 打印
$usage
|
|
$ getopt_std.pl -h
| 打印
$usage
|
|
$ getopt_std.pl -v
| 打印版本信息 |
|
$ getopt_std.pl -x
| 报告错误:没有
-x
选项 |
|
$ getopt_std.pl -fpx
| 报告标志开启,
-p
的值为
x
|
4.2 Getopt::Long
Getopt::Long
用于更长、更具描述性的选项,例如
--version
。长选项名前面有两个连字符。对于需要参数的选项,选项和参数之间的空格不能省略,可以使用
--option x
或
--option=x
。以下是一个等效的模板:
#!/usr/bin/perl
# getopt_long.pl
use strict; use warnings;
use Getopt::Long;
my $usage = "usage: getopt.pl [options] <arguments…>
options:
--version
--help
--flag
--number <number>
--string <string>
";
my $flag; # some Boolean flag
my $number; # will contain a number
my $string; # will contain a string
GetOptions(
"flag" => \$flag,
"number=i" => \$number,
"string=s" => \$string,
"version" => sub {print "1.0\n"; exit},
"help" => sub {print $usage; exit},
);
if ($flag) {print "flag turned on\n"}
if ($string) {print "string set: $string\n"}
if ($number) {print "number set: $number\n"}
print "Other arguments were: @ARGV\n";
这个脚本的执行步骤如下:
1. 第 1 - 4 行是典型的头部信息,使用了
Getopt::Long
模块。
2. 第 6 - 13 行包含使用说明。
3. 第 15 - 17 行声明了与用户可能选择的命令行选项对应的变量。
4. 第 19 - 25 行调用
GetOptions()
函数解析指定的命令行选项。
5. 第 20 行展示了如何设置一个简单的标志选项。
6. 第 21 和 22 行展示了如何将参数与特定选项关联起来,
i
和
s
分别表示选项将接收整数和字符串。
7. 第 23 和 24 行包含用于报告版本和使用信息的匿名子例程。
8. 第 27 - 29 行用于测试哪些命令行选项已被指定。
9. 第 31 行打印命令行中指定的其他参数。
可以使用以下命令行选项进行实验:
| 命令 | 说明 |
| ---- | ---- |
|
$ getopt_long.pl
| 无帮助信息 |
|
$ getopt_long.pl --version
| 打印版本信息 |
|
$ getopt_long.pl --help
| 打印使用信息 |
|
$ getopt_long.pl --flag --number=2
| 标志开启,数字为 2 |
|
$ getopt_long.pl --string hooray
| 字符串为
hooray
|
4.3 Getopt::Std 还是 Getopt::Long?
这两个模块的功能比这里展示的要多得多,建议阅读完整的
Getopt
文档以了解更多。有些人喜欢使用标准版本的
Getopt
函数,而有些人则更喜欢长版本。随着脚本中选项的增加,使用标准版本可能会导致某些选项没有合适的单字母字符可用;而使用长版本添加大量选项时,运行命令可能需要更多的输入。不过,长格式选项的一个优点是可以简化为最短的唯一字符串。建议对两个版本都进行实验,选择自己喜欢的版本,但不要自行发明新的选项格式。
高级 Perl 编程:引用、复杂数据结构与命令行选项
5. 两种模块的对比总结
为了更清晰地对比
Getopt::Std
和
Getopt::Long
这两个模块,下面通过表格形式进行总结:
| 对比项 | Getopt::Std | Getopt::Long |
| ---- | ---- | ---- |
| 选项格式 | 单字符选项,可拼接,如
-lt
| 长且描述性强的选项,以
--
开头,如
--version
|
| 参数处理 | 选项与参数空格可省略,如
-ofile
| 选项与参数空格不可省略,可用
--option=x
或
--option x
|
| 选项指定 | 用字符串指定选项,如
'hvfp:'
| 用键值对指定选项,如
"number=i"
|
| 适用场景 | 选项较少,追求简洁输入 | 选项较多,需要更具描述性的选项名 |
6. 实际应用中的考虑因素
在实际编写 Perl 程序时,选择合适的模块来处理命令行选项至关重要。以下是一些需要考虑的因素:
-
选项数量
:如果程序的选项较少,使用
Getopt::Std
可以让用户更方便地输入命令,因为单字符选项输入更快捷。例如,一个简单的文件处理脚本,可能只需要
-f
表示文件路径,
-v
表示详细模式等少数几个选项,此时
Getopt::Std
就很合适。
-
选项描述性
:当程序的选项较多且需要明确的描述时,
Getopt::Long
是更好的选择。比如一个复杂的数据分析程序,可能有
--input-file
、
--output-directory
、
--analysis-method
等选项,长选项名能让用户更清楚每个选项的作用。
-
用户习惯
:考虑目标用户的使用习惯也很重要。如果用户熟悉 Unix 传统的单字符选项风格,那么
Getopt::Std
可能更受欢迎;如果用户更倾向于使用具有明确描述的长选项,那么
Getopt::Long
会更合适。
7. 复杂数据结构的优化建议
除了将嵌套循环拆分为子程序来处理复杂数据结构外,还可以考虑以下优化建议:
-
数据抽象
:将复杂的数据结构抽象成类或对象,通过封装数据和操作方法,使代码更易于理解和维护。例如,对于基因组数据,可以创建一个
Genome
类,将与基因组相关的操作封装在类中。
package Genome;
sub new {
my ($class, $data) = @_;
my $self = {
data => $data
};
return bless $self, $class;
}
sub get_exon_start {
my ($self, $chrom, $gene, $transcript, $exon) = @_;
return $self->{data}{$chrom}{$gene}[$transcript][$exon]{begin};
}
1;
# 使用示例
my %genome_data = (...); # 初始化基因组数据
my $genome = Genome->new(\%genome_data);
print $genome->get_exon_start('chr1', 'gene1', 0, 0);
- 使用中间变量 :在访问复杂数据结构时,使用中间变量可以减少代码的嵌套深度,提高代码的可读性。例如:
my $chrom_data = $genome{$chrom};
my $gene_data = $chrom_data->{$gene};
my $transcript_data = $gene_data->[$transcript];
my $exon_data = $transcript_data->[$exon];
print $exon_data->{begin};
8. 引用计数与内存管理的深入理解
引用计数垃圾回收机制是 Perl 内存管理的重要部分,但在处理复杂引用关系时需要特别注意。下面通过一个流程图来展示引用计数的变化过程:
graph TD;
A[创建变量] --> B[引用计数为 1];
B --> C{是否有新引用};
C -- 是 --> D[引用计数加 1];
C -- 否 --> E{是否超出作用域};
D --> E;
E -- 是 --> F[引用计数减 1];
F --> G{引用计数是否为 0};
G -- 是 --> H[释放内存];
G -- 否 --> I[保留内存];
从流程图可以看出,引用计数的变化与变量的创建、引用和作用域有关。在编写代码时,要避免创建循环引用,因为循环引用会导致内存无法正常释放,从而造成内存泄漏。
9. 总结与最佳实践
在高级 Perl 编程中,合理使用引用、处理复杂数据结构和添加命令行选项是提升程序性能和可维护性的关键。以下是一些最佳实践总结:
-
引用传递
:始终通过引用传递数组和哈希,减少数据复制的开销;在必要时传递标量引用,避免大量数据的复制。
-
复杂数据结构处理
:尽量避免使用过多的嵌套循环,将复杂的嵌套逻辑拆分为子程序;使用数据抽象和中间变量来提高代码的可读性。
-
命令行选项
:根据程序的需求和用户习惯选择合适的模块来处理命令行选项;提供清晰的使用说明和帮助信息,方便用户使用。
-
内存管理
:理解引用计数垃圾回收机制,避免创建循环引用,确保内存的有效利用。
通过遵循这些最佳实践,可以编写出更高效、更易维护的 Perl 程序,应对各种复杂的编程场景。
超级会员免费看
964

被折叠的 条评论
为什么被折叠?



