Perl模块如何运作"?

时间:2017-06-20 07:08:51

标签: perl perl-module

我对Perl模块感到困惑。我得到一个模块可以用于转储一大堆子,整理主代码。

但是,模块之间的关系是什么?

模块可以"使用"其他模块?

我必须使用导出,还是可以放弃那些东西?

如何解决循环使用? (Security.pm使用Html.pmHtml.pm使用Security.pm)。我知道明显的答案,但在某些情况下,我需要在Security.pm中使用Html.pm例程,反之亦然 - 不确定如何解决问题。

如果我删除所有"使用"来自我所有模块的子句......然后我必须使用完整的子限定符。例如,Pm::Html::get_user_friends($dbh, $uid)将使用Security来确定朋友是否是被禁用的用户(被禁止属于Security)。

我只是没有得到这个模块的东西。所有"教程"只说一个模块,从不多个,也不使用现实世界的例子。

我遇到多个模块的唯一时间是使用OO代码。但是,没有什么可以确切地告诉我多个模块如何相互作用。

2 个答案:

答案 0 :(得分:14)

Perl中的模块有多种形式,并且有几种不同的东西使它们成为模块。

定义

如果满足以下条件,则某些内容属于模块

约定

然后有一些公认的惯例:

  • 模块通常只应包含一个package
  • 模块名称应为camel case且不应包含下划线_(例如:Data::DumperWWW::Mechanize::Firefox
  • 完全不是模块的小写字母模块,它们是pragmas

通常模块包含一组函数(sub s)或object oriented。让我们先看一下这些系列。

模块作为函数集合

捆绑一组相关功能的典型模块使用一种方法将这些函数导出到您的代码命名空间中。一个典型的例子是List::Util。有几种方法可以导出东西。最常见的是Exporter

当你从模块中取出一个函数将它放入你的代码中时,它会被称为导入它。如果你想多次使用这个函数,这很有用,因为它可以保持名称简短。导入时,可以直接按名称调用它。

use List::Util 'max';
print max(1, 2, 3);

如果您没有导入它,则需要使用完全限定名称。

use List::Util (); # there's an empty list to say you don't want to import anything
print List::Util::max(1, 2, 3); # now it's explicit

这是有效的,因为Perl在名称List::Util::max下将max后面的函数引用安装到您的命名空间中。如果你不这样做,你需要使用全名。它有点像Windows桌面上的快捷方式。

您的模块不必提供导出/导入。你可以把它当作一个东西的集合,用它们的全名来称呼它们。

模块作为包的集合

虽然每个.pm文件都称为模块,但人们通常也会将发布的整个集合称为模块。像DBI之类的东西浮现在脑海中,其中包含很多.pm个文件,这些文件都是模块,但人们仍然只谈论 DBI模块

面向对象的模块

并非每个模块都需要包含独立功能。一个模块(现在我们更多地谈论上面的模块)也可以包含class。在这种情况下,它通常不会导出任何函数。实际上,我们不再调用sub s 函数,而是调用方法package名称成为类的名称,您创建名为 objects 的类的实例,并在这些对象上调用方法,最终成为包中的功能。

加载模块

在Perl中加载模块有两种主要方式。您可以在编译时运行时执行此操作。 perl 1 编译器(是的,有一个编译器,虽然它的解释语言)加载文件,编译它们,然后切换到运行时运行编译的代码。遇到要加载的新文件时,它会切换回编译时间,编译新代码,依此类推。

编译时间

要在编译时加载模块,请use

use Data::Dumper;
use List::Util qw( min max );
use JSON ();

这相当于以下内容。

BEGIN {
  require Data::Dumper;
  Data::Dumper->import;

  require List::Util;
  List::Util->import('min', 'max');

  require JSON;
  # no import here
}

在编译期间调用BEGIN block。链接文档中的示例有助于来回理解这些切换的概念。

use语句通常位于程序的顶部。您首先执行 pragma use strictuse warnings应始终是shebang之后的第一件事),然后是use语句。应该使用它们,以便您的程序加载启动期间所需的一切。这样在运行时,它会更快。对于运行时间很长或启动时间不重要的事情,例如在Plack上运行的Web应用程序,这就是您想要的。

运行时间

如果要在运行时加载某些内容,请使用require。它不会为您导入任何内容。它也会暂时切换到新文件的编译时间,但后来又回到它剩下的运行时间。这使得有条件地加载模块成为可能,这在CGI上下文中尤其有用,其中在运行期间解析新文件所花费的额外时间超过了为程序的每次调用加载所有内容的成本,尽管它可能不是需要的。

require Data::Dumper;

if ($foo) {
    require List::Util;
    return List::Util::max( 1, 2, 3, $foo );
}

也可以将字符串或变量传递给require,这样您不仅可以有条件地加载东西,还可以动态加载。

my $format = 'CSV'; # or JSON or XML or whatever
require "My::Parser::$format";

这是非常先进的,但它有用例。

此外,还可以require正常的Perl文件,其中.pl在运行时结束。这通常在遗留代码中完成(我称之为spaghetti)。不要用新代码来做。也不要用旧代码来做。这是不好的做法。

在哪里加载

通常,您应该始终userequire您在任何给定模块中依赖的每个模块。永远不要依赖于代码的其他downstream部分为您加载内容的事实。模块意味着encapsulate功能,因此它们应该能够至少自己站立一点。如果您想稍后重用其中一个模块,而忘记包含依赖项,则会让您感到悲伤。

它还使您更容易阅读代码,因为在顶部明确声明的依赖关系和导入可以帮助维护人员(或future you)了解您的代码是什么,它做什么以及如何做到这一点

两次不加载相同的东西

Perl会为您解决这个问题。当它在编译时解析代码时,它会跟踪它的加载内容。进入超全局variable %INC的那些东西,它是已加载的名称的散列,以及它们来自何处。

$ perl -e 'use Data::Dumper; print Dumper \%INC'
$VAR1 = {
          'Carp.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/Carp.pm',
          'warnings.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/warnings.pm',
          'strict.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/strict.pm',
          'constant.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/constant.pm',
          'XSLoader.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/x86_64-linux/XSLoader.pm',
          'overloading.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/overloading.pm',
          'bytes.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/bytes.pm',
          'warnings/register.pm' => '/home/julien/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/warnings/register.pm',
          'Exporter.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/Exporter.pm',
          'Data/Dumper.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/x86_64-linux/Data/Dumper.pm',
          'overload.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/overload.pm'
        };

userequire的每次调用都会在该哈希中添加一个新条目,除非它已经存在。在这种情况下,Perl不会再次加载它。如果您use模块,它仍会为您导入名称。这可以确保没有循环依赖。

关于遗留代码,要记住的另一个重要事项是,如果您require正常.pl个文件,则需要获得正确的路径。因为%INC中的键不是模块名,而是您传递的字符串,执行以下操作将导致同一文件被加载两次。

perl -MData::Dumper -e 'require "scratch.pl"; require "./scratch.pl"; print Dumper \%INC'
$VAR1 = {
          './scratch.pl' => './scratch.pl',
          'scratch.pl' => 'scratch.pl',
          # ...
        };

加载模块的位置

就像%INC一样,还有一个超级全局variable @INC,其中包含Perl查找模块的路径。您可以使用lib编译指示向其添加内容,或者通过环境变量PERL5LIB等。

use lib `lib`;
use My::Module; # this is in lib/My/Module.pm

命名空间

您在模块中使用的包在Perl中定义名称空间。默认情况下,在创建没有package的Perl脚本时,您位于包main

#!/usr/bin/env perl
use strict;
use warnings;

sub foo { ... }

our $bar;

sub foo foo位于主.pl文件中,但也可以main::foo位于其他任何位置。简写为::foo。包变量$bar也是如此。这真的是$main::bar$::bar。谨慎使用。您不希望脚本中的内容泄漏到您的模块中。这是一种非常糟糕的做法,它会在以后回来并咬你。

在你的模块中,事物在声明它们的包的命名空间中。这样,你可以从外部访问它们(除非它们词法范围my,你应该为大多数事情做什么)。这基本上没问题,但你不应该搞乱其他代码的内部。除非你想破坏东西,否则请使用定义的界面。

当您将某些内容导入命名空间时,所有内容都是如上所述的快捷方式。这可能很有用,但您也不希望污染您的命名空间。如果你将很多东西从一个模块导入到另一个模块,那么那些东西也将在该模块中可用。

package Foo;
use List::Util 'max';

sub foo { return max(1, 2, 3) }

package main; # this is how you switch back
use Foo;

print Foo::max(3, 4, 5); # this will work

因为您经常不希望这种情况发生,所以您应该仔细选择要导入命名空间的内容。另一方面,你可能不在乎,这也可能没问题。

私有化

Perl不了解私人或公共的概念。当你知道命名空间如何工作时,你几乎可以得到一些非词汇的东西。甚至还有办法让词汇,但它们涉及一些神秘的黑魔法,我不会进入它们。

但是,有关如何将事物标记为私人的惯例。每当函数或变量以下划线开头时,都应将其视为私有。像Data::Printer这样的现代工具在显示数据时会考虑到这一点。

package Foo;

# this is considered part of the public interface
sub foo { 
    _bar();
}

# this is considered private
sub _bar {
    ...
}

开始做这样的事情是很好的做法,并远离CPAN模块的内部。这样命名的东西不算稳定,它们不是API的一部分,它们可以随时更改。

结论

这是对此处涉及的一些概念的非常广泛的概述。一旦你使用它几次,它中的大多数将迅速成为你的第二天性。我记得在我作为开发人员的培训期间花了大约一年的时间来解决这个问题,特别是出口。

当您启动新模块时,perldoc页面perlnewmod非常有用。你应该阅读并确保你理解它的内容。

1:注意 perl 中的小p?我在这里讨论的是这个程序,而不是the name of the language,它是 Perl

答案 1 :(得分:6)

(如果您使用大写字母,您的问题会更容易阅读。)

  

模块可以“使用”其他模块吗?

是。您可以在另一个模块中加载模块。如果您已经查看了几乎所有CPAN模块代码,您会看到这样的示例。

  

我必须使用导出,还是可以放弃丢弃这些东西?

如果需要,您可以停止使用Exporter.pm。但是,如果要从模块中导出符号名称,则可以使用Exporter.pm或实现自己的导出机制。大多数人选择使用Export.pm,因为它更容易。或者你可以看看像Exporter :: Lite和Exporter :: Simple这样的替代品。

  

我如何解决循环使用(security.pm使用html.pm和html.pm使用security.pm)

通过重新分区库来摆脱这些循环依赖关系。这可能意味着你在一个模块中投入太多。也许制作更小,更专业的模块。没有看到更明确的例子,这里很难得到很多帮助。

  

如果我删除了所有PM的所有“使用”条款......那么我必须使用完整的子限定符。例如,pm :: html :: get_user_friends($ dbh,$ uid)将使用安全性来确定朋友是否是被禁用的用户(被禁止是安全的一部分)

你在这里误解了事情。

调用use会做两件事。首先,它加载模块,然后运行模块的import()子例程。它是执行所有Exporter.pm魔术的import()子程序。它是Exporter.pm魔术,允许您使用短名称而不是完全限定名称从其他模块调用子程序。

所以,是的,如果从模块中删除use语句,那么您可能会失去为其他模块的子例程使用短名称的能力。但是你也依赖程序中的其他代码来实际加载模块。因此,如果删除加载特定模块的所有use语句,那么您将无法从该模块调用子例程。这似乎适得其反。

对于所有代码(无论是主调用程序还是模块)来说,通常一个非常好的想法是显式加载(使用use)所需的任何模块。 Perl会跟踪已经加载的模块,因此模块被多次加载会导致效率低下。如果要加载模块并关闭任何符号名称导出,则可以使用以下语法执行此操作:

use Some::Module (); # turn off exports

你的问题的其余部分似乎是一个咆哮。我找不到任何问题要回答。