命名空间与作用域

命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。我们举一个计算机系统中的例子,一个文件夹(目录)中可以包含多个文件夹,每个文件夹中不能有相同的文件名,但不同文件夹中的文件可以重名。

一般有三种命名空间:

  • 内置名称(built-in names),Python 语言内置的名称,比如函数名 abschr 和异常名称 BaseExceptionException 等等。
  • 全局名称(global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
  • 局部名称(local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)

python 文件夹和文件 变量 python 文件名包含变量_python 文件夹和文件 变量

命名空间查找顺序:

假设我们要使用变量 LintCode,则 Python 的查找顺序为:局部的命名空间 -> 全局命名空间 -> 内置命名空间

如果找不到变量 LintCode,它将放弃查找并引发一个 NameError 异常:NameError: name 'LintCode' is not defined。

命名空间的生命周期:

命名空间的生命周期取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束。

因此,我们无法从外部命名空间访问内部命名空间的对象。

print(chr)  # 访问内置名称 chr
chr = 'chr'  # chr 是全局名称
print(chr)  # 访问全局名称 chr

def outer_function():
    chr = 'char'
    print(chr)  # 访问局部名称 chr
    
    outer_var = 'out_var'
    print(outer_var) 
    # 无法从外部命名空间访问内部命名空间的对象
    # print(inner_var) 

    def inner_function():
        inner_var = 'inner_var'
        # 可以从内部命名空间访问外部命名空间的对象
        print(outer_var) 
        print(inner_var)
    
    inner_function()
outer_function()

作用域

scope is a textual region of a Python program where a namespace is directly accessible.

作用域是一个代码区域,是一个命名空间可以直接引用的区域。

在一个 Python 程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到,否则会报未定义的错误。

Python 中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。

变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。Python 的作用域一共有4种,分别是:

  • L(Local):最内层,包含局部变量,比如一个函数/方法内部。
  • E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 non-local。
  • G(Global):当前脚本的最外层,比如当前模块的全局变量。
  • B(Built-in): 包含了内建的变量/关键字等,最后被搜索。

规则顺序: L –> E –> G –> B

在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内置中找。

python 文件夹和文件 变量 python 文件名包含变量_作用域_02

global_count = 0  # 全局作用域
def outer():
    outer_count = 1  # 闭包函数外的函数中
    def inner():
        inner_count = 2  # 局部作用域

内置作用域是通过一个名为 builtin 的标准模块来实现的,但是这个变量名自身并没有放入内置作用域内,所以必须导入这个文件才能够使用它。在 Python 3 中,可以使用以下的代码来查看到底预定义了哪些变量:

>>> import builtins
>>> dir(builtins)

Python 中只有模块(module),类(class)以及函数(deflambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/exceptfor/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问,如下代码:

>>> if True:
...     msg = 'I am from LintCode'
... 
>>> msg
'I am from LintCode'
>>>

实例中 msg 变量定义在 if 语句块中,但外部还是可以访问的。

如果将 msg 定义在函数中,则它就是局部变量,外部不能访问:

>>> def test():
...     msg_inner = 'I am from LintCode'
... 
>>> msg_inner
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'msg_inner' is not defined
>>>

从报错的信息上看,说明了 msg_inner 未定义,无法使用,因为它是局部变量,只有在函数内可以使用。

 

局部变量与全局变量

定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。

局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入到作用域中。如下实例:

#!/usr/bin/python3

total = 0 # 这是一个全局变量
# 可写函数说明
def sum(arg1, arg2):
    #返回2个参数的和.
    total = arg1 + arg2 # total在这里是局部变量.
    print ("函数内是局部变量 : ", total)
    return total
 
# 调用sum函数
sum(10, 20)
print("函数外是全局变量 : ", total)

global 和 nonlocal 关键字

当内部作用域想修改外部作用域的变量时,就要用到 global 和 nonlocal 关键字了。

global 关键字修饰变量后标识该变量是全局变量,对该变量进行修改就是修改全局变量。

global 关键字可以用在任何地方,包括最上层函数中和嵌套函数中,即使之前未定义该变量,global 修饰后也可以直接使用

num = 1
def function():
    global num  # 需要使用 global 关键字声明
    print(num) 
    num = 123
    print(num)
function()
print(num)

如果要修改嵌套作用域(enclosing 作用域,外层非全局作用域)中的变量则需要 nonlocal 关键字了。

nonlocal 关键字修饰变量后标识该变量是上一级函数中的局部变量,如果上一级函数中不存在该局部变量,nonlocal 位置会发生错误(最上层的函数使用 nonlocal 修饰变量必定会报错)。

def outer():
    num = 10
    def inner():
        nonlocal num # nonlocal关键字声明
        num = 100
        print(num)
    inner()
    print(num)
outer()

可变参数传递——在 python 中,strings, tuples, 和 numbers 是不可更改的对象,而 list,dict 等则是可以修改的对象。

不使用可变参数的传统方式

我们以数学题为例子,给定一组数字 a,b … z ,请计算 sum = a * a + b * b + .....+ z * z

要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a, b, …, z作为一个 list 或 tuple 传进来,这样,函数可以定义如下:

def cout(numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

但是调用的时候,需要先组装出一个 list 或 tuple

使用列表传递参数:

def cout(numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum
    
print(cout([1, 2, 3]))

使用可变参数

如果利用可变参数,调用函数的方式可以简化成这样:

def cout(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

print(cout(1, 2, 3))

定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个 * 号。在函数内部,参数 numbers 接收到的是一个 tuple,因此,函数代码完全不变。

但是,调用该函数时,可以传入任意个参数,包括0个参数。

不可变参数传递

请看示例:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

函数 person 除了必选参数 name 和 age 外,还接受关键字参数 **kw。在调用该函数时,可以只传入必选参数:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

person('Michael', 30)
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

person('Bob', 35, city='Beijing')
#name: Bob age: 35 other: {'city': 'Beijing'}
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)
    
person('Adam', 45, gender='M', job='Engineer')
#name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

关键字参数有什么用?它可以扩展函数的功能。比如,在 person 函数里,我们保证能接收到 name 和 age 这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个 dict,然后,把该 dict 转换为关键字参数传进去:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, city=extra['city'], job=extra['job'])
#name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

简化版:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, **extra)
#name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

 

请你设计一个函数 print_avg,这个函数接收多个关键字参数作为学生的信息,接收多个数字参数作为这个学生多次考试的成绩,请从学生信息中提取出学生的 student_namestudent_age,然后求出这个学生多次考试的平均成绩 Average(保留两位小数),返回一个字符串,格式如下:

name: student_name, age: student_age, avg: Average
# Please write your code here
def print_avg(*args,**kwargs):
    name=kwargs['student_name']
    age=kwargs['student_age']
    avg=sum(args)/len(args)
    return f'name: {name}, age: {age}, avg: {avg:.2f}'
    #return 'name : {name}, age : {age} ,avg :{avg}'.format(name,age,avg)

 

默认参数

def 函数名(...,形参名,形参名=默认值):
    代码块

注意,在使用此格式定义函数时,指定有默认值的形式参数必须在所有没默认值参数的最后,否则会产生语法错误。

 

为参数指定默认值是非常有用的方式。调用函数时,可以使用比定义时更少的参数,例如:

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

 

该函数可以用以下方式调用:

  • 只给出必选实参:ask_ok('Do you really want to quit?')
  • 给出一个可选实参:ask_ok('OK to overwrite the file?', 2)
  • 给出所有实参:ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')

本例还使用了关键字 [object Object],用于确认序列中是否包含某个值。

默认值在 定义 作用域里的函数定义中求值,所以:

i = 5


def f(arg=i):

print(arg)


i = 6

f()

#5

 

重要警告: 默认值只计算一次。默认值为列表、字典或类实例等可变对象时,会产生与该规则不同的结果。例如,下面的函数会累积后续调用时传递的参数:

def f(a, L=[]):
    L.append(a)
    return L

print(f(1)) #[1]
print(f(2)) #[1,2]
print(f(3)) #[1,2,3]

 

 关键字参数与特殊参数

关键字参数

kwarg=value 形式的 关键字参数 也可以用于调用函数。函数示例如下:

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

该函数接受一个必选参数(voltage)和三个可选参数(stateaction 和 type)。该函数可用下列方式调用:

parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

以下调用函数的方式都无效:

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword argument

函数调用时,关键字参数必须跟在位置参数后面。所有传递的关键字参数都必须匹配一个函数接受的参数(比如,actor 不是函数 parrot 的有效参数),

关键字参数的顺序并不重要。这也包括必选参数,(比如,parrot(voltage=1000) 也有效)。不能对同一个参数多次赋值。

最后一个形参为 **name 形式时,接收一个字典,该字典包含与函数中已定义形参对应之外的所有关键字参数。

**name 形参可以与 *name 形参(下一小节介绍)组合使用(*name 必须在 **name 前面), *name 形参接收一个元组,该元组包含形参列表之外的位置参数。

例如,可以定义下面这样的函数:

def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])
def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

        
cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

特殊参数

默认情况下,参数可以按位置或显式关键字传递给 Python 函数。为了让代码易读、高效,最好限制参数的传递方式,这样,开发者只需查看函数定义,即可确定参数项是仅按位置、按位置或关键字,还是仅按关键字传递。

函数定义如下:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

/ 和 * 是可选的。这些符号表明形参如何把参数值传递给函数:位置、位置或关键字、关键字。关键字形参也叫作命名形参。

1. 位置或关键字参数

函数定义中未使用 / 和 * 时,参数可以按位置或关键字传递给函数。

2. 仅位置参数

此处再介绍一些细节,特定形参可以标记为 仅限位置仅限位置 时,形参的顺序很重要,且这些形参不能用关键字传递。仅限位置形参应放在 / (正斜杠)前。/ 用于在逻辑上分割仅限位置形参与其它形参。如果函数定义中没有 /,则表示没有仅限位置形参。

/ 后可以是 位置或关键字 或 仅限关键字 形参。

3. 仅限关键字参数

把形参标记为 仅限关键字,表明必须以关键字参数形式传递该形参,应在参数列表中第一个 仅限关键字 形参前添加 *