自省

许多python对象在运行的代码之外还带有metadata(元数据)。这些信息在框架的使用或写自己的代码中相当有用。

当试图开发复用的程序时,python的自省工具很有帮助的。因为他们允许python代码提取编程者所写的信息,编程者不用多次重复写的东西。

这小节里描述的一些特性依赖于强大的python标准库inspect,inspect模块提供了方便的函数来执行高级的自省。

inspect的很多用法这里只列出了一部分,因为这些用法对于用django编写的应用提供了很有价值的东西。这个模块的许多其他细节,查阅python的标准库文档。

**

这个小节的内容全部采用的是新的类定义方式,这一点本章前面已经描述过了。它和老的类定义方式不同,尤其是自省方面。这种全部的差别已经超过本书要讲的内容,因为现在强烈推荐只采用新的类定义方式。

如果你的代码和这里说的不同,请确保所有的类都继承自object,采用正确的新的类定义方式。

****

常见的类和函数属性

所有的类和函数提供了一些常见的属性,用来标识它们。

1)__name__ -- 用来声明类和函数的名称
2)__doc__ -- 为函数声明使用的文档说明
3)__module__ -- 导入模块的路径,模块里有类和函数的声明。

另外,所有对象都包含一个特殊的属性,__class__,它是用来创建对象的实际类对象。这个属性有很多用途,诸如测试一下类中是否提供一个特殊的属性或者它是否设置在对象上。

>>> class ValueClass(object):
...     source = 'The class'
...
>>> value_instance = ValueClass()
>>> value_instance.source = 'The instance'
>>> value_instance.__class__
<class '__main__.ValueClass'>
>>> value_instance.source
'The instance'
>>> value_instance.__class__.source
'The class'

标识对象类型

因为python使用动态类型,任何变量都可以任意类型的对象。duck-typing的原则,推荐的用法是测试一下支持的约定(难翻译),这用来标识你要处理的对象很有用处。这里有几个方法处理这个

获得任意对象的类型

使用早先介绍的内置函数type来判断任何python对象是轻而易举。调用type,只带一个参数,返回一个type对象,通常是一个类,被实例化来生成对象。

>>> type('this is a string')
<type 'str'>
>>> type(42)
<type 'int'>
>>> class TestClass(object):
...     pass
...
>>> type(TestClass)
<type 'type'>
>>> obj = TestClass()
>>> type(obj)
<class '__main__.TestClass'>

这个方法通常不是最好的办法去判定一个对象的类型,尤其是在通过对象类型来决定执行的分支的情况下。它只是告诉你一个从来不使用的特定类,即使为同一个执行分支的子类。(难翻译)相反这个方法适用于对象类型不是条件,而仅仅是输出信息,可能输出给用户或者输出到日志文件。

举个例子,如果报告异常,包括异常类型和值是非常有用处的。在这种情况,type用来返回一个类对象,它的__name__属性也包括进日志文件,容易表示异常类型。

检查特定类型

更多的情况,你需要检查一个特定类型,检查是否是派生于它或是否是一个实例。这是比使用type更有效的解决方案。因为是成功或失败,它考虑了类继承的因素,

python为此提供了2个内置的函数:

1)issubclass(cls, base) -- 如果cls和base是同一类型,或者cls是从base继承而来,返回True,
2)isinstance(obj, base) -- 测试obj是否是base的实例或它的父类对象的实例。

>>> class CustomDict(dict):
...     pass
...
>>> issubclass(CustomDict, dict)
True
>>> issubclass(CustomDict, CustomDict)
True
>>> my_dict = CustomDict()
>>> isinstance(my_dict, dict)
True
>>> isinstance(my_dict, CustomDict)
True

在issubclass和isinstance之间存在一个明显的关系,isinstance(obj, SomeClass)等同于insubclass(obj.__class__, SomeClass)。

函数签名

正如本章前面描述的,python 函数用多种方式来声明,在你的代码中,函数的直接访问的声明信息是相当有用的。

检查函数最重要的一个函数是inspect.getargspec(),它返回的信息是关于函数接受的参数。它只接受一个参数,被检查的函数对象,返回一个元组,有以下的值:

1)args -- 所有参数名称的列表,如果函数不接受任何参数,这就是一个空列表
2)varargs -- 过量位置参数中的变量名称,就像先前说的。如果函数不接受过量位置参数,它就为None
3)varkwargs -- 过量关键字参数的变量名称,前面也说过。如果函数不接受过量关键字参数,它就为None
4)defaults -- 函数参数中默认值的元组,如果没有参数的默认值,就是None,而不是空元组

这些值代表了任何方式去调用函数的所有的必要信息。当接受一个函数,然后用合适的参数来调用它,这样处理时是上面的值非常有用。

>>> def test(a, b, c=True, d=False, *e, **f):
...     pass
...
>>> import inspect
>>> inspect.getargspec(test)
(['a', 'b', 'c', 'd'], 'e', 'f', (True, False))

处理默认值

前面的例子表明,默认值是放在一个单独列表中返回的,因此怎样告诉你哪个参数对应哪个默认值,可能看起来不是很明显。然后,有一个相对简单的方法来处理这个情况,它基于前面讨论过量参数的一个小细节:必须的参数总是要在任何可选参数前被声明。

这是关键,因为它意味着参数和它的默认值按他们在函数中的声明次序来指定的。因此前面的例子中,有2个默认值,意味着最后两个参数是可选的,默认值按次序排列。下面的代码可用来创建一个字典,它将可选的参数名称和声明的默认值一一对应。

>>> def get_defaults(func):
...     args, varargs, varkwargs, defaults = inspect.getargspec(func)
...     index = len(args) - len(defaults) # Index of the first optional argument
...     return dict(zip(args[index:], defaults))
...
>>> get_defaults(test)
{'c': True, 'd': False}

文档字符串

前面提到了,类和函数都有一个特殊的__doc__属性,它包含了一个字符串作为代码的文档说明。不过,它的格式完全就像它在源码文件中一样,包括换行符和不必要的缩进。

为了用更可读的方式来格式化文档字符串,python的inspect模块提供了另一个有用的函数,getdoc()。它删除了不必要的换行符,和任何缩进符,缩进对于文档字符串的书写格式有一些副作用的。

缩进的删除要作一点解释,getdoc()找到字符串最左边非空格的字符,计算出这个字符和行开头的位置之间所有的空白,并且删除文档字符串其他行的所有空白。这样处理之后,最后的结果,字符串的左边作了调整,但也保留一些缩进,这些缩进是已经存在格式化文档中的。

>>> def func(arg):
...     """
...     Performs a function on an argument and return the result.
...
...     arg
...         The argument to be processed
...     """
...     pass
...
>>> print func.__doc__

    Performs a function on an argument and return the result.

    arg
        The argument to be processed
    
>>> print inspect.getdoc(func)
Performs a function on an argument and return the result.

arg
    The argument to be processed

那些文档字符串要显示给用户的时候,比如自动文档或帮助系统,对于原始的文档字符串,getdoc()提供了一个很有用的可选方式。