Programação#

Carregando e Anexando Arquivos do Sage#

A seguir ilustramos como carregar no Sage programas escritos em um arquivo separado. Crie um arquivo chamado example.sage com o seguinte conteúdo:

print("Hello World")
print(2^3)

Você pode ler e executar o arquivo example.sage usando o comando load.

sage: load "example.sage"
Hello World
8

Você também pode anexar um arquivo em Sage à sessão em execução usando o comando attach.

sage: attach "example.sage"
Hello World
8

Agora se você alterar example.sage e adicionar uma linha em branco (por exemplo), então o conteúdo de example.sage será automaticamente recarregado no Sage.

Em particular, attach automaticamente recarrega um arquivo toda vez que ele for modificado, o que é útil para desenvolver e testar um programa, enquanto load carrega o arquivo apenas uma vez.

Quando o Sage carrega example.sage ele converte esse arquivo para o Python, o qual é então executado pelo interpretador do Python. Essa conversão é mínima; ela essencialmente consiste em encapsular inteiros em Integer(), números float em RealNumber(), substituir ^ por **, e substituir, por exemplo, R.2 por R.gen(2). A versão convertida de example.sage é armazenada no mesmo diretório de example.sage e é chamada example.sage.py. Esse arquivo contém o seguinte código:

print("Hello World")
print(Integer(2)**Integer(3))

Inteiros literais são encapsulados e ^ é substituído por **. (Em Python, ^ significa “ou exclusivo” e ** significa “exponenciação”.)

Esse “” está implementado em sage/misc/interpreter.py.)

Você pode colar código tabulado com muitas linhas no Sage desde que existam linhas em branco separando blocos de código (isso não é necessário em arquivos). Todavia, a melhor forma de adicionar tal código a uma sessão do Sage é salvá-lo em um arquivo e usar attach, como descrito anteriormente.

Criando Código Compilado#

Velocidade é crucial em cálculos matemáticos. Embora o Python seja uma linguagem conveniente de alto nível, certos cálculos podem ser várias vezes mais rápidos do que em Python se eles forem implementados usando tipos estáticos em uma linguagem compilada. Alguns aspectos do Sage seriam muito lentos se eles fossem escritos inteiramente em Python. Para lidar com isso, o Sage suporta uma “versão” compilada do Python chamada Cython ([Cyt] and [Pyr]). O Cython é simultaneamente similar ao Python e ao C. A maior parte das construções em Python, incluindo “list comprehensions”, expressões condicionais, código como += são permitidos; você também pode importar código que você escreveu em outros módulos em Python. Além disso, você pode declarar variáveis em C arbitrárias, e qualquer chamada de bibliotecas em C pode ser feita diretamente. O código resultante é convertido para C e compilado usando um compilador para C.

Para criar o seu próprio código compilado em Sage, nomeie o arquivo com uma extensão .spyx (em vez de .sage). Se você está trabalhando na linha de comando, você pode anexar e carregar código compilado exatamente como se faz com código interpretado (no momento, anexar e carregar código em Cython não é suportado no Notebook). A compilação é realizada implicitamente sem que você tenha que fazer qualquer coisa explicitamente. Veja $SAGE_ROOT/examples/programming/sagex/factorial.spyx para um exemplo de uma implementação compilada para a função fatorial que usa diretamente a biblioteca GMP em C. Experimente o seguinte, usando cd, vá para o diretório $SAGE_ROOT/examples/programming/sagex/, e então faça o seguinte:

sage: load "factorial.spyx"
***************************************************
                Recompiling factorial.spyx
***************************************************
sage: factorial(50)
30414093201713378043612608166064768844377641568960512000000000000
sage: time n = factorial(10000)
CPU times: user 0.03 s, sys: 0.00 s, total: 0.03 s
Wall time: 0.03

Note que o Sage vai recompilar factorial.spyx se você encerrar e reiniciar o Sage. A biblioteca compilada e compartilhada é armazenada em $HOME/.sage/temp/hostname/pid/spyx. Esses arquivos são excluídos quando você encerra o Sage.

Nenhum pré-processamento é aplicado em arquivos spyx, por exemplo, 1/3 vai resultar em 0 em um arquivo spyx em vez do número racional \(1/3\). Se foo é uma função da biblioteca Sage, para usá-la em um arquivo spyx importe sage.all e use sage.all.foo.

import sage.all
def foo(n):
    return sage.all.factorial(n)

Acessando Funções em C em Arquivos Separados#

É fácil também acessar funções em C definidas em arquivos *.c separados. Aqui vai um exemplo. Crie os arquivos test.c e test.spyx no mesmo diretório contendo:

Código C puro: test.c

int add_one(int n) {
  return n + 1;
}

Código Cython: test.spyx:

cdef extern from "test.c":
    int add_one(int n)

def test(n):
    return add_one(n)

Então o seguinte funciona:

sage: attach "test.spyx"
Compiling (...)/test.spyx...
sage: test(10)
11

Se uma biblioteca foo adicional é necessária para compilar código em C gerado a partir de um arquivo em Cython, adicione a linha clib foo no arquivo fonte em Cython. De forma similar, um arquivo em C adicional bar pode ser incluído na compilação declarando cfile bar.

Scripts Independentes em Python/Sage#

O seguinte script em Sage fatora inteiros, polinômios, 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])))

Para usar esse script, sua SAGE_ROOT precisa estar na sua variável PATH. Se o script acima for chamado factor, aqui está um exemplo de como usá-lo:

bash $ ./factor 2006
2 * 17 * 59

Tipo de Dados#

Cada objeto em Sage possui um tipo bem definido. O Python possui diversos tipos de dados, e a biblioteca do Sage adiciona ainda mais. Os tipos de dados de Python incluem strings, listas, tuplas, inteiros e floats, como ilustrado:

sage: s = "sage"; type(s)
<... 'str'>
sage: s = 'sage'; type(s)      # you can use either single or double quotes
<... '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'>

Além disso, o Sage acrescenta vários outros tipos. Por exemplo, espaços vetoriais:

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'>

Apenas certas funções podem ser aplicadas sobre V. Em outros softwares de matemática, essas seriam chamadas usando a notação “funcional” foo(V,...). Em Sage, algumas funções estão anexadas ao tipo (ou classe) de V, e são chamadas usando uma sintaxe orientada a objetos como em Java ou C++, por exemplo, V.foo(). Isso ajuda a manter o espaço de variáveis global sem milhares de funções, e permite que várias funções diferentes com comportamento diferente possam ser chamadas foo, sem a necessidade de usar um mecanismo de identificação de tipos (ou casos) para decidir qual chamar. Além disso, se você reutilizar o nome de uma função, essa função continua ainda disponível (por exemplo, se você chamar algo zeta, e então quiser calcular o valor da função zeta de Riemann em 0.5, você continua podendo digitar s=.5; s.zeta()).

sage: zeta = -1
sage: s=.5; s.zeta()
-1.46035450880959

Em alguns casos muito comuns, a notação funcional usual é também suportada por conveniência e porque expressões matemáticas podem parecer confusas usando a notação orientada a objetos. Aqui vão alguns exemplos.

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

Para listar todas as funções para \(A\), use completamento tab. Simplesmente digite A., então tecle [tab] no seu teclado, como descrito em Busca Reversa e Completamento Tab.

Listas, Tuplas e Sequências#

O tipo de dados lista armazena elementos de um tipo arbitrário. Como em C, C++, etc. (mas diferentemente da maioria dos sistemas de álgebra computacional), os elementos da lista são indexados a partir do \(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

(Quando se indexa uma lista, é permitido que o índice não seja um int do Python!) Um Inteiro do Sage (ou Racional, ou qualquer objeto que possua um método __index__) também ira funcionar.

sage: v = [1,2,3]
sage: v[2]
3
sage: n = 2      # Sage Integer
sage: v[n]       # Perfectly OK!
3
sage: v[int(n)]  # Also OK.
3

A função range cria uma lista de int’s do Python (não Inteiros do Sage):

sage: list(range(1, 15))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

Isso é útil quando se usa “list comprehensions” para construir listas:

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]

Para mais sobre como criar listas usando “list comprehensions”, veja [PyT].

Fatiamento de lista (list slicing) é um recurso fantástico. Se L é uma lista, então L[m:n] retorna uma sub-lista de L obtida começando do \(m\)-ésimo elemento e terminando no \((n-1)\)-ésimo elemento, como ilustrado abaixo.

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]

Tuplas são semelhantes à listas, exceto que elas são imutáveis: uma vez criadas elas não podem ser alteradas.

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

Sequências são um terceiro tipo de dados do Sage semelhante a listas. Diferentemente de listas e tuplas, Sequence não é um tipo de dados nativo do Python. Por definição, uma sequência é mutável, mas usando o método set_immutable da classe Sequence elas podem ser feitas imutáveis, como mostra o exemplo a seguir. Todos os elementos da sequência possuem um parente comum, chamado o universo da sequência.

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.

Sequências são derivadas de listas e podem ser usadas em qualquer lugar que listas são usadas.

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'>

Como um outro exemplo, bases para espaços vetoriais são sequências imutáveis, pois é importante que elas não sejam modificadas.

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

Dicionários#

Um dicionário (também chamado as vezes de lista associativa ou “hash table”) é um mapeamento de objetos em objetos arbitrários. (Exemplos de objetos que admitem uma lista associativa são strings e números; veja a documentação Python em https://docs.python.org/3/tutorial/index.html para detalhes).

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

A terceira chave (key) ilustra como os índices de um dicionário podem ser complicados, por exemplo, um anel de inteiros.

Você pode transformar o dicionário acima em uma lista com os mesmos dados:

sage: list(d.items())
[(1, 5), ('sage', 17), (Integer Ring, Finite Field of size 7)]

É comum iterar sobre os pares em um dicionário:

sage: d = {2:4, 3:9, 4:16}
sage: [a*b for a, b in d.items()]
[8, 27, 64]

Um dicionário não possui ordem, como o exemplo acima mostra.

Conjuntos#

O Python possui um tipo de conjuntos (set) nativo. O principal recurso que ele oferece é a rápida verificação se um objeto está ou não em um conjunto, juntamente com as operações comuns em conjuntos.

sage: X = set([1,19,'a']);   Y = set([1,1,1, 2/3])
sage: X   # random
{1, 19, 'a'}
sage: Y   # random
{2/3, 1}
sage: 'a' in X
True
sage: 'a' in Y
False
sage: X.intersection(Y)
{1}

O Sage também possui o seu próprio tipo de dados para conjuntos que é (em alguns casos) implementado usando o tipo nativo do Python, mas possuir algumas funcionalidades adicionais. Crie um conjunto em Sage usando Set(...). Por exemplo,

sage: X = Set([1,19,'a']);   Y = Set([1,1,1, 2/3])
sage: X   # random
{'a', 1, 19}
sage: Y   # random
{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

Iteradores#

Iteradores foram adicionados recentemente ao Python e são particularmente úteis em aplicações matemáticas. Aqui estão vários exemplos; veja [PyT] para mais detalhes. Vamos criar um iterador sobre o quadrados dos números inteiros até \(10000000\).

sage: v = (n^2 for n in range(10000000))
sage: next(v)
0
sage: next(v)
1
sage: next(v)
4

Criamos agora um iterador sobre os primos da forma \(4p+1\) com \(p\) também primo, e observamos os primeiros valores.

sage: w = (4*p + 1 for p in Primes() if is_prime(4*p+1))
sage: w         # in the next line, 0xb0853d6c is a random 0x number
<generator object at 0xb0853d6c>
sage: next(w)
13
sage: next(w)
29
sage: next(w)
53

Certos anéis, por exemplo, corpos finitos e os inteiros possuem iteradores associados a eles:

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)

Laços, Funções, Enunciados de Controle e Comparações#

Nós já vimos alguns exemplos de alguns usos comuns de laços (loops) for. Em Python, um laço for possui uma estrutura tabulada, tal como

>>> for i in range(5):
...      print(i)
...
0
1
2
3
4

Note que os dois pontos no final do enunciado (não existe “do” ou “od” como no GAP ou Maple), e a identação antes dos comandos dentro do laço, isto é, print(i). A tabulação é importante. No Sage, a tabulação é automaticamente adicionada quando você digita enter após “:”, como ilustrado abaixo.

sage: for i in range(5):
....:     print(i)  # now hit enter twice
0
1
2
3
4

O símbolo = é usado para atribuição. O símbolo == é usado para verificar igualdade:

sage: for i in range(15):
....:     if gcd(i,15) == 1:
....:         print(i)
1
2
4
7
8
11
13
14

Tenha em mente como a tabulação determina a estrutura de blocos para enunciados if, for, e 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

Obviamente essa não é uma implementação eficiente do símbolo de Legendre! O objetivo é ilustrar vários aspectos da programação em Python/Sage. A função {kronecker}, que já vem com o Sage, calcula o símbolo de Legendre eficientemente usando uma biblioteca em C do PARI.

Finalmente, observamos que comparações, tais como ==, !=, <=, >=, >, <, entre números irão automaticamente converter ambos os números para o mesmo tipo, se possível:

sage: 2 < 3.1; 3.1 <= 1
True
False
sage: 2/3 < 3/2;   3/2 < 3/1
True
True

Use bool para desigualdades simbólicas:

sage: x < x + 1
x < x + 1
sage: bool(x < x + 1)
True

Quando se compara objetos de tipos diferentes no Sage, na maior parte dos casos o Sage tenta encontrar uma coação canônica para ambos os objetos em um parente comum (veja Famílias, Conversão e Coação para mais detalhes). Se isso for bem sucedido, a comparação é realizada entre os objetos que foram coagidos; se não for bem sucedido, os objetos são considerados diferentes. Para testar se duas variáveis fazem referência ao mesmo objeto use is. Como se vê no próximo exemplo, o int 1 do Python é único, mas o Inteiro 1 do Sage não é.

sage: 1 is 2/2
False
sage: 1 is 1
False
sage: 1 == 2/2
True

Nas duas linhas seguintes, a primeira igualdade é falsa (False) porque não existe um morfismo canônico \(QQ\to \GF{5}\), logo não há uma forma de comparar o \(1\) em \(\GF{5}\) com o \(1 \in \QQ\). Em contraste, existe um mapa canônico entre \(\ZZ \to \GF{5}\), logo a segunda comparação é verdadeira (True)

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

ATENÇÃO: Comparação no Sage é mais restritiva do que no Magma, o qual declara \(1 \in \GF{5}\) igual a \(1 \in \QQ\).

sage: magma('GF(5)!1 eq Rationals()!1')            # optional magma required
true

Otimização (Profiling)#

Autor desta seção: Martin Albrecht (https://martinralbrecht.wordpress.com/)

“Premature optimization is the root of all evil.” - Donald Knuth

As vezes é útil procurar por gargalos em programas para entender quais partes gastam maior tempo computacional; isso pode dar uma boa ideia sobre quais partes otimizar. Python e portanto Sage fornecem várias opções de “profiling” (esse é o nome que se dá ao processo de otimização).

O mais simples de usar é o comando prun na linha de comando interativa. Ele retorna um sumário sobre o tempo computacional utilizado por cada função. Para analisar (a atualmente lenta! – na versão 1.0) multiplicação de matrizes sobre corpos finitos, por exemplo, faça o seguinte:

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)
  ...

Aqui ncalls é o números de chamadas, tottime é o tempo total gasto por uma determinada função (excluíndo o tempo gasto em chamadas de subfunções), percall é o quociente de tottime dividido por ncalls. cumtime é o tempo total gasto nessa e em todas as subfunções (isto é, desde o início até o término da execução da função), percall é o quociente de cumtime dividido pelas chamadas primitivas, e filename:lineno(function) fornece os dados respectivos para cada função. A regra prática aqui é: Quanto mais no topo uma função aparece nessa lista, mais custo computacional ela acarreta. Logo é mais interessante para ser optimizada.

Como usual, prun? fornece detalhes sobre como usar o “profiler” e como entender a saída de dados.

A saída de dados pode ser escrita em um objeto para permitir uma análise mais detalhada:

sage: %prun -r A*A
sage: stats = _
sage: stats?

Note: digitando stats = prun -r A\*A obtém-se um erro de sintaxe porque prun é um comando do IPython, não uma função comum.

Para uma representação gráfica dos dados do “profiling”, você pode usar o “hotspot profiler”, um pequeno script chamado hotshot2cachetree e o programa kcachegrind (apenas no Unix). O mesmo exemplo agora com o “hotspot profiler”:

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()

Isso resulta em um arquivo pythongrind.prof no diretório de trabalho atual. Ele pode ser convertido para o formato cachegrind para visualização.

Em uma linha de comando do sistema, digite

hotshot2calltree -o cachegrind.out.42 pythongrind.prof

O arquivo de saída cachegrind.out.42 pode ser examinado com kcachegrind. Note que a convenção de nomes cachegrind.out.XX precisa ser obedecida.