简介
在这个实验中,你将学习如何探究 Python 函数的内部机制。Python 中的函数是具有自身属性和方法的对象,了解这些属性和方法可以让你更深入地理解函数的运行方式。这些知识将使你能够编写更强大、更具适应性的代码。
你将学习检查函数的属性和特性,使用 inspect 模块检查函数签名,并应用这些检查技术来改进类的实现。你要修改的文件是 structure.py。
在这个实验中,你将学习如何探究 Python 函数的内部机制。Python 中的函数是具有自身属性和方法的对象,了解这些属性和方法可以让你更深入地理解函数的运行方式。这些知识将使你能够编写更强大、更具适应性的代码。
你将学习检查函数的属性和特性,使用 inspect 模块检查函数签名,并应用这些检查技术来改进类的实现。你要修改的文件是 structure.py。
在 Python 中,函数被视为一等对象(first-class objects)。这是什么意思呢?这就好比在现实世界中有不同类型的对象,比如一本书或一支笔。在 Python 里,函数也是对象,并且和其他对象一样,它们有自己的一组属性。这些属性可以为我们提供很多关于函数的有用信息,比如函数的名称、定义位置以及实现方式。
让我们通过打开一个 Python 交互式 shell 来开始探究。这个 shell 就像一个游乐场,你可以立即在其中编写和运行 Python 代码。为此,你首先要导航到项目目录,然后启动 Python 解释器。以下是在终端中要运行的命令:
cd ~/project
python3
现在你已经进入了 Python 交互式 shell,让我们定义一个简单的函数。这个函数将接受两个数字并将它们相加。以下是定义该函数的方法:
def add(x, y):
'Adds two things'
return x + y
在这段代码中,你创建了一个名为 add 的函数。它接受两个参数 x 和 y,并返回它们的和。字符串 'Adds two things' 被称为文档字符串(docstring),用于记录函数的功能。
dir() 检查函数属性在 Python 中,dir() 函数是一个非常实用的工具。它可以用来获取对象拥有的所有属性和方法的列表。让我们用它来看看 add 函数有哪些属性。在 Python 交互式 shell 中运行以下代码:
dir(add)
当你运行这段代码时,你会看到一长串属性。以下是输出可能的样子:
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
这个列表展示了与 add 函数相关的所有属性和方法。
现在,让我们仔细看看一些基本的函数属性。这些属性可以告诉我们关于函数的重要信息。在 Python 交互式 shell 中运行以下代码:
print(add.__name__)
print(add.__module__)
print(add.__doc__)
当你运行这段代码时,你会看到以下输出:
add
__main__
Adds two things
让我们来理解一下这些属性的含义:
__name__:这个属性给出了函数的名称。在我们的例子中,函数名为 add。__module__:它告诉我们函数定义所在的模块。当你在交互式 shell 中运行代码时,模块通常是 __main__。__doc__:这是函数的文档字符串(docstring),它简要描述了函数的功能。函数的 __code__ 属性非常有趣。它包含了关于函数实现方式的信息,包括字节码(bytecode)和其他细节。让我们看看能从它那里了解到什么。在 Python 交互式 shell 中运行以下代码:
print(add.__code__.co_varnames)
print(add.__code__.co_argcount)
输出将是:
('x', 'y')
2
这些属性告诉我们以下信息:
co_varnames:它是一个元组,包含函数使用的所有局部变量的名称。在我们的 add 函数中,局部变量是 x 和 y。co_argcount:这个属性告诉我们函数期望的参数数量。我们的 add 函数期望两个参数,所以值为 2。如果你想进一步探究 __code__ 对象的更多属性,可以再次使用 dir() 函数。运行以下代码:
dir(add.__code__)
这将显示代码对象的所有属性,其中包含了关于函数实现的底层细节。
inspect 模块在 Python 中,标准库提供了一个非常有用的 inspect 模块。这个模块就像一个侦探工具,能帮助我们收集 Python 中活动对象(live objects)的信息。活动对象可以是模块、类和函数等。与手动挖掘对象的属性来查找信息不同,inspect 模块提供了更有条理、更高级的方式来了解函数的属性。
让我们继续使用同一个 Python 交互式 shell 来探索这个模块的工作原理。
inspect.signature() 函数是一个实用工具。当你将一个函数传递给它时,它会返回一个 Signature 对象。这个对象包含了函数参数的重要细节。
以下是一个示例。假设我们有一个名为 add 的函数。我们可以使用 inspect.signature() 函数来获取它的签名:
import inspect
sig = inspect.signature(add)
print(sig)
当你运行这段代码时,输出将是:
(x, y)
这个输出展示了函数的签名,它告诉我们函数可以接受哪些参数。
我们可以更进一步,获取函数每个参数的更深入信息。
print(sig.parameters)
这段代码的输出将是:
OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">)])
函数的参数存储在一个有序字典中。有时,我们可能只对参数的名称感兴趣。我们可以将这个有序字典转换为元组,以提取参数名称。
param_names = tuple(sig.parameters)
print(param_names)
输出将是:
('x', 'y')
我们还可以仔细查看每个单独的参数。以下代码会遍历函数中的每个参数,并打印出关于它的一些重要细节。
for name, param in sig.parameters.items():
print(f"Parameter: {name}")
print(f" Kind: {param.kind}")
print(f" Default: {param.default if param.default is not param.empty else 'No default'}")
这段代码将向我们展示每个参数的详细信息。它会告诉我们参数的类型(是位置参数、关键字参数等)以及是否有默认值。
inspect 模块还有许多其他用于函数自省(introspection)的实用函数。以下是一些示例:
inspect.getdoc(obj):这个函数用于获取对象的文档字符串。文档字符串就像是程序员为解释对象的功能而编写的注释。inspect.getfile(obj):它帮助我们找出对象定义所在的文件。当我们想要定位对象的源代码时,这非常有用。inspect.getsource(obj):这个函数用于获取对象的源代码。它让我们能够确切地看到对象是如何实现的。现在,我们将运用所学的函数检查知识来改进类的实现。函数检查能让我们深入了解函数的结构,比如它们所接受的参数。在这个例子中,我们将利用它让类代码更高效且更不易出错。我们会修改 Structure 类,使其能自动从 __init__ 方法的签名中检测字段名。
Structure 类structure.py 文件中包含一个 Structure 类。这个类是一个基类,这意味着其他类可以继承它来创建结构化的数据对象。目前,要定义从继承自 Structure 的类创建的对象的属性,我们需要设置一个 _fields 类变量。
让我们在编辑器中打开这个文件。我们可以使用以下命令导航到项目目录:
cd ~/project
运行此命令后,你可以在 WebIDE 中的 structure.py 文件里找到并查看现有的 Structure 类。
Stock 类让我们创建一个继承自 Structure 类的 Stock 类。继承意味着 Stock 类将拥有 Structure 类的所有特性,并且还能添加自己的特性。我们将以下代码添加到 structure.py 文件的末尾:
class Stock(Structure):
_fields = ('name', 'shares', 'price')
def __init__(self, name, shares, price):
self._init()
然而,这种方法存在一个问题。我们必须同时定义 _fields 元组和 __init__ 方法,且参数名要相同。这是多余的,因为我们本质上是在重复编写相同的信息。如果在修改其中一个时忘记更新另一个,就可能会导致错误。
set_fields 类方法为了解决这个问题,我们将在 Structure 类中添加一个 set_fields 类方法。这个方法将自动从 __init__ 方法的签名中检测字段名。以下是需要添加到 Structure 类中的代码:
@classmethod
def set_fields(cls):
## Get the signature of the __init__ method
import inspect
sig = inspect.signature(cls.__init__)
## Get parameter names, skipping 'self'
params = list(sig.parameters.keys())[1:]
## Set _fields attribute on the class
cls._fields = tuple(params)
这个方法使用了 inspect 模块,它是 Python 中用于获取函数和类等对象信息的强大工具。首先,它获取 __init__ 方法的签名。然后,它提取参数名,但跳过 self 参数,因为 self 是 Python 类中引用实例本身的特殊参数。最后,它用这些参数名设置 _fields 类变量。
Stock 类现在我们有了 set_fields 方法,就可以简化 Stock 类了。用以下代码替换之前的 Stock 类代码:
class Stock(Structure):
def __init__(self, name, shares, price):
self._init()
## Call set_fields to automatically set _fields from __init__
Stock.set_fields()
这样,我们就不必手动定义 _fields 元组了。set_fields 方法会为我们处理这个问题。
为了确保修改后的类能正常工作,我们将创建一个简单的测试脚本。创建一个名为 test_structure.py 的新文件,并添加以下代码:
from structure import Stock
def test_stock():
## Create a Stock object
s = Stock(name='GOOG', shares=100, price=490.1)
## Test string representation
print(f"Stock representation: {s}")
## Test attribute access
print(f"Name: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
## Test attribute modification
s.shares = 50
print(f"Updated shares: {s.shares}")
## Test attribute error
try:
s.share = 50 ## Misspelled attribute
print("Error: Did not raise AttributeError")
except AttributeError as e:
print(f"Correctly raised: {e}")
if __name__ == "__main__":
test_stock()
这个测试脚本创建了一个 Stock 对象,测试了它的字符串表示形式,访问了它的属性,修改了一个属性,并尝试访问一个拼写错误的属性,以检查是否会引发正确的错误。
要运行测试脚本,请使用以下命令:
python3 test_structure.py
你应该会看到类似以下的输出:
Stock representation: Stock('GOOG',100,490.1)
Name: GOOG
Shares: 100
Price: 490.1
Updated shares: 50
Correctly raised: No attribute share
set_fields 方法使用 inspect.signature() 从 __init__ 方法中获取参数名。这个函数为我们提供了 __init__ 方法参数的详细信息。_fields 类变量。这样,我们就不必在两个不同的地方编写相同的参数名了。_fields 和 __init__ 并使参数名匹配的需求。它让我们的代码更易于维护,因为如果我们修改了 __init__ 方法中的参数,_fields 会自动更新。这种方法利用函数检查让我们的代码更易于维护且更不易出错。这是 Python 自省能力的一个实际应用,它允许我们在运行时检查和修改对象。
在这个实验中,你学习了如何检查 Python 函数的内部结构。你使用 dir() 等方法直接检查了函数的属性,并访问了 __name__、__doc__ 和 __code__ 等特殊属性。你还使用 inspect 模块获取了关于函数签名和参数的结构化信息。
函数检查是 Python 的一项强大功能,它使你能够编写更具动态性、灵活性和可维护性的代码。在运行时检查和操作函数属性的能力为元编程、创建自文档化代码以及构建高级框架提供了可能性。