Perl模块如何"工作"?

-3 perl perl-module

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

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

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

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

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

如果我从所有模块中删除所有"use"子句......那么我必须使用完整的子限定符.例如,Pm::Html::get_user_friends($dbh, $uid)将用于Security确定朋友是否是被禁用的用户(被禁止的是其中一部分Security).

我只是没有得到这个模块的东西.所有"教程"只涉及一个模块,从不多重,也不使用真实世界的例子.

我遇到多个模块的唯一一次是使用OO代码.但没有任何东西可以确切地告诉我多个模块如何相互作用.

sim*_*que 15

Perl中的模块有几种形式,有几种不同的东西使它们成为一个模块.

定义

如果满足以下条件,则有资格作为模块:

约定

然后有一些公认的约定:

  • 模块通常只应包含一个package,
  • 模块名称应该是驼峰式的,不应包含下划线_(例如:Data::Dumper,WWW::Mechanize::Firefox)
  • 完全不是模块的小写字母模块,它们是pragma.

通常,模块包含一组函数sub或者是面向对象的.我们先来看看这些系列.

模块作为函数集合

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

当您从模块中获取函数将其放入代码中时,这称为导入它.如果你想多次使用这个函数,这很有用,因为它可以保持名称简短.导入时,可以直接通过名称调用它.

use List::Util 'max';
print max(1, 2, 3);
Run Code Online (Sandbox Code Playgroud)

如果不导入它,则需要使用完全限定名称.

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
Run Code Online (Sandbox Code Playgroud)

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

您的模块不必提供导出/导入.你可以将它用作一组东西,并用它们的全名来称呼它们.

模块作为包的集合

虽然每个.pm文件,称为模块,人们通常也指的东西,是一个整体的收集分布作为一个模块.像DBI这样的东西浮现在脑海中,其中包含许多.pm文件,这些文件都是模块,但人们仍然只谈论DBI模块.

面向对象的模块

并非每个模块都需要包含独立功能.一个模块(现在我们更多地谈论上面的那个)也可以包含一个.在这种情况下,它通常不会导出任何函数.实际上,我们不再调用subs 函数,而是调用方法.该package名称将成为类的名称,创建实例类叫的对象,并调用这些对象,从而结束了在你的包是功能的方法.

加载模块

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

编译时间

要在编译时加载模块,就可以use了.

use Data::Dumper;
use List::Util qw( min max );
use JSON ();
Run Code Online (Sandbox Code Playgroud)

这相当于以下内容.

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

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

  require JSON;
  # no import here
}
Run Code Online (Sandbox Code Playgroud)

在编译期间调用该BEGIN.链接文档中的示例有助于来回理解这些开关的概念.

这些use陈述通常都在你的程序之上.你首先做pragma(use strict并且use warnings应该总是你在shebang之后的第一件事),然后是use陈述.应该使用它们,以便您的程序加载启动期间所需的一切.这样在运行时,它会更快.对于运行很长时间或启动时间无关紧要的事情,比如在Plack上运行的Web应用程序,这就是您想要的.

运行

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

require Data::Dumper;

if ($foo) {
    require List::Util;
    return List::Util::max( 1, 2, 3, $foo );
}
Run Code Online (Sandbox Code Playgroud)

也可以将字符串或变量传递给require,因此您不仅可以有条件地加载内容,还可以动态加载.

my $format = 'CSV'; # or JSON or XML or whatever
require "My::Parser::$format";
Run Code Online (Sandbox Code Playgroud)

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

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

在哪里加载什么

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

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

两次不加载相同的东西

Perl会为您解决这个问题.当它在编译时解析代码时,它会跟踪它的加载内容.进入超全局变量的%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'
        };
Run Code Online (Sandbox Code Playgroud)

每次调用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',
          # ...
        };
Run Code Online (Sandbox Code Playgroud)

从哪里加载模块

就像%INC,还有一个超级全局变量@INC,它包含Perl查找模块的路径.您可以使用libpragma或环境变量PERL5LIB等来添加内容.

use lib `lib`;
use My::Module; # this is in lib/My/Module.pm
Run Code Online (Sandbox Code Playgroud)

命名空间

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

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

sub foo { ... }

our $bar;
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

让事情变得私密

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

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

package Foo;

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

# this is considered private
sub _bar {
    ...
}
Run Code Online (Sandbox Code Playgroud)

开始做这样的事情是好的做法,并远离CPAN模块的内部.那些被命名的东西不被认为是稳定的,它们不是API的一部分,它们可以随时改变.

结论

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

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


1:注意小pPerl的?我在这里谈论的是程序,而不是语言的名称,即Perl.


Dav*_*oss 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()子程序.它import()是执行所有Exporter.pm魔术的子程序.它是Exporter.pm魔术,允许您使用短名称而不是完全限定名称从其他模块调用子程序.

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

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

use Some::Module (); # turn off exports
Run Code Online (Sandbox Code Playgroud)

你的其余问题似乎只是一个咆哮.我找不到更多的问题要回答了.