Python中的基础用法#
到目前为止,我们已经通过一些基础示例学习了如何使用Spyder IDE构建和运行模型。本节将介绍modelx的基本使用方法。与之前的示例不同,在本节中我们将主要使用IPython控制台与modelx进行交互操作,而非使用带有modelx插件的Spyder。您可以继续沿用之前示例中使用的Spyder环境,只需为本节练习新建一个MxConsole即可。或者,您也可以使用其他Python工具自带的IPython环境,例如Jupyter notebook、JupyterLab、PyCharm、Visual Studio Code等。
本节中的代码片段假设是从IPython控制台交互式执行的,但这些代码片段也可以写入Python脚本并作为Python程序运行。
导入modelx#
要开始使用modelx,请通过import
语句导入modelx模块。
按照惯例,建议使用缩写名mx
来导入该模块:
>>> import modelx as mx
通过这种方式,modelx API函数可以通过mx
访问。
完整的modelx API函数列表可以在
参考指南中的modelx模块部分找到。
本教程中的所有示例脚本都假设modelx模块
被导入为mx
:
注意
你也可以选择通过import modelx as *
将所有API函数直接导入__main__
模块,
但一般来说在Python中这不是一个好的做法。
如果你想直接导入某个API函数,
建议单独导入它,例如:
from modelx import defcells
使用模型#
如前文所述, Models(模型)是包含其他类型modelx对象的顶级对象,以层级树状结构组织。 Models之于modelx,就如同工作簿之于电子表格程序。 Models可以保存到文件中,并重新加载回来。
创建模型#
当首次在Python会话中导入modelx时,该会话中不存在任何Model。
要创建一个新的Model,请调用new_model()
。
new_model()
会返回一个新的Model,
其名称通过name
参数传递。
例如,下面的语句创建了一个名为MyModel的Model,
并将其赋值给全局变量model
。
>>> model = mx.new_model(name="MyModel")
如果没有传递名称给函数,
返回的模型将由modelx自动命名,例如Model1
。
检索模型#
如果不小心删除了绑定到MyModel的变量model
会发生什么?
MyModel本身不会被删除,您仍然可以通过访问modelx模块mx
来恢复它。
从Python 3.7开始,可以通过名称作为modelx模块的属性来获取Models。继续前面的例子,Model MyModel可以通过以下方式获取:
>>> mx.MyModel
<Model MyModel>
modelx模块的models
属性返回一个dict
,其中包含所有现有模型及其关联名称:
>>> mx.models
{'Model1': <Model Model1>}
对于Python 3.6,请使用get_models()
函数而非models
:
>>> mx.get_models()
{'Model1': <Model Model1>}
当前Model#
当创建、读取或恢复一个Model时,该Model会被设为当前Model。modelx拥有当前Model的概念,在某种程度上类似于电子表格程序拥有活动工作簿的方式。
当没有传递参数时,cur_model()
函数会返回当前模型:
>>> mx.cur_model()
<Model Model1>
如果将Model或其名称传递给cur_model()
,则当前Model会被更改为该Model。
删除模型#
要删除一个模型,请在模型上调用close()
方法。
保存和读取模型#
模型可以通过write_model()
函数保存到目录树中的文件里。假设model
是一个Model对象,以下代码将该Model保存到指定路径:
>>> mx.write_model(model, r"C:\Users\path\to\model")
路径也可以相对于当前目录来表示。
model目录包含一个__init__.py
文件以及
与model
中UserSpaces对应的子目录树。
在每个子目录中,都有一个__init__.py
文件。
__init__.py
文件是一个用Python编写的伪脚本。
UserSpace中包含的单元格公式
以Python函数的形式写在__init__.py
中,
同时还包括其他信息,例如UserSpace的公式(如果有的话)
以及所包含引用的元数据。
虽然__init__.py
并不直接由Python解释执行,
但它是一个语义正确的Python脚本,
这使得可以将子目录作为Python包导入。
这允许Sphinx(Python的文档生成工具)
从__init__.py
文件中的文档字符串自动生成模型文档。
write()
方法的功能与在其自身上调用 write_model()
函数相同。
要将Model保存为单个zip文件,请改用zip_model()
或zip()
。zip文件的内容与通过write_model()
或write()
保存的目录树内容相同。
这些函数和方法会保存单元格的输入值,但不会保存计算结果值。它们也不会保存DynamicSpace
对象,除非这些对象有输入值。要保存包含计算值和DynamicSpace的模型,请使用backup()
方法。
使用read_model()
来读取已保存的Model,无论它是保存为zip文件还是目录树:
>>> model = mx.read_model(r"C:\Users\path\to\model")
如果已存在同名的模型,现有模型的名称将被添加后缀 _BAKn
,其中 n
为整数。
备份与恢复#
警告
备份和恢复功能自0.18.0版本起已弃用。
建议改用write_model()
和read_model()
替代。
还有另一种保存模型的方法。backup()
方法将模型写入二进制文件。与write_model()
不同,backup()
方法还会保存计算值和DinamicSpaces。
restore_model()
用于恢复通过该方法备份的Model。
备份模型比写入或压缩模型更快。 然而,备份的模型是二进制文件,人类无法直接阅读。 它可能无法通过不同版本的modelx恢复。 也可能无法在 模型备份所用的Python环境之外的其他Python环境中恢复, 因此建议仅将模型备份用于临时保存。
使用空间#
空间(Spaces)是modelx中的容器对象,用于将模型内容划分为不同组件。 空间可以直接在模型中创建,也可以嵌套在其他空间内,从而形成空间树状结构。空间非常类似于文件夹(Linux用户称之为目录),因为两者都采用树状结构来组织内容。
Space的另一个重要作用是为其中的Formulas提供命名空间。我们稍后将更详细地讨论这一点。
Space有几种类型。用户可以显式创建的Space类型是UserSpace
。
创建用户空间#
要在Model中创建一个UserSpace
,可以使用
Model.new_space
方法。
以下代码创建了一个名为‘MySpace’的新UserSpace,并将其赋值给全局变量space
:
>>> space = model.new_space('MySpace')
model
被称为 MySpace 的 父级。
每个 Space 有且仅有一个父级。
UserSpace 也可以在另一个 UserSpace 中创建。
为此,需调用另一个 UserSpace 的 new_space()
方法。
在这种情况下,该 UserSpace 的父级就是另一个 UserSpace。
例如,以下代码在之前代码创建的 MySpace 中创建了一个名为 'SubSpace' 的 UserSpace:
>>> subspace = space.new_space('SubSpace')
如果不向该方法传递任何名称,那么modelx会为新用户空间分配一个名称,例如'Space1'。
还有一个函数,new_space()
。
这个函数会在当前Model中创建一个新的UserSpace。
如果没有当前Model,modelx会创建一个并将其设为当前Model。
检索用户空间#
可以通过名称获取UserSpaces,就像它们是父级的属性一样。
要获取模型中所有空间与其名称的映射关系,可以查看模型的spaces
属性:
>>> model.spaces
mappingproxy({'Space1': <Space Space1 in Model1>})
返回的MappingProxy对象类似于一个不可变的字典,因此你可以通过model.spaces['Space1']
获取Space1。你可以看到返回的空间与space
所引用的是同一个对象:
>>> space is model.spaces['Space1']
True
要获取一个空间,其名称可作为包含模型的属性使用:
>>> model.Space1
<Space Space1 in Model1>
当前空间#
当您创建一个新的用户空间(UserSpace)时,modelx会将其作为当前空间持有。下次当您使用defcells()
装饰器创建单元格(Cells)且未指定其父空间时,新的单元格将在当前空间中创建。
你可以通过调用cur_space()
函数(无需参数)来获取当前Model的当前空间。
删除用户空间#
用户空间可以通过del语句删除,像这样:
>>> del model.Space1
或者这样:
>>> del model.spaces["Space1"]
两种语句效果相同。
单元格操作#
Cells对象用于定义计算和存储数值。 Cells之于modelx,就如同单元格之于电子表格。 不过,正如"Cells"这个名字所示,一个Cells对象 可以为其关联的公式存储多个值。 Cells的公式由一个底层Python函数定义。 如果该公式没有参数, Cells最多只能存储一个值。 如果公式包含参数,Cells就可以存储多个值, 这些值与传入公式的参数相关联。
创建单元格并定义其公式#
有几种方法可以创建单元格对象并定义与之关联的公式。最快的方法是使用defcells()
装饰器定义一个python函数。
model, space = mx.new_model(), mx.new_space()
@mx.defcells
def fibo(n):
if n == 0 or n == 1:
return n
else:
return fibo(n - 1) + fibo(n - 2)
通过defcells()
装饰器,当前作用域中的名称fibo
指向
从公式定义刚创建的Cells对象。
根据这个定义,单元格是在当前模型的当前Space中创建的。
如前所述,
modelx会将最后操作的模型保持为当前模型,
并将每个模型最后操作的Space保持为当前空间。
cur_model()
API函数返回
当前模型,
而模型的cur_space()
方法保存
其当前空间。
即使不创建模型和空间,您也可以创建新的单元格。
如果不存在模型,那么defcells()
会首先自动创建一个模型和该模型内的一个空间,两者都由modelx自动命名,
例如Model1和Space1,然后在该空间中创建单元格。
要指定创建单元格的空间,您可以将空间对象作为参数传递给defcells()
装饰器。以下与上述定义相同,但明确指定了定义单元格的空间:
@mx.defcells(space)
def fibo(n):
if n == 0 or n == 1:
return n
else:
return fibo(n - 1) + fibo(n - 2)
除了defcells()
外,还有其他创建单元格的方法。
详情请参阅参考手册中的defcells()
章节。
另一种创建单元格的方法是使用Space的
new_cells()
方法。
该方法会创建一个新单元格,其公式由传入
formula
参数的函数定义:
>>> def fibo2(n):
return fibo2(n-1) + fibo2(n-2) if n > 0 else n
>>> space.new_cells(formula=fibo2)
formula
参数可以是一个函数对象,也可以是一个函数定义的字符串。
获取单元格#
类似于模型中包含在spaces
属性中的空间,空间中的单元格与其名称相关联,并包含在模型的cells
属性中:
>>> fibo is space.cells['fibo']
True
正如你可以通过属性访问用.
获取model中的空间一样,你也可以通过用.
访问单元格名称的空间属性来获取空间中的单元格:
>>> space.fibo
<Cells fibo(n) in Model1.Space1>
>>> fibo is space.fibo
True
获取值#
单元格 fibo
在刚创建时还没有值。
要获取某个参数对应的单元格值,只需用圆括号或方括号传入参数调用 fibo
:
>>> fibo[10]
55
>>> fibo(10)
55
其值由关联公式自动计算,当请求单元格值时。
请注意,不仅会为指定参数计算值,还会为公式中递归引用的参数计算值,以获取指定参数的值。
要查看为哪些参数计算了值,请将fibo
导出为Pandas Series对象。(当然,您需要已安装Pandas。)
>>> fibo[10]
55
>>> fibo.series
n
0 0
1 1
2 1
3 2
4 3
5 5
6 8
7 13
8 21
9 34
10 55
Name: fibo, dtype: int64
由于fibo[10]
引用了fibo[9]
和fibo[8]
,
fibo[9]
又引用了fibo[8]
和fibo[7]
,
这种递归引用会持续进行直到停止于fibo[1]
和fibo[0]
,
因此只需调用fibo[10]
即可计算出参数0
到10
对应的fibo
值。
与Python函数不同,单元格公式的全局命名空间与其在源文件中的定义位置无关。公式中的名称解析是在与单元格父空间关联的命名空间中进行的。在该命名空间中,可用的名称包括该空间包含的单元格、该空间包含的子空间(即该空间的子空间)以及在该空间中可访问的"引用"。
清除值#
要清除单元格的值,可以使用clear()
方法。下面展示了当n=5时fibo
的值被清除后的情况:
>>> fibo.clear(5)
>>> fibo.series
n
0 0
1 1
2 1
3 2
4 3
Name: fibo, dtype: int64
如你所见,不仅当n=5时,对于n=6到10的情况,fibo
的值也被清除了。这是因为计算fibo[6]
到fibo[10]
依赖于fibo[5]
的值。所有依赖值会随着指定值一起被清除。
要清除所有值,只需不带参数调用 clear()
:
>>> fibo.clear()
>>> fibo.series
Series([], Name: fibo, dtype: float64)
设置值#
除了让公式计算单元格值外,您还可以通过设置项([] =
)操作手动输入单元格值。如果单元格在指定参数值处已有值,则会先清除依赖单元格的值,然后再分配指定值:
>>> fibo[10]
55
>>> fibo.series
n
0 0
1 1
2 1
3 2
4 3
5 5
6 8
7 13
8 21
9 34
10 55
Name: fibo, dtype: int64
>>> fibo[5] = 0
>>> fibo.series
n
0 0
1 1
2 1
3 2
4 3
5 0
Name: fibo, dtype: int64
命名空间与引用#
定义公式#
大多数公式需要引用其他单元格和引用的值来计算自身的值。与Python函数不同,modelx公式的名称绑定独立于Python模块。每个空间都有与之关联的独立命名空间,该空间中子单元格公式内的名称会绑定到该空间关联的命名空间内。关联命名空间中定义的名称包括:该空间的子对象名称(如子单元格、子空间和引用),此外还包含全局引用、特殊名称和内置名称。全局引用是指在模型层级定义的引用,作为模型的属性。特殊名称由modelx定义,这些名称以"_"开头。目前仅有一个特殊名称_space
,用于指代空间本身。下方列表总结了与用户空间关联的命名空间中定义的各种名称类型。
子单元格、空间和引用
模型中定义的全局引用
特殊名称 (
_space
)Python内置名称
以下示例代码取自我们之前看到的抵押贷款示例。
Balance
全局变量引用了Cells对象Balance
,但变量名称不需要与Cells名称相同:
>>> Balance.formula
def Balance(t):
if t > 0:
return Balance(t-1) * (1+Rate) - Payment()
else:
return Principal
>>> Balance(30)
1.2096279533579946e-10
如果Balance
是一个Python函数,那么在Balance
定义中的名称,如Balance
、Rate
、Payment
、Principal
将引用定义该函数的模块中的全局变量。
然而,如上所述,Balance
的公式是在与其父空间Fixed
关联的命名空间中计算的。
Fixed
空间包含子单元格,如Payment
和Balance
。它还包含子引用,如Principal
和Rate
。因此,Balance
定义中的名称引用的是Fixed
空间的这些子单元格和引用。
要获取Fixed
命名空间中定义的所有名称,可以使用Python内置函数dict
。
以下代码假设Fixed
变量引用的是Fixed
空间:
>>> dir(Fixed)
['Balance',
'Payment',
'Principal',
'Rate',
'Term',
'__builtins__',
'_self',
'_space']
(注意:上述列表中的 _self
已弃用,不应再使用。)
运行模型#
与用编程语言编写的程序不同, modelx模型没有单一的入口点,比如C语言中的main函数。 modelx与Excel也有所不同,因为 modelx在打开模型时不会用计算值填充其模型。 当用户直接或通过依赖该公式值的其他公式间接请求时, modelx才会对公式进行求值。
下面示例中的 Fibo
单元格取自前面的部分:
>>> Fibo.formula
def Fibo(n):
if n > 1:
return Fibo(n-1) + Fibo(n-2)
else:
return n
最初,Fibo
没有任何值。您可以通过将其转换为dict
来检查Fibo
的值:
>>> dict(Fibo)
{}
当你请求Fibo
在n=5
时的值时,Fibo
在n=0
到n=4
的值也会被计算出来:
>>> Fibo(5)
5
>>> dict(Fibo)
{1: 1, 0: 0, 2: 1, 3: 2, 4: 3, 5: 5}