Tópicos
Até agora, você tem visto dois tipos compostos: strings, que são compostos de caracteres; e listas, que são compostas de elementos de qualquer tipo. Uma das diferenças que notamos é que os elementos de uma lista podem ser modificados, mas os caracteres em uma string não. Em outras palavras, strings são imutáveis e listas são mutáveis.
Há um outro tipo em Python chamado tupla (tuple) que é similar a uma lista exceto por ele ser imutável. Sintaticamente, uma tupla é uma lista de valores separados por vírgulas:
>>> tupla = 'a', 'b', 'c', 'd', 'e'
Embora não seja necessário, é convencional colocar tuplas entre parênteses:
>>> tupla = ('a', 'b', 'c', 'd', 'e')
Para criar uma tupla com um único elemento, temos que incluir uma vírgula final:
>>> t1 = ('a',)
>>> type(t1)
<type 'tuple'>
Sem a vírgula, Python entende ('a') como uma string entre parênteses:
>>> t2 = ('a')
>>> type(t2)
<type 'string'>
Questões de sintaxe de lado, as operações em tuplas são as mesmas operações das listas. O operador índice seleciona um elemento da tupla.
>>> tupla = ('a', 'b', 'c', 'd', 'e')
>>> tupla[0]
'a'
E o operador slice (fatia) seleciona uma “faixa” (range) de elementos.
>>> tupla[1:3]
('b', 'c')
Mas se tentarmos modificar um dos elementos de uma tupla, teremos um erro:
>>> tupla[0] = 'A'
TypeError: object doesn't support item assignment
Naturalmente, mesmo que não possamos modificar os elementos de uma tupla, podemos substituí-la por uma tupla diferente:
>>> tupla = ('A',) + tupla[1:]
>>> tupla
('A', 'b', 'c', 'd', 'e')
De vez em quando, é necessário trocar entre si os valores de duas variáveis. Com operações de atribuição convencionais, temos que utilizar uma variável temporária. Por exemplo, para fazer a troca entre a e b:
>>> temp = a
>>> a = b
>>> b = temp
Se você tiver que fazer isso com frequência, esta abordagem se torna incômoda. Python fornece uma forma de atribuição de tupla que resolve esse problema elegantemente:
>>> a, b = b, a
O lado esquedo é uma tupla de variáveis; o lado direito é uma tupla de valores. Cada valor é atribuído à sua respectiva variável. Todas as expressões do lado direito são avaliadas antes de qualquer das atribuições. Esta característica torna as atribuições de tupla bastante versáteis.
Naturalmente, o número de variáveis na esquerda e o número de valores na direita deve ser igual:
>>> a, b, c, d = 1, 2, 3
ValueError: unpack tuple of wrong size
Funções podem retornar tuplas como valor de retorno. Por Exemplo, nós poderíamos escrever uma função que troca dois parâmetros entre si:
def troca(x, y):
return y, x
Então nós poderíamos atribuir o valor de retorno para uma tupla com duas variáveis:
a, b = troca(a, b)
Neste caso, não existe uma grande vantagem em fazer de troca (swap) uma função. De fato, existe um perigo em tentar encapsular troca, o qual é a tentação de cometer o seguinte erro:
def troca(x, y): # versao incorreta
x, y = y, x
Se nós chamarmos esta função desta forma:
troca(a, b)
então a e x são apelidos para um mesmo valor. Mudar x dentro da função troca, faz com que x se referencie a um valor diferente, mas sem efeito sobre a dentro de __main__. Do mesmo modo, a mudança em y não tem efeito sobre b.
Esta função roda sem produzir uma mensagem de erro, mas ela não faz o que pretendemos. Este é um exemplo de um erro semântico.
Como exercício, desenhe um diagrama de estado pra esta função de modo que você possa ver porque ela não funciona.
A maioria dos programas de computador fazem a mesma coisa sempre que são executados, então, podemos dizer que eles são determinísticos. Determinismo em geral é uma coisa boa, se nós esperamos que um cálculo dê sempre o mesmo resultado. Entretanto, para algumas aplicações queremos que o computador se torne imprevisível. Jogos são um exemplo óbvio, mas existem outros.
Fazer um programa realmente não-determinístico se mostra não ser tão fácil, mas existem maneiras de fazê-lo ao menos parecer não-determinístico. Uma dessas maneiras é gerar números aleatórios e usá-los para determinar o resultado de um programa. Python tem uma função nativa que gera números pseudo aleatórios, os quais não são verdadeiramente aleatórios no sentido matemático, mas para os nossos propósitos eles são.
O módulo random contém uma função chamada random que retorna um número em ponto flutuante (floating-point number) entre 0.0 e 1.0. Cada vez que você chama random, você recebe o próximo número de uma longa série. Para ver uma amostra, execute este loop:
import random
for i in range(10):
x = random.random()
print x
Para gerar um número aleatório ente 0.0 e um limite superior, digamos superior, multiplique x por superior.
Como exercício, gere um número aleatório entre ‘inferior’ e ‘superior’.
Como exercício adicional, gere um número inteiro aleatório entre ‘inferior’ e ‘superior’, inclusive os dois extremos.
O primeiro passo é gerar uma lista aleatória de valores. listaAleatoria pega um parâmetro inteiro e retorna uma lista de números aleatórios com o comprimento dado. Inicia-se com uma lista de n zeros. A cada iteração do loop, ele substitui um dos elementos por um número aleatório. O valor retornado é uma referência para a lista completa:
def listaAleatoria(n):
s = [0] * n
for i in range(n):
s[i] = random.random()
return s
Vamos realizar um teste desta função com uma lista de oito elementos. Para efeitos de depuração, é uma boa idéia começar com uma lista pequena.
>>> listaAleatoria(8)
0.15156642489
0.498048560109
0.810894847068
0.360371157682
0.275119183077
0.328578797631
0.759199803101
0.800367163582
Os números gerados por random são supostamente uniformemente distribuídos, o que significa que cada valor tem uma probabilidade igual de acontecer.
Se nós dividirmos a faixa de valores possíveis em intervalos do mesmo tamanho, e contarmos o número de vezes que um determinado valor aleatório caiu em seu respectivo intervalo, nós devemos obter o mesmo número aproximado de valores em cada um dos intervalos.
Nós podemos testar esta teoria escrevendo um programa que divida a faixa de valores em intervalos e conte o número de valores de cada intervalo.
Uma boa maneira de abordar problemas como esse é dividir o problema em subproblemas, e encontrar um subproblema que se enquadre em um padrão de solução computacional que você já tenha visto antes.
Neste caso, queremos percorrer uma lista de números e contar o número de vezes que valor se encaixa em um determinado intervalo. Isso soa familiar. Na Seção 7.8, nós escrevemos um programa que percorria uma string e contava o número de vezes que uma determinada letra aparecia.
Assim, podemos prosseguir copiando o programa original e adaptando-o para o problema atual. O programa original era:
contador = 0
for letra in fruta:
if letra == 'a':
contador = contador + 1
print contador
O primeiro passo é substituir fruta por lista e letra por numero. Isso não muda o programa, apenas o ajusta para que ele se torne mais fácil de ler e entender.
O segundo passo é mudar o teste. Nós não estamos interessados em procurar letras. Nós queremos ver se numero está entre inferior e superior.:
contador = 0
for numero in lista
if inferior < numero < superior:
contador = contador + 1
print contador
O último passo é encapsular este código em uma função chamada noIntervalo. Os parâmetros são a lista e os valores inferior e superior:
def noIntervalo(lista, inferior, superior):
contador = 0
for numero in lista:
if inferior < numero < superior:
contador = contador + 1
return contador
Através da cópia e da modificação de um programa existente, estamos aptos a escrever esta função rapidamente e economizar um bocado de tempo de depuração. Este plano de desenvolvimento é chamado de casamento de padrões. Se você se encontrar trabalhando em um problema que você já solucionou antes, reuse a solução.
Conforme o número de intervalos aumenta, noIntervalo torna-se intragável. Com dois intervalos, não é tão ruim:
inferior = noIntervalo(a, 0.0, 0.5)
superior = noIntervalo(a, 0.5, 1)
Mas com quatro intervalos, começa a ficar desconfortável.:
intervalo1 = noIntervalo(a, 0.0, 0.25)
intervalo2 = noIntervalo(a, 0.25, 0.5)
intervalo3 = noIntervalo(a, 0.5, 0.75)
intervalo4 = noIntervalo(a, 0.75, 1.0)
Existem aqui dois problemas. Um é que temos que criar novos nomes de variável para cada resultado. O outro é que temos que calcular os limites de cada intervalo.
Vamos resolver o segundo problema primeiro. Se o número de intervalos é numeroDeIntervalos, então a largura de cada intervalo é 1.0 / numeroDeIntervalos.
Vamos usar um laço (loop) para calcular a faixa, ou largura, de cada intervalo. A variável do loop, i, conta de 0 até numeroDeIntervalos-1:
larguraDoIntervalo = 1.0 / numeroDeIntervalos
for i in range(numeroDeIntervalos):
inferior = i * larguraDoIntervalo
superior = inferior + larguraDoIntervalo
print "do" inferior, "ao", superior
Para calcular o limite inferior (inferior) de cada intervalo, nós multiplicamos a variável do loop (i) pela largura do intervalo (larguraDoIntervalo). O limite superior (superior) está exatamente uma “largura de intervalo” acima.
Com numeroDeIntervalos = 8, o resultado é:
0.0 to 0.125
0.125 to 0.25
0.25 to 0.375
0.375 to 0.5
0.5 to 0.625
0.625 to 0.75
0.75 to 0.875
0.875 to 1.0
Você pode confirmar que cada intervalo tem a mesma largura, que eles não se sobrepõe, e que eles cobrem toda a faixa de valores de 0.0 a 1.0.
Agora, de volta ao primeiro problema. Nós precisamos de uma maneira de guardar oito inteiros, usando a váriavel do loop para indicar cada um destes inteiros. Você deve estar pensando, “Lista!”
Nós temos que criar a lista de intervalos fora do loop, porque queremos fazer isto apenas uma vez. Dentro do loop, nós vamos chamar noIntervalo repetidamente e atualizar o i-ésimo elemento da lista:
numeroDeIntervalos = 8
intervalos = [0] * numeroDeIntervalos
larguraDoIntervalo = 1.0 / numeroDeIntervalos
for i in range(numeroDeIntervalos):
inferior = i * larguraDoIntervalo
superior = inferior + larguraDoIntervalo
intervalos[i] = noIntervalo(lista, inferior, superior)
print intervalos
Com uma lista de 1000 valores, este código vai produzir esta lista de quantidades de valores em cada intervalo:
[138, 124, 128, 118, 130, 117, 114, 131]
Esses números estão razoavelmente póximos de 125, o que era o que esperávamos. Pelo menos eles estão próximos o bastante para nos fazer acreditar que o gerador de número aleatórios está funcionando.
Como exercício, teste esta função com algumas listas longas, e veja se o número de valores em cada um dos intervalos tendem a uma distribuição nivelada.
Embora este programa funcione, ele não é tão eficiente quanto poderia ser. Toda vez que ele chama noIntervalo, ele percorre a lista inteira. Conforme o número de intervalos aumenta, a lista será percorrida um bocado de vezes.
Seria melhor fazer uma única passagem pela lista e calcular para cada valor o índice do intervalo ao qual o valor pertença. Então podemos incrementar o contador apropriado.
Na seção anterior, pegamos um índice, i, e o multiplicamos pela larguraDoIntervalo para encontrar o limite inferior daquele intervalo. Agora queremos pegar um valor entre 0.0 e 1.0 e encontrar o índice do intervalo ao qual ele se encaixa.
Já que este problema é o inverso do problema anterior, podemos imaginar que deveríamos dividir por larguraDoIntervalo em vez de multiplicar. Esta suposição está correta.
Já que larguraDoIntervalo = 1.0 / numeroDeIntervalos, dividir por larguraDoIntervalo é o mesmo que multiplicar por numeroDeIntervalos. Se multiplicarmos um número na faixa entre 0.0 e 1.0 por numeroDeIntervalos, obtemos um número na faixa entre 0.0 e numeroDeIntervalos. Se arredondarmos este número para baixo, ou seja, para o menor inteiro mais próximo, obtemos exatamente o que estamos procurando - o índice do intervalo:
numeroDeIntervalos = 8
intervalos = [0] * numeroDeIntervalos
for i in lista:
indice = int(i * numeroDeIntervalos)
intervalos[indice] = intervalos[indice] + 1
Usamos a função int para converter um número em ponto flutuante (float) para um inteiro.
Existe a possibilidade deste cálculo produzir um índice que esteja fora dos limites (seja negativo ou maior que len(intervalos)-1)?
Uma lista como intervalos que contém uma contagem do número de valores em cada intervalo é chamada de histograma.
Como exercício, escreva uma função chamada ``histograma`` que receba uma lista e um número de intervalos como argumentos e retorne um histograma com o número de intervalos solicitado.