Perl与Python变量作用域 - 需注意的陷阱
在研究Perl和Python中的作用域时,我发现了Perl中一种默默无闻的作用域相关行为,这可能导致很难追踪的错误。特别是对于那些刚接触这门语言的程序员,他们可能还不完全了解所有的细微差别。我提供了Perl和Python的示例代码,以说明这两种语言中的作用域是如何工作的。
在Python中,如果我们运行以下代码:
x = 30
def g():
s1 = x
print "Inside g(): Value of x is %d" % s1
def t(var):
x = var
print "Inside t(): Value of x is %d" % x
def tt():
s1 = x
print "Inside t()-tt(): Value of x is %d" % x
tt()
g()
t(200)
得到的输出是:
Inside t(): Value of x is 200
Inside t()-tt(): Value of x is 200
Inside g(): Value of x is 30
这就是通常的词法作用域行为。Python默认将一个代码块中的赋值视为定义并赋值给一个新变量,而不是对可能存在于外部作用域的全局变量进行赋值。要想改变这种行为,需要明确使用关键字global
来修改全局变量x。在函数g()
中,变量x
的作用域是由它在程序中定义的位置决定的,而不是函数g()
被调用的位置。因此,当在函数t()
中调用g()
时,如果在t()
中也定义了一个词法作用域的变量x
并将其设置为200,g()
仍然会显示旧值30,因为那是g()
定义时的x
的值。函数tt()
显示的x
的值是200,这在tt()
的词法作用域内。Python只有词法作用域,这是默认行为。
相比之下,Perl提供了使用词法作用域和动态作用域的灵活性。这在某些情况下是个好处,但如果程序员不小心并且不理解Perl中的作用域工作原理,也可能导致难以发现的错误。
为了说明这种微妙的行为,如果我们执行以下Perl代码:
use strict;
our $x = 30;
sub g {
my $s = $x;
print "Inside g\(\)\: Value of x is ${s}\n";
}
sub t {
$x = shift;
print "Inside t\(\)\: Value of x is ${x}\n";
sub tt {
my $p = $x;
print "Inside t\(\)-tt\(\)\: Value of x is ${p}\n";
}
tt($x);
g();
}
sub h {
local $x = 2000;
print "Inside h\(\)\: Value of x is ${x}\n";
sub hh {
my $p = $x;
print "Inside h\(\)-hh\(\)\: Value of x is ${p}\n";
}
hh($x);
g();
}
sub r {
my $x = shift;
print "Inside r\(\)\: Value of x is ${x}\n";
sub rr {
my $p = $x;
print "Inside r\(\)-rr\(\)\: Value of x is ${p}\n";
}
rr($x);
g();
}
g();
t(500);
g();
h(700);
g();
r(900);
g();
得到的输出是:
Inside g(): Value of x is 30
Inside t(): Value of x is 500
Inside t()-tt(): Value of x is 500
Inside g(): Value of x is 500
Inside g(): Value of x is 500
Inside h(): Value of x is 2000
Inside h()-hh(): Value of x is 2000
Inside g(): Value of x is 2000
Inside g(): Value of x is 500
Inside r(): Value of x is 900
Inside r()-rr(): Value of x is 900
Inside g(): Value of x is 500
Inside g(): Value of x is 500
行our $x
定义/声明了一个全局变量$x
,在整个包/代码体内可见。第一次调用t()
时,全局变量$x
被修改,这个变化是全局可见的。
与Python不同,Perl默认只是将一个值赋给一个变量,而Python默认是在一个作用域内定义一个新变量并赋值给它。这就是为什么在上面的Python和Perl代码中结果不同的原因。
因此,即使在t()
中调用g()
也会打印出500的值。在调用t()
后立即调用g()
也会打印500,这证明了调用t()
确实修改了全局作用域中的全局变量$x
。在函数t()
中的$x
是词法作用域的,但由于第8行的赋值对全局作用域中的$x
进行了全局修改,因此没有表现出预期的行为。这导致在t()
中调用g()
时显示500而不是30。
在调用函数h()
时(第25行),调用g()
打印出2000,类似于函数t()
的输出。然而,当函数h()
返回后,我们再次立即调用g()
,发现$x
根本没有改变。这是因为在h()
中的$x
的变化并没有改变全局作用域中的$x
,而只是改变了h()
的作用域内的$x
。在当前作用域中使用local
关键字时,$x
的变化在某种程度上是暂时限制在当前作用域内的。这就是Perl中动态作用域的实际表现。调用g()
返回的是当前执行作用域内的$x
的值,而不是g()
定义时的x
的值,也就是词法作用域。
最后,在第28行调用函数r()
时,关键字my
强制创建一个新的词法作用域局部变量,这与Python代码片段中的t()
的行为相同。这与h()
或t()
中的情况形成鲜明对比,因为在这些地方从未创建新变量。在函数r()
中,我们观察到调用g()
实际上打印出$x
的值为500,这是g()
定义时的词法作用域中的$x
的值,而不是当前执行作用域中的值(与h()
中的动态作用域结果相对)。Perl函数r()
在作用域行为上与原始Python函数t()
最为接近。
默认情况下,Perl修改全局变量$x
,而不是像Python那样创建一个新的词法作用域的$x
变量,这有时会让Perl的新手感到困惑和出错。对于静态类型语言来说,这不是问题,因为变量需要明确声明,因此不会出现混淆是否在赋值给一个已存在的变量或定义并赋值给一个新变量的情况。在动态类型语言中,不需要明确声明,而程序员又不清楚不正确使用作用域语法的后果(例如在Perl中使用my
),这往往会导致意想不到的后果。如果程序员认为在第8行声明了一个新变量,但实际上是修改了全局变量$x
。这正是Perl希望的使用方式,但如果程序员不小心并且不完全了解其含义,可能会导致有趣的效果。这种错误在几百或几千行代码的大型程序中可能会变得难以捕捉和调试。需要记住的是,没有my
前缀时,Perl将对变量的赋值视为简单的赋值,而不是定义加赋值。
Perl默认将代码块中的赋值视为对同名全局变量的赋值,需要使用my
关键字明确覆盖,以定义并赋值给一个词法作用域的局部变量。Python的默认行为正好相反,默认将代码块中的所有赋值视为定义并赋值给一个局部词法作用域变量。需要明确使用global
关键字来覆盖这种默认行为。我觉得Python的默认行为对初学者和中级程序员来说比Perl的默认作用域行为更安全,也许更友好。
请补充任何其他与作用域相关的细微问题,无论是Perl还是Python,您可能知道的。
1 个回答
在你第二个Perl例子中的那行代码$x = shift
,其实就是在覆盖一个全局的、局部作用域的变量,就像你在Python代码里加上global x
一样。
这和动态作用域没关系,很多其他语言的行为和Perl是一样的。我觉得Python在这方面比较特别,因为它需要你明确导入一个在局部作用域可见的变量名。
Perl代码真正的问题不是局部作用域,而是缺少声明参数。如果有了声明参数,就不可能忘记加my
,这样问题也就解决了。
我觉得Python(在Python 2时)的作用域处理方式更让人困惑:它不一致(全局变量需要明确导入才能读写,而嵌套函数中的局部变量是自动绑定的,但只能读不能写),这让闭包变得很麻烦。