为Sage编写Python代码

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

Python语言标准

Sage库代码需要与Sage支持的所有版本的Python兼容。有关受支持版本的信息可在这些文件中找到 build/pkgs/python3/spkg-configure.m4src/setup.cfg.m4

Python3.9是最旧的受支持版本。因此,可以使用在Python3.9中可用的所有语言和库特性;但是不能使用在Python3.10中引入的特性。如果某个功能在较新的受支持版本中被弃用,则必须确保由Python发出的弃用警告不会导致文档测试失败。

一些关键的语言和库功能已使用以下两种机制之一重新移植到较早的Python版本:

  • from __future__ import annotations (有关信息,请参阅《Python参考 __future__ )根据以下内容使类型批注现代化 PEP 563 (推迟了对批注的评估)。所有使用类型批注的Sage库代码都应该包括以下内容 __future__ 导入并遵循PEP 563。

  • Backport包

    Sage库将这些包声明为依赖项,并确保提供了Python3.11特性的版本可用。

梅塔 :issue:`29756` 跟踪较新的Python特性,并作为讨论如何在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 In Sage应该支持命令 latex(x) ,使得任何Sage对象都可以通过LaTeX轻松准确地显示出来。下面是如何使一个类(以及它的实例)支持该命令 latex

  1. 定义一种方法 _latex_(self) 返回对象的LaTeX表示。它应该是可以在数学模式下正确排版的东西。不包括开盘和收盘$‘S。

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

  3. 不要忘了包括一个文档字符串和一个示例,它说明了对象的LaTeX生成。

  4. 您可以使用中包含的任何宏 amsmathamssymb ,或 amsfonts ,或中定义的 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/generic_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/src/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
    
  • 不需要准备原始文字,从效率的角度来看,这可能很有用。在Sage中,原始整型和浮点型后跟一个表示RAW的“r”(或“R”),表示未准备好。例如::

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

查阅该文件 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预编译器代码。

Sage胁迫模型

强制的主要目标是能够在不同集合的元素之间透明地进行算术、比较等。例如,当一个人写下 3 + 1/2 ,人们希望将操作数作为有理数执行算术运算,尽管左边的项是整数。考虑到整数明显而自然地包含在有理数中,这是有意义的。强制系统的目标是促进这一点(以及更复杂的算法),而不必显式地将所有内容映射到同一个领域,同时足够严格,不能解决歧义或接受胡说八道。

Sage参考手册的强制部分详细描述了Sage的强制模型,并举例说明。

易变性

父结构(例如环、域、矩阵空间等)只要有可能,就应该是不变的和全局唯一的。不变性意味着,生成器标签和默认强制精度等属性不能更改。

在不浪费内存的同时实现全局唯一性的最佳方法是使用标准的PythonWeinref模块、工厂函数和模块作用域变量。

某些对象,例如矩阵,可能一开始是可变的,后来变得不可变。请参阅该文件 SAGE_ROOT/src/sage/structure/mutability.py

这个 __hash__ 特殊方法

以下是对 __hash__ 摘自《Python参考手册》:

由内置函数调用 hash() 以及对散列集合成员的操作,包括 setfrozenset ,以及 dict__hash__() 应返回一个整数。唯一需要的属性是比较相等的对象具有相同的散列值;建议将对象的组件的散列值混合在一起,这些组件也在对象的比较中发挥作用,方法是将它们打包到元组中并对该元组进行散列。

如果类没有定义 __eq__() 方法,它不应定义 __hash__() 操作;如果它定义了 __eq__() 但不是 __hash__() ,则其实例将不能用作可哈希集合中的项。如果类定义了可变对象并实现了 __eq__() 方法,则它不应实现 __hash__() 因为Hasable集合的实现要求键的散列值是不可变的(如果对象的散列值改变,它将位于错误的散列桶中)。

看见 https://docs.python.org/3/reference/datamodel.html#object.__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相等作为附加到每个对象的新方法来实现。然后,我们可以遵循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 列出作为元组捕获的所有异常,后跟一条错误消息。

方法或函数接受 INPUT 区块 the docstring 。如果代码无法处理输入,则可能会引发异常。以下内容旨在指导您从与Sage最相关的例外中进行选择。加薪

  • TypeError :如果输入属于该方法不支持的对象类。例如,一种方法只适用于有限域上的一次多项式,但给出了有理数上的多项式。

  • ValueError :如果输入的值不受该方法支持。例如,给出了上述方法的一个非一元多项式。

  • ArithmeticError :如果该方法执行算术运算(和、积、商等),但输入不合适。

  • ZeroDivisionError :如果该方法执行除法,但输入为零。请注意,对于不可逆输入值, ArithmeticError 是更合适的。作为派生自 ArithmeticErrorZeroDivisionError 可以被抓获为 ArithmeticError

  • NotImplementedError :如果输入的是尚未由该方法实现的功能。请注意,此异常源自 RuntimeError

如果没有特定错误似乎适用于您的情况, RuntimeError 可以使用。在所有情况下,与异常关联的字符串都应该描述出错原因的详细信息。

整型返回值

Sage中的许多函数和方法返回整数值。这些通常应该作为类的Sage整数返回 Integer 而不是作为类的 int ,因为用户可能想要探索所得到的整数的数论性质,例如素因式分解。如果有很好的理由,如性能或与Python代码的兼容性,则应进行例外处理,例如,在 __hash____len__ ,以及 __int__

返回一个Python整数的步骤 i 作为Sage整数,使用:

from sage.rings.integer import Integer
return Integer(i)

返回Sage整数的步骤 i 作为一名Python ineger,请使用:

return int(i)

正在导入

我们提到了关于导入的两个问题:循环导入和导入大型第三方模块。另请参阅 依赖项和分发包 从模块化的角度讨论导入。

首先,你必须避免循环进口。例如,假设文件 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中的一个重要机制是延迟导入,它并不实际执行导入,而是将其延迟到对象实际使用时才执行。看见 sage.misc.lazy_import 有关懒惰导入的更多详细信息,以及 文件和目录结构 例如,对新模块使用惰性导入。

如果您的模块需要使一些预计算的数据在顶级可用,则可以减少其加载时间(从而减少启动时间,除非使用 sage.misc.lazy_import )通过使用装饰器 sage.misc.cachefunc.cached_function() 取而代之的是。例如,替换

big_data = initialize_big_data()  # bad: runs at module load time

通过

from sage.misc.cachefunc import cached_function

@cached_function                  # good: runs on first use
def big_data():
    return initialize_big_data()

静态打字

越来越多的Python库使用静态类型信息进行注释;请参阅 Python reference on typing

为了检查Sage库的类型,该项目使用 pyright ;它自动在GitHub Actions配置项中运行,也可以在本地运行。

从Sage 10.2开始,Sage库只包含此类类型注释的最小集合。添加更多注释的拉请求通常是受欢迎的。

Sage库非常广泛地使用了Cython(参见第章 在Cython中编码 )。尽管Cython源代码经常声明静态类型,以便将其编译为高效的机器码,但不幸的是,这种类型信息对于诸如Pyright这样的静态检查器是不可见的。有必要创造出 type stub files (".pyi") 它们提供了这些信息。尽管有各种各样的 tools for writing and maintaining type stub files ,为Cython文件创建存根文件需要手动操作。有希望很快就能得到更好的工具,例如 cython/cython #5744 。与手动编写类型存根文件相比,参与此类工具的开发和测试可能会产生更大的影响。

对于Sage库的Cython模块,这些类型存根文件将放置在 .pyx.pxd 档案。

从不提供足够类型信息的其他Python库导入时,可以增强库的类型信息以检查Sage库的类型:

  • 创建类型存根文件并将其放置在目录中 SAGE_ROOT/src/typings 。例如,分布 pplpy 提供顶级包 ppl ,它不发布任何打字信息。我们可以创建一个TypeStub文件 SAGE_ROOT/src/typings/ppl.pyiSAGE_ROOT/src/typings/ppl/__init__.pyi

  • 当这些类型存根文件运行良好时,从Sage项目的角度来看,它们最好是“上游的”,即对维护库的项目做出贡献。如果上游库的新版本提供了必要的输入信息,我们可以更新Sage发行版中的包,并再次从 SAGE_ROOT/src/typings

  • 作为退路,当上游项目既不欢迎向源文件添加类型批注也不欢迎添加类型存根文件时,可以 contribute typestubs files instead to the typeshed community project

不推荐使用

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

备注

过时的代码只能在它出现的第一个稳定版本一年后才能删除。

每个弃用警告都包含定义它的GitHub公关的编号。我们在下面的示例中使用666。对于每个条目,请查阅该函数的文档以了解有关其行为和可选参数的更多信息。

  • Rename a keyword: 通过用来修饰函数/方法 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)
    
  • Moving an object to a different module: 如果您重命名一个源文件或将一些函数(或类)移到一个不同的文件,应该仍然可以从旧模块导入该函数。这可以使用 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)
    
  • Remove a name from a global namespace: 这是当您想要从全局命名空间中删除名称时(例如, sage.all 或其他一些人 all.py 文件),但您希望通过显式导入来保持该功能可用。本例与前一例类似:使用带有弃用的惰性导入。一个细节:在本例中,您不想要这个名称 lazy_import 在全局命名空间中可见,因此我们添加一个前导下划线:

    from sage.misc.lazy_import import lazy_import as _lazy_import
    _lazy_import('sage.some.package', 'some_function', deprecation=666)
    
  • Any other case: 如果上述情况均不适用,请致电 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 class: 使用装饰物 experimental 对于ITS __init__ 。下面是一个例子:

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

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

使用可选套餐

如果某个函数需要可选的包,则该函数应该正常失败-也许可以使用 try -异常``块-当可选包不可用时,应提示如何安装。例如,输入  ``sage -optional 给出了所有可选程序包的列表,因此它可能会建议用户键入该包。该命令 optional_packages() Sage也从内部返回此列表。