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自动命名, 例如Model1Space1,然后在该空间中创建单元格。

要指定创建单元格的空间,您可以将空间对象作为参数传递给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]即可计算出参数010对应的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定义中的名称,如BalanceRatePaymentPrincipal将引用定义该函数的模块中的全局变量。 然而,如上所述,Balance的公式是在与其父空间Fixed关联的命名空间中计算的。 Fixed空间包含子单元格,如PaymentBalance。它还包含子引用,如PrincipalRate。因此,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)
{}

当你请求Fibon=5时的值时,Fibon=0n=4的值也会被计算出来:

>>> Fibo(5)
5

>>> dict(Fibo)
{1: 1, 0: 0, 2: 1, 3: 2, 4: 3, 5: 5}