Python Avancado
Python Avancado
- 2024 -
Capítulo 14
EXCEÇÕES - APROFUNDAMENTO
Tecnicamente falando, uma exceção é um objeto da classe Exception, ou de uma de suas classes
herdeiras. Elas permitem rastrear informações sobre situações excepcionais e erros que venham a ocorrer
durante a execução de algum código. O Python prevê uma série de exceções pré-definidas conhecidas pelo
termo "exceções embutidas". A figura 14.1 mostra uma parte das exceções embutidas.
Já vimos como usar o comando try-except para tratar a ocorrência de uma exceção. Agora vamos
aprender a criar exceções nosso código. Para isso vamos utilizar como ponto de partida o programa
resolvido 12.7 (apresentado no capítulo 12), no qual criamos uma função para verificar se um número é
primo ou não. Ela é reproduzida a seguir.
Imagine que a função tenha sido desenvolvida tempos atrás e por algum programador que não está
mais na empresa e novos programadores precisem utilizá-la. Inadvertidamente esses novo programadores
podem passar algum valor menor que 2 ao fazer a chamada da função e isso vai gerar um retorno True. Ou
ainda pior, pode ocorrer de alguém tentar passar à função um número real, um string, ou qualquer outra
coisa inválida para efeitos de verificação de números primos.
Uma função de verificação de números primos é um exemplo muito simples, mas se em seu lugar
imaginarmos funções que realizem tarefas críticas em um software, fica clara a importância de se levantar
uma exceção nos casos de falhas potenciais.
No exemplo 14.1 mostramos um caso concreto de levantamento de exceção aplicado a uma função
que recebe um número inteiro e retorna se ele é par ou ímpar. A forma como a função e a parte principal do
programa estão escritas propicia a ocorrência de erros no caso de o parâmetro passado não ser um número
inteiro.
Exemplo 14.1
def Paridade(pValor: int) -> str: # usamos anotações para indicar os tipos
if pValor % 2 == 0: # esperados, mas as anotações não interferem
return 'PAR' # na execução da função
else:
return 'ÍMPAR'
n = input('Digite algo: ') # nesta linha não fizemos a conversão para int
r = Paridade(n) # então o uso da função Paridade vai gerar um erro
print(f'{n} é {r}')
Digite algo: texto
Traceback (most recent call last):
File "D:\Python\cap14_exemplo_14.1.py", line 8, in <module>
r = Paridade(n)
^^^^^^^^^^^
File "D:\Python\cap14_exemplo_14.1.py", line 2, in Paridade
if pValor % 2 == 0:
~~~~~~~^~~
TypeError: not all arguments converted during string formatting
Perceba acima que em função de passar um string para a função Paridade(), o interpretador
Python levantou uma exceção TypeError que não foi tratada na parte principal. Esse programa pode ficar
bem melhor se forem feitas as alterações mostradas a seguir:
Nesta reformulação do exemplo 14.1 foram introduzidas no código da função estas linhas:
if type(pValor) != int:
raise Exception('A função Paridade deve receber um int')
que são responsáveis pela geração de uma exceção da classe Exception. Ao executar a chamada da
função passando como parâmetro qualquer objeto de classe diferente de int a exceção será levantada
com o uso do comando raise. Alternativamente, poderíamos substituir a exceção da classe Exception,
por outra exceção mais específica, como a da classe TypeError, desta forma.
if type(pValor) != int:
raise TypeError('A função Paridade deve receber um int')
Neste caso, o erro reportado será como abaixo.
Digite algo: texto
Traceback (most recent call last):
File "D:\Python\.venv\cap14_exemplo_14.1.py", line 10, in <module>
r = Paridade(n)
^^^^^^^^^^^
File "D:\ Python \.venv\cap14_exemplo_14.1.py", line 3, in Paridade
raise TypeError('A função Paridade deve receber um int')
TypeError: A função Paridade deve receber um int
Agora vamos fazer uma revisão do exercício resolvido 12.7, com a inclusão de um tratamento de erro
para situações em que o parâmetro V seja menor que 2.
No próximo exercício resolvido queremos implementar uma função que recebe uma lista ou uma
tupla e retorna uma lista com os valores da lista recebida elevados ao quadrado.
>>> # Segundo teste: o parâmetro é lista ou tupla, mas há um elemento não numérico
>>> L = [2, 5, 8, 12, 15, 'texto']
>>> AoQuadrado(L)
Traceback (most recent call last):
File "<pyshell#101>", line 1, in <module>
AoQuadrado(L)
File "<pyshell#86>", line 5, in AoQuadrado
raise ValueError(f'Os elementos de dados devem ser numéricos')
ValueError: Os elementos de dados devem ser numéricos
Neste exercício utilizamos o método .isinstance() para verificar se o parâmetro dados é uma
lista ou uma tupla. Caso não seja levantamos uma exceção TypeError. Se passar neste teste, na
sequência verificamos todos os elementos da sequência dados para saber se todos são inteiros ou reais,
caso contrário levantamos um ValueError.
Para a verificação de todos os elementos da lista foi utilizada a função all(), nesta construção:
all(isinstance(x, int) or isinstance(x, float) for x in dados)
Esta função aplica o teste duplo isinstance(x, int) or isinstance(x, float) a cada
elemento da sequência dados. E se todos passarem nesse teste o retorno é True; caso contrário o retorno
é False.
Informação de suporte para solução do ex. proposto 14.4 – Cálculo do D.V. do EAN13
fonte: https://pt.wikipedia.org/wiki/EAN-13
Capítulo 15
EXPRESSÕES yield E FUNÇÕES GERADORAS
• implantação inadequada usando linguagem de programação sem suporte nativo para essa
estratégia, exigindo codificação complexa, nem sempre bem-sucedida e com geração sobrecarga
computacional (o que era para ficar mais eficiente, acaba ficando o oposto);
• complexidade de depuração, pois a avaliação tardia torna complexo o rastreamento de erros, o
monitoramento dos valores contidos em variáveis e o acompanhamento da sequência de
execução das instruções do programa;
• possibilidade de vazamento de memória no caso erros que levem ao descontrole quanto à
alocação de memória e controle do conteúdo das variáveis do programa.
Para começar precisamos conhecer o conceito de Função Geradora (generator function), que é um
tipo especial de função capaz de retornar um objeto que pode ser usado como iterável em um laço for, do
mesmo modo como usamos listas e tuplas.
No entanto, há uma diferença enorme: o objeto iterável gerado segue os princípios da avaliação
preguiçosa. Com isso, não mantém todos os seus dados em memória, ou seja, não se trata de uma
sequência previamente calculada e armazenada em memória. Com este recurso, cada elemento é gerado
sob demanda apenas no momento em que será usado.
O yield também envia o retorno de volta ao chamador, porém ele não encerra a função, apenas a
suspende. Neste caso, a função permanece em memória com seu estado (valores dos objetos internos)
mantido de modo a permitir novas chamadas. Quando a função é retomada, ela continua a execução
imediatamente após a última execução do comando yield. Esse modo de execução permite que seu
código produza uma série de valores ao longo do tempo, e em vez de calculá-los todos de uma só vez, cada
valor é calculado em uma chamada.
Assim como o return, o uso do yield só é permitido dentro de funções. E quando uma função o
contém, essa função é chamada de função geradora.
Vamos analisar essa ideia através de um exemplo. Importante ressaltar que o exemplo 15.1 é apenas
um código de estudo, com fins didáticos. Ele serve para explicar o conceito e a dinâmica de implementação
de uma função geradora, mas não é exatamente um exemplo de aplicação real.
Exemplo 15.1
>>> def gera_simples():
yield 38
yield 159
yield 47
yield 26
38
159
47
26
>>> for valor in gera_simples(): # aqui está correto, com os parênteses colocados
print(valor)
38
159
47
26
(exemplo interativo feito com IDE Idle)
A função deste exemplo 15.1 é uma função geradora. Dentro dela o comando yield foi usado 4
vezes para retornar algum valor inteiro. Cada vez que chega o momento de executar um comando yield, a
função retorna o valor especificado e a função entra em estado de suspensão, permanecendo no aguardo
de uma nova chamada.
Antes de realizar chamadas à função geradora precisamos atribui-la a um objeto que passará a ser o
objeto gerador. Em seguida, usamos o objeto gerador como iterável em um comando for.
Também é possível usar a função diretamente, porém não se esqueça de colocar os parênteses junto
ao nome da função. Note que se você tentar usar a função geradora diretamente no comando for e
esquecer dos parênteses, ocorrerá um erro de interpretação do código.
Imagine agora uma situação semelhante, porém com significativa mudança de quantidade, talvez
com centenas de milhares ou dezenas de milhões de valores, e que a cada um estivesse associado algum
processamento adicional. O consumo de recursos (processamento e memória) necessários para calcular e
armazenar previamente todos os valores degradaria o desempenho do computador.
No exemplo 15.2 mostramos o uso do next(). E para deixar mais evidente a suspensão da função a
cada yield fizemos uma alteração em seu código acrescentando um print antes de cada retorno.
Exemplo 15.2
>>> def gera_simples():
print(' ...próximo valor retornado = 38')
yield 38
print(' ...próximo valor retornado = 159')
yield 159
print(' ...próximo valor retornado = 47')
yield 47
print(' ...próximo valor retornado = 26')
yield 26
Neste exemplo podemos perceber com mais nitidez (em especial ao assistir ao vídeo) o
comportamento do comando yield. Com a atribuição abaixo criamos um gerador, com o qual poderemos
interagir usando o comando next().
gerador = gera_simples()
O primeiro uso do comando next() provoca a primeira chamada ao gerador e isto faz com que o
primeiro print() seja executado, seguido do yield. Este yield irá suspender o gerador e a prova disso é
que o segundo print() não será executado. Esse segundo print() é executado apenas após a segunda
chamada usando o next(). E assim por diante. A função geradora possui 4 comandos yield, o que faz
com que seja possível usar a função next() 4 vezes. Depois que todos os yield forem executados, qualquer
nova chamada gerará a exceção StopIteration. Quando isso ocorre dizemos que o gerador está
esgotado ou exaurido.
Ao usar o next()pela quinta vez, ocorre o erro StopIteration, que é uma exceção natural gerada
para sinalizar o fim de um iterador. Essa quinta execução excedeu a capacidade do gerador. Isso ocorre
porque os geradores, assim como todos os iteradores, tem essa característica: eles se esgotam, ou seja,
eles chegam a um fim. Os laços for são construídos de forma a verificar a ocorrência da exceção
StopIteration. Na prática, isso quer dizer que o laço for encerra quando o iterador (ou gerador) se
esgota.
É possível usar um gerador apenas uma vez. Após seu esgotamento não será possível voltar a usá-lo,
a menos que uma nova atribuição gerador = gera_simples() seja feita.
Ao usar o termo infinito, a impressão que se tem é que faremos algo que ficará executando para
sempre em um computador. Bem, não é essa a ideia real. Trata-se na verdade de escrever um código que
tem potencial para ser infinito e quando formos utilizá-lo definiremos um limite.
Exemplo 15.3
>>> def gerador_pares():
a = 2
while True:
yield a
a += 2
# primeira parte
gen = gerador_pares()
next(gen)
2
next(gen)
4
next(gen)
6
next(gen)
8
# segunda parte
>>> for _ in range(20):
>>> print(next(gen), end = ' ')
10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48
Observe este código com atenção e perceba que ele produz números pares sucessivos. O código
dentro da função tem um laço infinito while True .
a = 2
while True:
yield a
a += 2
Somando-se a isso o fato de que dentro do laço o comando break não é utilizado, garantimos que
esse laço será infinito. Porém, dentro do laço é usado o comando yield, que ao ser executado suspende a
função e retorna o valor que o objeto a contém no momento.
Depois de pronta a função passamos a usá-la através do gerador gen. No exemplo dividimos esse
uso em duas etapas. Na primeira, com next() podemos constatar que ela funciona e podemos gerar
tantos pares quanto quisermos. Na segunda parte usamos o gerador em conjunto com um comando for
para gerar 20 elementos. E ao invés de 20 poderiam ser 50, 100 ou qualquer outra quantidade desejada.
A função geradora tem capacidade de gerar valores infinitamente. Mas o programa não ficará rodando
infinitamente, pois o programa principal garantirá sua finitude.
Agora estude e reproduza o exercício resolvido 15.1 onde aplicamos essa mesma técnica para gerar
uma sequência de números primos.
gen = gerador_primos()
Qtde = int(input('Digite a quantidade de primos: '))
for cont in range(Qtde):
print(next(gen), end = ' ')
print('\n\nFim do Programa')
Digite a quantidade de primos: 22
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79
Fim do Programa
No próximo exercício resolvido, 15.2, faremos uma função geradora que retorna uma tupla contendo
um número inteiro e seu fatorial. A função geradora é implementada contendo um laço infinito, sendo que
em cada execução ela incrementa o objeto num e calcula o fatorial associado a esse novo num. Veja seu
funcionamento rodando o código deste exercício.
print('\nFim do Programa')
Digite um valor: 12
(0, 1)
(1, 1)
(2, 2)
(3, 6)
(4, 24)
(5, 120)
(6, 720)
(7, 5040)
(8, 40320)
(9, 362880)
(10, 3628800)
(11, 39916800)
Fim do Programa
No programa principal um número inteiro é lido e usado como quantidade de tuplas a serem geradas
usando a função geradora funcao_fatorial().
Agora que você já testou esse programa considere fazer uma alteração. O objetivo dela é aprender o
que acontece quando usamos uma função geradora infinita, sem incluir qualquer situação de restrição no
programa principal. Por exemplo, se neste exercício forem executadas as linhas de código a seguir, teremos
problemas:
for ret in funcao_fatorial():
print(ret)
Ao executar esse laço for no programa principal, você vai provocar um laço infinito que gerará
valores sem qualquer parada, até que um erro ocorra. Neste caso específico, o erro vai ocorrer quando o
fatorial calculado for grande o bastante para estourar a capacidade de armazenamento de dígitos para os
números inteiros do Python.
É claro que um programa não pode ficar gerando erros assim. Portanto, seja tenha cuidado ao criar
funções geradoras infinitas. Você pode usá-las sempre que necessário, mas não se esqueça de fazer o
controle no programa principal.
Neste exemplo, vamos implementar um filtro no qual a função geradora recebe uma lista de preços
(números reais) e dois limites, mínimo (pmin) e máximo (pmax). Para cada valor da lista, que esteja dentro
do intervalo [pmin, pmax], a função retorna uma tupla contendo o valor original e o valor acrescido de 10%.
Exemplo 15.4
def filtro(dados, pmin, pmax):
for valor in dados:
if pmin <= valor <= pmax:
yield valor, valor * 1.1
Fim do Programa
No programa principal ocorre a carga da lista de dados, bem como fazemos a leitura dos limites
mínimo e máximo. Em seguida a função filtro() é usada como iterável para produzir o resultado final.
É claro que esta não é a única forma de solução, mas ela tem a vantagem de percorrer a lista original
sem a necessidade de realizar qualquer duplicação da lista original.
def processa(dados):
for item in dados:
item_processado = executa_proc(item)
yield item_processado
D = [d1, d2, d3, d4, ...] # considere que d1, d2, d3, etc sejam grandes
for resultado in processa(D):
salvar_bancodados(resultado)
Essa abordagem é benéfica ao lidar com grandes conjuntos de dados e reduz a sobrecarga do
processador e o consumo de memória.
Um caso de uso típico para esse modelo conceitual acontece em web-scraping (raspagem de dados),
que consiste em realizar a busca e extração de dados de várias páginas da internet. Você pode usar essa
técnica para buscar e carregar páginas da web, uma de cada vez a partir de uma lista de endereços, e após
essa carga, fazer a análise e extração das informações de interesse nela contida.
Nas áreas de BigData e Análise de Dados é comum trabalhar com arquivos muito grandes. Em geral
são arquivos texto no formato CSV (comma separated values), nos quais os dados de uma linha são
separados por vírgulas, ou algum outro caractere. Este formato é uma forma comum de compartilhar
dados. Agora, e se você quiser contar o número de linhas em um arquivo CSV? O bloco de código abaixo
mostra uma maneira de ler todas as linhas do arquivo:
arq = open(nome_arquivo)
linhas = arq.read().split("\n")
Esse código será satisfatório caso o tamanho do arquivo seja razoável. Porém, se ele contiver
centenas de milhares de linhas isso pode ser um problema.
Em casos como esse pode ser bem mais eficiente executar a leitura linha a linha e uma função
geradora pode ser uma alternativa interessante. No exemplo 15.6 implementamos um exemplo de uma
aplicação assim.
Exemplo 15.6
def le_arquivo(nome_arq):
for uma_linha in open(nome_arq, "r"):
yield uma_linha.rstrip() # o rstrip() remove o \n do final da linha
Fim do Programa
A função geradora le_arquivo() abre o arquivo e retorna uma linha por vez ao programa principal, onde
cada linha retornada poderá passar por qualquer processamento que seja necessário e adequado ao
problema para o qual o programa esteja sendo desenvolvido.
Nesta seção vamos dar o próximo passo, apresentando o conceito de expressão geradora. Na
documentação técnica oficial de Python o termo em inglês usado para esse conceito é Generator
Expression. Porém, na literatura técnica em geral sobre esse assunto encontram-se também os termos
Generator Comprehension e Comprehension Expressions.
Neste texto vamos padronizar o uso do temo em português "expressão geradora", que é encontrado
nas traduções para português da documentação oficial de Python.
Uma expressão geradora também é referida na literatura técnica
pelos termos em inglês
Generator Comprehension e
Comprehension Expressions
Este conceito está relacionado com a ideia de funções geradoras e também está relacionado com o
conceito de list comprehension, visto no capítulo 13 do Módulo Intermediário deste curso.
Naquele capítulo vimos que list comprehension é uma forma inteligente de criar sequências de dados
iteráveis que possam ser usados na implementação de algum algoritmo.
Uma expressão geradora permite fazer o mesmo, porém, consumindo uma quantidade muito menor
de memória. imagine que queiramos produzir uma lista com números inteiros elevados ao quadrado, de 1 a
10. Podemos fazer isso de duas formas, mostradas nas linhas a seguir:
quadrados_lc = [num**2 for num in range(1, 11)] # list comprehension
quadrados_eg = (num**2 for num in range(1, 11)) # expressão geradora
Note que a única diferença sintática é o uso de colchetes [] para list comprehension e parênteses()
para expressão geradora.
Ambos produzem a mesma sequência de valores e podem ser usados nos mesmos contextos. Veja o
exemplo 15.7. Na parte 1 desse exemplo fazemos a geração das duas estruturas e as usamos em uma
iteração com o comando for, produzindo o mesmo resultado com ambas.
valores dessa precisam ser armazenados em memória. Será que existe alguma forma de mensurar essa
memória consumida pelos dois objetos: quadrados_eg e quadrados_lc ?
A resposta é sim e fazemos exatamente isso na parte 2 do exemplo 15.7. Mas para a parte 2 mudamos
a quantidade de elementos de 10 para 10.000 (dez mil elementos). Com essa quantidade de elementos não
faz sentido exibir todos em tela, mas vamos exibir quanto cada um consome de memória usando o método
sys.getsizeof() do módulo sys.
Veja a diferença na quantidade de memória consumida. A lista, com os 10 mil elementos consome
mais de 85 mil bytes, enquanto a expressão geradora consome apenas 200 bytes.
Por outro lado, se a lista for menor que a memória disponível na máquina em uso, uma list
comprehension poderá ser processada mais rapidamente do que a expressão geradora equivalente. Na
parte 3 do exemplo 15.7 fazemos uma verificação de tempo de processamento usando o método
cProfile.run().
Observe que o tempo de processamento do list comprehension foi 3 vezes mais rápido que o tempo
da função geradora. Uma verificação de tempo como essa é dependente da máquina e dos programas que
estão em execução no momento em que o teste for feito.
Agora imagine isso levado à casa dos muitos milhões de elementos – algo frequente de ocorrer
quando se trabalha com Big Data – a escolha sobre usar um ou outro método deverá ser avaliado para cada
caso levando em consideração esses dois aspectos:
As expressões geradoras devem ser vistas como um recurso valioso em processamentos dessa
natureza (grandes volumes de dados) e sempre que possível usadas em substituição às listas.
Lembre-se de que um list comprehension retorna uma lista completa,
enquanto as expressões geradoras retornam geradores.
Por fim, lembre-se sempre que geradores funcionam da mesma forma, sejam eles construídos a
partir de uma função ou de uma expressão. A sintaxe das duas são diferentes, mas o resultado é a criação
de um gerador.
Assim, os objetos geradores que tenham uma expressão yield podem fazer uso dos seguintes
métodos:
Exemplo 15.8
def fg():
resto = 0
num = 2
while True:
if num % 2 == resto: # resto calculado é comparado com o objeto resto
gen = fg()
print('Gera 5 pares')
for i in range(5):
print(next(gen))
print('\nGera 5 ímpares')
ret = gen.send(1) # este método retorna o 1º valor da sequência
print(ret)
for i in range(4): # então precisamos gerar o próximos 4
print(next(gen))
print('\nFim do Programa')
Gera 5 pares
2
4
6
8
10
Gera 5 ímpares
1
3
5
7
9
Parâmetro incorreto
(Daqui para frente nada mais acontece. O programa está em laço infinito.
O programa não chega ao fim)
Vamos compreender a expressão yield contida neste exemplo. Quando construído da forma
mostrada nas linhas a seguir, dentro da função geradora,
if dado is not None:
dado = (yield num)
o objeto dado recebe o valor da expressão yield e a partir daí podem acontecer uma de duas coisas:
• se o chamado for feito com next() ou em um laço for, a expressão assume o valor None. É por
esse motivo que devemos usar o if para perguntar se ele não é None e, caso não o usássemos, a
função geradora ficará errada;
• se o chamado for feito com o método .send(), o valor passado como argumento será recebido
pelo yield e será diferente de None, podendo ser atribuído a algum objeto;
Portanto, neste exemplo, ao usarmos o método .send() do modo abaixo, o yield receberá o valor
passado, e esse valor será atribuído ao objeto dado dentro da função.
gen.send(1)
Para que a função gere as sequências corretamente deve ser passado o argumento 0 para sequência
de pares e 1 para sequência de ímpares. Outros valores que sejam passados não estão previstos e fazem
com que a função geradora entre em laço infinito.
É preciso corrigir isso e para fazê-lo acrescentamos uma verificação se dado é 0 ou 1, caso não seja
uma exceção é levantada.
>>> next(gen) # mostra que o gerador está esgotado (causado pela exceção)
Traceback (most recent call last):
File "<pyshell#44>", line 1, in <module>
next(gen)
StopIteration
(destaques em negrito feitos pelo autor)
Quando uma exceção é levantada dentro de um gerador ele tem sua execução automaticamente
esgotada. Isso pode ser visto acima quando fizemos com a chamada next(gen) após a exceção causada
pelo envio do valor 2 feito com o método .send().
Para poder voltar a usar o gerador é preciso reconstruí-lo com uma nova atribuição gen = fg().
Cuidado importante
É preciso ter atenção com um detalhe relevante quanto ao uso do método .send(). Não é permitido
que esse método seja usado imediatamente após a criação do gerador, ou seja, antes que o gerador seja
usado pelo menos uma vez. Com isso uma tentativa de usar o código abaixo resultará em erro.
gen = função_geradora()
gen.send(10) # vai gerar erro
Para eliminar o erro deve-se usar o gerador pelo menos uma vez, antes do .send(), por exemplo,
deste modo:
gen = função_geradora()
next(gen)
gen.send(10) # ok, vai funcionar corretamente
• O método .throw(...) faz a interrupção do gerador lançando uma exceção. Ele é útil em situações
em que podemos precisar capturar e tratar essa exceção lançada. Este método deve receber um
objeto Exception como parâmetro. Após o uso deste método qualquer tentativa de usar o
gerador gerará um StopIteration;
• O método .close() faz a interrupção do gerador de modo silencioso, simplesmente colocando-o
no estado StopIteration.
Vamos exemplificar os dois métodos, com essa função geradora que produz múltiplos de 10.
def simples():
num = 10
while True:
yield num
num += 10
Exemplo de uso do método .throw():
Exemplo 15.9
>>> gen = simples()
>>> for _ in range(5):
print(next(gen))
10
20
30
40
50
>>> next(gen)
Traceback (most recent call last):
File "<pyshell#81>", line 1, in <module>
next(gen)
StopIteration
Observe que após o uso do método .throw() não é mais possível usar a função .next() e haverá
uma sinalização da situação de StopIteration. Também não é possível usar um iterador for, mas neste
caso nada acontece porque o for verifica se o gerador está em estado de StopIteration e, caso esteja,
nada faz.
Vamos agora fazer um exemplo de uso do método .close(). Nesse exemplo faremos exatamente o
mesmo que no exemplo 15.8, apenas substituindo o .throw() pelo .close().
Com este exemplo podemos perceber que após o uso do método .close() o gerador não pode ser
mais usado pois foi colocado no estado de StopIteration.
No exercício resolvido 15.3 vamos alterar a função geradora de números fatoriais criada no exercício
resolvido 15.2, incluindo a possibilidade de o número base para cálculo do fatorial ser resetado usando o
método .send().
Fim do Programa
No próximo exercício resolvido, 15.4, criamos uma função geradora que recebe dois parâmetros de
entrada e os utiliza internamente, mostrando que essa também é uma opção possível em funções
geradoras.
# Programa principal
inicio = int(input('Digite o início da faixa: '))
final = int(input('Digite o final da faixa: '))
qtde = int(input('Digite quantos valores quer gerar: '))
# Cria o gerador
gen = gerador_aleatorio(inicio, final)
for _ in range(qtde):
print(next(gen), end=' ')
print('\n\nFim do Programa')
Digite o início da faixa: 30
Digite o final da faixa: 200
Digite quantos valores quer gerar: 8
79 72 149 57 141 147 30 149
Fim do Programa
Neste exercício resolvido 15.5 faremos uma função geradora recursiva capaz de gerar todas as
permutações de elementos dentro de uma lista.
Fim do Programa
Com este exercício resolvido, vemos que a função gera_permutacoes() faz chamadas a si
mesma, mostrando que a recursividade também pode ser utilizada em funções geradoras.
Essa chamada recursiva tem o objetivo de reduzir em um elemento o tamanho do texto a ser
permutado. A ideia central é que a permutação de um texto de tamanho N pode ser visto como a
concatenação: texto[i] + texto[sem o caractere i].
Nessa recursividade o caso recursivo ocorre quando o objeto texto tem comprimento maior que 1 e o
caso base quando esse tamanho é menor ou igual a 1.
Média móvel é um conceito de média dinâmica que se altera a cada novo valor acrescentado a um
conjunto de valores. Geralmente é usada para demonstrar o que está acontecendo em um processo,
enquanto o mesmo ainda está em andamento.
Por exemplo, se você mandar copiar um arquivo muito grande para um pendrive, o sistema
operacional pode te mostrar a média de bytes que está sendo transferida por segundo enquanto a cópia
ocorre. E a partir dessa média, estimar quanto tempo falta para terminar a cópia. Se você tiver outros
programas rodando enquanto faz a cópia, outros fatores podem afetar o desempenho do sistema e a média
móvel, calculada a cada 200 milissegundos (por exemplo), sofrerá variações.
O programa a seguir faz esse tipo de cálculo usando uma função geradora capaz de receber dados
através do método .send().
print('\nFim do Programa')
Digite um valor (ou FIM para sair): 6.0
média móvel atual = 6.000
Fim do Programa
Capítulo 16
MÓDULOS E PACOTES
Quando usamos o modo interativo, ao fechar o programa, tudo o que foi digitado é perdido. Ao
contrário, quando trabalhamos com scripts, os programas ficam salvos no armazenamento do computador
e podemos utilizá-lo sempre que necessário.
Os scripts podem se tornar cada vez maiores à medida que acrescentamos funcionalidades a um
programa e nestes casos entram em cena o Módulos de Python. À medida que um programa vai se tornando
maior, é uma boa prática dividi-lo em arquivos menores, com os propósitos de facilitar a organização e
futuras manutenções. A ideia essencial é usar arquivos separados para conter funções que podem ser
usadas em vários programas diferentes. Isso evita a necessidade de repetir a função no código de
programas distintos.
Esses arquivos que contém parte do código são chamados de Módulos (modules). Elementos
existentes em um módulo podem ser importados em outros módulos com o comando import e com
certeza você deve lembrar que neste curso já usamos algumas vezes esse comando.
incidência de erros.
Capacidade de manutenção
Em um bom projeto modular, cada módulo terá determinados limites lógicos para resolver diferentes
partes do problema maior. Se os módulos forem projetados de uma forma que haja pouca
interdependência, há uma boa probabilidade de que modificações em um único módulo não causem
impacto em outras partes do programa. Isso torna mais viável para uma equipe de muitos programadores
trabalhar de forma colaborativa em um aplicativo grande.
Reutilização
A funcionalidade definida em um único módulo pode ser facilmente reutilizada por outras partes da
aplicação, desde que haja uma interface definida adequadamente. Isso elimina a necessidade de duplicar
código.
Escopo
Os módulos normalmente definem um "namespace" separado, o que ajuda a evitar duplicidade de
identificadores (nomes) de objetos em diferentes áreas de um programa – essa duplicidade de nomes
também é chamada de colisão de nomes. Dê uma nova olhada no capítulo 1, seção 1.8, do Ebook do
módulo básico deste curso: reveja o Zen do Python – um de seus postulados é: "Namespaces são uma
grande ideia - vamos fazer mais deles!". Neste capítulo vamos falar mais sobre o que é um namespace.
Alguns autores se referem a módulos e pacotes usando o termo "biblioteca". Não há nada errado em
usar esse termo e isso ocorre porque nas linguagens de programação mais antigas esse é um termo
historicamente usado. De forma geral pode-se pensar que módulos prontos para uso constituem uma
biblioteca à disposição do programador.
Para evitar confusão neste texto, vamos sempre usar o termo "módulo" quando nos referirmos a um
arquivo a ser importado. O termo "pacote" será usado quando precisarmos nos referir a um grupo de
módulos. Por fim, o termo "biblioteca" será usado para grupos constituídos por vários pacotes.
fonte: o Autor
Os módulos podem conter quaisquer elementos Python, tais como: objetos, funções e classes. Neste
capítulo vamos ver módulos com objetos e funções. No próximo capítulo veremos as classes.
O mais interessante dos módulos escritos em Python é que eles são extremamente simples de
construir. Tudo o que você precisa fazer é criar um arquivo que contenha um código Python correto e salvá-
lo em um arquivo com extensão .py em algum diretório (pasta). Nenhuma sintaxe especial é necessária.
Atenção
Nos nomes dos módulos use apenas
letras minúsculas, algarismos e underline.
Veja o exemplo 16.1 a seguir. Escreva este código e salve-o com o nome "utilidades.py" em um
diretório de sua escolha. Usaremos o diretório "C:\CursoPython".
def paridade(valor):
"""Retorna PAR ou ÍMPAR conforme o valor passado"""
if valor % 2 == 0:
return 'PAR'
else:
return 'ÍMPAR'
def primo(valor):
"""Se valor for primo retorna True, senão retorna False"""
if valor == 2:
return True
elif valor % 2 == 0:
return False
else:
raiz = pow(valor, 0.5)
i = 3
while i <= raiz:
if valor % i == 0:
return False
i += 2
return True
(Isto é um módulo. Ele deve ser escrito e salvo dentro de algum diretório)
Como você pode ver neste código, o módulo utilidades.py possui quatro elementos:
• o string texto;
• a lista meses;
• a função paridade()que retorna um string indicando se um número é par ou ímpar;
• a função primo() que retorna True se um número é primo ou False, caso contrário;
Note que nesse arquivo não existe o que seria a "parte principal" do programa. Isso ocorre porque
esse código é um módulo que será importado em um outro programa (que será o principal). No entanto, é
possível que um módulo tenha também uma parte principal, mas isso será abordado mais adiante.
Nos módulos é usual, recomendável e considerado uma boa prática que em seu início haja um
docstring (comentário de múltiplas linhas) indicando qual é a finalidade do módulo. Também para cada
função recomenda-se colocar um docstring esclarecendo o que a função faz.
Depois de escrito o módulo é hora de testá-lo. Para isso vamos usar a interatividade do Idle e depois
escrevemos um programa em arquivo à parte no qual faremos a importação e uso do módulo.
Exemplo 16.1 – Parte 2: uso do módulo utilidades no Idle veja o vídeo 16.1
import utilidades
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
import utilidades
ModuleNotFoundError: No module named 'utilidades'
(exemplo interativo feito com IDE Idle)
Provavelmente você obterá a mensagem de erro mostrada acima. A importação falhou porque o
interpretador Python não conseguiu localizar o módulo. Precisamos informar ao interpretador onde
encontrá-lo. Há diferentes formas de fazê-lo e na seção 16.4 isso será visto em detalhes. Para fazermos
testes vamos alterar o diretório corrente do Idle. Isso pode ser feito usando um método do módulo os. No
caso, vamos usar os.chdir() para alterar o diretório corrente no Idle, fazendo com que o Idle "enxergue"
o módulo.
Exemplo 16.1 – Parte 3: uso do módulo utilidades no Idle veja o vídeo 16.1
>>> import os # importa o módulo os (operating system)
>>> os.getcwd() # consulta o diretório corrente
'C:\\Users\\sergi\\AppData\\Local\\Programs\\Python\\Python312'
>>> os.chdir('C:\CursoPython') # altera o diretório corrente
>>> os.getcwd()
'C:\\CursoPython'
>>> import utilidades # agora conseguimos importar o módulo
>>> utilidades.texto # acesso ao string texto
'Este é o módulo utilidades.py'
>>> utilidades.meses # acesso à lista meses
['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov',
'dez']
>>> utilidades.paridade(17) # acesso à função paridade()
'ÍMPAR'
>>> utilidades.primo(17) # acesso à função primo()
True
(exemplo interativo feito com IDE Idle)
Outro uso possível para o nosso módulo utilidades é importá-lo em um programa como mostrado a
seguir. É importante que você salve o arquivo desse programa no mesmo diretório onde salvou o módulo. Se
não fizer isso, ocorrerá erro na importação.
Exemplo 16.1 – Parte 4: programa que usa o módulo utilidades teste este código no PyCharm
import utilidades
print('\nFim do Programa')
Digite um inteiro: 317
317 é ÍMPAR
317 é primo
Digite um inteiro: 86
86 é PAR
86 não é primo
Digite um inteiro: 0
Fim do Programa
Este programa importa o módulo utilidades. A partir dessa importação, todos os elementos do
módulo – funções, classes, objetos, etc – passam a estar disponíveis ao programa, através da notação
qualificada com o nome do módulo. Ou seja, a maneira correta de realizar a chamada é escrever a notação
<nome módulo>.<nome elemento>, como mostrado acima no exemplo. Com isto você já pode
começar a criar seus próprios módulos, e terá condição de organizar seus programas mais complexos
dividindo-os em unidades lógicas menores e juntando-as em um programa principal através da importação.
Quando o interpretador Python executa um import, ele procura o módulo nos seguintes locais:
O caminho de pesquisa resultante está acessível na variável Python .path, que é obtida de um
módulo chamado . Experimente usar o sys.path como mostrado no exemplo 16.2.
Exemplo 16.2
>>> import sys
>>> for c in sys.path:
>>> print(c)
C:\Users\sergi\AppData\Local\Programs\Python\Python312\Lib\idlelib
C:\Users\sergi\AppData\Local\Programs\Python\Python312\python312.zip
C:\Users\sergi\AppData\Local\Programs\Python\Python312\DLLs
C:\Users\sergi\AppData\Local\Programs\Python\Python312\Lib
C:\Users\sergi\AppData\Local\Programs\Python\Python312
C:\Users\sergi\AppData\Local\Programs\Python\Python312\Lib\site-packages
(exemplo interativo feito com IDE Idle)
Os caminhos mostrados neste exemplo são os que estão configurados na máquina onde este texto foi
escrito. No caso, trata-se de uma máquina com sistema operacional MS-Windows.
Lembre-se
O conteúdo exato de sys.path depende do ambiente e da instalação.
O texto acima com certeza será diferente no seu computador.
Para ter certeza de que o seu módulo será encontrado pelo interpretador adote uma dessas ações:
• Colocar o módulo no mesmo diretório do programa principal ou, caso esteja usando o Idle
interativo colocá-lo no diretório atual;
• Modificar a variável de ambiente PYTHONPATH para incluir o diretório onde o módulo está
localizado ou colocar o módulo em um dos diretórios já contidos na variável PYTHONPATH, antes
de iniciar o interpretador;
• Colocar o módulo em um dos diretórios configurados durante a instalação do Python, ao qual
você pode ou não ter acesso de gravação, dependendo do sistema operacional;
• Colocar o arquivo do módulo em qualquer diretório de sua escolha e então modificar o atributo
sys.path, que é uma lista, fazendo a inclusão desse diretório com .append().
Nós usaremos esta última alternativa porque ela é bem prática e independe de qual sistema
operacional esteja em uso. Como o nosso módulo está no diretório C:\CursoPython então basta
executar o código mostrado no exemplo 16.3.
Exemplo 16.3
>>> import utilidades
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
import utilidades
ModuleNotFoundError: No module named 'utilidades'
16.5.1 MOTIVAÇÃO
Tudo o que fizemos neste curso até este momento mostra a relevância e a importância dos objetos
em Python. Os objetos estão por toda parte e, em última análise, tudo o que o seu programa Python cria e
atua é um objeto.
Identificador ou Nome ?
Uma simples instrução de atribuição cria um identificador e faz referência a um objeto que é criado
na memória do computador. Uma instrução como txt = 'algo' cria um nome simbólico txt que se
refere ao objeto que contém o string 'algo'.
Em um programa de qualquer complexidade, você criará dezenas, centenas ou até milhares desses
nomes, cada um levando para um objeto específico em memória. O ponto que queremos ressaltar aqui é
sobre como o Python gerencia todos esses nomes. Como isso é feito?
16.5.2 NAMESPACE
Namespace é uma coleção de diferentes objetos criados em memória sendo que cada um está
associado a um nome. Esses nomes são únicos em determinado momento, ou seja, não pode haver dois ou
mais nomes iguais nesse momento.
Em termos práticos você pode pensar em um namespace como um dicionário no qual a chave é o
nome do objeto e o valor é o próprio objeto. Cada par de chave-valor mapeia um nome para seu objeto
correspondente, que já está criado e, portanto, presente na memória do computador naquele momento.
O namespace built-in
Este namespace contém os nomes de todos os objetos embutidos do interpretador Python. Tais
objetos estão disponíveis sempre que o Python está em execução e é possível listá-los com o comando
dir(__builtins__) como mostrado no exemplo a seguir.
A função dir() é usada para listar os nomes definidos dentro de algum escopo. Seu retorno é uma
lista de strings organizada em ordem alfabética. Usada sem argumentos, ela retorna a lista de nomes no
escopo local atual. Por outro lado, se for usada com um argumento, ela tentará retornar uma lista de
atributos válidos para esse argumento. No exemplo, por uma questão de espaço, estamos exibindo apenas
parte do resultado, mas observe bem o que é retornado pela dir(__builtins__):
Exemplo 16.4
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError',
...
'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray',
'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex',
'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate',
'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset',
'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input',
'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list',
'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct',
'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr',
'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod',
'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
Esta lista é extensa então reproduzimos apenas parte dela. Observe os nomes acima,
você reconhecerá vários deles que já usou.
(exemplo interativo feito com IDE Idle - destaques em negrito feitos pelo autor)
Acima destacamos em negrito funções que já foram usadas diversas vezes neste curso, como
print(), id(), type(), len(), min(), max() e a própria função dir() que usaremos bastante
neste capítulo.
O Python cria este namespace quando é inicializado e ele permanece existente até que o
interpretador termine. Este namespace é único, ou seja, não existirão dois deles em uma execução do
interpretador.
O namespace global
O namespace global contém quaisquer nomes definidos no âmbito do programa principal. Mas este
não é o único namespace global que existe. Cada módulo que seja carregado durante a execução do
programa também terá seu namespace global.
No caso do programa principal, o interpretador cria seu namespace global quando o corpo principal
do código é iniciado. Esse namespace permanece existente até que o interpretador termine. No caso de um
módulo, seu namespace global é criado apenas quando o módulo é carregado com o comando import e
permanece existente até que o programa termine.
Nesta continuação do exemplo 16.4 fazemos a importação do módulo utilidades e listamos com
dir() os objetos de seu namespace.
Essa categoria global de namespace às vezes pode induzir um raciocínio incorreto no estudante de
Python. Ao usar o termo "global" pode-se passar a impressão de que ele está associado ao programa
principal e que deve existir apenas um namespace global. Mas não é esse o conceito. Todo programa e todo
módulo terá seu namespace global.
Ficará mais simples de entender depois que virmos as próximas duas categorias.
O namespace local
Sempre que estamos trabalhando com funções, o interpretador cria um novo namespace sempre que
uma função é executada. Esse é o namespace local e ele tem validade para a função apenas enquanto ela
está em execução. Quando a função terminar seu namespace e quaisquer objetos que tenham sido criados
dentro dele são destruídos. O exemplo 16.5 mostra a relação entre os três namespace: built-in, global e
local.
Exemplo 16.5
def soma(a, b):
print('\nParte 3 - Escopo local - função soma')
print(dir())
return a + b
26 + 15 = 41
26 - 15 = 11
As linhas em negrito são um destaque feito pelo autor
Na execução deste exemplo estão presentes as três categorias de namespace – built-in, global, e
local. Assim que o programa inicia são exibidos os nomes presentes no namespace built-in que é o mais
externo.
Em um terceiro nível estão os namespaces locais das funções soma() e diferença(). Note que os
dois possuem os objetos a e b, porém não há risco de colisão de nomes porque esses objetos só existem
quando a função está sendo executada. Dados que ambas não são executadas ao mesmo tempo, não há
risco de conflito.
No que diz respeito ao código interno de uma função existe um conhecimento relacionado aos
namespaces local e global que todo programador Python deve conhecer e dominar. Considere o seguinte:
Essas afirmações são sempre verdadeiras e o exemplo a seguir é uma ilustração delas.
Exemplo 16.6
def funcao1():
print(' ...início da funcao1')
if criterio == 'alterar':
valor = 999
print(f' valor dentro de funcao1 = {valor}')
def funcao2():
print(' ...início da funcao2')
global valor # faz referência ao objeto global valor
if criterio == 'alterar':
valor = 999
print(f' valor dentro de funcao2 = {valor}')
criterio = 'alterar'
valor = 10
print(f'pgm principal valor = {valor} (antes funcao1)')
funcao1()
print(f'pgm principal valor = {valor} (apos funcao1)')
print()
print(f'pgm principal valor = {valor} (antes funcao2)')
funcao2()
print(f'pgm principal valor = {valor} (apos funcao2)')
pgm principal valor = 10 (antes funcao1)
...início da funcao1
valor dentro de funcao1 = 999
pgm principal valor = 10 (apos funcao1)
No programa principal são criados dois objetos: criterio que contém o texto 'alterar' e valor
que contém o inteiro 10. O objeto criterio é usado apenas "para consulta", ou seja, não estamos
interessados em alterar seu conteúdo, mas precisamos verificar seu conteúdo em uma condição. Já o
objeto valor tem seu conteúdo alterado dentro das funções.
Como o programa roda normalmente, fica claro que o objeto criterio está disponível dentro da
função. E como ele não foi criado localmente a única conclusão é que se trata do objeto pertencente ao
programa principal, o objeto no namespace global.
Agora vamos analisar o que ocorre com o objeto valor, cujo conteúdo precisa ser alterado. Dentro
de funcao1() fizemos a atribuição valor = 999. Essa atribuição provoca a criação de um novo objeto
local totalmente desassociado do objeto global de mesmo nome. Com isto, a atribuição feita dentro de
funcao1() não teve qualquer efeito sobre o objeto global valor.
Por outro lado, dentro de funcao2() incluímos a declaração global valor. Isso cria um vínculo
da função com o objeto global para efeitos de alterações de conteúdo. Essa declaração garante que
qualquer atribuição feita ao nome valor irá afetar diretamente o objeto global.
Inicialmente, cuidado! Poderíamos usar o termo não-local em português (em inglês são comuns os
termos non-local namespace ou enclosing namespace) mas não imagine que esse termo seja uma
negação de local e, portanto, global. Em Python namespace não-local é um conceito a mais nesse assunto.
Para evitar confusão com termos vamos adotar o termo namespace nonlocal para esse conceito.
Um namespace nonlocal só existirá nas situações em que tenhamos funções aninhadas. Isso
mesmo, uma função criada dentro de outra função. Veja o exemplo 16.7. Neste código temos a função
soma(). Além disso, dentro de soma() foi criada a função aninhada calcula().
v1 = 26
v2 = 15
r = soma(v1, v2)
print(f'\nsoma de {v1} com {v2} = {r}')
Escopo local - função soma
['a', 'b', 'calcula']
soma de 26 com 15 = 41
Quando o programa principal chama soma(), Python cria um namespace local para soma().
Da mesma forma, quando soma() chama calcula(), calcula() obtém seu próprio namespace
separado. O namespace criado para calcula() é o namespace local. Nessa situação o namespace local
de soma() é o namespace nonlocal para calcula(). Assim, dentro de uma função aninhada, além de seu
namespace local, estará disponível o namespace da função que a contém, que é o nonlocal.
O exercício resolvido 16.1 ilustra a relação entre os namespaces global, nonlocal e local.
Observação sobre a solução do exercício resolvido 16.1
A solução apresentada é bem mais complicada do que precisaria ser.
Porém, o objetivo são os namespaces, não escrever o melhor programa.
Vamos apresentar duas versões para a solução deste enunciado e a versão 1 está incorreta quanto
ao resultado fornecido. Note que o programa é executado normalmente, sem qualquer ocorrência de erro,
porém o resultado produzido está incorreto. Veja a seguir:
PcCusto = 100
CodProd = '1280'
PcVenda = valor_venda(CodProd, PcCusto)
print(f'Produto {CodProd}: compra = {PcCusto:.2f} e venda = {PcVenda:.2f}')
CodProd = '8280'
PcVenda = valor_venda(CodProd, PcCusto)
print(f'Produto {CodProd}: compra = {PcCusto:.2f} e venda = {PcVenda:.2f}')
CodProd = '9280'
PcVenda = valor_venda(CodProd, PcCusto)
print(f'Produto {CodProd}: compra = {PcCusto:.2f} e venda = {PcVenda:.2f}')
Produto 1280: compra = 100.00 e venda = 116.00
Produto 8280: compra = 100.00 e venda = 116.00
Produto 9280: compra = 100.00 e venda = 116.00
Neste resultado os preços de venda estão todos com 16% de margem, mesmo para os códigos com
dígito inicial '8' e '9'. O programa principal fez três chamadas à função valor_venda() e em cada uma
passou os parâmetros corretos. Dentro dessa função o objeto margem foi carregado com 16% e em seguida
a função aninhada altera_margem() foi chamada.
Vamos nos concentrar nessa função aninhada. Perceba que em seu código ela utiliza os objetos
codigo e margem, sem que eles tenham sido definidos dentro dela.
Resposta simples → não ocorre erro porque esses objetos estão disponíveis para
altera_margem().
def altera_margem():
if codigo[0] == '8':
margem = 12/100
elif codigo[0] == '9':
margem = 10/100
Resposta não tão simples → não ocorre erro por dois motivos diferentes:
• não ocorre erro relacionado ao objeto margem porque ele está sendo atribuído dentro da
função e, portanto, ele é um objeto local que será criado no momento da atribuição;
• não ocorre erro relacionado ao objeto codigo porque, embora não seja definido dentro de
altera_margem() ele existe na função valor_venda(). É como se os objetos de
valor_venda() fossem globais para altera_margem() e a eles damos o nome de não-
locais (usaremos o termo nonlocal), pois o termo global é reservado para objetos do
programa principal.
Na situação à esquerda temos o "ponto de vista" da função valor_venda() (amarela) e para essa
função os objetos da área amarela são locais e os da área azul são globais.
Na situação à direita temos o "ponto de vista" da função altera_margem() (cinza) onde os objetos
da área cinza são locais, os da área amarela são não-locais e os da área azul são globais.
Figura 16.2 – Ilustração de namespaces global, nonlocal e local para a Versão 1 da solução do exemplo 16.7
fonte: o Autor
Agora que conhecemos a relação local – nonlocal – global vamos entender o erro do exercício
resolvido 16.1 versão 1.
Observe na figura que o ambiente da função valor_venda() (área amarela) contém um objeto com
o nome margem; e o ambiente da função altera_margem() (área cinza) também contém um objeto com
esse nome, que é criado no momento da atribuição do valor.
Esse é o motivo pelo qual essa versão do programa está produzindo os resultados errados. Quando a
função altera_margem() é chamada ela deveria alterar o objeto margem da área amarela e não criar um
novo objeto local.
Para resolver esse problema é preciso que a função altera_margem() faça a alteração
diretamente no objeto margem que pertence à função valor_venda().
Exercício Resolvido 16.1 – Versão 2: solução correta A apresentação das duas versões está no mesmo vídeo
def valor_venda(codigo, val_custo):
PcCusto = 100
CodProd = '1280'
PcVenda = valor_venda(CodProd, PcCusto)
print(f'Produto {CodProd}: compra = {PcCusto:.2f} e venda = {PcVenda:.2f}')
CodProd = '8280'
PcVenda = valor_venda(CodProd, PcCusto)
print(f'Produto {CodProd}: compra = {PcCusto:.2f} e venda = {PcVenda:.2f}')
CodProd = '9280'
PcVenda = valor_venda(CodProd, PcCusto)
print(f'Produto {CodProd}: compra = {PcCusto:.2f} e venda = {PcVenda:.2f}')
Produto 1280: compra = 100.00 e venda = 116.00
Produto 8280: compra = 100.00 e venda = 112.00
Produto 9280: compra = 100.00 e venda = 110.00
Após essa alteração a realidade do programa é outra e está ilustrada na figura 16.3 onde está indicado
que na função altera_margem() não existem objetos locais.
De fato, após a alteração nenhum novo objeto é criado dentro dessa função e ela apenas utiliza os
objetos que estão em seu namespace não-local.
Figura 16.3 – Ilustração de namespaces global, nonlocal e local para a Versão 2 da solução do exemplo 16.7
fonte: o Autor
Agora que já conhecemos o conceito de namespace vamos tratar do conceito de escopo. Muitos
autores que escreve sobre a linguagem Python tratam esses dois conceitos como sinônimos. Não chegam a
estar errados pois os dois conceitos estão intimamente associados. Porém existe uma diferença sutil que
acreditamos que vale a pena conhecer e que vamos apresentar agora.
• O escopo define quais namespaces serão pesquisados e em que ordem. O escopo de qualquer
referência sempre começa no namespace local e se move para os níveis acima até atingir o
namespace built-in, o nível mais alto criado na inicialização do interpretador Python.
fonte: o Autor
Assim, quando dizemos que o objeto xpto está no namespace de uma função, queremos dizer que
ele foi criado localmente dentro da função. Quando dizemos que xpto está no escopo da função,
queremos dizer que xpto está no namespace da função ou em qualquer um dos namespaces externos nos
quais o namespace da função está atualmente aninhado.
Em síntese: o que é Escopo?
Sempre que você define uma função, você cria um novo namespace e um novo escopo. O namespace
é o novo dicionário de armazenamento de pares nome-objeto. O escopo é a cadeia implícita de
namespaces que começa nesse novo namespace e, em seguida, segue o caminho descrito através de
quaisquer namespaces externos (escopos externos), até o namespace global (o escopo global) e por fim o
built-in do interpretador.
Essa investigação pode ser feita com a função dir() e no exemplo a usamos diversas vezes para ver
o que ocorre à medida que interagimos com o interpretador Pyhton. Sugerimos que antes de prosseguir na
leitura desse texto, assista o vídeo desse exemplo, analise o código e reproduza-o no Idle. Ao fazer isso você
compreenderá com mais facilidade as explicações que se seguem.
Nas partes de 1 a 3 do exemplo 16.4 usamos dir() sem argumentos para listar os objetos existentes
no namespace atual (onde dir() está sendo usado). Quando essa função é usada com um argumento, ela
mostrará os objetos existentes no namespace do argumento que foi passado.
A parte 1 registra a situação em que o Idle acabou de ser aberto nenhum objeto foi criado. Assim,
nesse momento o namespace do interpretador contém apenas os objetos criados automaticamente em
sua inicialização. Você pode perceber que há um grupo de nomes que iniciam e terminam com dois
caracteres underline no formato __nome__. Não se preocupe com isso agora. Mais adiante vamos falar
sobre eles e as situações em que são usados.
Na parte 3 importamos o módulo sys (necessário para alterar sys.path) e em seguida importamos
nosso módulo utilidades. Usando dir() pela terceira vez vemos que o namespace global agora
contém esses dois módulos.
Exemplo 16.8
Parte 1
>>> dir() # Namespace global de um interpretador recém iniciado
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']
Parte 2
>>> a = 236 # criação de alguns objetos
>>> b = 17.3
>>> txt = 'algum texto'
>>> dir() # os objetos criados estão presentes na tabela
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'a', 'b', 'txt']
Parte 3
>>> import sys
>>> sys.path.append('C:\CursoPython')
>>> import utilidades
>>> dir() # agora 'sys' e 'utilidades' também estão incluídos no namespace global
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'a', 'b', 'sys', 'txt', 'utilidades']
Parte 4
>>> dir(utilidades) # inspeção do namespace interno ao módulo utilidades
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__',
'__package__', '__spec__', 'meses', 'paridade', 'primo', 'texto']
(exemplo interativo feito com IDE Idle - destaques em negrito feitos pelo autor)
Por fim, na parte 4 usamos dir(utilidades). Ao acrescentar um nome como argumento estamos
solicitando a listagem dos elementos internos ao módulo, ou seja, queremos conhecer quais elementos
existem dentro do módulo. Esse conjunto de elementos é o namespace do módulo utilidades e ali estão
presentes o objetos texto e meses, também as funções paridade() e primo().
Faça testes adicionais com a função dir(). Experimente usá-la com os objetos a, b e txt. Você
verá que serão listados seus elementos internos.
Temos usado a forma mais básica de importação que é essa mostrada na linha a seguir. Vamos
chamá-la de forma 1.
import <nomedomódulo>
Este tipo de execução torna o módulo disponível para o chamador, porém não torna o conteúdo do
módulo diretamente disponível. Isso significa que essa forma de chamada insere o módulo no namespace
do chamador, mas não insere seus elementos internos.
Notação com ponto
Também chamada de qualificação, é a notação usada na escrita do código
quando não temos acesso direto a um elemento que pertence a um módulo.
A consequência disso é que o módulo estará no escopo do chamador, mas para usar seus elementos
internos será necessário realizar as chamadas usando a notação com ponto. Veja no exemplo 16.9 que
após a importação do módulo utilidades não conseguimos acessar diretamente seus objetos internos.
Porém, ao usarmos o nome do módulo como prefixo esse acesso é possível.
Exemplo 16.9
>>> import sys
>>> sys.path.append('C:\CursoPython')
>>> import utilidades
>>> texto
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
texto
NameError: name 'texto' is not defined.
>>> meses
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
meses
NameError: name 'meses' is not defined
>>> utilidades.texto
'Este é o módulo utilidades.py'
>>> utilidades.meses
['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov',
'dez']
(exemplo interativo feito com IDE Idle)
Assim, após a importação do módulo usando essa forma 1 seus elementos internos estão acessíveis
apenas com o uso da notação com ponto – <modulo>.<objeto>, pois apenas o nome do módulo foi
acrescentado ao escopo do chamador.
Atenção
Esteja atento ao usar o comando import, pois ele vai sobrescrever qualquer elemento de mesmo
nome pré-existente. Veja o exemplo:
Exemplo 16.10
Parte 1
>>> import sys
>>> sys.path.append('C:\CursoPython')
>>> utilidades = 'Este objeto é um string'
>>> type(utilidades)
<class 'str'>
>>> print(utilidades)
Este objeto é um string
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'sys', 'utilidades']
Parte 2
>>> # Agora vamos importar o módulo utilidades
>>> import utilidades
>>> type(utilidades)
<class 'module'>
>>> print(utilidades)
<module 'utilidades' from 'C:\\CursoPython\\utilidades.py'>
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'sys', 'utilidades']
(exemplo interativo feito com IDE Idle - destaques em negrito feitos pelo autor)
O primeiro comando da parte 2 é a importação do módulo utilidades. Você pode ver nos
comandos subsequentes que o identificador utilidades continua inserido no escopo do interpretador,
porém agora categorizado como <class 'module'>, ou seja, o mesmo nome agora está associado a um
elemento totalmente diferente. O objeto de mesmo nome existente anteriormente foi descartado e seu
conteúdo está perdido. E isso é feito normalmente pelo Python, sem qualquer aviso da existência do objeto
anterior.
Esta situação que acabamos de exemplificar pode representar um problema significativo no tocante à
organização do código de um sistema computacional, em especial aos de grande porte, que podem conter
dezenas ou centenas de módulos e milhares de objetos, funções e classes. Quando uma situação como
essa acontece dizemos que ocorreu uma Colisão de Nomes.
Colisão de Nomes
>>> print(util)
<module 'utilidades' from 'C:\\CursoPython\\utilidades.py'>
(exemplo interativo feito com IDE Idle - destaques em negrito feitos pelo autor)
Importar um módulo com apelido não causa mudança no nome físico do módulo, apenas atribui um
nome temporário em tempo de execução. Isso pode ser visto quando fazemos print(util), veja acima o
que é exibido nesse print: o nome físico do módulo (caminho + nome do arquivo gravado no disco).
O uso de apelidos pode ser útil para evitar conflitos de nomes, melhorar a legibilidade do código ou
fornecer um nome mais curto ou mais significativo para um módulo ou componente importado. Na seção
16.6.2 alertamos sobre a questão da substituição de um nome em uma importação. O uso de apelidos
ajuda a contornar essa questão.
Por exemplo, considere que em seu software você tem o módulo codigo_barras.py no qual vem
trabalhando há algum tempo e já contém funções e classes que usa bastante. No entanto, você precisa
agora implementar um tipo de código de barras novo e descobre na comunidade Python que já existe um
módulo que faz isso e, melhor, que seu uso é livre. Você então baixa esse módulo feito por terceiros e
descobre que ele tem o mesmo nome que o seu módulo. Não é viável alterar o nome físico de um dos
módulos, e você passa a usar um apelido para o novo módulo, evitando a colisão de nomes. Simples assim.
Exemplo 16.13
>>> import sys
>>> sys.path.append('C:\CursoPython')
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'paridade', 'sys']
>>> print(paridade)
<function paridade at 0x0000016D2894F4C0>
>>> paridade(19)
'ÍMPAR'
(exemplo interativo feito com IDE Idle - destaques em negrito feitos pelo autor)
Ao usar essa segunda forma o método paridade foi inserido na tabela de símbolos tornando-se
disponível para uso direto sem a necessidade do prefixo com o nome do módulo.
Esteja atento ao usar esta forma de importação para que não ocorram colisões de nomes. Lembre-se
que se um nome já existir no namespace e ocorrer uma importação de objeto com o mesmo nome haverá a
substituição desse nome anterior como mostrado a seguir, no exemplo 16.14.
Exemplo 16.14
# Parte 1 – preparação do ambiente
>>> import sys
>>> sys.path.append('C:\CursoPython')
>>> texto = '... string pré-existente ...' # um string
>>> paridade = 90 # um inteiro
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'paridade', 'sys', 'texto']
>>> print(texto)
... string pré-existente ...
>>> print(paridade)
90
Assim, com esse exemplo, fica clara a ocorrência da colisão. Os objetos paridade e texto foram
substituídos no namespace do chamador após o import.
Exemplo 16.15
# Parte 1 – Inspeção dos elementos do módulo utilidades
>>> import sys
>>> sys.path.append('C:\CursoPython')
>>> import utilidades
>>> for s in dir(utilidades):
... print(s)
__builtins__
__cached__
__doc__
__file__
__loader__
__name__
__package__
__spec__
meses
paridade
primo
texto
(exemplo interativo feito com IDE Idle - destaques em negrito feitos pelo autor)
Na segunda parte usamos o coringa * para mostrar que isto colocará os nomes de todos os objetos
do módulo no namespace do chamador, com exceção de qualquer um que comece com o caractere de
underline (_).
Não é recomendável fazer importação com coringa em larga escala, em softwares de grande porte,
com muitos módulos com vários elementos internos. Isso é um tanto perigoso porque você está inserindo
nomes em massa no namespace. A menos que você conheça muito bem todos os módulos e seus
elementos e tenha certeza de que não haverá colisão de nomes, você terá uma boa chance de substituir
inadvertidamente nomes existentes. O que causará falhas severas na execução do software como um todo.
No entanto, essa sintaxe é bastante útil quando você está apenas usando o interpretador em
ambiente interativo, para fins de estudos e testes de módulos, porque ela fornece acesso rápido a tudo o
que um módulo tem a oferecer, sem muita digitação.
16.7 PACOTES
Suponha que você desenvolveu uma aplicação muito grande que inclui muitos módulos. À medida
que o número de módulos aumenta, pode-se tornar difícil gerenciá-los e controlá-los se forem mantidos em
um único local. Isto é especialmente verdade se eles tiverem nomes ou funcionalidades semelhantes. Em
casos assim é conveniente haver um meio de agrupá-los e organizá-los.
Os pacotes permitem uma estruturação hierárquica do namespace do módulo usando notação com
ponto. Da mesma forma que os módulos ajudam a evitar colisões entre nomes de objetos, os pacotes
ajudam a evitar colisões entre nomes de módulos.
Criar um pacote Python é bastante simples, pois utiliza a estrutura hierárquica de arquivos do sistema
operacional que você estiver usando no seu computador. Em outras palavras, para criar um pacote basta
criar uma pasta e salvar os arquivos .py do Python dentro dela. Simples assim.
A figura 16.5 ilustra esse arranjo. A pasta pacote foi criada dentro da pasta C:\CursoPython e dentro
dela foram salvos os dois arquivos Python mostrados no exemplo 16.17
fonte: o Autor
def paridade(valor):
"""Se valor for par retorna True, senão retorna False"""
if valor % 2 == 0:
return True
else:
return False
def primo(valor):
"""Se valor for primo retorna True, senão retorna False"""
if valor == 2:
return True
elif valor % 2 == 0:
return False
else:
raiz = pow(valor, 0.5)
i = 3
while i <= raiz:
if valor % i == 0:
return False
i += 2
return True
(módulo)
def paridade(valor):
"""imprime PAR ou ÍMPAR conforme o valor passado"""
if valor % 2 == 0:
print(f'{valor} é PAR')
else:
print(f'{valor} é ÍMPAR')
def primo(valor):
"""imprime PRIMO ou NÃO-PRIMO conforme o valor passado"""
if valor == 2:
print(f'{valor} é PRIMO')
elif valor % 2 == 0:
print(f'{valor} é NÃO-PRIMO')
else:
raiz = pow(valor, 0.5)
resto = i = 3
while i <= raiz and resto != 0:
resto = valor % i
i += 2
if resto != 0:
print(f'{valor} é PRIMO')
else:
print(f'{valor} é NÃO-PRIMO')
(módulo)
a = 17
print('Uso das funções do módulo utils_bool')
r = pacote.utils_bool.primo(a)
print(f'{a} é primo? {r}')
r = pacote.utils_bool.paridade(a)
print(f'{a} é par? {r}')
O vídeo do exemplo 16.17 mostra a criação do pacote, dos dois módulos e do programa principal.
No programa principal os módulos do pacote são importados usando a notação com ponto
<pacote>.<modulo>. A partir daí a as funções internas ao módulo podem ser acessados desde que a
notação com ponto seja utilizada.
Exemplo 16.18
import pacote.utils_bool as ub
import pacote.utils_txt as ut
a = 17
print('Uso das funções do módulo utils_bool')
r = ub.primo(a)
print(f'{a} é primo? {r}')
r = ub.paridade(a)
print(f'{a} é par? {r}')
Observe que essa notação com apelido torna a escrita do programa bem mais compacta e legível,
além de permitir que cada módulo do pacote tenha um apelido apropriado escolhido pelo programador.
Exemplo 16.19
# Código do módulo cap16_operacoes.py
def soma(*args):
r = 0
for x in args:
r = r + x
return r
def multi(*args):
r = 1
for x in args:
r = r * x
return r
Início do módulo
soma: 20
multi: 384
Ao executar o programa principal veja que a importação do módulo causou a execução do comando
print('Inicio do módulo') fazendo com que a mensagem seja exibida na tela de execução do
programa.
Esse recurso existe para que tarefas de configuração e inicialização do módulo possam ser
executados no momento da carga do módulo
Exercício Proposto
Enunciado: Um exemplo da situação em que módulos se aplicam é o caso da função ExibeLista() que foi usada
na solução de alguns exercícios propostos do capítulo 12 deste curso. Abra os arquivos dos
exercícios propostos 12.4, 12.11 e 12.13 e observe-os com atenção. Você verá que essa função
está presente nos três programas.
Se tivéssemos criado um módulo contendo essa função, poderíamos importá-lo e usá-la sem a
necessidade de reescrever seu código.
Escreva a função ExibeLista() em um módulo e depois altere os programas dos exercícios
propostos 12.4, 12.11 e 12.13 para usar o módulo.
Capítulo 17
PROGRAMAÇÃO ORIENTADA A OBJETOS
Esse paradigma fornece uma maneira inteligente de definir elementos de código que podem ser
reutilizados conforme a necessidade dos desenvolvedores. Por exemplo, durante este curso já usamos
diversas vezes elementos int, float, string, list e dict que são classes disponíveis no Python e
com elas criamos objetos que foram usados para resolver vários exercícios.
Classe e Objetos
Uma analogia comum para explicar a relação entre classe e objeto é imaginar que a classe seja uma
forma de bolo e os objetos são os bolos que podem ser feitos com essa forma. A forma terá características
próprias como seu tamanho e formato e os bolos produzidos com ela seguirão tais características. Cada
bolo produzido terá o formato e o tamanho dessa forma, mas também poderá ter características próprias
com variações de sabor e textura decorrentes dos ingredientes usados e da sequência de preparo da
massa.
A forma, assim como a classe, define aspectos gerais e cada bolo produzido é uma instância
específica que terá um uso e um tempo de duração.
Levando essa ideia para programação veja os objetos abaixo. Todos eles são da mesma forma – a
classe list – mas cada um tem seus próprios ingredientes – os dados que armazenam.
L1 = [12, 50, 89] # lista com inteiros
L4 = [] # lista vazia
• Atributo: usado para se referir a um dado necessário ao objeto de uma determinada classe. Em
Python, atributos são objetos definidos dentro da classe e são eles que armazenam todos os
dados necessários para o funcionamento da classe. O conjunto de valores armazenados nos
atributos definem o estado do objeto.
• Método: usado para se referir a uma função implementada dentro da classe. Os métodos
conferem funcionalidades e comportamentos ao objeto. Essas funções normalmente realizam
operações usando os dados armazenado nos atributos do objeto.
O objetivo maior da POO é produzir classes totalmente funcionais para modelar elementos existentes
no mundo real. Por exemplo, você pode usar classes para criar objetos que emulem pessoas, produtos,
veículos, notas fiscais ou quaisquer outros objetos que seu software necessite.
Em Python, o corpo de uma determinada classe funciona como um
namespace onde residem atributos e métodos.
Esse estilo de nome pode parecer estranho aos iniciantes em Python, pois inicia e termina com dois
underlines. Bem, acostume-se com isso, pois são muito comuns nessa linguagem.
Eles são conhecidos como Métodos Mágicos e existem para que os programadores possam realizar
tarefas especiais e personalizar o comportamento dos objetos.
Métodos Mágicos (Magic Methods)
Em Python são os métodos que iniciam e terminam
com dois caracteres underline.
O exemplo 17.1 mostra o esquema mínimo necessário para criar e usar uma classe.
Exemplo 17.1
>>> class MinhaClasse:
... # corpo da classe
... pass # este comando não faz nada, mas garante a existência da classe
No corpo da classe você define os atributos e métodos conforme necessário – para que sejam
definidos de modo inteligente e funcional é preciso um bom levantamento de necessidades e planejamento
prévios.
Agora vamos criar uma classe que possua atributos e funcionalidades. Observe com atenção o
exemplo 17.2. No exemplo primeiramente definimos a classe Retangulo. Em seguida, na parte principal
do programa, ela é usada para criar os objetos r1 e r2.
Exemplo 17.2
class Retangulo:
def __init__(self, base, altura):
self.base = base
self.altura = altura
def calc_area(self):
return self.base * self.altura
def exibe(self):
print(f'retângulo {self.base} x {self.altura}')
print(f' área = {self.calc_area()}')
r1: retângulo 12 x 5
área = 60
r2: retângulo 3.5 x 9.0
área = 31.5
Vamos agora usar esse exemplo para aprofundar um pouco mais no entendimento das classes.
Esse método tem as finalidades de: alocar memória para o objeto, executar quaisquer tarefas
necessárias à configuração elementos dentro do objeto, bem como inicializar atributos com valores
passados pelo chamador.
Em linguagens como C++ e Java o programador deve escrever esse método, o qual terá o mesmo
nome da classe. Em Python é um pouco diferente, pois nós não escrevemos o método construtor. O
interpretador Python cuida disso para nós.
O processo de instanciação do Python é acionado sempre que fazemos a criação de uma nova
instância, ou seja, um objeto. Este processo é executado em duas etapas separadas que pode ser descrito
da seguinte forma:
• Criação da nova instância da classe: para isso usamos o método .__new__() que é
responsável pela criação e retorno de um novo objeto vazio. Depois da criação do objeto vazio ele
poderá ser referenciado através do identificador self;
• Inicialização da nova instância: para isso há o método especial .__init__() que usa o novo
objeto recém-criado e inicializa seus atributos com os argumentos que foram passados.
self, o que é isso?
Retornando ao exemplo 17.2, vemos que o construtor da classe Retangulo foi usado duas vezes
quando escrevemos essas linhas:
r1 = Retangulo(12, 5)
r2 = Retangulo(3.5, 9.0)
Essa chamada realiza a construção do objeto e como parte do processo o método .__init__()
será executado, sendo que ele tem esta sintaxe:
Depois que os objetos r1 e r2 foram construídos eles podem ser usados livremente pelo
programador, sendo que o acesso a seus membros – sejam atributos ou métodos – podem ser feitos com o
uso da notação com ponto: <objeto>.<elemento>. O restante do código do exemplo 17.2 nos mostra esse
uso.
r1.exibe() # uso do método .exibe() com a instância r1
class Retangulo:
def __init__(self, base, altura):
self.base = base
self.altura = altura
def calc_area(self):
return self.base * self.altura
def exibe(self):
print(f'retângulo {self.base} x {self.altura}')
print(f' área = {self.calc_area():.3f}')
"""Programa principal
usa o módulo m_exresolvido_17_1 que está no pacote cap17"""
from cap17.m_exresolvido_17_1 import Retangulo
ret = Retangulo(0,0)
s = input('Digite dois reais (base altura): ')
while s.upper() != 'FIM':
valores = s.split()
ret.base = float(valores[0])
ret.altura = float(valores[1])
ret.exibe()
s = input('Digite dois reais (base altura): ')
print('\nFim do Programa')
Fim do Programa
Neste exercício resolvido aproveitamos a classe Retangulo criada no exemplo 17.2, separando-a
em um módulo que foi colocado dentro de um pacote denominado cap16, para melhor organização de
todos os códigos deste capítulo. No programa principal fizemos a importação desse módulo e usamos a
classe Retangulo conforme pode ser visto acima e no vídeo deste exercício.
Agora vamos fazer uma melhoria na classe Retangulo. Veja o exercício resolvido 17.2
def calc_area(self):
return self.base * self.altura
def exibe(self):
if self.base <= 0:
raise ValueError('Base: valor numérico maior que zero esperado')
if self.altura <= 0:
raise ValueError('Altura: valor numérico maior que zero esperado')
print(f'retângulo {self.base} x {self.altura}')
print(f' área = {self.calc_area():.3f}')
"""Programa principal
usa o módulo m_exresolvido_16_2 que está no pacote cap16"""
from cap17.m_exresolvido_17_2 import Retangulo
ret = Retangulo(0,0)
s = input('\nDigite dois reais (base altura): ')
while s.upper() != 'FIM':
valores = s.split()
try:
ret.base = float(valores[0])
ret.altura = float(valores[1])
ret.exibe()
except ValueError as e:
print(f'Erro! {e}')
s = input('Digite dois reais (base altura): ')
print('\nFim do Programa')
Digite dois reais (base altura): 3.3 5.6
retângulo 3.3 x 5.6
área = 18.480
Fim do Programa
A novidade neste exercício resolvido 17.2 são essas linhas inseridas no método .exibe() da classe
Retangulo. Elas fazem a verificação do valor e se necessário levantam uma exceção ValueError.
if self.base <= 0:
raise ValueError('Base: valor numérico maior que zero esperado')
if self.altura <= 0:
raise ValueError('Altura: valor numérico maior que zero esperado')
No programa principal, por sua vez usamos o comando try-except para interceptar a exceção
levantada e exibir a mensagem para o usuário. Mais adiante vamos aprimorar esse tratamento de erro
usando uma técnica mais aprimorada disponível nas classes Python.
Vamos criar uma classe capaz de conter dados de veículos que estejam à venda em uma loja. As
informações que queremos manipular são: placa, modelo, ano e quilometragem. Os dados estão
disponíveis em um arquivo do tipo CSV no qual cada linha é um veículo. Eles deverão ser lidos, carregados
na classe que vamos criar e essa classe deve ser inserida em uma lista. A figura 17.1 mostra a estrutura do
arquivo que será usado como entrada de dados para os testes.
fonte o Autor
Primeiramente vamos criar a classe contendo atributos para os dados necessários. Além dos quatro
atributos vamos criar também o método .exibe() que poderá ser usado para exibição dos dados do
veículo na tela.
class Veiculo:
def __init__(self, placa, modelo, ano, km):
self.placa = placa
self.modelo = modelo
self.ano = ano
self.km = km
def exibe(self):
print(f'Veículo placa {self.placa}')
print(f' {self.modelo}, ano: {self.ano} - km: {self.km}')
(módulo)
Com a classe pronta, podemos escrever o programa principal que ficará da seguinte forma:
Fim do Programa
Observe esse código e vamos entender o que está sendo feito: o trecho a seguir usa os dados
contidos no string s que vieram da leitura do arquivo. O método .split() faz a separação das partes
contidas na linha lida, usando o caractere ';' como separador. Em seguida o objeto da classe Veiculo é
criado.
s = s.split(';')
v = Veiculo(
s[0], # placa
s[1], # modelo
int(s[2]), # ano
int(s[3]) # km
)
Na criação do objeto foi usado o construtor da classe com os quatro argumentos que vieram do
arquivo. O ano e a quilometragem foram convertidos para inteiros ao serem passados para a classe.
Com os dados já carregados o passo seguinte foi fazer a exibição em tela com este código:
for v in LstV:
v.exibe()
Como os elementos da lista LstV são da classe veículos, então basta criar uma iteração com essa
lista e cada objeto atribuído a v contém o método .exibe(). Basta executá-lo e os dados serão exibidos
em tela, no formato especificado dentro da classe.
No exemplo 17.4 implementamos uma nova solução para o exemplo anterior, trocando a lista LstV
pelo dicionário DictV. Neste caso precisamos escolher alguma chave para o dicionário e a placa do
veículo é a candidata perfeita, uma vez que não há dois veículos com a mesma placa.
Exemplo 17.4
from cap17.m_exemplo_17_3 import Veiculo
Fim do Programa
Se você comparar esta solução com a anterior verá que as mudanças são bem pequenas. A leitura do
arquivo e a criação do objeto da classe Veiculo são a mesma coisa nas duas soluções.
A primeira diferença acontece na inserção do objeto Veiculo no dicionário, que é feita com a linha:
DictV[s[0]] = v # inclui o objeto v no dicionário
Como o elemento s[0] contém a placa do veículo, então ele foi usado como chave. E o valor
atribuído a essa chave é v, o objeto criado com a classe Veiculo.
Replique e execute esses dois exemplos, 17.3 e 17.4, faça seus testes e constate como é prático
combinar classes personalizadas com listas e dicionários.
No entanto, às vezes usar uma classe não é a melhor solução. Há situações em que algumas funções
são suficientes para resolver um determinado problema.
A linguagem Python é multiparadigma, ou seja, ela permite que adotemos diferentes abordagens de
programação, tais como: programação procedural, orientação a objetos e até mesmo programação
funcional. Assim, você deve ter em mente que em Python você pode escolher não usar classes quando elas
não forem necessárias.
• Quando você apenas precisa armazenar dados: nestes casos use tuplas, listas ou dicionários,
que são especialmente projetados para armazenar dados. Então, eles podem ser a melhor
solução se você não precisar de funcionalidades associadas a esses dados;
• Quando você apenas precisa executar uma tarefa: nestes casos escreva uma função. Se sua
classe tiver um, dois, enfim, uns poucos métodos e não fortemente relacionados a dados, então
talvez você não precise de uma classe. Em vez disso, use funções comuns.
Você encontrará várias situações em que talvez não seja necessário ou uma boa abordagem usar
classes em seu código. Por exemplo:
• Um programa pequeno e simples que não requer estruturas de dados ou lógica complexas. Em
casos assim usar classes pode ser um exagero.
• Um programa para situações críticas em termos de desempenho e performance. As classes
adicionam sobrecarga ao seu programa, especialmente quando você precisa criar muitos
objetos. Isso pode afetar o desempenho geral do seu código.
• Em sistemas legados (programas antigos que ainda estão em uso). Se um sistema antigo usado
em produção não usa classes, então não é uma boa ideia introduzi-las em eventuais
manutenções. Isso quebrará o estilo de codificação usado e poderá gerar uma quebra de
consistência no código.
Não é porque Python suporta orientação a objetos que precisamos adotar o paradigma. Precisamos
conhecer o paradigma e suas ferramentas e em cada projeto usar discernimento e bom senso para decidir.
E sempre lembrar de um dos princípios de Python: simples é melhor que complexo.
E talvez programadores que já conhecem essas outras linguagens e estão acostumados com os
modificadores de acesso, vão considerar isso muito estranho. Mas vamos entender como Python trata a
visibilidade dos membros de uma classe.
Python tem uma convenção de nomenclatura bem estabelecida que deve ser usada para declarar se
um membro de classe, atributo ou método, pode ser usado fora da classe ou não. Essa convenção de
nomenclatura consiste em adicionar um sublinhado inicial ao nome do membro. Então, teremos:
Nas convenções de Python, os membros públicos constituem a parte da interface oficial da classe.
Essa interface será normalmente utilizada pelos programadores que fazer uso da classe nos seus
programas. Por outro lado, os membros não-públicos não se destinam a fazer parte dessa interface. Isso
significa que você não deve usar membros não públicos fora da classe que os define.
Os membros não públicos existem para dar suporte à implementação interna da classe. Por exemplo,
atributos não-públicos podem armazenar dados temporários e métodos não-públicos podem realizar parte
de uma tarefa, mas não a tarefa toda. E seus detalhes de implementação só interessam a quem
desenvolveu a classe, não a quem usa a classe.
Além disso, todo pacote de classes evolui ao longo do tempo com o lançamento de versões com
melhorias e novos recursos. Nessas novas versões os membros públicos serão mantidos, mas nada
garante que os membros não-públicos ainda estarão lá e se você usá-los seu código "vai quebrar".
Outro aspecto é que você pode descobrir a existência de um determinado membro não-público e
sentir-se tentado a usá-lo. Mas você não saberá o que ele faz, porque não haverá documentação para ele.
Os desenvolvedores de classes não tem a obrigação de documentar membros não-públicos, quase nunca
o fazem e com isso você não terá referências. O exemplo 17.5 ilustra uma situação dessas.
Neste exemplo você precisa usar uma lista. Você a cria e faz uma inspeção usando a função dir() e
descobre um membro não público com o nome __sizeof__ e supõe que esse método irá retornar o
tamanho da lista. Ao usá-lo você recebe de retorno um valor completamente fora do esperado. Com isso
constata que não está fazendo certo. Use um elemento público da linguagem como len(lista) e você
terá o resultado que necessita.
Exemplo 17.5
>>> lista = [34, 5, 16, 41, 27] # lista com 5 inteiros
>>> dir(lista)
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__',
'__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__',
'__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__',
'__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__',
'__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear',
'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse',
'sort']
>>> print(lista.__sizeof__()) # você tenta usar __sizeof__()
88 # e não entende o valor que ele retorna
>>> lista = [34, 5, 16, 41, 27, 63, 19, 44] # em seguida aumenta a lista
>>> print(lista.__sizeof__()) # tenta usá-lo novamente
104 # e continua a não entender o que ele faz
Exemplo 17.6
>>> class MinhaClasse:
def __init__(self, valor):
self.valor = valor
def __fazalgo(self):
print(f'Valor = {self.valor} em método fazalgo')
def exibevalor(self):
print(f'Valor = {self.valor} em método exibevalor')
>>> dir(MinhaClasse)
['_MinhaClasse__fazalgo', '__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__',
'__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'exibevalor']
Veja que a deformação do nome de .__fazalgo() realmente ocorreu (destaque em negrito). Como
consequência dessa transformação, ao tentarmos usar esse método, o interpretador Python retorna um
AttibuteError – o método não foi encontrado.
Este comportamento interno oculta o nome, gerando uma ilusão de que o membro da classe se
tornou privado, mas não é verdade. Eles ainda podem ser acessados através do nome deformado,
conforme mostrado na linha final do exemplo acima.
Porém é um recurso muito útil quando estamos estudando uma classe e seus objetos, buscando
entender seu funcionamento e conhecer seus membros.
Observe com atenção o exemplo 17.7 no qual foi criada a classe Exemplo, a partir da qual criamos o
objeto obj. Em seguida usamos __dict__ para inspecionar ambos.
Exemplo 17.7
>>> class Exemplo:
def __init__(self, dado):
self.dado = dado
def calculo(self):
a = 10
self.b = 5
return self.dado + a + self.b
>>> id(Exemplo.calculo)
2193035756064 # converta esse valor para hexadecimal e você obterá 1FE9B1E0220
(exemplo interativo feito com IDE Idle - destaques em negrito feitos pelo autor)
1. Com este código criamos o objeto obj. O valor 23 é atribuído a self.dado no método
.__init__().
obj = Exemplo(23)
2. Exibindo o __dict__ constatamos a associação entre o nome dado e o valor 23.
obj.__dict__
{'dado': 23}
3. Executamos o método público .calculo(). Esse método usa um objeto local a que existirá apenas
dentro do método e também usa o atributo self.b que é inserido no dicionário de atributos de obj.
Com isso, podemos constatar que um atributo será criado apenas depois de usado uma primeira vez.
Constatamos também que objetos não qualificados com self não são inseridos no dicionário de
atributos.
print(obj.calculo())
38
obj.__dict__
Nas seções a seguir vamos usar o dicionário de atributos para nos aprofundar no conhecimento sobre
os atributos de objetos e classes.
Para um atributo existir basta que ele seja criado através de uma atribuição como essa:
self.atributo = <valor atribuído>
Essa atribuição pode ser feita em qualquer método da classe, embora, em geral, isso seja feito no
método .__init__().
Volte ao exemplo 17.7 da seção anterior e observe que na classe Exemplo dois atributos são criados.
O atributo self.dado é criado no método .__init__() e o atributo self.b é criado no método
.calculo().
Vamos fazer agora um novo exemplo para demonstrar e reforçar esse aspecto da POO com Python e
também fazer algumas considerações sobre consistência na criação de atributos. Considere a classe
Produto criada no exemplo 17.8 a seguir.
Exemplo 17.8
>>> class Produto:
def __init__(self, codbarras, nome):
self.codbarras = codbarras
self.nome = nome
self.estq = 0
def cad_custo(self, custo):
self.custo = custo
def exibe(self):
print(f'Produto: {self.nome} ({self.codbarras})')
print(f' preço de custo: R$ {self.custo}')
prod.exibe()
File "<pyshell#35>", line 10, in exibe
print(f' preço de custo: R$ {self.custo}')
AttributeError: 'Produto' object has no attribute 'custo'
>>> prod.cad_custo(7.23)
>>> prod.__dict__
{'codbarras': '7893316002386', 'nome': 'Arroz 1kg', 'estq': 0, 'custo': 7.23}
>>> prod.exibe()
Produto: Arroz 1kg (7893316002386)
preço de custo: R$ 7.23
Observe que ao construir o objeto prod os três atributos dentro do método .__init__(),
self.codbarras, self.nome e self.estq são devidamente criados. Os dois primeiros recebem
valores iniciais passados como argumento, e o terceiro é criado com a atribuição do valor zero. Com o
atributo __dict__ constatamos que os três atributos realmente foram criados.
Em seguida, usamos o método .exibe() para mostrar os dados na tela, mas obtivemos um erro do
tipo AtributeError, pois o atributo self.custo ainda não existe no nosso objeto.
Depois de usarmos o método .cad_custo() o atributo self.custo foi criado, de modo que a
partir daí esse quarto atributo passa a constar de __dict__ e o método .exibe() funciona
corretamente.
Perceba então que essa classe tem um problema de projeto. E é um problema severo, uma vez que o
método de exibição dos dados depende da existência do atributo self.custo que não será criado com os
demais no momento da criação da instância da classe Produto. Com certeza essa não é uma boa prática de
programação orientada a objetos em Python. Teria sido muito simples evitar esse problema se o atributo
self.custo tivesse sido criado junto com os outros no .__init__(). E isso poderia ser feito
tranquilamente através da atribuição do valor zero a ele assim como foi feito com o estoque.
Com isso mostramos que atributos podem ser criados em qualquer método da classe, porém
convém tomar cuidado com esse recurso, fazendo um bom projeto de classes sempre com o objetivo de
serem elas sejam corretas e consistentes.
Atributos de instância armazenam dados vinculados a uma instância da classe, ou seja, um objeto.
Eles são definidos dentro de qualquer método existente na classe;
Atributos de classe armazenam dados vinculados à classe e todos os objetos dessa classe
compartilham seu conteúdo. Eles são definidos no corpo da classe, fora dos métodos existentes;
Exemplo 17.9
>>> class Attr:
attrclasse = 9.99
def __init__(self, num, txt):
self.num = num
self.txt = txt
def exibedados(self):
self.outro = 999
print(f'attrclasse = {self.attrclasse}\n' +
f'num = {self.num}\n' +
f'txt = {self.txt}\n' +
f'outro = {self.outro}')
>>> Attr.attrclasse
9.99
>>> objA.attrclasse
9.99
>>> objB.attrclasse
9.99
>>> Attr.attrclasse = 1989.95
>>> Attr.attrclasse
1989.95
>>> objA.attrclasse
1989.95
>>> objB.attrclasse
1989.95
(exemplo interativo feito com IDE Idle)
Os atributos de instância são definidos dentro dos métodos de instância, conforme vimos na seção
anterior 17.8, os métodos são aqueles que recebem self como primeiro argumento e é neles que os
atributos de instância são criados.
Lembre-se
No exemplo 17.10 definimos uma classe que contém três atributos de instância e em seguida a
utilizamos para criar três objetos com diferentes dados
Exemplo 17.10
>>> class Musica:
def __init__(self, titulo, artista, album, ano, genero):
self.titulo = titulo
self.artista = artista
self.album = album
self.ano = ano
self.genero = genero
def exibir(self):
s = '{}\n {}\n {}\n {}\n {}'
print(s.format(self.titulo, self.artista,
self.album, self.ano, self.genero))
>>> m1 = Musica('Time', 'Pink Floyd', 'The dark side of the moon', 1973, 'Rock')
>>> m2 = Musica('Óculos', 'Paralamas do Sucesso', 'O passo do Lui', 1984, 'MPB')
>>> m3 = Musica('Evidências', 'Chitãozinho e Xororó', 'Cowboy do Asfalto', 1990,
'Sertanejo')
>>> m1.titulo
'Time'
>>> m2.titulo
'Óculos'
>>> m3.titulo
'Evidências'
>>> m1.exibir()
Time
Pink Floyd
The dark side of the moon
1973
Rock
>>> m2.exibir()
Óculos
Paralamas do Sucesso
O passo do Lui
1984
MPB
>>> m3.exibir()
Evidências
Chitãozinho e Xororó
Cowboy do Asfalto
1990
Sertanejo
(exemplo interativo feito com IDE Idle)
Nesta classe são definidos 5 atributos de instância dentro do método .__init__() que são
inicializados com os valores passados como argumentos ao construtor da classe Musica().
Em seguida, criamos três instâncias m1, m2 e m3, que são objetos da classe Musica, cada um
contendo seu próprio conteúdo.
Note que para acessar um atributo de instância, dentro de um dos métodos da classe, é preciso usar
a notação com a palavra-chave self, desta forma: self.nome_do_atributo. Essa notação foi usada
nos dois métodos existentes na classe Musica.
E depois, fora da classe, para acessar qualquer um de seus atributos usa-se a notação
nome_do_objeto.nome_do_atributo.
Se você já conhece programação orientada a objetos em C++ ou Java, então saiba que os atributos de
classe em Python são semelhantes aos atributos estáticos dessas outras duas linguagens.
Os atributos de classe são definidos diretamente no corpo da classe, fora de qualquer método. Eles
estão vinculados à própria classe e são compartilhados por todos os objetos (instâncias) dessa classe.
Todos os objetos criados a partir de uma classe compartilham os mesmos atributos de classe com
seus valores originais. Por causa disso, se você alterar um atributo de classe, essa alteração afetará todos
os objetos derivados.
Por exemplo, digamos que você deseja criar uma classe que mantenha uma contagem interna das
instâncias que você criou. Nesse caso, você pode usar um atributo de classe como o cont_instancias
criado da forma mostrada no exemplo 17.11.
Exemplo 17.11
>>> class Contador:
cont_instancias = 0
def __init__(self):
Contador.cont_instancias += 1
>>> Contador.cont_instancias
0
>>> Contador() # cria a primeira instância da classe Contador
<__main__.Contador object at 0x000001E3E47FFEF0>
>>> Contador() # cria a segunda instância da classe Contador
<__main__.Contador object at 0x000001E3E47FFF50>
>>> Contador.cont_instancias
2
>>> obj = Contador() # cria a teceira instância da classe Contador
>>> Contador.cont_instancias
3
>>> obj.cont_instancias
3
(exemplo interativo feito com IDE Idle)
Esse objeto cont_instancias está definido no corpo da classe e dentro do método .__init__()
ele é inicializado com zero. A forma de fazer referência ao atributo de classe requer usar o nome da classe
como qualificador da forma como está indicado na linha abaixo:
Contador.cont_instancias += 1 # acrescentamos 1 ao atributo cont_instancias
No exemplo 17.11 a classe foi instanciada 3 vezes de modo que o valor do atributo
cont_instancias é 3 e ele pode ser acessado fora da classe, tanto através do nome da classe, como
através de uma de suas instâncias.
No próximo exemplo, 17.12, usamos o atributo __dict__ para inspecionar os atributos de classe.
Exemplo 17.12
# Parte 1 – Criação da classe e de instâncias dessa classe
>>> class Exemplo:
fruta = 'Laranja'
cor = 'Azul'
def __init__(self, valor):
self.valor = valor
# Criação do objeto 1
>>> obj1 = Exemplo(125)
>>> obj1.fruta
'Laranja'
>>> obj1.cor
'Azul'
>>> obj1.valor
125
# Criação do objeto 2
>>> obj2 = Exemplo(3.75)
>>> obj2.fruta
'Laranja'
>>> obj2.cor
'Azul'
>>> obj2.valor
3.75
Na primeira parte deste exemplo definimos a classe Exemplo que contém dois atributos de classe –
fruta e cor – e um atributo de instância – valor. Em seguida, criamos dois objetos:
• obj1 é criado com o valor 125 e nós o utilizamos para exibir os três atributos;
• obj2 é criado com o valor 3.75 e nós o utilizamos para exibir os três atributos;
• no final usamos a própria classe Exemplo para exibir seus atributos de classe e tudo funciona.
Porém, quando tentamos usar a classe para exibir o valor obtemos uma mensagem de erro. Isso
ocorre porque valor é um atributo de instância e não pode ser acessado através do nome da
classe.
Na segunda parte usamos a classe Exemplo para alterar os dois atributos de classe, atribuindo
novos conteúdos aos atributos fruta e cor. Na sequência mostramos que os dois objetos – obj1 e obj2
– refletem essa alteração, passando a conter os novos valores atribuídos, pois atributos de classe são
compartilhados por todas as instâncias.
Por fim, inspecionamos o dicionário de elementos da classe Exemplo e nele podemos verificar a
presença dos dois atributos fruta e cor, constatando assim que ambos estão ligados à classe e não aos
objetos.
Mas não foi isso que aconteceu, pois a única forma de alterar um atributo de classe é através da
classe. O que acabamos de fazer foi criar um Atributo Dinâmico na instância obj1.
Do que foi visto até aqui, extraímos um novo conhecimento sobre a linguagem Python. Ela possui o
recurso de permitir que atributos e métodos sejam acrescentados de forma dinâmica tanto às classes,
quanto aos objetos.
Este recurso permite agregar novos dados e funcionalidades ao software, permitindo realizar
adaptações a classes existentes conforme necessidades específicas de um determinado projeto.
Por outro lado, devemos ter cuidado com esse recurso para evitar cometer o erro de, acidentalmente,
criar um atributo de instância com o mesmo nome de um atributo de classe previamente existente. Para
evitar fazer isso, sempre poderemos consultar o atributo __dict__ da classe e das instâncias para
verificar se não criamos uma sobreposição de nomes.
Ao fazer uma atribuição como nas linhas a seguir, e supondo que nome_do_atributo ainda não
exista, você estará criando um atributo dinâmico.
objeto.nome_do_atributo
Classe.nome_do_atributo
Para quem já conhece programação orientada a objetos com C++ ou Java sabe da existência do
princípio do encapsulamento de atributos e do acesso através dos métodos getters e setters. Nessas
linguagens temos que:
• método getter: existe para retornar o valor que está armazenado no atributo;
• método setter: existe para que um novo valor possa ser atribuído ao atributo;
A vantagem de usar essa abordagem é que dentro desses métodos pode-se fazer tarefas associadas
com a manipulação dos atributos, por exemplo, validações de seu conteúdo.
Em Python, as propriedades assumem esse papel. O objetivo de usar esse recurso é garantir a
consistência aos valores contidos nos atributos de um objeto.
Vamos ver isso na prática recorrendo à classe Retangulo escrita no exemplo 17.2 e para a qual
desejamos implementar uma melhoria. Assim, criamos o exemplo 17.13. no qual queremos validar os
valores atribuídos ao atributo base para que apenas números positivos sejam aceitos. Para termos um
elemento de comparação não vamos alterar o código para o atributo altura.
Para validar o atributo base vamos implementá-lo como uma propriedade. Veja o código a seguir:
Exemplo 17.13
class Retangulo:
def __init__(self, base, altura):
self.base = base
self.altura = altura
@property
def base(self):
return self._base
@base.setter
def base(self, valor):
if not isinstance(valor, int | float) or valor <= 0:
raise ValueError('Número positivo esperado')
self._base = valor
def calc_area(self):
return self.base * self.altura
def exibe(self):
print(f'retângulo {self.base} x {self.altura}')
print(f' área = {self.calc_area()}')
(exemplo interativo feito com IDE Idle – destaques em negrito feitos pelo autor)
No método __init__() não é necessário fazer qualquer alteração. Ele continua recebendo os dois
valores de base e altura e atribuindo-os aos atributos correspondentes.
Para transformar um atributo em uma propriedade devemos criar os métodos: getter e setter.
decorator
Um decorator em Python é
um modificador de comportamento de uma função
Note que ao criar os métodos getter e setter nossa classe terá dois métodos com o mesmo nome, no
exemplo este nome é base. Porém, cada um é afetado pela aplicação do respectivo decorator.
E é isso mesmo. Essa é uma das formas de criação de getters e setters no Python.
Mas não é qualquer conteúdo que será aceito, apenas números positivos. Assim, antes de fazer a
atribuição nós fazemos a validação do valor recebido: se for inválido levantamos uma exceção da classe
ValueError com uma mensagem apropriada. O valor será inválido se acontecer uma de duas situações:
Caso ambas as condições acima sejam falsas, então o valor é válido e será atribuído a _base.
É importante não confundir o atributo não público _base com base (com e sem underline). Na forma
como essa classe está escrita, não existe mais um atributo base (sem underline). O nome base existe na
forma de getter e setter e o nome _base existe na forma de atributo.
Agora que as alterações na classe já foram feitas precisamos testar seu funcionamento. Para isso o
código a seguir é a continuação do exemplo 17.13 onde usamos a classe Retangulo.
>>> r1 = Retangulo(-10, 5)
Traceback (most recent call last):
File "<pyshell#92>", line 1, in <module>
r1 = Retangulo(-10, 5)
File "<pyshell#86>", line 3, in __init__
self.base = base
File "<pyshell#86>", line 12, in base
raise ValueError('Número positivo esperado')
ValueError: Número positivo esperado
A respeito deste código, é importante destacar que a validação da base é feita tanto na atribuição
direta de um valor à propriedade, quanto na construção de um objeto. Assim, atribuições feitas como
abaixo passam pela validação feita no @base.setter:
r1.base = -9 # a validação será feita e o valor negativo não será aceito
E a construção de novo objeto feito com a linha a seguir também passam pela validação feita no
@base.setter
r1 = Retangulo(-10, 5) # neste caso a validação também será feita
O uso de getters e setters é um recurso amplamente usado em programação orientada a objetos e em
Python essa é a forma moderna e mais atual de fazer sua implementação. Existem outras formas, mas por
serem mais antigas e menos usadas atualmente, não serão abordadas neste texto. Caso queira conhecer
mais sobre isso pesquise sobre a função property. Um ótimo ponto de partida para isso é este link da
referência de Python docs https://docs.python.org/3/library/functions.html#property
Os métodos mágicos são exatamente esses métodos que iniciam e terminam com duplo underline.
É óbvio que não há qualquer mágica envolvida, mas eles ganharam essa denominação porque são
métodos que realizam tarefas nos bastidores. A documentação oficial de Python afirma literalmente que
"Método Mágico" é um termo informal para "Método Especial".
Eles existem para executar um código que um programador Python não sabe que está sendo
executado, a menos que seja um profissional avançado que conhece e que seja capaz de escrever classes
Python. Um programador que apenas utiliza as classes existentes não toma conhecimento daquilo que os
métodos mágicos são capazes de realizar. Em termos práticos, você estudante deste curso, já usou mais
métodos mágicos do que é capaz de imaginar, e nem sabia que eles estavam lá.
E isso é bom, pois se você foi capaz de usar tais recursos sem saber de sua existência, isso ocorreu
porque Python está construído de uma maneira inteligente e funcional a tal ponto que permitiu essa
situação. Vamos ver no exemplo a seguir quando você usou métodos mágicos sem saber:
Neste exemplo, definimos o objeto texto que é da classe str e fizemos três operações com ele:
exibimos seu conteúdo de duas formas e exibimos seu comprimento. Na prática o que aconteceu foi:
• exibição usando a função print(): nos casos de uso que envolvem a função print() o
interpretador Python tem o comportamento padrão de usar o método mágico .__str__() para
determinar o que deve ser exibido na tela;
• exibição usando diretamente o nome do objeto: este é um caso que se aplica aos ambientes
interativos e o interpretador Python tem o comportamento padrão de usar o método
.__repr__(), sendo que "repr" vem de "representação do objeto" – a documentação afirma que
este método é destinado aos desenvolvedores e seu objetivo é permitir que o programador acesse
e tenha conhecimento de detalhes da classe;
• exibição do tamanho usando a função len(): nos casos de uso que envolvem a função len() o
interpretador Python tem o comportamento padrão de usar o método mágico .__len__() para
determinar o tamanho. Esse tamanho retornado por len() sempre se refere à quantidade de
elementos contidos em um objeto e se aplica a objetos compostos como strings, listas, tuplas,
dicionários e conjuntos. Se você escrever uma classe que contenha elementos, então convém
que faça a implementação do método __len__() para seguir o padrão definido na linguagem.
Ainda no exemplo acima, no seu final, fizemos um código para exibir todos os elementos que contém
duplo underline no nome. Dentre esses elementos há aqueles que são atributos e outros são métodos.
Esses são os métodos mágicos implementados na classe str.
Cada classe terá seus próprios métodos mágicos, segundo específicas necessidades de
implementação. Vamos ver isso de forma prática.
Exemplo 17.15
# módulo m_exemplo_17_15.py
class Veiculo:
def __init__(self, placa, modelo, ano, km):
self.placa = placa
self.modelo = modelo
self.ano = ano
self.km = km
def exibe(self):
print(f'Veículo placa {self.placa}')
print(f' {self.modelo}, ano: {self.ano} - km: {self.km}')
def __str__(self):
s = f'Veículo placa {self.placa}\n'
s += f' {self.modelo}, ano: {self.ano} - km: {self.km}'
return s
def __repr__(self):
s = 'instância da classe Veiculo,\n'
s += ' carregada com os seguintes dados:\n'
s += f' . {self.placa}\n'
s += f' . {self.modelo}\n'
s += f' . {self.ano}\n'
s += f' . {self.km}'
return s
def __len__(self):
return 1
(módulo)
No exemplo 17.15 mostramos a criação de uma classe na qual implementamos os três métodos
mágicos .__str__(), .__repr__(), .__len__(). Essa classe é uma variação da classe Veiculo
Agora que já conhecemos o conceito de métodos mágicos, vamos mostrar como eles podem ser
usados para aprimorar a criação de getters e setters.
17.14 DESCRITORES
Outra forma de criar getters e setters é usando um elemento de Python conhecido como descritor. Os
descritores são um recurso poderoso para criar atributos gerenciados.
Você deve ter percebido que aplicamos o conceito de atributos gerenciados apenas ao atributo base
da classe Retangulo. Propositalmente, deixamos o atributo altura de lado, para incluí-lo agora. Com
essa inclusão vamos mostrar como os descritores podem ser interessantes e poderosos.
Veja abaixo como fica o Exemplo 17.13 ao transformar o atributo altura em uma propriedade.
@property
def base(self):
return self._base
@base.setter
def base(self, valor):
if not isinstance(valor, int | float) or valor <= 0:
raise ValueError('Número positivo esperado')
self._base = valor
@property
def altura(self):
return self._altura
@altura.setter
def altura(self, valor):
if not isinstance(valor, int | float) or valor <= 0:
raise ValueError('Número positivo esperado')
self._altura = valor
def calc_area(self):
return self.base * self.altura
def exibe(self):
print(f'retângulo {self.base} x {self.altura}')
print(f' área = {self.calc_area()}')
(exemplo interativo feito com IDE Idle – destaques em negrito feitos pelo autor)
Compare os trechos em azul (propriedade base) e verde (propriedade altura) e verifique como os
dois trechos de código são essencialmente iguais, com a óbvia distinção apenas quanto ao nome da
propriedade. É muito comum que isso aconteça quando estamos desenvolvendo classes com certo grau de
complexidade, que tem a necessidade de muitas propriedades e existe a grande chance de várias delas
terem comportamento e funcionalidades parecidas, diferenciando-se apenas quanto à natureza do valor
contido.
Em virtude da frequente e repetitiva ocorrência de trechos de código assim é possível tornar o código
mais compacto se forem usados os descritores (em inglês, descriptors).
Um descritor é uma classe que segue o chamado protocolo de descritor (em inglês, descriptor
protocol). Para seguir esse protocolo, basta que na classe seja implementado um dos métodos mágicos
listados abaixo:
__get__(self, obj, type=None) # deve retornar um objeto (getter)
__set__(self, obj, value) # deve armazenar o valor passado em value (setter)
__delete__(self, obj) # executado na exclusão do objeto (deleter)
Uma classe que implemente pelo menos um destes métodos será um descritor.
Por outro lado, um descritor pode ser embutido em uma classe e desse modo permitir a criação de
propriedades de uma maneira mais compacta e sem redundância de código.
Adicionalmente para que um descritor seja efetivamente útil é preciso que a classe que o contém
tenha acesso ao nome do objeto (instância) e para isso deve-se, também, implementar o método
__set_name__().
__set_name__(self, owner, name) # proverá acesso ao nome da instância no código
Assim, vejamos como isso é feito em termos práticos, através do exemplo 17.16
Exemplo 17.16
# módulo m_exemplo_17_16.py
class NumeroPositivo:
def __set_name__(self, owner, name):
self._name = name
class Retangulo:
base = NumeroPositivo()
altura = NumeroPositivo()
def calc_area(self):
return self.base * self.altura
def exibe(self):
print(f'retângulo {self.base} x {self.altura}')
print(f' área = {self.calc_area()}')
No código acima, foi declarada a classe NumeroPositivo que é o descritor a ser usado para
implementar os getters e setters da classe Retangulo. Nesta classe temos os seguintes detalhes:
Para implementar uma propriedade com o uso do descritor é preciso criar um atributo de classe
desta forma:
base = NumeroPositivo()
Esse atributo passa a ser uma instância da classe NumeroPositivo e, por consequência, passa a
ser uma propriedade, ou atributo gerenciado. O mesmo vale para o atributo altura.
E assim, com um código mais compacto temos dois atributos gerenciados que se comportam de
modo semelhante, porém armazenando dados diferentes. Isso pode ser visto no programa principal
mostrado abaixo para testar o funcionamento dessa versão da classe Retangulo.
class Derivada(Base):
# todo o código da classe herdeira vem aqui
Nessa relação a classe Base é chamada de classe superior e a classe Derivada e chamada de
herdeira. Esta construção implementa o que é conhecido como uma herança simples, pois a classe
herdeira tem apenas uma classe superior.
Em Python, uma classe pode ter várias classes superiores, caracterizando o que se conhece como
herança múltipla. Este aspecto não será abordado neste texto.
O exemplo 17.17 mostra um exemplo bastante simples e seu objetivo é ilustrar uma implementação
de relação de herança entre uma classe base e duas classes herdeiras. Pela simplicidade do exemplo não é
possível extrair todo o potencial da herança, mas ele serve para ilustrar a forma de implementação de
herança em Python.
Exemplo 17.17
# módulo m_exemplo_17_17.py
class FormaGeometrica:
def __init__(self):
self.tipo_forma = 'forma não definida'
def exibe_tipo_forma(self):
print(self.tipo_forma)
class Circulo(FormaGeometrica):
def __init__(self, raio):
self.tipo_forma = 'Círculo'
self.raio = raio
def exibe_dados(self):
super().exibe_tipo_forma()
print(f' detalhes: raio = {self.raio}')
class Retangulo(FormaGeometrica):
def __init__(self, base, altura):
self.tipo_forma = 'Retângulo'
self.base = base
self.altura = altura
def exibe_dados(self):
super().exibe_tipo_forma()
print(f' detalhes: {self.base} x {self.altura}')
(módulo)
Em seguida são definidas duas classes especializadas: Circulo e Retangulo. Essas duas classes
são herdeiras da classe FormaGeometrica e implementam dois métodos .__init__() e
.exibe_dados().
Em cada uma dessas classes o método .__init__() tem argumentos específicos conforme o
caso. O método .exibe_dados() de cada uma delas, por sua vez, utiliza o método .tipo_forma().
Note que as classes especializadas não possuem tal método definido em seu próprio código. No entanto,
essas classes possuem esse método através da herança, pois ele está definido na classe base.
No código abaixo criamos e usamos um objeto de cada uma das classes contidas no módulo
m_exemplo_17_17.py.
>>> c = Circulo(4.4)
>>> c.exibe_dados()
Círculo
detalhes: raio = 4.4
>>> f = FormaGeometrica()
>>> f.exibe_dados()
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
f.exibe_dados()
AttributeError: 'FormaGeometrica' object has no attribute 'exibe_dados'
>>> f.exibe_tipo_forma()
forma não definida
Como mostrado na segunda parte do exemplo 17.17, no programa principal podemos usar tanto a
classe base como as classes herdeiras, desde que respeitemos os métodos existentes em cada uma.
Capítulo 18
USO DE PYTHON COM SQLITE 3
Anteriormente neste curso vimos como usar arquivos texto para armazenar dados de forma
permanente. Neste capítulo vamos trabalhar com o gerenciador de banco de dados SQLite 3, que
representa uma outra forma de armazenamento de dados.
Um dos objetivos deste capítulo é mostrar como um bom conjunto de módulos de funções amplia
grandemente a capacidade de Python 3 ser usada como uma ferramenta de programação, poderosa e
produtiva.
Como consequência desse desenvolvimento surgiram duas vertentes distintas dessa categoria de
software, cada uma com suas especificidades e características. Elas são conhecidas de forma genérica
por:
• Bancos de Dados relacionais: neste tipo de gerenciador é usada uma linguagem conhecida como
SQL, que é a abreviação de Structured Query Language;
• Bancos de Dados não-relacionais: também conhecidos como NoSQL, pois nesse tipo de
gerenciador não é usada a linguagem SQL;
Existem vários SGBD's SQL como: Oracle, Microsoft SQL Server, MySQL, etc. A lista é bem grande e o
SQLite é um deles. Alguns deles são softwares proprietários e licenças precisam ser adquiridas para que se
possa utilizá-los. Por outro lado, há os que são softwares livres e podem ser usados sob determinadas
condições, a depender da licença de software livre sob a qual é disponibilizado.
Neste capítulo vamos ver como utilizar o SQLite em conjunto com a linguagem Python 3. Com isso,
você terá uma visão geral de como é possível usar Python para interagir com gerenciadores de bancos de
dados. Os conceitos essenciais vistos com SQLite se aplicam também a outros SGBDs.
Mas, antes de começar precisamos apresentar e compreender conceitos básicos essenciais dos
gerenciadores de banco de dados relacionais. Então faremos uma pausa em Python e nas próximas seções
apresentamos conceitos de banco de dados para depois seguirmos com o Python.
fonte: o Autor
Esta figura contém os elementos essenciais de uma tabela de banco de dados relacional, a saber:
• Nome da tabela: toda tabela existente na base de dados deve ter um nome de acordo com as
regras de nomes adotadas no SGBD escolhido. Normalmente se utilizam letras, números e o
caractere underline "_". Na Figura 18.1 a tabela se chama Contratos_Aluguel.
• Registro: cada linha da tabela é denominada registro e representa uma coleção heterogênea de
dados interligados. Os registros são subdivisíveis em campos.
• Chave primária: é responsável pela identificação de cada registro no banco de dados. Pode ser
constituída por um ou mais campos, e é obrigatório que seja única e não nula. No exemplo, a
chave primária é o campo NumContrato.
Os bancos de dados podem conter muitas tabelas e recursos que estabelecem relacionamentos
entre as tabelas. Cada tabela pode ter grande quantidade de campos e milhões de registros. Além das
tabelas, os bancos de dados também têm outros elementos, como índices (indexes), visões (views),
gatilhos (triggers), procedimentos armazenados (stored procedures), direitos de acesso (grants) etc.
Para a criação de uma estrutura completa de banco de dados são necessários conhecimentos de
gerais de modelagem de dados e também conhecimentos específicos a respeito do SGBD adotado em uma
aplicação. É bastante coisa a ser aprendida e, como você pode perceber pela leitura desta seção, tal
volume de conceitos requer um tempo considerável de estudo e prática, justificando a existência das
disciplinas de banco de dados nos cursos de desenvolvimento de sistemas. Não há espaço neste curso
para todo esse aprofundamento, mas podemos aprender um conjunto básico de elementos de banco de
dados, o suficiente para que possamos aplicá-lo em muitos exercícios envolvendo a linguagem Python e o
gerenciador de banco de dados SQLite.
Não se trata de uma linguagem de uso geral, como C, Java ou Python. SQL é específica e
exclusivamente utilizada para interagir com bancos de dados relacionais. Seus comandos divididos em
categorias, segundo as operações que realizam. A seguir listamos essas categorias:
• DQL (Data Query Language): nesta categoria há apenas um comando que é o select. O select
é usado para buscar dados que já estão armazenados em tabelas. É o mais usado de todos os
comandos SQL e tem muitas opções e possibilidades. Vamos usá-lo bastante neste capítulo.
• DML (Data Manipulation Language): são os comandos usados para realizar alterações nos
conteúdos das tabelas. Com os comandos dessa categoria é possível fazer inclusões, alterações e
exclusões de dados nas tabelas, a saber: insert, update e delete. Também vamos usá-los
neste capítulo.
• DDL (Data Definition Language): são os comandos usados para manipular a estrutura das tabelas
e seus elementos associados. Eles permitem criar, alterar e excluir tais elementos. Esses
comandos são: create, alter e drop. Também vamos usá-los.
• DCL (Data Control Language): são os comandos utilizados para controlar o acesso aos dados das
tabelas. Com eles é possível definir a visibilidade e os direitos de realizar operações aos usuários
do banco de dados. Não veremos esses comandos.
No que diz respeito aos comandos dessas categorias, há muito a ser aprendido e não temos espaço
suficiente neste curso para tudo. Porém, vamos apresentar o suficiente para que você consiga ter um bom
conhecimento inicial dos comandos e consiga aprofundar-se em estudos posteriores. Faremos a
implementação de diversos programas Python que vão se conectar ao SQLite para realizar tarefas de
criação de tabelas; seu preenchimento e recuperação de dados armazenados; alteração da estrutura de
tabelas; exclusão de tabelas; importação de dados a partir de arquivos externos; entre outras.
Adiante, quando formos usar um comando SQL explicaremos sua estrutura e como ele é usado.
Se você já instalou a linguagem Python no seu computador, o SQLite está instalado também e o que é
melhor: pronto para uso. Como o SQLite acompanha o pacote de Python você não precisará fazer mais
nada: não requer download, não requer instalação, não requer configurações difíceis de entender, não
requer um administrador de banco de dados (DBA) para preparar o ambiente para você.
Por outro lado, não imagine que você vai utilizar o SQLite em aplicações capazes de conter tabelas
com milhões de registros e muitos giga bytes de tamanho ou atender um alto volume de transações e
tráfego com milhares de acessos simultâneos em um segundo. Não, definitivamente, o SQLite não foi
criado para isso.
É disponibilizado na forma de software livre, com código-fonte aberto e está disponível para diversas
plataformas como Windows, Linux, macOS, Android e iOS. Há milhões de aplicações que o utiliza e, como é
nativo em instalações Android e iOS, está instalado em bilhões de equipamentos ao redor do mundo. A
versão mais recente disponível quando este texto foi escrito é a 3.45.1, de 30 de janeiro de 2024 (fonte:
www.sqlite.org, acessado em 10/02/2024).
Para mais informações sobre o SQLite,
acesse seu site oficial
https://www.sqlite.org/
fonte: o Autor
Há muitos IDE's adequados para abrir e explorar um banco de dados SQLite. Neste curso usaremos o
DB Browser for SQLite, que tem a aparência mostrada na figura 18.3.
fonte: o Autor
O uso deste IDE é bastante simples. Faça seu download, instale no seu computador e comece a
explorá-lo. Sua interface está disponível em português e rapidamente você absorverá as operações que são
possíveis com ele. Em caso de dúvida recorra à sua página oficial (em inglês) e você obterá informações que
necessita.
Você poderá baixar o BD Browser for SQLite
a partir do endereço
https://sqlitebrowser.org/
Exemplo 18.1
>>> import os # módulo de interação com o sistema operacional
>>> os.chdir('C:\\CursoPython\\cap18') # troca da pasta default no Idle
Este código simples é responsável pela criação do arquivo do banco de dados, porém ele estará
vazio, como é mostrado no vídeo do exemplo.
O método .connect() do módulo sqlite3 fará a conexão do Python com o arquivo do banco de
dados, caso ele exista. Caso não exista, esse arquivo é criado. O nome do arquivo deve ser fornecido e pode
incluir um caminho qualificado, indicando a unidade de disco e a pasta desejados. Deste modo poderíamos
substituir o uso do método .os.chdir() mostrado acima, pela linha abaixo:
>>> conector = sqlite3.connect('C:\\CursoPython\\cap18\\loja.db')
Toda conexão aberta com o banco de dados, deve ser encerrada com o método .close() para
liberar eventuais recursos alocados.
Agora vamos ampliar um pouco esse exemplo, com a criação de uma tabela e a inserção de alguns
dados.
conector = sqlite3.connect('C:\\CursoPython\\cap18\\loja.db')
# Criação da tabela
cursor = conector.cursor()
sql = """
create table produto
(codigo integer, descr text, preco numeric, qtdeestq integer)
"""
cursor.execute(sql)
# inserção de alguns dados
sql = """
insert into produto (codigo, descr, preco, qtdeestq)
values (1138, 'lápis preto', 1.90, 376)
"""
cursor.execute(sql)
sql = """
insert into produto (codigo, descr, preco, qtdeestq)
values (1251, 'papel sulfite A4 100fls', 7.25, 188)
"""
cursor.execute(sql)
cursor.close()
conector.close()
print('\nFim do Programa')
Fim do Programa
Para executar comandos SQL é preciso criar um cursor. E essa criação é feita a partir do objeto criado
no momento da conexão:
cursor = conector.cursor()
A partir do cursor já criado, usando-se o método .execute() é possível executar os comandos da
linguagem SQL para interagir com o gerenciador de banco de dados. Para que você possa compreender a
execução dos dois comandos SQL envolvidos neste programa destacamos as explicações no quadro 18.1.
Os comandos SQL, em geral, são extensos e é normal escrevermos usando mais de uma linha.
Assim, cada um desses comandos é um string que pode ser atribuído um objeto ou posicionado
diretamente como argumento do método .execute(). No programa usamos o objeto sql para isso.
Observação Importante
fonte: o Autor
Como limitação deste exemplo, destacamos que a tabela criada não contém uma chave primária. A
chave primária é um campo (ou campos combinados) usado para identificação única de cada registro na
tabela. A chave primária não pode ter valor nulo, nem valores repetidos. Na próxima seção criaremos uma
tabela com chave primária.
O arquivo CSV fornecido e que será usado neste exemplo tem o nome "papelaria.txt", contém 30
linhas com produtos e foi gravado com padrão de codificação "utf-8". A figura 18.3 mostra todos os dados
contidos nele. Note que embora o nome do arquivo tenha extensão .txt, a forma como está gravado é
característica de arquivos CSV, com os dados em cada linha separados pelo caractere ';'.
O exemplo 18.2 tem um aspecto diferente do anterior. Neste exemplo a tabela produto será criada
contendo uma chave primária, que será o campo codigo.
fonte: o Autor
O programa do exemplo 18.2 faz a leitura desse arquivo e a importação para o banco de dados.
Exemplo 18.2
import sqlite3
conector = sqlite3.connect('C:\\CursoPython\\cap18\\loja.db')
cursor = conector.cursor()
# exclui a tabela, caso exista
try:
sql = "drop table produto"
cursor.execute(sql)
conector.commit()
except sqlite3.OperationalError:
pass
# cria a tabela
sql = """
create table produto
(codigo integer not null, descr text, preco numeric, qtdeestq integer,
primary key (código))
""" # note a cláusula primary key que define a chave primária
cursor.execute(sql)
sql = """
insert into produto (codigo, descr, preco, qtdeestq)
values(?, ?, ?, ?)
"""
nome_arq = 'C:\\CursoPython\\cap18\\papelaria.txt'
for linha_arq in open(nome_arq, encoding='utf-8'):
dados = linha_arq.rstrip().split(';')
print(dados)
cursor.execute(sql, dados)
conector.commit()
cursor.close()
conector.close()
print('\nFim do Programa')
['10336', 'Apontador metálico para lápis', '4.60', '87']
['11415', 'Bloco adesivo Post-it 3 cores', '9.85', '43']
['10512', 'Caneta marca texto Amarela cx. 12un.', '23.20', '142']
['10513', 'Caneta marca texto Rosa cx. 12un.', '23.20', '28']
['10514', 'Caneta marca texto Verde cx. 12un.', '23.20', '36']
['12184', 'Caderno espiral grande 96fls', '21.35', '135']
Fim do Programa
E para executar esse comandos SQL assim precisamos utilizar um segundo parâmetros no método
.execute(), da seguinte forma:
cursor.execute(sql, lista_de_parametros)
onde o objeto lista_de_parametros deve ser uma sequência – lista ou tupla – contendo a exata
quantidade de parâmetros que irão substituir as interrogações posicionadas no string sql.
A substituição é feita por posição, ou seja, a primeira interrogação receberá o primeiro elemento da
lista, a segunda interrogação receberá o segundo elemento e assim por diante. Por isso, a quantidade de
elementos na lista deve coincidir com a quantidade de interrogações e se isso não ocorrer haverá erro.
Neste programa fizemos a opção de criar a tabela antes de iniciar a importação dos dados. Porém, ao
tentar criar a tabela obteríamos um erro, caso ela já existisse. Para facilitar a execução do programa várias
vezes, durante os testes, teríamos que escolher uma de duas estratégias:
a) Eliminar a tabela existente e, caso ela não exista porque o banco de dados é novo, fazer o
tratamento da exceção na ocorrência de um erro; (esta foi a opção que escolhemos)
b) Tentar criar a tabela e tratar a exceção caso a tabela já exista. Nesta opção, e caso a tabela já
exista, teríamos que limpá-la, excluindo todos seus registros antes da importação;
Por fim, após a execução deste programa teremos a tabela de produtos com 30 registros.
fonte: o Autor
Exemplo 18.3
import sqlite3
conector = sqlite3.connect('C:\\CursoPython\\cap18\\loja.db')
cursor = conector.cursor()
sql = "select * from produto"
cursor.execute(sql)
dados = cursor.fetchall()
cursor.close()
conector.close()
Fim do programa
Este exemplo consiste em fazer um acesso à tabela produto, recuperar todos os seus registros e
depois fazer a exibição em tela. Observe que claramente o programa é dividido em duas partes distintas. Na
primeira parte é feita a conexão com o banco de dados e executada a consulta. Na segunda parte fazemos a
exibição dos dados em tela.
Enquanto os comandos SQL de outras categorias realizam uma ação sem produzir retorno de dados,
o select produz um retorno de dados. Por isso, após o .execute(), precisamos usar um dos métodos
de recuperação de dados (fetch). Existem três opções:
• .fetchall() – retorna de uma única vez uma lista com todos os registros produzidos pelo
select, sendo que cada registro é uma sublista dentro da lista. Deve-se tomar cuidado com esta
opção quando a quantidade de registros retornados é muito grande;
• .fetchmany(size) – retorna uma lista com uma certa quantidade de registros produzidos pelo
select, sendo que cada registro é uma sublista dentro da lista. A quantidade retornada é
especificada pelo argumento size. Com esta opção é possível recuperar sucessivamente um
bloco de size registros por vez;
• .fetchone()– retorna um único registro produzido pelo select, os dados estarão uma lista.
Com esta opção é possível recuperar sucessivamente um registro por vez;
Este programa não cria a tabela. Ele supõe que a tabela já exista.
Quanto ao comando SQL necessário à inserção dos dados não há qualquer novidade, pois é o
mesmo insert into usado no exemplo 18.2.
Exemplo 18.4
import sqlite3
conector = sqlite3.connect('C:\\CursoPython\\cap18\\loja.db')
cursor = conector.cursor()
sql = """
insert into produto (codigo, descr, preco, estq)
values (?, ?, ?, ?)
"""
print('Digite os dados separados por vírgulas')
print('Codigo,Descrição,Preço,Estoque')
Ler = input()
while Ler != '':
D = Ler.split(',')
try:
cursor.execute(sql, D)
conector.commit()
except:
print('{} Dados inválidos'.format(D))
else:
print(' '*30, '...dados inseridos com sucesso')
finally:
print('\nCodigo,Descrição,Preço,Estoque')
Ler = input()
Digite os dados separados por vírgulas
Codigo,Descrição,Preço,Estoque
19010,Produto 19010,19.01,100
['19010', 'Produto 19010', '19.01', '100']
...dados inseridos com sucesso
Codigo,Descrição,Preço,Estoque
19020,Produto 19020,19.02,50
['19020', 'Produto 19020', '19.02', '50']
...dados inseridos com sucesso
Codigo,Descrição,Preço,Estoque
O resultado da execução do código acima pode ser visto na imagem 18.7 a seguir, na qual
evidenciamos que novos registros estão presentes na tabela produto, contendo os dados digitados para
os dois produtos, 19010 e 19020.
fonte: o Autor
Quadro 18.4 – Forma mais geral do comando select com where e order by
Comando Descrição
Este SQL é um DQL (Data Query Language)
Ele é usado para recuperar registros já armazenados em alguma
tabela.
select * from produto
A cláusula where é usada para filtrar registros segundo o critério
where preco < 10
order by descr especificado através de uma ou mais condições que acompanham
a palavra-chave where.
A cláusula order by é usada para ordenar o conjunto de registros
retornados. A ordem pode ser crescente ou decrescente.
No vídeo do exemplo 18.3 acrescentamos essas duas cláusulas e mostramos possibilidades variadas
de filtragem e ordenação. Como no exemplo abaixo onde filtramos quantidades menores que 30 e
ordenamos pela descrição do produto.
Fim do programa
No exemplo 18.5 são incluídos três novos campos na tabela "produto", a saber:
É de se supor que tabelas já existentes contenham registros. Em casos assim, quando um novo
campo é inserido, os registros já armazenados passarão a conter esse campo mas não haverá nenhum
dado para ele, de modo que seu estado será NULL (nulo).
Após a inclusão do novo campo é possível substituir esses valores nulos por algum valor adequado.
No exemplo 18.5, como os campos são numéricos, vamos atribuir o valor zero para os campos inseridos.
O quadro 18.5 mostra os comandos SQL que usaremos nesse exemplo. É importante destacar que o
comando SQL alter table só atua em um campo por vez. Como queremos incluir três campos, teremos
que usá-lo três vezes. O comando update deve ser usado para alterar o conteúdo dos campos e ele pode
atuar sobre vários campos simultaneamente.
Assim, o código do exemplo 18.5 tem duas partes: na primeira os campos são incluídos, um por vez.;
na segunda parte seus valores são atualizados para 0.
Exemplo 18.5
import sqlite3
conector = sqlite3.connect('C:\\CursoPython\\cap18\\loja.db')
cursor = conector.cursor()
# insere os novos campos
sql = "alter table produto add custo numeric"
cursor.execute(sql)
sql = "alter table produto add aliqicms numeric"
cursor.execute(sql)
sql = "alter table produto add qtdemin integer"
cursor.execute(sql)
# atribui 0 aos novos campos
sql = "update produto set custo = 0, aliqicms = 0, qtdemin = 0"
cursor.execute(sql)
conector.commit()
print('Os três campos foram inseridos e inicializados com zero')
cursor.close()
conector.close()
Os três campos foram inseridos e inicializados com zero
fonte: o Autor
Nesse programa não há nenhum aspecto novo no que diz respeito ao Python. São os mesmos
comandos utilizados nos exemplos anteriores para fazer as tarefas típicas de banco de dados, a saber:
conexão com o banco; criação do cursor; execução dos comandos DDL e DML, commit e encerramento da
conexão. Esse programa não interage com o usuário, e ao seu término uma mensagem é mostrada na tela
indicando que o BD foi atualizado.
fonte: o Autor
Note que nesse este comando update é parametrizado com quatro interrogações. Isso implica que
no momento da sua execução precisaremos passar uma lista com quatro valores que serão usados como
parâmetro e a ordem importa, ou seja, a ordem em que cada interrogação aparece no update deve ser
usada na lista. E essa ordem não corresponde à ordem que obteremos ao ler uma linha do arquivo. A ordem
a ser usada é esta:
# indicação da ordem dos argumentos para o update
dados = [custo, aliqicms, qtdemin, codigo]
Com isso após obter a linha do arquivo e fazer sua separação com o método .split() usamos as duas
linhas abaixo para posicionar o codigo no final da linha, removendo-o da primeira posição:
dados = linha_arq.rstrip().split(';')
dados.append(dados[0]) # coloca o código no final da lista dados
del(dados[0]) # elimina o código do início
Exemplo 18.6
import sqlite3
conector = sqlite3.connect('C:\\CursoPython\\cap18\\loja.db')
cursor = conector.cursor()
sql = """
update produto
set custo = ?, aliqicms = ?, qtdemin = ?
where codigo = ?
"""
nome_arq = 'C:\\CursoPython\\cap18\\papelaria_atualiz.txt'
for linha_arq in open(nome_arq, encoding='utf-8'):
dados = linha_arq.rstrip().split(';')
dados.append(dados[0]) # coloca o código no final da lista
del(dados[0]) # elimina o código do início da lista
print(dados)
cursor.execute(sql, dados)
conector.commit()
print('A tabela produto foi atualizada')
cursor.close()
conector.close()
fonte: o Autor
O exemplo 18.7 permanece em laço lendo o código do produto e excluindo o registro correspondente.
Exemplo 18.7
import sqlite3
conector = sqlite3.connect('C:\\CursoPython\\cap18\\loja.db')
cursor = conector.cursor()
sql = "delete from produto where codigo = ?"
excluidos = []
codigo = input('Digite o código a ser excluído: ')
while codigo.upper() != 'FIM':
cursor.execute(sql, [int(codigo)])
excluidos.append(int(codigo))
codigo = input('Digite o código a ser excluído: ')
conector.commit()
Note que dos códigos acima, o 10099 não existe na tabela e nenhum erro ocorre na sua tentativa de
exclusão. Além disso, esse código acaba fazendo parte da lista de códigos que foram excluídos da tabela, o
que não é necessariamente verdadeiro, afinal ele não existe no cadastro. Dá para melhorar um pouco isso.
Fim do programa
Tabela nomeplaylist
Campo Tipo Descrição
Chave primária
nomepl text
Nome da playlist
data text Data de criação da playlist
Tabela playlists
Campo Tipo Descrição
nomepl text Nome da playlist Estes dois campos compõe a
Id integer Id da música chave primária