【问题标题】:How do Perl modules "work"? [closed]Perl 模块如何“工作”? [关闭]
【发布时间】:2017-06-20 07:08:51
【问题描述】:

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

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

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

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

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

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

我只是不明白这个模块的东西。所有的“教程”都只讲一个模块,从来没有多个,也没有使用真实世界的例子。

我遇到多个模块的唯一一次是在使用 OO 代码时。但是没有任何东西可以明确地告诉我多个模块如何交互。

【问题讨论】:

  • 这不是适合 Stackoverflow 的问题(缺乏明确的具体问题和明确而具体的答案/解决方案),但它引出了几个非常高质量的答案,提供了一个初始点。但是,这不是一种可以从这些答案中学到的材料。最好由getting a book 提供服务,并在就具体问题提出具体问题的同时研究材料。另请参阅与 Perl 一起安装的 perldoc perlmod
  • @SinanÜnür 现在他们删除了他们的帐户。嗯。如果 SO 文档没有那么糟糕,我会说我们将答案移到那里,但是...

标签: perl perl-module


【解决方案1】:

Perl 中的模块有多种风格,并且有几种不同的东西使它们成为一个模块。

定义

如果满足以下条件,则某些东西可以称为 模块

约定

还有一些公认的约定:

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

通常一个模块要么包含一组函数 (subs),要么是 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。在这种情况下,它通常不会导出任何函数。实际上,我们不再调用subs 函数,而是调用方法package 名称成为类的名称,您创建名为 objects 的类的 instances,然后在这些对象上调用方法,最终成为你的包裹。

加载模块

在 Perl 中加载模块有两种主要方式。您可以在编译时运行时进行。 perl1 编译器(是的,有一个编译器,尽管它是解释性语言)加载文件,编译它们,然后切换到运行时运行编译后的代码。当它遇到要加载的新文件时,它会切换回编译时间,编译新代码,等等。

编译时间

要在编译时加载一个模块,你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 语句通常位于程序的顶部。您首先执行 pragmasuse 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 结尾。这通常在遗留代码中完成(我称之为意大利面条)。不要在新代码中这样做。也不要在旧代码中这样做。这是不好的做法。

在哪里加载什么

一般来说,您应该始终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 不会再次加载它。如果您 used 模块,它仍然会为您导入名称。这样可以确保没有循环依赖。

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

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 pragma 或通过环境变量@ 向其中添加内容987654387@ 等等。

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 将在主 .pl 文件中以 foo 的形式提供,但也可以在其他任何地方以 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

【讨论】:

    【解决方案2】:

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

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

    是的。您可以在另一个模块中加载一个模块。如果您看过几乎所有 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
    

    您的问题的其余部分似乎是在咆哮。我找不到更多问题要回答。

    【讨论】:

    • 该死的,我快完成了... :D
    • 糟糕。很抱歉:-P
    • 你写了 use Some::Module (); # turn off exports。为什么你使用模块开发者的视角来评论而不是关闭导入
    • @Wolf:嗯,我不记得我在 18 个月前写这篇文章时在想什么。但我经常交替使用这两个术语。
    猜你喜欢
    • 2011-04-30
    • 1970-01-01
    • 2015-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-14
    • 2014-02-22
    • 1970-01-01
    相关资源
    最近更新 更多