用Python为Sage编写代码

本章讨论了Sage中编码的一些问题和建议。

设计

如果您计划为Sage开发一些新代码,那么设计是很重要的。所以,考虑一下你的程序将做什么,以及它如何与Sage的结构相匹配。特别是,Sage的大部分是用面向对象语言Python实现的,并且有一个组织代码和功能的类的层次结构。例如,如果您实现了一个环的元素,那么您的类应该从 sage.structure.element.RingElement ,而不是从头开始。试着找出你的代码应该如何与其他Sage代码相匹配,并相应地进行设计。

特殊Sage函数

带前导和尾随双下划线的函数 __XXX__ 都是由Python预定义的。带前导和尾随单下划线的函数 _XXX_ 是为Sage定义的。带有一个前导下划线的函数是半私有的,而带有双前导下划线的函数被认为是真正私有的。用户可以创建带有前导和尾随下划线的函数。

正如Python对对象有许多标准的特殊方法一样,Sage也有特殊的方法。它们是典型的形式 _XXX_ . 在少数情况下,尾随下划线不包括在内,但这最终将被更改,以便始终包含尾随下划线。本节介绍这些特殊方法。

Sage中的所有对象都应该派生自Cython扩展类 SageObject

from sage.structure.sage_object import SageObject

class MyClass(SageObject,...):
    ...

或者其他已经存在的Sage阶层:

from sage.rings.ring import Algebra

class MyFavoriteAlgebra(Algebra):
    ...

你应该实现 _latex__repr_ 方法。其他方法取决于对象的性质。

Latex 表现

每一个物体 x 在萨奇应该支持命令 latex(x) ,因此任何Sage的对象都可以通过 Latex 轻松准确地显示出来。下面是如何使类(及其实例)支持该命令 latex .

  1. 定义方法 _latex_(self) 返回对象的LaTeX表示。它应该是可以在数学模式下正确排版的东西。不包括期初和期末美元。

  2. 通常对象是用其他Sage对象构建的,这些组件应该使用 latex 功能。例如,如果 c 是你的对象的系数,你想排版 c 使用 Latex ,使用 latex(c) 而不是 c._latex_() ,因为 c 可能没有 _latex_ 方法,以及 latex(c) 知道如何处理这件事。

  3. 别忘了包括一个docstring和一个说明对象的LaTeX生成的示例。

  4. 可以使用中包含的任何宏 amsmathamssymbamsfonts ,或 SAGE_ROOT/doc/commontex/macros.tex .

一个示例模板 _latex_ 方法如下。注意 .. skip 代码中不应该包含行;它是为了防止doctest在这个伪示例上运行。

class X:
   ...
   def _latex_(self):
       r"""
       Return the LaTeX representation of X.

       EXAMPLES::

           sage: a = X(1,2)
           sage: latex(a)
           '\\frac{1}{2}'
       """
       return '\\frac{%s}{%s}'%(latex(self.numer), latex(self.denom))

如示例所示, latex(a) 将生成表示对象的LaTeX代码 a . 打电话 view(a) 将显示此的排版版本。

来自对象的矩阵或向量

提供一个 _matrix_ 方法,该对象可以强制为环上的矩阵 R . 那么Sage函数 matrix 将为这个对象工作。

以下是来自 SAGE_ROOT/src/sage/graphs/graph.py

class GenericGraph(SageObject):
    ...
    def _matrix_(self, R=None):
        if R is None:
            return self.am()
        else:
            return self.am().change_ring(R)


    def adjacency_matrix(self, sparse=None, boundary_first=False):
        ...

同样,提供 _vector_ 方法,该对象可以强制为环上的向量 R . 那么Sage函数 vector 将为这个对象工作。以下是文件中的内容 SAGE_ROOT/sage/sage/modules/free_module_element.pyx

cdef class FreeModuleElement(element_Vector):   # abstract base class
    ...
    def _vector_(self, R):
        return self.change_ring(R)

Sage准备

为了使Python更具交互性,当您从命令行或通过笔记本使用Sage时,对语法进行了许多调整(但Sage库中的Python代码则没有)。从技术上讲,这是由 preparse() 重写输入字符串的函数。最值得注意的是,进行了以下替换:

  • Sage支持一种特殊的语法来生成环,或者更普遍地说,使用命名生成器生成父元素:

    sage: R.<x,y> = QQ[]
    sage: preparse('R.<x,y> = QQ[]')
    "R = QQ['x, y']; (x, y,) = R._first_ngens(2)"
    
  • 整数和实数是Sage整数和Sage浮点数。例如,在纯Python中,这将是一个属性错误:

    sage: 16.sqrt()
    4
    sage: 87.factor()
    3 * 29
    
  • 未准备好原始文字,从效率的角度来看,这可能是有用的。就像Python的int由L表示一样,在Sage的raw integer和浮点字面值后面跟一个表示raw的“r”(或“r”),意思是没有准备好。例如::

    sage: a = 393939r
    sage: a
    393939
    sage: type(a)
    <... 'int'>
    sage: b = 393939
    sage: type(b)
    <type 'sage.rings.integer.Integer'>
    sage: a == b
    True
    
  • 原始文字在某些情况下非常有用。例如,当Python整数非常小时,它们可以比Sage整数更有效。大型Sage整数比Python整数要高效得多,因为它们是使用gmpc库实现的。

查阅档案 preparser.py 关于Sage准备的更多细节,更多涉及原始文字的例子,等等。

当一个文件 foo.sage 在Sage会话中加载或附加,它是 foo.sage 是用名称创建的 foo.sage.py . 准备好的文件的开头说明:

This file was *autogenerated* from the file foo.sage.

可以使用 --preparse 命令行选项:运行::

sage --preparse foo.sage

创建文件 foo.sage.py .

以下文件与在Sage中编制有关:

  1. SAGE_ROOT/src/bin/sage

  2. SAGE_ROOT/src/bin/sage-preparse

  3. SAGE_ROOT/src/sage/repl/preparse.py

尤其是文件 preparse.py 包含Sage preparser代码。

Sage强制模式

主要的目的是使算术的主要目的是透明地进行集合之间的比较。例如,当一个人写 3 + 1/2 ,尽管左边的项是一个整数,但人们希望对作为有理数的操作数执行算术运算。这是有意义的,因为在有理数中包含了明显和自然的整数。强制系统的目标是促进这一点(以及更复杂的算法),而不必显式地将所有内容映射到同一个域中,同时还要严格到不解决歧义或接受无稽之谈。

Sage的强制模型在Sage参考手册的强制部分详细描述,并附有示例。

易变性

父结构(例如环、字段、矩阵空间等)应该是不可变的,并且尽可能是全局唯一的。不变性意味着,除其他外,诸如生成器标签和默认强制精度之类的属性不能更改。

在不浪费内存的情况下,全局唯一性最好使用标准pythonweakref模块、工厂函数和module scope变量来实现。

某些对象,例如矩阵,开始时可能是可变的,后来变为不可变的。查看文件 SAGE_ROOT/src/sage/structure/mutability.py .

这个 __hash__ 特殊方法

以下是对 __hash__ 从Python参考手册:

由内置函数调用 hash() 以及对散列集合成员(包括set、frozenset和dict)的操作。 __hash__() 应返回一个整数。唯一需要的属性是比较相等的对象具有相同的哈希值;建议以某种方式混合(例如使用exclusive or)对象组件的哈希值,这些组件在对象比较中也起着一定的作用。如果类没有定义 __cmp__() 方法不应定义 __hash__() 操作;如果它定义 __cmp__()__eq__() 但不是 __hash__() ,它的实例将不能用作字典键。如果类定义可变对象并实现 __cmp__()__eq__() 方法,不应实现 __hash__() ,因为字典实现要求键的哈希值是不可变的(如果对象的哈希值更改,它将位于错误的哈希桶中)。

注意这句话,“唯一需要的属性是比较相等的对象具有相同的哈希值。”这是Python语言所做的一个假设,在Sage中我们根本无法做出(!)违反它会有后果。幸运的是,后果是非常明确的,并且相当容易理解,所以如果你知道了它们,它们不会给你带来麻烦。下面的例子很好地说明了它们:

sage: v = [Mod(2,7)]
sage: 9 in v
True
sage: v = set([Mod(2,7)])
sage: 9 in v
False
sage: 2 in v
True
sage: w = {Mod(2,7):'a'}
sage: w[2]
'a'
sage: w[9]
Traceback (most recent call last):
...
KeyError: 9

下面是另一个例子:

sage: R = RealField(10000)
sage: a = R(1) + R(10)^-100
sage: a == RDF(1)  # because the a gets coerced down to RDF
True

但是 hash(a) 不应相等 hash(1) .

不幸的是,在Sage身上我们根本无法要求

(#)   "a == b ==> hash(a) == hash(b)"

因为严肃的数学对于这个规则来说太复杂了。例如,等式 z == Mod(z, 2)z == Mod(z, 3) 会迫使 hash() 在整数上保持不变。

我们能够“修复”这个问题的唯一方法就是放弃使用 == 运算符,并将Sage equality作为附加到每个对象的新方法来实现。然后我们可以遵循Python规则 == 而我们对其他一切的规则,以及所有的Sage代码都将变得完全不可读(在这个问题上是不可改写的)。所以我们只能接受它。

所以Sage所做的就是试图满足 (#) 当这样做是相当容易的,但要运用判断力,不要过火。例如,

sage: hash(Mod(2,7))
2

输出2比一些随机散列(也涉及模)要好,但是从Python的角度来看,它当然不是正确的,因为 9 == Mod(2,7) . 目标是生成一个快速的散列函数,但在合理的范围内考虑任何明显的自然包含和强制。

例外情况

请避免像这样捕获所有代码:

try:
    some_code()
except:               # bad
    more_code()

如果您没有显式列出任何异常(作为元组),那么您的代码将捕获任何内容,包括 ctrl-C ,代码中的错误和警报,这将导致混乱。此外,这可能会捕捉到应该传播给用户的实际错误。

总而言之,只捕获特定的异常,如下例所示:

try:
    return self.__coordinate_ring
except (AttributeError, OtherExceptions) as msg:           # good
    more_code_to_compute_something()

注意中的语法 except 是列出作为元组捕获的所有异常,后跟错误消息。

进口

我们提到了导入的两个问题:循环导入和导入大型第三方模块。

首先,你必须避免循环进口。例如,假设文件 SAGE_ROOT/src/sage/algebras/steenrod_algebra.py 从一行开始:

from sage.sage.algebras.steenrod_algebra_bases import *

那文件呢 SAGE_ROOT/src/sage/algebras/steenrod_algebra_bases.py 从一行开始:

from sage.sage.algebras.steenrod_algebra import SteenrodAlgebra

这就建立了一个循环:加载其中一个文件需要另一个文件,而另一个文件又需要第一个文件,依此类推。

使用此设置,运行Sage将产生错误:

Exception exceptions.ImportError: 'cannot import name SteenrodAlgebra'
in 'sage.rings.polynomial.polynomial_element.
Polynomial_generic_dense.__normalize' ignored
-------------------------------------------------------------------
ImportError                       Traceback (most recent call last)

...
ImportError: cannot import name SteenrodAlgebra

相反,您可以替换 import * 在文件顶部的行中按代码中需要的更具体的导入。例如 basis 类的方法 SteenrodAlgebra 可能如下所示(省略文档字符串):

def basis(self, n):
    from steenrod_algebra_bases import steenrod_algebra_basis
    return steenrod_algebra_basis(n, basis=self._basis_name, p=self.prime)

第二,不要在模块的顶层导入第三方模块(例如matplotlib)。如上所述,您可以在需要时导入模块的特定组件,而不是在文件的顶层。

重要的是 from sage.all import * 尽可能快,因为这是Sage启动时间的主导因素,控制顶级进口有助于做到这一点。Sage中的一个重要机制是lazy导入,它不实际执行导入,而是将其延迟到对象实际使用之前。看到了吗 sage.misc.lazy_import 有关惰性导入的更多详细信息,以及 文件和目录结构 例如,为新模块使用lazy导入。

贬低

当制作 backward-incompatible 在Sage中的修改,旧代码应该继续工作,并显示一条消息,指示将来应该如何更新/编写它。我们称之为 贬抑 .

注解

弃用的代码只能在其出现的第一个稳定版本一年后删除。

每个弃用警告都包含定义它的trac票证号。我们在下面的例子中使用666。对于每个条目,请参考函数的文档,以获取有关其行为和可选参数的更多信息。

  • 重命名关键字: 通过用 rename_keyword ,任何用户呼叫 my_function(my_old_keyword=5) 将看到警告:

    from sage.misc.decorators import rename_keyword
    @rename_keyword(deprecation=666, my_old_keyword='my_new_keyword')
    def my_function(my_new_keyword=True):
        return my_new_keyword
    
  • Rename a function/method: 调用 deprecated_function_alias() 要获取引发不推荐警告的函数的副本,请执行以下操作:

    from sage.misc.superseded import deprecated_function_alias
    def my_new_function():
        ...
    
    my_old_function = deprecated_function_alias(666, my_new_function)
    
  • 将对象移动到其他模块: 如果重命名源文件或将某个函数(或类)移到其他文件,则仍可以从旧模块导入该函数。可以使用 lazy_import() 带着嘲讽。在旧模块中,您将写下:

    from sage.misc.lazy_import import lazy_import
    lazy_import('sage.new.module.name', 'name_of_the_function', deprecation=666)
    

    您还可以使用 * 或者一些使用元组的函数:

    from sage.misc.lazy_import import lazy_import
    lazy_import('sage.new.module.name', '*', deprecation=666)
    lazy_import('sage.other.module', ('func1', 'func2'), deprecation=666)
    
  • 从全局命名空间中删除名称: 在这种情况下,您需要从全局命名空间中删除名称(例如, sage.all 或者其他什么 all.py 文件),但您希望通过显式导入保持功能可用。本例与前一个类似:使用带有deprecation的lazy import。一个细节:在这种情况下,你不需要名字 lazy_import 若要在全局命名空间中可见,请添加前导下划线:

    from sage.misc.lazy_import import lazy_import as _lazy_import
    _lazy_import('sage.some.package', 'some_function', deprecation=666)
    
  • 任何其他情况: 如果以上情况均不适用,请致电 deprecation() 在要弃用的函数中。它将显示您选择的消息(并与doctest框架正确交互):

    from sage.misc.superseded import deprecation
    deprecation(666, "Do not use your computer to compute 1+1. Use your brain.")
    

实验/不稳定代码

您可以将新创建的代码(类/函数/方法)标记为实验性的/不稳定的。在这种情况下,在更改此代码、其功能或其接口时,不需要任何弃用警告。

这可以让你尽早把你的东西放在Sage中,而不用担心以后会做出(设计)改变。

当对代码满意(稳定一段时间,比如一年)时,可以删除此警告。

和往常一样,所有代码都必须经过充分的文档化处理,并通过我们的审阅过程。

  • Experimental function/method: 使用装饰器 experimental . 下面是一个例子:

    from sage.misc.superseded import experimental
    @experimental(66666)
    def experimental_function():
        # do something
    
  • 实验班: 使用装饰器 experimental 为其 __init__ . 下面是一个例子:

    from sage.misc.superseded import experimental
    class experimental_class(SageObject):
        @experimental(66666)
        def __init__(self, some, arguments):
            # do something
    
  • 任何其他情况: 如果以上情况均不适用,请致电 experimental_warning() 在你想要警告的代码中。它将显示您选择的消息:

    from sage.misc.superseded import experimental_warning
    experimental_warning(66666, 'This code is not foolproof.')
    

使用可选软件包

如果一个函数需要一个可选的包,那么该函数应该很正常地失败——也许使用 try -除了“block”——当可选软件包不可用时,应该给出关于如何安装它的提示。例如,键入 ``sage -optional 提供所有可选包的列表,因此它可能建议用户键入该列表。命令 optional_packages() 从Sage内部也返回此列表。