Programmation#
Charger et attacher des fichiers Sage#
Nous décrivons maintenant la manière de charger dans Sage des programmes
écrits dans des fichiers séparés. Créons un fichier appelé
example.sage
avec le contenu suivant :
print("Hello World")
print(2^3)
Nous pouvons lire et exécuter le contenu du fichier example.sage
en utilisant la commande load
.
sage: load("example.sage")
Hello World
8
Nous pouvons aussi attacher un fichier Sage à la session en cours avec
la commande attach
:
sage: attach("example.sage")
Hello World
8
L’effet de cette commande est que si nous modifions maintenant
example.sage
et entrons une ligne vierge (i.e. appuyons sur
entrée
), le contenu de example.sage
sera automatiquement
rechargé dans Sage.
Avec attach
, le fichier est rechargé automatiquement
dans Sage à chaque modification, ce qui est pratique pour déboguer du
code ; tandis qu’avec load
il n’est chargé qu’une fois.
Lorsque Sage lit example.sage
, il le convertit en un programme
Python qui est ensuite exécuté par l’interpréteur Python. Cette
conversion est minimale, elle se résume essentiellement à encapsuler les
littéraux entiers dans des Integer()
et les littéraux flottants dans
des RealNumber()
, à remplacer les ^
par des **
, et à
remplacer par exemple R.2
par R.gen(2)
. La version convertie en
Python de example.sage
est placée dans le même répertoire sous le
nom example.sage.py
. Elle contient le code suivant :
print("Hello World")
print(Integer(2)**Integer(3))
On voit que les littéraux entiers ont été encapsulés et que le ^
a
été remplacé par **
(en effet, en Python, ^
représente le ou
exclusif et **
l’exponentiation).
(Ce prétraitement est implémenté dans le fichier
sage/misc/interpreter.py
.)
On peut coller dans Sage des blocs de code de plusieurs lignes avec des
indentations pourvu que les blocs soient délimités par des
retours à la ligne (cela n’est pas nécessaire quand le code est dans
un fichier). Cependant, la meilleure façon d’entrer ce genre de code
dans Sage est de l’enregistrer dans un fichier et d’utiliser la commande
attach
comme décrit ci-dessus.
Écrire des programmes compilés#
Dans les calculs mathématiques sur ordinateur, la vitesse a une
importance cruciale. Or, si Python est un langage commode et de très haut
niveau, certaines opérations peuvent être plus rapides de plusieurs
ordres de grandeur si elles sont implémentées sur des types statiques
dans un langage compilé. Certaines parties de Sage auraient été trop
lentes si elles avaient été écrites entièrement en Python. Pour pallier
ce problème, Sage supporte une sorte de « version compilée » de Python
appelée Cython (voir [Cyt] et [Pyr]). Cython ressemble à la fois
à Python et à C. La plupart des constructions Python, dont la définition
de listes par compréhension, les expressions conditionnelles, les
constructions comme +=
sont autorisées en Cython. Vous pouvez aussi
importer du code depuis d’autres modules Python. En plus de cela,
vous pouvez déclarer des variables C arbitraires, et faire directement
des appels arbitraires à des bibliothèques C. Le code Cython est
converti en C et compilé avec le compilateur C.
Pour créer votre propre code Sage compilé, donnez à votre fichier source
l’exension .spyx
(à la place de .sage
). Avec l’interface en
ligne de commande, vous pouvez charger ou attacher des fichiers de code
compilé exactement comme les fichiers interprétés. Pour l’instant,
le notebook ne permet pas d’attacher des fichiers compilés. La
compilation proprement dite a lieu « en coulisse », sans que vous ayez à
la déclencher explicitement. La bibliothèque d’objets partagés compilés se trouve
dans $HOME/.sage/temp/hostname/pid/spyx
. Ces fichiers sont supprimés
lorsque vous quittez Sage.
Attention, le prétraitement des fichiers Sage mentionné plus haut N’EST
PAS appliqué aux fichiers spyx, ainsi, dans un fichier spyx, 1/3
vaut 0 et non le nombre rationnel \(1/3\). Pour appeler une fonction
foo
de la bibliothèque Sage depuis un fichier spyx, importez
sage.all
et appelez sage.all.foo
.
import sage.all
def foo(n):
return sage.all.factorial(n)
Appeler des fonctions définies dans des fichiers C séparés#
Il n’est pas difficile non plus d’accéder à des fonctions écrites en C,
dans des fichiers *.c séparés. Créez dans un même répertoire deux
fichiers test.c
et test.spyx
avec les contenus suivants :
Le code C pur : test.c
int add_one(int n) {
return n + 1;
}
Le code Cython : test.spyx
:
cdef extern from "test.c":
int add_one(int n)
def test(n):
return add_one(n)
Vous pouvez alors faire :
sage: attach("test.spyx")
Compiling (...)/test.spyx...
sage: test(10)
11
Si la compilation du code C généré à partir d’un fichier Cython
nécessite une bibliothèque supplémentaire foo
, ajoutez au source
Cython la ligne clib foo
. De même, il est possible d’ajouter un
fichier C supplémentaire bar
aux fichiers à compiler avec la
déclaration cfile bar
.
Scripts Python/Sage autonomes#
Le script autonome suivant, écrit en Sage, permet de factoriser des entiers, des polynômes, etc. :
#!/usr/bin/env sage
import sys
from sage.all import *
if len(sys.argv) != 2:
print("Usage: %s <n>" % sys.argv[0])
print("Outputs the prime factorization of n.")
sys.exit(1)
print(factor(sage_eval(sys.argv[1])))
Pour utiliser ce script, votre répertoire SAGE_ROOT
doit apparaître
dans la variable d’environnement PATH. Supposons que le script ci-dessus
soit appelé factor
, il peut alors être utilisé comme dans l’exemple
suivant :
bash $ ./factor 2006
2 * 17 * 59
Types de données#
Chaque objet Sage a un type bien défini. Python dispose d’une vaste gamme de types intégrés et la bibliothèque Sage en fournit de nombreux autres. Parmi les types intégrés de Python, citons les chaînes, les listes, les n-uplets, les entiers et les flottants :
sage: s = "sage"; type(s)
<... 'str'>
sage: s = 'sage'; type(s) # guillemets simples ou doubles
<... 'str'>
sage: s = [1,2,3,4]; type(s)
<... 'list'>
sage: s = (1,2,3,4); type(s)
<... 'tuple'>
sage: s = int(2006); type(s)
<... 'int'>
sage: s = float(2006); type(s)
<... 'float'>
Sage ajoute de nombreux autres types. Par exemple, les espaces vectoriels :
sage: V = VectorSpace(QQ, 1000000); V
Vector space of dimension 1000000 over Rational Field
sage: type(V)
<class 'sage.modules.free_module.FreeModule_ambient_field_with_category'>
Seules certaines fonctions peuvent être appelées sur V
. Dans
d’autres logiciels mathématiques, cela se fait en notation
« fonctionnelle », en écrivant foo(V,...)
. En Sage, certaines
fonctions sont attachés au type (ou classe) de l’objet et appelées avec
une syntaxe « orientée objet » comme en Java ou en C++, par exemple
V.foo(...)
. Cela évite de polluer l’espace de noms global avec des
dizaines de milliers de fonctions, et cela permet d’avoir plusieurs
fonctions appelées foo
, avec des comportements différents, sans devoir
se reposer sur le type des arguments (ni sur des instructions case) pour
décider laquelle appeler. De plus, une fonction dont vous réutilisez le
nom demeure disponible : par exemple, si vous appelez quelque chose
zeta
et si ensuite vous voulez calculer la valeur de la fonction
zêta de Riemann au point 0.5, vous pouvez encore écrire s=.5;
s.zeta()
.
sage: zeta = -1
sage: s=.5; s.zeta()
-1.46035450880959
La notation fonctionnelle usuelle est aussi acceptée dans certains cas courants, par commodité et parce que certaines expressions mathématiques ne sont pas claires en notation orientée objet. Voici quelques exemples.
sage: n = 2; n.sqrt()
sqrt(2)
sage: sqrt(2)
sqrt(2)
sage: V = VectorSpace(QQ,2)
sage: V.basis()
[
(1, 0),
(0, 1)
]
sage: basis(V)
[
(1, 0),
(0, 1)
]
sage: M = MatrixSpace(GF(7), 2); M
Full MatrixSpace of 2 by 2 dense matrices over Finite Field of size 7
sage: A = M([1,2,3,4]); A
[1 2]
[3 4]
sage: A.charpoly('x')
x^2 + 2*x + 5
sage: charpoly(A, 'x')
x^2 + 2*x + 5
Pour obtenir la liste de toutes les fonctions membres de \(A\),
utilisez la complétion de ligne de commande : tapez A.
, puis appuyez
sur la touche [tab]
de votre clavier, comme expliqué dans la section
Recherche en arrière et complétion de ligne de commande.
Listes, n-uplets et séquences#
Une liste stocke des éléments qui peuvent être de type arbitraire. Comme en C, en C++ etc. (mais au contraire de ce qu’il se passe dans la plupart des systèmes de calcul formel usuels) les éléments de la liste sont indexés à partir de \(0\) :
sage: v = [2, 3, 5, 'x', SymmetricGroup(3)]; v
[2, 3, 5, 'x', Symmetric group of order 3! as a permutation group]
sage: type(v)
<... 'list'>
sage: v[0]
2
sage: v[2]
5
Lors d’un accès à une liste, l’index n’a pas besoin d’être un entier
Python. Un entier (Integer) Sage (ou un Rational, ou n’importe quoi
d’autre qui a une méthode __index__
) fait aussi l’affaire.
sage: v = [1,2,3]
sage: v[2]
3
sage: n = 2 # Integer (entier Sage)
sage: v[n] # ça marche !
3
sage: v[int(n)] # Ok aussi
3
La fonction range
crée une liste d’entiers Python (et non d’entiers
Sage) :
sage: list(range(1, 15))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
Cela est utile pour construire des listes par compréhension :
sage: L = [factor(n) for n in range(1, 15)]
sage: L
[1, 2, 3, 2^2, 5, 2 * 3, 7, 2^3, 3^2, 2 * 5, 11, 2^2 * 3, 13, 2 * 7]
sage: L[12]
13
sage: type(L[12])
<class 'sage.structure.factorization_integer.IntegerFactorization'>
sage: [factor(n) for n in range(1, 15) if is_odd(n)]
[1, 3, 5, 7, 3^2, 11, 13]
Pour plus d’information sur les compréhensions, voir [PyT].
Une fonctionnalité merveilleuse est l’extraction de tranches d’une
liste. Si L
est une liste, L[m:n]
renvoie la sous-liste de L
formée des éléments d’indices \(m\) à \(n-1\) inclus :
sage: L = [factor(n) for n in range(1, 20)]
sage: L[4:9]
[5, 2 * 3, 7, 2^3, 3^2]
sage: L[:4]
[1, 2, 3, 2^2]
sage: L[14:4]
[]
sage: L[14:]
[3 * 5, 2^4, 17, 2 * 3^2, 19]
Les n-uplets ressemblent aux listes, à ceci près qu’ils sont non mutables, ce qui signifie qu’ils ne peuvent plus être modifiés une fois créés.
sage: v = (1,2,3,4); v
(1, 2, 3, 4)
sage: type(v)
<... 'tuple'>
sage: v[1] = 5
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
Les séquences sont un troisième type Sage analogue aux listes.
Contrairement aux listes et aux n-uplets, il ne s’agit pas d’un type
interne de Python. Par défaut, les séquences sont mutables, mais on
peut interdire leur modification en utilisant la méthode
set_immutable
de la classe Sequence
, comme dans l’exemple
suivant. Tous les éléments d’une séquence ont un parent commun, appelé
l’univers de la séquence.
sage: v = Sequence([1,2,3,4/5])
sage: v
[1, 2, 3, 4/5]
sage: type(v)
<class 'sage.structure.sequence.Sequence_generic'>
sage: type(v[1])
<class 'sage.rings.rational.Rational'>
sage: v.universe()
Rational Field
sage: v.is_immutable()
False
sage: v.set_immutable()
sage: v[0] = 3
Traceback (most recent call last):
...
ValueError: object is immutable; please change a copy instead.
Les séquences sont des objets dérivés des listes, et peuvent être utilisées partout où les listes peuvent l’être :
sage: v = Sequence([1,2,3,4/5])
sage: isinstance(v, list)
True
sage: list(v)
[1, 2, 3, 4/5]
sage: type(list(v))
<... 'list'>
Autre exemple : les bases d’espaces vectoriels sont des séquences non mutables, car il ne faut pas les modifier.
sage: V = QQ^3; B = V.basis(); B
[
(1, 0, 0),
(0, 1, 0),
(0, 0, 1)
]
sage: type(B)
<class 'sage.structure.sequence.Sequence_generic'>
sage: B[0] = B[1]
Traceback (most recent call last):
...
ValueError: object is immutable; please change a copy instead.
sage: B.universe()
Vector space of dimension 3 over Rational Field
Dictionnaires#
Un dictionnaire (parfois appelé un tableau associatif) est une correspondance entre des objets « hachables » (par exemple des chaînes, des nombres, ou des n-uplets de tels objets, voir http://docs.python.org/tut/node7.html et http://docs.python.org/lib/typesmapping.html dans la documentation de Python pour plus de détails) vers des objets arbitraires.
sage: d = {1:5, 'sage':17, ZZ:GF(7)}
sage: type(d)
<... 'dict'>
sage: list(d.keys())
[1, 'sage', Integer Ring]
sage: d['sage']
17
sage: d[ZZ]
Finite Field of size 7
sage: d[1]
5
La troisième clé utilisée ci-dessus, l’anneau des entiers relatifs, montre que les indices d’un dictionnaire peuvent être des objets compliqués.
Un dictionnaire peut être transformé en une liste de couples clé-objet contenant les mêmes données :
sage: list(d.items())
[(1, 5), ('sage', 17), (Integer Ring, Finite Field of size 7)]
Le parcours itératifs des paires d’un dictionnaire est un idiome de programmation fréquent :
sage: d = {2:4, 3:9, 4:16}
sage: [a*b for a, b in d.items()]
[8, 27, 64]
Comme le montre la dernière sortie ci-dessus, un dictionnaire stocke ses éléments sans ordre particulier.
Ensembles#
Python dispose d’un type ensemble intégré. Sa principale caractéristique est qu’il est possible de tester très rapidement si un élément appartient ou non à un ensemble. Le type ensemble fournit les opérations ensemblistes usuelles.
sage: X = set([1,19,'a']); Y = set([1,1,1, 2/3])
sage: X # random sort order
{1, 19, 'a'}
sage: X == set(['a', 1, 1, 19])
True
sage: Y
{2/3, 1}
sage: 'a' in X
True
sage: 'a' in Y
False
sage: X.intersection(Y)
{1}
Sage a son propre type ensemble, qui est (dans certains cas) implémenté
au-dessus du type Python, mais offre quelques fonctionnalités
supplémentaires utiles à Sage. Pour créer un ensemble Sage, on utilise
Set(...)
. Par exemple,
sage: X = Set([1,19,'a']); Y = Set([1,1,1, 2/3])
sage: X # random sort order
{'a', 1, 19}
sage: X == Set(['a', 1, 1, 19])
True
sage: Y
{1, 2/3}
sage: X.intersection(Y)
{1}
sage: print(latex(Y))
\left\{1, \frac{2}{3}\right\}
sage: Set(ZZ)
Set of elements of Integer Ring
Itérateurs#
Les itérateurs sont un ajout récent à Python, particulièrement utile dans les applications mathématiques. Voici quelques exemples, consultez [PyT] pour plus de détails. Fabriquons un itérateur sur les carrés d’entiers positifs jusqu’à \(10000000\).
sage: v = (n^2 for n in range(10000000))
sage: next(v)
0
sage: next(v)
1
sage: next(v)
4
Nous créons maintenant un itérateur sur les nombres premiers de la forme \(4p+1\) où \(p\) est lui aussi premier, et nous examinons les quelques premières valeurs qu’il prend.
sage: w = (4*p + 1 for p in Primes() if is_prime(4*p+1))
sage: w
<generator object <genexpr> at 0x...>
sage: next(w)
13
sage: next(w)
29
sage: next(w)
53
Certains anneaux, par exemple les corps finis et les entiers, disposent d’itérateurs associés :
sage: [x for x in GF(7)]
[0, 1, 2, 3, 4, 5, 6]
sage: W = ((x,y) for x in ZZ for y in ZZ)
sage: next(W)
(0, 0)
sage: next(W)
(0, 1)
sage: next(W)
(0, -1)
Boucles, fonctions, structures de contrôle et comparaisons#
Nous avons déjà vu quelques exemples courants d’utilisation des boucles
for
. En Python, les boucles for
ont la structure suivante, avec
une indentation :
>>> for i in range(5):
... print(i)
...
0
1
2
3
4
Notez bien les deux points à la fin de l’instruction for (il n’y a pas de
« do » ou « od » comme en Maple ou en GAP) ainsi que l’indentation du
corps de la boucle, formé de l’unique instruction print(i)
. Cette
indentation est significative, c’est elle qui délimite le corps de la
boucle. Depuis la ligne de commande Sage, les lignes suivantes sont
automatiquement indentées quand vous appuyez sur entrée
après un
signe « : », comme illustré ci-dessous.
sage: for i in range(5):
....: print(i) # appuyez deux fois sur entrée ici
0
1
2
3
4
Le signe =
représente l’affectation.
L’opérateur ==
est le test d’égalité.
sage: for i in range(15):
....: if gcd(i,15) == 1:
....: print(i)
1
2
4
7
8
11
13
14
Retenez bien que l’indentation détermine la structure en blocs des
instructions if
, for
et while
:
sage: def legendre(a,p):
....: is_sqr_modp=-1
....: for i in range(p):
....: if a % p == i^2 % p:
....: is_sqr_modp=1
....: return is_sqr_modp
sage: legendre(2,7)
1
sage: legendre(3,7)
-1
Naturellement, l’exemple précédent n’est pas une implémentation efficace du symbole de Legendre ! Il est simplement destiné à illustrer différents aspects de la programmation Python/Sage. La fonction {kronecker} fournie avec Sage calcule le symbole de Legendre efficacement, en appelant la bibliothèque C de PARI.
Remarquons aussi que les opérateurs de comparaison numériques comme ==
,
!=
, <=
, >=
, >
, <
convertissent automatiquement leurs
deux membres en des nombres du même type lorsque c’est possible :
sage: 2 < 3.1; 3.1 <= 1
True
False
sage: 2/3 < 3/2; 3/2 < 3/1
True
True
Pour évaluer des inégalités symboliques, utilisez bool
:
sage: x < x + 1
x < x + 1
sage: bool(x < x + 1)
True
Lorsque l’on cherche à comparer des objets de types différents, Sage essaie le
plus souvent de trouver une coercition canonique des deux objets dans un même
parent (voir la section Parents, conversions, coercitions pour plus de détails). Si cela
réussit, la comparaison est faite entre les objets convertis ; sinon, les
objets sont simplement considérés comme différents. Pour tester si deux
variables font référence au même objet, on utilise l’opérateur is
. Ainsi,
l’entier Python (int
) 1
est unique, mais pas l’entier Sage 1
:
sage: 1 is 2/2
False
sage: 1 is 1
False
sage: 1 == 2/2
True
Dans les deux lignes suivantes, la première égalité est fausse parce qu’il n’y a pas de morphisme canonique \(\QQ\to \GF{5}\), et donc pas de manière canonique de comparer l’élément \(1\) de \(\GF{5}\) à \(1 \in \QQ\). En revanche, il y a une projection canonique \(\ZZ \to \GF{5}\), de sorte que la deuxième comparaison renvoie « vrai ». Remarquez aussi que l’ordre des membres de l’égalité n’a pas d’importance.
sage: GF(5)(1) == QQ(1); QQ(1) == GF(5)(1)
False
False
sage: GF(5)(1) == ZZ(1); ZZ(1) == GF(5)(1)
True
True
sage: ZZ(1) == QQ(1)
True
ATTENTION : La comparaison est plus restrictive en Sage qu’en Magma, qui considère \(1 \in \GF{5}\) comme égal à \(1 \in \QQ\).
sage: magma('GF(5)!1 eq Rationals()!1') # optional - magma
true
Profilage (profiling)#
Auteur de la section : Martin Albrecht (malb@informatik.uni-bremen.de)
« Premature optimization is the root of all evil. » - Donald Knuth (« L’optimisation prématurée est la source de tous les maux. »)
Il est parfois utile de rechercher dans un programme les goulets d’étranglements qui représentent la plus grande partie du temps de calcul : cela peut donner une idée des parties à optimiser. Cette opération s’appelle profiler le code. Python, et donc Sage, offrent un certain nombre de possibilités pour ce faire.
La plus simple consiste à utiliser la commande prun
du shell
interactif. Elle renvoie un rapport qui résume les temps d’exécution des
fonctions les plus coûteuses. Pour profiler, par exemple, le produit de
matrices à coefficients dans un corps fini (qui, dans Sage 1.0, est
lent), on entre :
sage: k,a = GF(2**8, 'a').objgen()
sage: A = Matrix(k,10,10,[k.random_element() for _ in range(10*10)])
sage: %prun B = A*A
32893 function calls in 1.100 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
12127 0.160 0.000 0.160 0.000 :0(isinstance)
2000 0.150 0.000 0.280 0.000 matrix.py:2235(__getitem__)
1000 0.120 0.000 0.370 0.000 finite_field_element.py:392(__mul__)
1903 0.120 0.000 0.200 0.000 finite_field_element.py:47(__init__)
1900 0.090 0.000 0.220 0.000 finite_field_element.py:376(__compat)
900 0.080 0.000 0.260 0.000 finite_field_element.py:380(__add__)
1 0.070 0.070 1.100 1.100 matrix.py:864(__mul__)
2105 0.070 0.000 0.070 0.000 matrix.py:282(ncols)
...
Ici, ncalls
désigne le nombre d’appels, tottime
le temps total
passé dans une fonction (sans compter celui pris par les autres
fonctions appelées par la fonction en question), percall
est le
rapport tottime
divisé par ncalls
. cumtime
donne le temps
total passé dans la fonction en comptant les appels qu’elle effectue, la
deuxième colonne percall
est le quotient de cumtime
par le
nombre d’appels primitifs, et filename:lineno(function)
donne pour
chaque fonction le nom de fichier et le numéro de la ligne où elle est
définie. En règle générale, plus haut la fonction apparaît dans ce tableau,
plus elle est coûteuse — et donc intéressante à optimiser.
Comme d’habitude, prun?
donne plus d’informations sur l’utilisation
du profileur et la signification de sa sortie.
Il est possible d’écrire les données de profilage dans un objet pour les étudier de plus près :
sage: %prun -r A*A
sage: stats = _
sage: stats?
Remarque : entrer stats = prun -r A\*A
à la place des deux premières
lignes ci-dessus provoque une erreur de syntaxe, car prun n’est pas une
fonction normale mais une commande du shell IPython.
Pour obtenir une jolie représentation graphique des données de
profilage, vous pouvez utiliser le profileur hotshot, un petit script
appelé hotshot2cachetree
et (sous Unix uniquement) le programme
kcachegrind
. Voici le même exemple que ci-dessus avec le profileur
hotshot :
sage: k,a = GF(2**8, 'a').objgen()
sage: A = Matrix(k,10,10,[k.random_element() for _ in range(10*10)])
sage: import hotshot
sage: filename = "pythongrind.prof"
sage: prof = hotshot.Profile(filename, lineevents=1)
sage: prof.run("A*A")
<hotshot.Profile instance at 0x414c11ec>
sage: prof.close()
À ce stade le résultat est dans un fichier pythongrind.prof
dans le
répertoire de travail courant. Convertissons-le au format cachegrind
pour le visualiser.
Dans le shell du système d’exploitation, tapez
hotshot2calltree -o cachegrind.out.42 pythongrind.prof
Le fichier cachegrind.out.42
peut maintenant être examiné avec
kcachegrind
. Notez qu’il est important de respecter la convention de
nommage cachegrind.out.XX
.