Python代码结构:命名空间、作用域和错误处理

Published on 2017 - 01 - 22

命名空间和作用域

一个名称在不同的使用情况下可能指代不同的事物。Python 程序有各种各样的命名空间,它指的是在该程序段内一个特定的名称是独一无二的,它和其他同名的命名空间是无关的。

每一个函数定义自己的命名空间。如果在主程序(main)中定义一个变量 x,在另外一个函数中也定义 x 变量,两者指代的是不同的变量。但是,天下也没有完全绝对的事情,需要的话,可以通过多种方式获取其他命名空间的名称。

每个程序的主要部分定义了全局命名空间。因此,在这个命名空间的变量是全局变量。

你可以在一个函数内得到某个全局变量的值:

>>> animal = 'fruitbat'
>>> def print_global():
...         print('inside print_global:', animal)
...
>>> print('at the top level:', animal)
at the top level: fruitbat
>>> print_global()
inside print_global: fruitbat

但是,如果想在函数中得到一个全局变量的值并且改变它,会报错:

>>> def change_and_print_global():
...         print('inside change_and_print_global:', animal)
...         animal = 'wombat'
...         print('after the change:', animal)
...
>>> change_and_print_global()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in change_and_report_it
UnboundLocalError: local variable 'animal' referenced before assignment

实际上,你改变的另外一个同样被命名为 animal 的变量,只不过这个变量在函数内部:

>>> def change_local():
...     animal = 'wombat'
...     print('inside change_local:', animal, id(animal))
...
>>> change_local()
inside change_local: wombat 4330406160
>>> animal
'fruitbat'
>>> id(animal)
4330390832

这里发生了什么?在函数第一行将字符串 fruitbat 赋值给全局变量 animal。函数 change_local() 也有一个叫作 animal 的变量。不同的是,它在自己的局部命名空间。

我们使用 Python 内嵌函数 id() 打印输出每个对象的唯一的 ID 值,证明在函数 change_local() 中的变量 animal 和主程序中的 animal 不是同一个。

为了读取全局变量而不是函数中的局部变量,需要在变量前面显式地加关键字 global(也正是《Python 之禅》中的一句话:明了胜于隐晦):

>>> animal = 'fruitbat'
>>> def change_and_print_global():
...         global animal
...         animal = 'wombat'
...         print('inside change_and_print_global:', animal)
...
>>> animal
'fruitbat'
>>> change_and_print_global()
inside change_and_print_global: wombat
>>> animal
'wombat'

如果在函数中不声明关键字 global,Python 会使用局部命名空间,同时变量也是局部的。函数执行后回到原来的命名空间。

Python 提供了两个获取命名空间内容的函数:

  • locals() 返回一个局部命名空间内容的字典;
  • globals() 返回一个全局命名空间内容的字典。

下面是它们的实例:

>>> animal = 'fruitbat'
>>> def change_local():
...         animal = 'wombat' #局部变量
...         print('locals:',locals())
...
>>> animal
'fruitbat'
>>> change_local()
locals: {'animal':'wombat'}
>>> print('globals:', globals())  #表示时格式稍微发生变化
globals:{'animal': 'fruitbat',
'__doc__': None,
'change_local': <function change_it at 0x1006c0170>,
'__package__': None,
'__name__': '__main__',
'__loader__': <class '_frozen_importlib.BuiltinImporter'>,
'__builtins__': <module 'builtins'>}
>>> animal
'fruitbat'

函数 change_local() 的局部命名空间只含有局部变量 animal。全局命名空间含有全局变量 animal 以及其他一些东西。

名称中和 _ 的用法

以两个下划线 __ 开头和结束的名称都是 Python 的保留用法。因此,在自定义的变量中不能使用它们。选择这种命名模式是考虑到开发者一般是不会选择它们作为自己的变量的。

例如,一个函数的名称是系统变量 function.__name__,它的文档字符串是 function.__doc__

>>> def amazing():
...         '''This is the amazing function.
...         Want to see it again?'''
...         print('This function is named:', amazing.__name__)
...         print('And its docstring is:', amazing.__doc__)
...
>>> amazing()
This function is named: amazing
And its docstring is: This is the amazing function.
    Want to see it again?

如同之前 globals 的输出结果所示,主程序被赋值特殊的名字 __main__

使用try和except处理错误

在一些编程语言中,错误是通过特殊的函数返回值指出的,而 Python 使用异常,它是一段只有错误发生时执行的代码。

之前已经接触到一些有关错误的例子,例如读取列表或者元组的越界位置或者字典中不存在的键。所以,当你执行可能出错的代码时,需要适当的异常处理程序用于阻止潜在的错误发生。

在异常可能发生的地方添加异常处理程序,对于用户明确错误是一种好方法。即使不会及时解决问题,至少会记录运行环境并且停止程序执行。如果发生在某些函数中的异常不能被立刻捕捉,它会持续,直到被某个调用函数的异常处理程序所捕捉。在你不能提供自己的异常捕获代码时,Python 会输出错误消息和关于错误发生处的信息,然后终止程序,例如下面的代码段:

>>> short_list = [1, 2, 3]
>>> position = 5
>>> short_list[position]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

与其让错误随机产生,不如使用 try 和 except 提供错误处理程序:

>>> short_list = [1, 2, 3]
>>> position = 5
>>> try:
...         short_list[position]
... except:
...     print('Need a position between 0 and', len(short_list)-1, ' but got',
...            position)
...
Need a position between 0 and 2 but got 5

在 try 中的代码块会被执行。如果存在错误,就会抛出异常,然后执行 except 中的代码;否则,跳过 except 块代码。

像前面那样指定一个无参数的 except 适用于任何异常类型。如果可能发生多种类型的异常,最好是分开进行异常处理。当然,没人强迫你这么做,你可以使一个 except 去捕捉所有的异常,但是这样的处理方式会比较泛化(类似于直接输出发生了一个错误)。当然也可以使用任意数量的异常处理程序。

有时需要除了异常类型以外其他的异常细节,可以使用下面的格式获取整个异常对象:

except exceptiontype as name

下面的例子首先会寻找是否有 IndexError,因为它是由索引一个序列的非法位置抛出的异常类型。将一个 IndexError 异常赋给变量 err,把其他的异常赋给变量 other。示例中会输出所有存储在 other 中的该对象的异常。

>>> short_list = [1, 2, 3]
>>> while True:
...         value = input('Position [q to quit]? ')
...         if value == 'q':
...             break
...         try:
...             position = int(value)
...             print(short_list[position])
...         except IndexError as err:
...             print('Bad index:', position)
...         except Exception as other:
...             print('Something else broke:', other)
...
Position [q to quit]? 1
2
Position [q to quit]? 0
1
Position [q to quit]? 2
3
Position [q to quit]? 3
Bad index: 3
Position [q to quit]? 2
3
Position [q to quit]? two
Something else broke: invalid literal for int() with base 10: 'two'
Position [q to quit]? q

输入 3 会抛出异常 IndexError;输入 two 会使函数 int() 抛出异常,被第二个 except 所捕获。

编写自己的异常

前面一节讨论了异常处理,但是其中讲到的所有异常(例如 IndexError)都是在 Python 或者它的标准库中提前定义好的。根据自己的目的可以使用任意的异常类型,同时也可以自己定义异常类型,用来处理程序中可能会出现的特殊情况。

一个异常是一个类,即类 Exception 的一个子类。现在编写异常 UppercaseException,在一个字符串中碰到大写字母会被抛出。

>>> class UppercaseException(Exception):
...         pass
...
>>> words = ['eeenie', 'meenie', 'miny', 'MO']
>>> for word in words:
...     if word.isupper():
...         raise UppercaseException(word)
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
__main__.UppercaseException: MO

即使没有定义 UppercaseException 的行为(注意到只使用 pass),也可以通过继承其父类 Exception 在抛出异常时输出错误提示。

你当然能够访问异常对象本身,并且输出它:

>>> try:
...         raise OopsException('panic')
... except OopsException as exc:
...         print(exc)
...
panic

参考文档