Expressões Regulares (regex)


Expressões regulares, também chamadas de regex (de regular expression), são meios de descrever padrões que podem ser encontrados dentro de um texto. Os padrões podem ser simples, como a busca de um ou dois dígitos especificados, ou padrões complexos que incluem a posição do padrão no texto, o número de repetições, etc.

Regex são usados basicamente para a busca de um padrão, substituição de texto, validação de formatos e filtragem de informações. Praticamente todas as linguagens de programação possuem ferramentas de uso de regex, assim como grande parte dos editores de texto. As diferentes linguagens de programação e aplicativos que incorporam funcionalidade de regex possuem sintaxes ligeiramente diferentes, cada uma, mas há uma embasamento geral que serve a todas. Existem aplicativos de teste de regex em diversas plataformas e aplicativos online para o mesmo fim.
Exemplos de uso são:

  • filtragem de linhas de um texto longo com determinados caracteres no início, meio ou fim da linha;
  • remoção de tags de um texto escrito em html, com possível seleção dentro de uma tag determinada;
  • validação de texto para representar datas e horas;
  • validação de texto digitado pelo usuário para representar CPF, email, URL, CEP, cartão de crédito, etc;
  • extração de dados de um arquivo csv, json, XML, markdown ou outro formato estruturado qualquer.

Usaremos, por concisão, a expressão sensível (insensivel) ao caso significando sensível (insensivel) à minúsculas e maiúsculas.

Tags do HTML são comandos de formatação de texto envolvidos por dois sinais < >. Alguns exemplos são
<p>um parágrafo</p>, <b>letras em negrito</b>, etc.

Dizemos que um determinado padrão regex é procurado no texto até que ocorra um ou mais “casamentos” (match, em inglês). Por exemplo, o padrão ab é encontrado (e, portanto, casa com) os textos abraço, absolvido ou aberração, mas não casa com asbestos. Se for pedida uma busca insensível ao caso, ele também casa com Abraço ou ABROLHOS. Em todo esse artigo marcaremos as partes “casadas” pela essa formatação especial.

Nos exemplos e tabelas desse artigo há uma certa quantidade de repetições, buscando facilitar o aprendizado. Tabelas enxutas para consultas serão listadas em outra parte.

Caracteres simples ou conjuntos de caracteres (strings comuns) são casados literalmente.

padrão significado exemplo: casa ou não com
a caracter comum a casa Afazer aaa
ab grupos de caracteres comuns ab absoluto abraço Abcd trabalho
em 1945 caracteres alfanuméricos em 1945 a segunda guerra mundial terminou em 1945.

Metacaracteres

Claro que não precisaríamos de nenhuma tecnologia especial para encontrar letras ou conjuntos de letras. Para construir padrões mais complexos usamos os metacaracteres (o que significa que têm significados especiais):
. * ^ $ + ? { } [ ] \ | ( ).

O ponto (.) representa (substitui ou casa com) qualquer caracter único.

padrão casa com não casa com
p.ata prata, plata pata pirata
lu.a luta, luma lua lucca
phylos.net phylos.net, phylos-net (1) phylosnet

(1): O ponto (.) casa com todos os caracteres, inclusive o próprio ponto.

Quantificadores

Os quantificadores * ? + {} permitem o controle de quantas repetições de um padrão ocorrem.

O asterisco * representa zero, uma ou várias repetições do caracter que o precede.

padrão casa com não casa com
Jo*e Je, Joe, Jooe Jose
1*23 23, 123, 1123 1021
.* qualquer número de qualquer caracter

A interrogação ? representa zero ou uma repetição do caracter que o precede.

padrão casa com não casa com
Jo?e Je, Joe Jooe
1?23 23, 123 1123 1021
.? nenhum ou uma ocorrência de qualquer caracter

O sinal de mais + representa uma ou mais repetições do caracter que o precede.

padrão casa com não casa com
Jo+e Joe, Jooe Je
1+23 123, 1123 23 1021
.+ 1 ou mais ocorrências de qualquer caracter


As chaves {n} indicam n repetições do caracter precedente.
{n,} significa mínimo de n repetições do caracter precedente.
{n,m} significa mínimo de n, máximo de m repetições do caracter precedente.

padrão casa com não casa com
A{3} AAA, GAAAA AA AAB
Ot{2}o Otto Oto
1{2}9{3} 11999, A:11999 119, 11199
s{2,} passar, asssb casa, sapo
s{2,4} passar, asssb , sssss casa, sapo
.{5} 5 repetições de qualquer caracter

Os marcadores de posição ou âncoras ^ $ \b \B permitem o controle da posição onde o padrão ocorre.

O circunflexo ^ indica que o padrão seguinte está no início da linha.
O cifrão $ indica que o padrão prévio está no final da linha.
A marca \b indica início ou fim de uma palavra (uma sequência contínua de caracteres). \b marca a passagem de um caracter alfanumérico ou sublinhado \w para um não-caracter \W.
A marca \B, a negação de \b, representa início ou fim de uma palavra envolta em um não-caracter \W.

padrão casa com não casa com
^f filho afazia
^3{2} 333-123 133-7654
s$ bois essência
m{3}$ booommm mmmassa
\bPedro PedroPedro .Pedro LPedro _Pedro
Pedro\b Pedro PedroPedro. PedroL Pedro_
\BMaria LMaria _Maria 3Maria Maria -Maria
Maria\b Maria MariaMaria. MariaL Maria_

Observe que o sinal ^ serve como âncora, para marcar início da string, ou como sinal de negação.
A negação ocorre quando ^ é usado como primeiro sinal dentro de uma lista. Nesse caso ele nega todos os caracteres da lista.

padrão casa com não casa com
^[0-9].* 1989, 5G (1) G5
^[^0-9].* G7, I-99 (2) 5G
[0-9]^ 0^, 1^ (3) 55
^^.* uma linha iniciada por ^
^$ uma linha vazia
.{5}$ casa 5 últimos caracteres de uma linha
^.{15,30}$ casa linhas com 15 até 30 caracteres

(1): O padrão casa com textos que possuem um dígito no início.
(2): O padrão casa com textos que não se iniciam com um dígito.
(3): Qualquer dígito seguido do circunflexo literal ^

Classes de caracteres


Classes de caracteres são marcações que funcionam como “shortcuts”, representando um grupo de caracteres ou controles.
\s representa um espaço simples.
\S representa espaço negado (não é um espaço).
\d representa qualquer dígito. O mesmo que [0-9].
\D \d negado. Qualquer não dígito. O mesmo que [^0-9].
\w caracter alfanumérico e sublinhado.
\W \w negado. Qualquer sinal exceto caracter alfanumérico e sublinhado.

padrão casa com não casa com
\s casadamãeJoana (espaços) casa_da_mãe_Joana
123\s456 123 456 99.123 456.90 123.456__123_456
\S casa “   ”
\d{3}/\d{2} 123/56 987/78 22/3 aaa/bb
\D+ basta 123 0000
\D{3}-\D{2} abc-de aAs-fg1 a2c-de 123-00
\w+ atr3v1d0_b3sta phylos.net — @@@ %%%
\W+ --- @@@ %%% phylos.net atr3v1d0_b3sta
[\b] caracter de backspace
\c caracter de controle

Para procurar por um dos metacaracteres como um literal dentro do texto usamos o escape \.

padrão casa com não casa com
\\s No regex \s casa espaço, número \s987 s ss “ ”
R\$45 R$45 R45
(U\$5|R\$25) U$5, R$25 U$ 5
\\d\(\d{2}\) \d(69), \d(00) \d(123), \d12

Agrupamentos

É possível encontrar um dos caracteres dentro de um conjunto de caracteres usando chaves []. Essa notação permite a descrição de intervalos como [a-z], significando todas as letras minúsculas de a até z, ou [0-9], todos os dígitos.

padrão casa com não casa com
pr[ae]to prato, preto prto, proto
[a-e]rm arm, permanente, ermitão frm, rm
[5-7]00 500, 600, 700 800, 00
c[ep]* c, ce, cp, cep, ceep, ceepp bep, ep
</[bi]*> </b>, </i>, </>, </bi>, /b, /i>
</[bi]{1}> </b>, </i> </>, </bi>


Outros agrupamentos: alguns caracteres de controle permitem o agrupamento de texto e padrões.
(abc) permite o agrupamento simples de caracteres (no caso ‘abc’).
[abc] quaisquer dos caracteres (no caso abc).
[a-z]: qualquer caracter no intervalo de a até z.
[^a-z]: negação de [a-z]. Todos os caracteres exceto aqueles entre a e z.
[0-9]: qualquer dígito entre 0 e 9.
[^0-9]: negação de [0-9]. Tudo o que não é dígito.
a|b: opcional, a ou b.
(padrao1|padrao1): busca por um ou outro padrão.

padrão casa com não casa com
(est) muito estudo, sem estado Estimo, set
[est]ato sato, tato mato
[f-h]ato fato, hato, gato mato, rato, feto
[^f-h]ato mato, rato, lato fato, gato
[^a-c]123 r123, M-123, s/123 a123, b123
[1-5]-[6-9] 1-6, 123-456, A5-6 a1-b2, 6-1
[^1-5]6789 06789, 12345678900, R56789 16789
p(l|r)at platão, prata pato, piato
p(at|len)o pato, pleno patleno, po, plo
p(ratic|ublic|enal)idade praticidade, publicidade, penalidade pcidade, pibcidade
(s{2}|r{2})os carros passos nossos erros casos, cappos, cassas
[0-2][0-9]:[0-5][0-9] 04:20, 12:50 34:30, 11:65

Uma observação importante: dentro de um agrupamento por colchetes ([]) o ponto e todos os demais metacaracteres têm valor literal e não precisam ser “escapados”. A única exceção é o hífen, - que serve para representar intervalos. Para inserir um hífen no padrão agrupado devemos colocá-lo como o último do grupo. Para usar o caracter ] ele deve ser posto como o primeiro elemento para não ser confundido com o fechamento do grupo. Não há restrição sobre onde colocar o [.

padrão casa com não casa com
12[:. ]45 12:45, 12.45, 12 45 1245, 12-45, 12/45
[$€¥]137 $137, €137, ¥137 137, R137
876[/_-]543 876/543, 876_543, 876-543 876 543, 876|543
doid[]oa] doid], doido, doida doid[, doide, doid*
doid[[oa] doid[, doido, doida doid], doide, doid*
9[1-5-][a-f] 92b, 9-b 95, 9-, 91g
[*ab]\d *9, a3, b5 64

Observe que [*] = \*.

Correspondência gananciosa ou preguiçosa

Por default as buscas quantificadas por * + {} são gananciosas (greddy), o que significa que englobam a maior porção de texto casada com o padrão.

Em inglês se usa os termos greddy e lazy, que significam literalmente “ganancioso” e “preguiçoso” para se referir à correspondência do maior ou menor intervalo possível de texto. Em português são comuns as traduções ganancioso e prequiçoso ou guloso e não-guloso.


O padrão <.+> significa ‘todos os caracteres entre os sinais < > e casa com o maior texto possível iniciado por < e finalizado por >, mesmo que o caracter finalizador ocorra nesse intervalo casado. Portanto, se quisermos extrair o texto circundado pela tag div devemos ser capazes de escrever um padrão que é interrompido no primeiro encontro do caracter finalizador >. Essas são as chamadas buscas preguiçosas, o que é possível se obter com o acréscimo do sinal ? após o quantificador.

padrão casa com não casa com
<.+> <div class="classe">Todo o texto dentro da TAG</div> texto sem tags
<.+?> <div class="classe">Todo o texto dentro da TAG</div> texto sem tags
.*(\sR) primeiro R segundo R fim (1) primeiro, segundo, fim
.*?(\sR) primeiro R segundo R fim (2) primeiro, segundo, fim
\d+ 1957 2021 258.1258 abcd
\d+? 1957 2021

(1): .*(\sR) significa “qualquer quantidade de caracteres seguido de espaço, depois um R”.
(2): A versão “prequiçosa” pára na primeira ocorrência de ” R”, depois casa um segundo grupo.

Observe que o padrão .*? significa “zero ou qualquer número de qualquer caracter” e portando não casa com coisa alguma.

Resumindo:

Ganancioso casa
ab* abbbb
ab+ abbb
ab? ab
ab{1,3} abbb
Preguiçoso casa
ab*? a
ab+? ab
ab?? a
ab{1,3}? ab

Grupos

Caracteres dentro de chaves ()são tratados como grupos e casados juntos. Todos os quantificadores e âncoras podem ser aplicadas aos grupos e podemos inclusive aninhá-los (usar um grupo dentro de outro).

padrão casa com não casa com
([a-z]+) casa com palavras de minúsculas
(bem|mal)\sfeito bem feito, mal feito
(bem|mal)?\sfeito bem feito, mal feito, feito
(in|con)?certo incerto, concerto, certo
(in|con)+certo incerto, concerto certo
(\.\d){2} .1.2, .4.5, .0.0 12, 1.2
(www\.)?phylos.net www.phylos.net, phylos.net
(hiper|hipo)(trofia|plasia) hipertrofia, hipotrofia, hiperplasia, hipoplasia plasia, hipo
((su|hi)per)?mercado supermercado, hipermercado, mercado plasia, hipo

Retrovisores

O grupos permitem o uso de retrovisores com a sintaxe (grupo)\1...9. Esse uso de \1...9 (\ seguido de um dígito, 1 – 9) não tem relação com escape mas denota um grupo casado. Ele serve para a reutilização do trecho casado para uma nova busca no texto alvo.

Por ex.:
([a-z]+) casa com palavras de uma ou mais letras minúsculas.
([a-z]+)- casa com palavras de uma ou mais letras minúsculas seguidas de hífen.
([a-z]+)-\1 armazena o texto casado e o procura novamente, uma vez.

padrão casa com não casa com
([a-z]+)-\1 quero-quero, mau-mau, asdfg-asdfg quero
([a-z]+)-?\1 quero-quero, queroquero, bombom, bombomzeiro bom
in(co)lo(r) = sem \1\2 incolor = sem cor
\b([a-z]+)-?\1\b quero-quero, queroquero, bombom (1) bombomzeiro
\b(bo(na|to))\1\b bonabona, botoboto (2) rbotoboto, bonabonas
(rapida)(mente) conseguimos uma \2 \1 rapidamente conseguimos uma mente rapida (3)
(su)d(ão) do \1l n\2 sudão do sul não
(AA)(99)(hh) \3 \1 \2 AA99hh hh AA 99 (4) AA99hhhhAA99
(AA)(99)(hh)\3\1\2 AA99hhhhAA99 (5) AA99hh hh AA 99
(\d{2})(\d{3})(\d{2}) \1-\2-\3 1122233 11-222-33, 9988877 99-888-77 998877 99-88-77
((band)eira)nte \1 \2alheira bandeirante bandeira bandalheira (6)

(1): \b inicial e final significa que o padrão circundado é uma “palavra”.
(2): O padrão casado e repetido é bona ou boto no início e no fim da “palavra”.
(3): o 1º grupo é rapida, o 2º é mente.
(4): os grupos 1, 2 e 3 são capturados na ordem, e repetidos em outra ordem, separados com espaços.
(5): o mesmo que (4), exceto que os grupos são repetidos sem espaços.
(6): ilustrado na figura abaixo.

O retrovisor serve para procurar grupos repetidos. Os grupos são numerados de 1 até 9, contando-se da esquerda para a direita, sendo que o primeiro parêntese encontrado define a ordem do grupo.

Outras técnicas de grupos

Algumas técnicas mais sofisticadas foram implementadas nas expressões regulares, nem todas reconhecidas por todos os editores, IDEs e linguagens de programação. Para isso o metacaracter ainda não utilizado, (?..) ganhou significado de operador em regex.

(?#texto de comentário)
(?:regex): grupo casado mas não armazenado nem incluído na contagem dos grupos.

padrão casa com não casa com
(?#nome)(pa)(pi) \2\1 papi pipa papi papi
(Jó) (?:Alto)- (Rui) \1 \2 Jó Alto Rui- Jó Rui
(?:Z)-(\d{2})-(\d{4}):\2:\1 Z-11-2222:2222:11, Z-45-9876:9876:45
(?:\w)-(\d{3}) \1-\1 a-123 123-123, b-456 456-456 a-123 123-121

padrao(?=regex): não é casado mas determina regex que deve existir após padrao.
(?<=regex)padrao: não é casado mas determina regex que deve existir antes de padrao.

padrão casa com não casa com
casa (?=\d{2}) casa 23, casa 899 casa dez, 852 casa
Pedro(?=\sCa) Pedro Cabral, Pedro Camilo Pedro Barata, Pedroca
\d{4}(?=[A-Z]) , 1234A, 0987H, 6666GGG 354W, G5432, 987G
(?<=Albert) Einstein Albert Einstein Alberto Einstein
(?<=\d{3}) [a-r.]{5} 123 roman, 987 coma. 123 ruela

(?!regex): não é casado mas determina regex que não deve existir após outro padrão.
(?<!regex): não é casado mas determina regex que não deve existir antes de outro padrão.

padrão casa com não casa com
casa (?!\d{2}) casa dez, casa verde casa 12, casa 123
Pedro(?!\sCa) Pedro Bernardo, Pedro Bento, Pedroca Pedro Cabral
\d{4}(?![A-Z]) , 1234890, 0987-987 3354W, 5432H, 987G
(?<!\d{2}) casa naquela casa, outra casa 12 casa, 123 casa
(?<!Pedro) Cabral José Cabral Pedro Cabral
(?<![A-Z])-\d{4} 987-1234, 4-0987 A-3354, H-5432

(?P<nome>regex): grupo casado e nomeado com nome, ao invés de numerado com \1…\9.
Obs.: Essa é a sintaxe usado no Python. Ela pode variar em outros ambientes.

» data= '23 de junho de 2021'
» regex= '^(?P\d{1,2})\sde\s(?P\w+)\sde\s(?P\d{4})'
» matches= re.search(regex, data)

» print('Dia: ', matches.group('dia'))
» print('Mês: ', matches.group('mes'))
» print('Ano: ', matches.group('ano'))
↳ Dia:  23
↳ Mês:  junho
↳ Ano:  2021

(?modificador): modificador é uma ou mais letras que ativam uma funcionalidade, sendo:

Modificador Significado
i busca insensível ao caso
m força o metacaracter . a casar com \n
s obriga as âncoras ^ e $ a casarem com \n
x permite a inclusão de espaços e comentários
L força o uso da localização do sistema (só Python)
u considera a tabela Unicode (só Python)
padrão casa com não casa com
(?i)[a-z]* Pedro, aLLana 654-654
(?i)[A-Z]* Pedro, aLLana 654-654
(?i)\d+\.png 1234.png, 1234.PNG foto.png, 987000.jpg

Precedência de metacaracteres

Quando vários metacaracteres aparecem juntos eles obedecem a uma ordem definida de precedência, definida pela ordem na tabela.

Ordem Tipo Exemplo Significado
0 () (grupo) grupos não quebrado
1 quantificador abc+ ab seguidos de c em qualquer quantidade
2 concatenar abc abc simples
3 | ab|c ab ou c
3 | ab|c ab ou c

Alguns exemplos dessas regras de precedência:

padrão casa com significado
abc+ abc abcc abccc “ab” seguido de 1 ou mais “c”
abc abc abcc abccc “abc”, juntos
(abc) abc abcc abccc “abc”, juntos, em grupo
ab|c abc abc “ab” ou “c”
a(b|c) ab ac abc a abcc accc “a” seguido de “b” ou “c”
ab|cd* ab cd cddd abcdddddddd (1) o mesmo que (ab)|(c(d*))
s/ n/|número \d* s/ n/ número 19000 o mesmo que (s/ n/)|(número (\d*))

Para forçar uma união de caracteres em um grupo inquebrável usamos ().
(1): A concatenação em ab tem prioridade sobre a alternância |. d* ocorre antes da concatenação com c. Portanto ab|cd* é o mesmo que (ab)|(c(d*)).

Caracteres acentuados

Em português e outras línguas européias precisamos criar padrões que incluem caracteres com acentos. Uma alterniva é usar as classes POSIX listadas abaixo. Alternativamente podemos extender grupos de acordo com a tabela ASCII, o que é útil quando POSIX não está disponível.

POSIX alternativa significado
[[:lower:]] [a-zà-ü] minúsculas, acentuadas ou não
[[:upper:]] [A-ZÀ-Ü] maiúsculas, acentuadas ou não
[[:alpha:]] [A-Za-zÀ-ü] minúsculas e maiúsculas, acentuadas ou não
[[:alnum:]] [A-Za-zÀ-ü0-9] todas as letras, acentuadas ou não, e dígitos
padrão casa com significado
(ção)|(ções) noção noções
[à-ü] estúpido eqüinócio
[a-zà-ü]* retratação RETRATAÇÃO
[A-Za-zÀ-ü0-9]* retratação RETRATAÇÃO 2001

Classes POSIX

Nem todas as linguagens de programação aceitam as classes POSIX. Java e C dão suporte a POSIX e existem bibliotecas Python para o mesmo resultado.

Classe Descrição
[:digit:] dígito, \d; equivalente a [0-9]
^[:digit:] não dígito, \D; equivalente a [^0-9]
[:alnum] letras e números ; equivalente a [A-Za-z0-9]
[:space:] caracteres brancos ; equivalente a [ \t\n\r\f\v]
^[:space:] não espaço: \S
[:alpha:] letras; equivalente a [A-Za-z]
[:lower:] minúsculas; equivalente a [a-z]
[:upper:] maiúsculas; equivalente a [A-Z]
[:xdigit:] números hexadecimais; equivalente a [0-9A-Fa-f]
[:word:] \w qualquer caractere alfanumérico mais underscore (_); equivalente a [[:alnum:]_]
^[:word:] \W, negação de \w
[:blank:] espaço em branco e TAB; equivalente a [\t]
[:punct:] pontuação; equivalente a [!”\#$%&'()*+,\-./:;<=>?@\[\\\]^_‘{|}~]

Exemplos de validações com Regex

Alguns exemplos de validações com Expressões Regulares

Tipo de Validação regex
Dígitos ^\d+$
Letras ^\w+$
Decimal ^[+-]?((\d+|\d{1,3}(\.\d{3})+)(\,\d*)?|\,\d+)$ ^[-+]?([0-9]*\,[0-9]+|[0-9]+)$
URL ^((http)|(https)|(ftp)):\/\/([\- \w]+\.)+\w{2,3}(\/ [%\-\w]+(\.\w{2,})?)*$
E-mail ^([\w\-]+\.)*[\w\- ]+@([\w\- ]+\.)+([\w\-]{2,3})$
Endereço IP \b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b
Tempo (24 horas) ^([0|1|2]{1}\d):([0|1|2|3|4|5]{1} \d)$
Data (dd/mm/aaaa) ^((0[1-9]|[12]\d)\/(0[1-9]|1[0-2])|30\/(0[13-9]|1[0-2])|31\/(0[13578]|1[02])) \/\d{4}$
Telefone ^\(\d{3}\)-\d{4}-\d{4}$
Senha ^\w{4,10}$ ^[a-zA-Z]\w{3,9}$ ^[a-zA-Z]\w*\d+\w*$
🔺Início do artigo

Bibliografia

Python: Escopos e namespaces


Escopos no Python

O escopo de uma variável é todo trecho de execução do código onde aquela variável pode ser encontrada, lida e alterada. Uma vez criada a variável fica disponível dentro desses trechos e só será apagada quando nenhuma referência á feita a ela. Fora de seu escopo a variável não existe e, por isso, o mesmo nome pode ser atribuído à outra variável, sem conflito.

A definição de uma função cria um novo escopo. Alguns exemplos abaixo demonstram escopos em relação a funções definidas na área principal do código (que já definiremos com mais rigor). Os casos são explicados depois do código.

» # Caso 1:
» def teste():
»     print(i)
» i = 42
» teste()
↳ 42

» #  Caso 2:
» def identidade(i):
»     return i
» i = 42
» print(identidade(5))
↳ 5

» # a variável externa à função não foi alterada
» print(i)
↳ 42

» #  manipulando i internamente
» def soma10(i):
»     i += 10
»     print(i)
» i = 42
» soma10(5)
↳ 15

» print(i)
↳ 42

» #  Caso 3:
» del h        # caso h já exista, delete a variável
» def func():
»     h=200
»     print(h)
» func()
↳ 200
» print(h)
↳ NameError: name 'h' is not defined

» # função definida dentro de outra (aninhada)
» def func():
»     h=200
»     def func2():
»         print(h)
» func2()
↳ NameError: name 'func2' is not defined

» # mas pode ser chamada na área onde foi definida
» def func():
»     h=800
»     def func2():
»         print(h)
»     func2()

» func()
↳ 800
  • Caso 1: a variável i definida fora da função pode ser lida internamente à função teste.
  • Caso 2: se o mesmo nome i é usado como parâmetro da função uma nova variável independente é criada. identidade.i não é a mesma variável que i fora da função. Aterações dentro do corpo da função não se propagam para fora dela.
  • Caso 3: uma variável ou uma função definida dentro de uma função não estão disponíveis fora dela.

Namespaces

“Namespaces are one honking great idea. Let’s do more of those!”
— The Zen of Python, Tim Peters
Em um projeto com código razoavelmente complexo muitos objetos são criados e destruídos. Namespaces é a forma usada para que conflitos entre esses nomes não ocorram. Definiremos como código, bloco ou programa principal a parte da execução do código por onde o interpretador (ou compilador) se inicia.

Um conceito importante, associado ao de escopo, é o de namespaces. Namespaces são as estruturas usadas para organizar os nomes simbólicos que servem para referenciar objetos em um programa. Uma atribuição cria um objeto na memória e associa a ele seu nome simbólico dado à variável, função ou método. Namespaces são coleções de nomes associados às informações sobre o objeto referenciados. Uma analogia pode ser feita com um dicionário onde os nomes dos objetos são as chaves os valores são os próprios objetos. Ele é um mapeamento entre nome e objeto na memória.

A existência de vários namespaces distintos significa que o mesmo nome pode ser usado em locais diferentes do código, desde que esteja em um namespace diferente.

Existem quatro tipos de namespaces no Python:

  • Interno (built-in),
  • global,
  • envolvente e
  • local.

Cada um deles é criado e existe por um tempo próprio, e destruído quando não mais necessário.

Namespace Interno (built-in)

O Namespace Interno (built-in) contém nomes e objetos criados internamente, antes mesmo que nada tenha sido importado e nem definido pelo usuário. Os nomes de váriáveis, funcões e métodos nele contidos podem ser vistos com o comando dir(__builtins__). Esse espaço é criado pelo interpretador quando é iniciado e existe enquanto ele não for encerrado.

» # No output abaixo (que está truncado) estão incluídos muitos nomes que já conhecemos
» dir(__builtins__)
↳ [ ...
↳    'chr', 'complex', 'dict', 'dir', 'divmod', 'enumerate', 'float', 'format', 'frozenset',
↳   'help', 'input', 'int', 'len', 'list', 'map', 'max', 'min', 'next', 'object', 'open', 'ord',
↳    'pow', 'print', 'range', 'reversed', 'round', 'set', 'slice', 'sorted', 'str', 'sum',
↳    'tuple', 'type', 'vars', 'zip'
↳ ]

Esses objetos estão disponíveis no ambiente mais geral e, portanto, em todos os setores do código, mesmo que distribuído em vários módulos.

Para ver onde residem esses valores em __builtins__ usamos:

» __builtins__.str.__module__
↳ 'builtins'

Isso significa que todas essas definições estão no módulo builtins.py.

Namespace global

O namespace global contém todos os nomes definidos no nível básico, abaixo apenas do namespace buil-in. Ele é criado quando o módulo é iniciado e só deixa de existir quando o interpretador é encerrado. Quando um novo módulo é importado com a instrução import um novo namespace é designado para ele. Além desses é possível criar nomes globais através palavra chave global, dentro da cada módulo.

Para ver o conteúdo do namespace global inciamos uma nova sessão do Jupiter Notebook, teclando 0-0 em qualquer célula, no modo de controle, para zerar as referências já criadas.

» # a função globals() retorna um dicionário
» type(globals())
» dict
» # para ver o conteúdo desse dicionário (output truncado)
» print(globals())
↳ {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', ...}

» # inserir uma variável é o mesmo que inserir um par no dicionário
» i = 23
» print(globals())
↳ {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', ..., 'i': 23}

» # a variável 'i' pode ser acessada e alterada diretamente no dicionário
» globals()['i']
↳ 23

» globals()['i'] = 789
» globals()['i']
↳ 789

# uma nova variável pode ser inserida no dicionário
» globals()['letras'] = 'Aa'
» print(letras)
↳ Aa

Os exemplos acima mostram que manipular o dicionário acessado por globals() é o mesmo que inserir, editar ou remover variáveis no namespaces global. Se um módulo externo for importado uma referência é feita à esse módulo, mas suas propriedades e métodos não são incluídos no dicionário.

» # importando o módulo datetime    
» import datetime
» globals()
↳ {'__name__': '__main__',  '__doc__': 'Automatically created module for IPython interactive environment', ...,
↳   'datetime': <module 'datetime' from '/home/guilherme/.anaconda3/lib/python3.8/datetime.py'>, ...}

Namespaces envolventes e locais

Quando uma nova função ou classe é inicializada o interpretador (ou compilador) cria num novo namespace reservado para aquela função ou classe. Objetos criados dentro deles não estarão disponíveis no ambiente externo (global), nem dentro de outras funcões no escopo global.

Isso significa que variáveis e funções podem ser definidas e usadas dentro de uma função com o mesmo nome de objetos em outras funções ou no programa principal, não ocorrendo confusão ou interferência porque são mantidos em namespaces separados. Isso contribui para que menos erros ocorram no código.

No exemplo seguinte o código exibe 4 diferentes namespaces:

» a = 1
» def f():
»     b = 2
»     c = 3
»     global g
»     def g():
»         c = 4
»         d = 5
»         print('a =%d, b=%d, c=%d, d=%d.'% (a, b, c, d))
        
» # f retorna None mas deve ser executada para que g() seja definida
» f()
» c = 3
» g()
↳ a = 1 , b = 2 , c = 4, d = 5

Em cada ambiente estão disponíveis:

  • built-in: a função print(), (por exemplo),
  • global: variável a, a função f() e a função g(),
  • ambiente de f(): as variáveis a, b, c e a função g(),
  • ambiente de g(): as variáveis a, b, c, d.

A função f() é global. A função g() foi tornada global devido à atribuição global.

Quando o módulo chama f() um novo namespace é criado. De dentro de f() a função g() é chamada, com seu namespace próprio.

O nome (ou identificador) g foi definido como global. Desta forma a função g(), embora tenha sido criada no namespace de f é global e pode ser chamada do módulo principal. Apesar disso sua variável interna d continua tendo escopo restrito a g. A variável c dentro de g não é a mesma que c em f, como se vê nos outputs do código.

Análogo ao dicionário para namespace global, podemos acessar o dicionário local através da função locals(). O código abaixo mostra o estado do namespace local para pontos diferentes de um código com a função g() aninhada em f().

» def f(x,y):
»     a = 'boa'
»     print('posição 1: ',locals())
»     def g():
»         b = 'tarde'
»         print('posição 2: ',locals())
»     g()
»     print('posição 3: ',locals())

» # executamos a função com x = 10, y = 55
» f(10, 55)
↳ posição 1:  {'x': 10, 'y': 55, 'a': 'boa'}
↳ posição 2:  {'b': 'tarde'}
↳ posição 3:  {'x': 10, 'y': 55, 'a': 'boa', 'g': <function f.<locals>.g at 0x7fc174050a60>}

No entanto, se tentarmos usar a variável a dentro de g() o interpretador a busca no namespace superior. Se ela for modificada dentro de namespace de g() uma nova variável é criada com aquele nome, preservando a variável do ambiente superior.

» def f(x,y):
»     a = 'boa'
»     def g():
»         b = 'tarde'
»         print('posição 1: ', a , b)
»         print('posição 2: ', locals())
»     g()
    
» f(10,55)
↳ posição 1:  boa tarde
↳ posição 2:  {'b': 'tarde', 'a': 'boa'}   

Uma forma diferente de se criar um novo namespace é através da criação de classes. Cada classe carrega seu próprio ambiente e é, portanto, seu próprio escopo.

» um = 1000
» tres = 3
» class Unidade:
»     um = 1
»     dois = 2000

» class Dezena:
»     um = 10
 
» class Soma:
»     um = 1 + tres

» print(um)
↳ 1000

» print(Unidade.um)
↳ 1

» print(Dezena.um)
↳ 10

» # a variável global tres existe dentro da classe Soma
» print(Soma.um)
↳ 4

Ao encontrar a requisição de um nome (uma referência à um objeto) o interpretador procura primeiro no ambiente local. Se não encontrar passa consecutivamente para os namespaces local, envolvente, global e built-in. Variáveis definidas em níveis mais externos podem ser acessadas nos níveis mais internos, mas não vice-versa. Em inglês é costume se referir a esse comportamento como Regra LEGB (Local, Enclosing, Global, Built-in).

A terminologia de namespaces local e envolvente não aparecem nas especificações oficiais do Python mas tem sido usadas em manuais e cursos. Ele simplesmente busca expressar o fato de que cada ambiente pode abrigar um ambiente aninhado criando uma hierarquia de precedências onde um nome será buscado.

Escopo:

Como vimos uma variável criada no corpo principal do código de Python está no global namespace e é chamada de variável global. Essas variáveis estão disponíveis em qualquer escopo, global e local.

Se, dentro de um escopo restrito, o programador necessite criar uma variável global (que possa ser acessada em todas as partes do código) dentro de uma área restrita podemos usar a palavra chave global.

Embora funções carreguem seu próprio escopo local, laços for ou while não criam uma área própria de escopo. Variáveis definidas dentro do laço continuam existindo depois de seu término.

» # r, definida dentro do laço, continua existindo após o seu final
» for t in range(10):
»     r = t
» print('Último t = %d' % r)
↳ Último t = 9

» # idem para um laço while
» while True:
»     j = 0
»     while j < 9:
»         j+=1
»     break
» 
» print(j)
↳ 9
    
» # global keyword: G fica acessível de fora de seu escopo de definição
» def func3():
»     global G
»     G = 300
» 
» func3()
» print(G) 
↳ 300

» # um variável 
» k = 1000
» def func4():
»     k = k + 23
»     print(k)
» 
» func4()
↳ UnboundLocalError: local variable 'k' referenced before assignment

» # no entanto ela pode ser acessada de dentro da função
» k = 1000
» def func4():
»     w = k + 23
»     print(w)
» 
» func4()
↳ 1023

# o mais seguro a fazer é passar um parâmetro que será confinado ao escopo da função
» w = 1000
» def func6(i):
»     return(i + 23)
» print(func6(w))
↳ 1023

Da mesma forma a função g() a seguir só poderia ser acessada de dentro de f() se não fosse usada a declaração global.

» def externa():
»     print('dentro de externa')
»     # global interna
»     def interna():
»         print('dentro de interna') 

» # executamos interna() para que ocorra a definição de interna()
» externa()
↳ dentro de externa

» interna()
↳ NameError: name 'interna' is not defined

» # mas, se definimos global interna
» def externa():
»     print('dentro de externa')
»     global interna
»     def interna():
»         print('dentro de interna') 

» externa()
↳ dentro de externa

» interna()
↳ dentro de interna

Sublinhados do Python

Sublinhados (_, __, underscores) tem significados específicos e diferentes no Python, de acordo com sua utilização.

Sublinhado simples:

No interpretador, seja no Idle, no prompt do Phyton ou no Jupyter Notebook, um sublinhado simples isolado (_) significa o valor da última expressão avaliada.

» pow(2,3)
↳ 8
» pow(_,3)
↳ 512

» 'cat' + 'egoria'
↳ 'categoria'
» _
↳ 'categoria'

» _ + 's'
↳ 'categorias'

Também, _ pode ser usado em lugar de uma variável que será ignorada. Isso pode tornar o código mais legível por mostrar que a tal variável não terá nenhum papel nas linhas a seguir. Outro uso é o de utilizar palavras chaves como nomes de variáveis, seguidos de um sublinhado.

Por exemplo:

» _, y = (1, 2)
» # ambos os valores são armazenados mas só y será usado (por convenção)
» print(y)
↳ 2

» for _ in range(10):
»     print('+', end='')
↳ ++++++++++

» # uma função pode retornar 4 valores mas apenas os 2 primeiros serão usados
» a, b, _, _ = funcao(parametro)

» # uma palavra chave (com sublinhado no final) pode ser usada como nome
» if_ = 1234
» print(if_)
↳ 1234

Essa última possibilidade pode tornar o código mais confuso e de difícil leitura.

Um sublinhado inicial, antes do nome da variável, função ou método é uma convenção usada para indicar que aquele objeto é apenas para uso interno e só deve ser acessado dentro da classe. Isso não previne, de fato, que ele seja acessado de fora da classe e, por isso o objeto é chamado de privado fraco. Mas, se o módulo em que a classe reside for importado com import * os nomes iniciados com _ não serão importados.

» # dentro de um módulo (gravado no arquivo classe.py) criamos 2 classes
» def metodo_publico():
»     print ("método público")
» def _metodo_privado():
»     print ("método privado")
» # ------ fim do módulo ------

» # importamos esse módulo
» from classe import *
» # uma chamada ao método público funciona normalmente
» metodo_publico()
↳ método público

» # uma chamada ao método privado não funciona
» _metodo_privado()
↳ NameError: name '_metodo_privado' is not defined
  
» # se o módulo inteiro for importado a classe _metodo_privado() pode ser chamada
» import classe
» classe._metodo_privado()
↳ método privado

Sublinhado duplo:

Uma variável ou método com duplo sublinhado inicial (__), como vimos antes, tem a função de reescrever em tempo de interpretação (ou compilação) o nome do objeto para evitar conflito com os mesmos nomes em subclasses.

» # O seguinte módulo está gravado como modulo.py
» class MinhaClasse():
»     def __init__(self):
»         self.__variavel = 987
» # ------ fim do módulo ------

» # importamos esse módulo e tentamos usar o acesso direto à variável __variavel
» import modulo
» obj = modulo.MinhaClasse()
» obj.__variavel
↳ AttributeError: Myclass instance has no attribute '__variavel'

» # para acessar a variável é necessário escrever um acesso público
» # e alterar o conteúdo de classe.py
» # O seguinte módulo está gravado como modulo.py
» class MinhaClasse():
»     def __init__(self):
»         self.__variavel = 987
»     def funcPublica(self)
»         print(self.___variavel)
» # ------ fim do módulo ------
» import modulo
» obj = modulo.MinhaClasse()
» obj.funcPublica()
↳ 987

Nomes iniciados e terminados com sublinhado duplo:

Métodos com nomes cercados por um duplo sublinhado, como __len__ e __init__, são considerados especiais no Python e servem para que o programador possa fazer sobrecarga ou overloading de métodos especiais das classes. Como vimos na seção sobre Métodos Especiais o método add() pode ser alterado pelo programador para executar função diferente da original.

» # O módulo seguinte está gravado no arquivo modulo2.py
» class MinhaClasse():
»     def __add__(self,a,b):
»         print (a*b)
» # ------ fim do módulo ------        

» import modulo2
» obj = modulo2.MinhaClasse()
» obj.__add__(5,2)
↳ 10

» # o operador + pode ser overloaded
» class novaSoma():
»     def __init__(self, valor):
»         self.valor = valor
»     def __add__(self, other):
»         return self.valor * other.valor

» a, b = novaSoma(56), novaSoma(10)
» print(a+b)
↳ 560

Resumindo

sublinhados em seu nome. Estas são as possibilidades:

  • Pública: significa que o membro pode ser acessado fora da classe onde foi definido, por outras instâncias ou objetos da mesma classe. Esses são nomes sem sublinhados. Por ex.: quantosAlunos = 367.
  • Protegida: o membro pode ser acessado pela classe onde ela está definida e seus filhos, outras classes que herdam dessa classe. Esses nomes são definidos iniciando com um sublinhado. Por ex.: _quantosAlunos = 367.
  • Privada: o membro só está acessível dentro da classe onde ela está definida. Esses são nomes iniciados com dois sublinhados. Por ex.: __quantosAlunos = 367.

Por default todos os membros de uma classe são públicos.

Argumentos de funções passados por valor e por referência

Terminologia:

Os valores usados na definição de uma função e manipulados por ela são chamados de parâmetros da função. Na chamada à função valores são fornecidos, normalmente chamado de argumentos. Na programação em geral (e não apenas no Python) argumentos podem ser passados por referência e por valor.

Um argumento passado por valor pode ser manipulado internamente na função e não tem seu valor alterado fora do escopo da função. Isso acontece porque a função manipula uma nova variável inicializada com aquele valor.

Argumentos passados por referência, se alterados no escopo interno da função, será alterado também fora do escopo da função. Nesse caso a função atua sobre o objeto em si, que é o mesmo que aquele do escopo de nível superior ao da função.

O comportamento dessas variáveis no Python é diferente se são referências a objetos mutáveis ou imutáveis.

São objetos imutáveis:

  • Números (Inteiros, Racionais, Ponto flutuante, Decimais, Complexos e Booleanos)
  • Strings
  • Tuplas
  • Conjuntos congelados (Frozen Sets)
  • Classes do usuário (desde que definida como imutável)

São objetos mutáveis:

  • Listas
  • Conjuntos (Sets)
  • Dicionários
  • Classes do usuário (desde que definida como mutável)

Um aspecto que pode ser difícil de debugar, em caso de erros, são as formas de tratamento dos parâmetros de uma função. Funções tratam de modo diferente argumentos mutáveis e imutáveis.

No Python, como as variáveis são nomes que fazem referências à objetos, toda variável é passada a uma função por referência. Se o objeto é mutável a variável original, fora do escopo da função, é alterado. Se o objeto é imutável a variável original fica inalterada e a função age sobre uma nova variável no seu próprio escopo, deixando inalterada a variável original.

Portanto, em comparação com outras linguagens, as funções agem como se variáveis mutáveis fossem passadas por referência e imutáveis por valor.

» # uma função que recebe um valor imutável trata seu parâmetro como passado por valor
» p = 'explícita'
» def concatena(p):
»     p = p.replace('í','i')
»     p +='mente'
»     return p

# a variável dentro da função está em escopo interno e não altera p global 
» print(concatena(a))
↳ explicitamente

» print(a)
↳ explícita

» # se o valor for mutável a função trata seu parâmetro como passado por referência
» alunos = {'Ana':28,'Julia':25,'José':32}
» def insere(alunos):
»     novos = {'Otto':30,'Mario':28}
»     alunos.update(novos)
»     print('Dentro da função:\n', alunos)

» insere(alunos)

» print('Fora da função:\n', alunos)
↳ Dentro da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}
↳ Fora da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}

» # forçando a função a se comportar "por valor"
» alunos = {'Ana':28,'Julia':25,'José':32}
» def byValue(alunos):
»     alunos2 = alunos.copy()
»     alunos2.update({'Otto':30,'Mario':28})
»     print("Dentro da função:\n", alunos2)
    
» byValue(alunos)
» print("Fora da função:\n", alunos)
↳ Dentro da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}
↳ Fora da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32}

» # forçando função a se comportar "por referência"
» a = 34
» def byRef(a):
»     a = 78
»     print('Dentro da função: a=', a)
»     return a
» a = byRef(a)
↳ Dentro da função: a= 78

» print('Fora da função: a=', a) 
↳ Fora da função: a= 78

No penúltimo exemplo reconstruimos a função de modo a não alterar o objeto alunos no escopo global. Para isso criamos uma cópia de alunos em alunos2 = alunos.copy(), cuja alteração não implica em alteração na variável global.

Em seguida usamos uma solução paliativa para o caso de querermos tratar um valor imutável como passado por referência. Ele consiste em retornar o valor alternar e reatribuir a variável a.

Gerenciamento de memória

Cada um desses namespaces existe na memória até que sua função termine. O Python possui um processo interno recuperar a memória neles alocada. Mesmo que essa limpeza não seja imediata para esses namespaces quando suas funções terminam, mas todas as referências aos objetos que eles contêm deixam de ser válidas.

Estritamente dizendo, Python não possui variáveis e sim nomes (names) que são referências para objetos. Um objeto pode ter mais de um nome. Por exemplo, no código abaixo, experimento 1, a e b são referências para o mesmo objeto, o inteiro 1. A função id(objeto) retorna um id único para o objeto especificado. Todo objeto possuem seu próprio id que é atribuído a ela em sua criação. No teste vemos que a e b se referem ao mesmo objeto que tem id = 94153176063296. No experimento 2 o nomes, x e y, referenciam o mesmo objeto, a string “algo”. Mesmo sem associar as duas diretamente elas têm o mesmo id e x is y = True.

» # experimento 1
» a = 1
» b = a
» print(id(a))
↳ 94153176063296

» print(id(b))
↳ 94153176063296

# experimento 2
» x = 'algo'
» y = 'algo'

» print(id(x))
↳ 139723955294064

» print(id(y))
↳ 139723955294064

» print(x is y)
↳ True

Um nome é um label (uma etiqueta) que serve para disponibilizar no código um objeto, com suas propriedades e métodos. Mas nem todo objeto tem um nome. Existem objeto simples, como um inteiro ou uma string, e objetos compostos de outros objetos como containeres, como dicionários, listas e classes definidas pelo usuário, que armazenam referências para vários objetos simples ou mesmo outros containeres. Definimos como referência um nome, ou um objeto conteiner que apontam para outros objetos.

O Python usa um sistema de contagem de referências (reference counter) que mantém uma tabela atualizada de quantos nomes (ou referências) são associados a cada instante com um objeto. Quando um nome é associado à outro objeto a contagem decresce. Também podemos desfazer a ligação entre nome e objeto com o comando del.

» # instanciamos um objeto
» x = 700          # referências para o objeto 700 = 1
» y = 700          # referências para o objeto 700 = 2
» z = [700, 700]   # referências para o objeto 700 = 4
»                  # duas novas referências foram criadas: z[0] e z[1]
                 
» # a contagem decresce quando um nome passa a se referir a outo objeto
» x = None         # referências para o objeto 700 = 3
» y = 1            # referências para o objeto 700 = 2

# também podemos usar o comando del
» del z            # referências para o objeto 700 = 0

No código seguinte criamos duas variáveis com o mesmo valor (ou, melhor dizendo, associamos dois nomes com o mesmo objeto, 700) o verificamos seus ids, que são os mesmos. O teste x is y testa se são o mesmo objeto. No segundo teste a variável x é incrementada de 1. Isso faz com que novo objeto seja criado na memória (701) e a contagem de 700 seja diminuída de 1.

» def mem_test():
»     x = 700
»     y = 700
»     print(id(x))
»     print(id(y))
»     print(x is y)

» mem_test()
↳ 139800599228816
↳ 139800599228816
↳ True

» def mem_test2():
»     x = 700
»     y = 700
»     x += 1
»     print(id(x))
»     print(id(y))
»     print(x is y)

» mem_test()
↳ 139800599228688
↳ 139800599228656
↳ False

Objetos do Python, além de ter suas propriedades e código de seus métodos, possui sempre uma tabela com sua contagem de referência e seu tipo.

Como mencionamos o comando del desfaz a referência entre nome e objeto, reduzindo o contador de referências. Mas ele não apaga instantaneamente o objeto da memória.

A figura ilustra o ciclo de vida de um objeto criado dentro de uma função e referenciado uma vez. Quando a função termina sua contagem de referência está zerada e ele pode ser apagado.

No entanto, um objeto no namespace global não sai do escopo enquanto o programa inteiro não termina. Sua contagem de referência não se anula e, por isso, ele não será apagado, mesmo que se torne obsoleto. Essa é uma justificativa válida para não se usar variáveis globais em um projeto, exceto quando são realmente necessárias. E certamente se deve evitar referências para objetos grandes (em termos de requisição de memória) e complexos no escopo global.

Quando o contador de referências atinge o valor 0, significando que nenhuma referência está ativa para aquele objeto, ele pode ser apagado, liberando espaço de memória. Essa função é executada pelo coletor de lixo (garbage collector) que é uma forma de liberar memória para outros usos. Com frequência o uso total de memória de um programa em Python, visto pelo sistema, não decresce quando o coletor apaga objetos. No entanto a memória interna livre, alocada para aquele programa, aumenta. Não é muito difícil depararmos com situações de esgotamento de memória, o que causa lentidão do computador e eventual travamento.

Coletor de lixo geracional:

Um problema existente com o coletor de lixo ocorre com as chamadas referências cíclicas. Temos uma referência cíclica se um objeto faz referência a si mesmo, ou, por ex, um objeto A faz referência à B, que faz referência à C, que por sua vez contém referência à A. Mesmo que os objetos sejam extintos o contador de referências não fica zerado e esses objetos não são apagados, mesmo que nenhum nome se refira a eles.

Referência Cíclica

Por esse motivo o coletor de lixo geracional (generational garbage collector, GGC) foi inserido. Esse mecanismo faz uma verificação de objetos que não são acessíveis por nenhuma parte do código e os remove, mesmo que ainda estejam referenciados. O GGC é mais lento que o simples apagamento quando o contador de referências é zerado e por isso ele não é executado imediatamente após a remoção de uma referência.

Cada objeto no Python pertence a uma de três gerações de objetos. Ao ser criado ele é de primeira geração (geração 0). A cada execução do GGC, se o objeto não for removido, ele é promovido para a geração seguinte. Quando o objeto atinge a última geração (geração 2) ele permanece lá até ser alcançado pela remoção.

Configurando o GGC


A ação do GGC é configurável. Podemos especificar o número máximo de objetos que podem permanecer na geração 0 e quantos devem existir nas gerações 1 e 2 antes que a coleta seja ativada. É possível determinar o número de coletas que devem ser executadas sem que o processamento atinja geração 1 e 2.

Uma coleta de lixo é executada quando o número de objetos na geração 0 atinje o limite, removendo objetos inacessíveis e promovendo os demais para a próxima geração. Nesse caso o coletor de lixo atua apenas sobre geração 0. Quando o segundo limite é alcançado o coletor processa os objetos da geração 0 e 1. Quando o limite da geração 2 é alcançado, o coletor processa todos os objetos, das três gerações. Isso, no entanto, não acontece todas as vezes. A geração 2 (final) tem um limite adicional. Um GGC completo só ocorre quando o número total de objetos na primeira e segunda geração excede 25% de todos os objetos na memória (um limite não configurável).

Esses parâmetros foram inseridos para impedir a execução muito frequente do coletor, pois sua execução paraliza a execução do programa, tornando-os muito mais lentos. A maioria dos aplicativos usa objetos de “vida curta”, por exemplo criados e destruídos dentro de uma função, que nunca são promovidos à próxima geração. O programador pode usar desse fato para impedir “memory leaks” em seu código.

Por que isso importa no Jupyter Notebook

No Jupyter Notebook as células usam o escopo global. Variáveis criadas dentro de uma célula continuam existindo durante todo o ciclo de vida do próprio notebook. Desta forma ele não perde sua contagem de referência e não é excluída da memória, exceto se o programador atribuir aquele mesmo nome à outro objeto ou apague sua referência com o comando del. É fácil ter problemas com memória se você criar variáveis diferentes ​​para as etapas intermediárias de um processamento de dados.

Exemplos comuns desse procedimento ocorrem quando carregamos um dataframe de tamanho razoável (veja o artigo sobre dataframes). Em seguida podemos fazer várias etapas de tratamento de dados, tais como a remoção de valores inválidos, a inserção ou remoção de colunas, atribuindo o resultado de cada etapa à um novo nome.

Como já vimos uma solução seria o apagamento da referência com del, o que não é recomendado porque não há garantias de que o apagamento seria feito no momento esperado. Outra solução, mais eficaz, é a de só usar novos nomes para objetos dentro de funções ou classes, de forma que a referência é extinta ao fim do escopo. Um exemplo é mostrado no código a seguir. Se você não conhece pandas e dataframes simplesmente pule este exemplo.

» import pandas as pd

» def processar_dados(dados_brutos):
»     '''
»     codigo de remoção de valores nulos
»     alteração de nomes de colunas
»     inserção e remoção de dados
»     '''
»     return dados_depurados

» dados = pd.read_csv('file.csv')
» dados_processdos = processar_dados(dados)

Em linguagens de programação mais antigas o programador tinha que alocar um espaço de memória declarando a variável e seu tipo. Após o uso ele devia promover o apagamento da variável. Isso cria dois tipos de problemas: (a) o esquecimento de uma limpeza apropriada leva ao acúmulo de memória usada, particularmente em programas que rodam por longo tempo; (b) o apagamento prematuro de um recurso que ainda pode ser necessário, causando queda do programa. Por esses motivos as linguagens modernas usam o gerencimento automático de memória.

No lado negativo, o coletor de lixo deve armazenar suas informações (o contador de referência, no caso do Python) em algum lugar e precisa usar recursos de processamento para sua função, o que onera o sistema tanto em memória usada quanto de tempo de preocessamento. Ainda assim o gerenciamento automático torna mais fácil para o programador a sua tarefa.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Classes, métodos especiais


Métodos especiais, ou métodos mágicos, em Python são métodos predefinidos em todos os objetos, com invocação automática sob circunstâncias especiais. Eles normalmente não são chamados diretamente pelo usuário mas podem ser overloaded (sobrescritos e alterados). Seus nomes começam e terminam com sublinhados duplos chamados de dunder (uma expressão derivada de double underscore). As operações abaixo são exemplos de métodos mágicos e como acioná-los, com o operador + e a função len().

» x, y = 34, 45
» x+y
↳ 79
» x.__add__(y)
↳ 79
» 
» l = [4,67,78]
» len(l)
↳ 3
» l.__len__()
↳ 3

Vemos que somar dois números usando o operador + aciona o método __add __ e calcular o comprimento de um objeto usando a função len() equivale a usar seu método __len__().

Uma das principais vantagens de usar métodos mágicos é a possibilidade de elaborar classes com comportamentos similares ou iguais aos de tipos internos.

Atributos especiais das classes

Vimos na seção anterior sobre Classes no Python que a função dir() exibe todos os atributos de uma classe. Muitos deles são built-in, herdados da classe object que é a base de todas as classes, portanto de todos os objetos. Em outras palavras object é uma superclasse para todos os demais objetos.

Podemos listar esses atributos criando uma classe T sem qualquer atributo e examinando o resultado de print(dir(T)).

» class T:
»     pass

» print(dir(T))
↳ ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
↳  '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__',
↳  '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
↳  '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

Já vimos e usamos os métodos __init__(), para inicializar objetos, e __str__(), para retornar uma representação de string do objeto, acessada por dir(objeto). Também fizemos o overloading de __eq__() e __add__().

Também usamos a possibilidade de sobrescrever os métodos __len__, que ativa a função len(), __str__, associado às chamadas de print() e __eq__ usado em comparações com ==. Para lembrar esse processo de overload observe os métodos na classe abaixo.

» class Comprimento:
»     def __init__(self, fim = 0):
»         self.minhaLista = list(range(fim))
»     def __len__(self):
»         return len(self.minhaLista)
»     def __eq__(self, outro):
»         return self.minhaLista == outro.minhaLista
»     def __str__(self):
»         return '%s \nlen = %d' % (str(self.minhaLista), len(self.minhaLista))

» comp1 = Comprimento(fim=9)
» comp2 = Comprimento(fim=9)

» print(comp1)
↳ [0, 1, 2, 3, 4, 5, 6, 7, 8] 

» len = 9
» comp1==comp2
↳ True

O método len() se refere à contagem de elementos discretos e só pode ser definido para retornar um número inteiro.

Em outro exemplo definimos na classe Ponto os operadores de comparação __gt__ e __ne__, respectivamente > e != da seguinte forma: consideramos, para esse nosso caso, que um ponto é “maior” que outro se estiver mais afastado da origem de coordenadas, o ponto (0, 0). Para isso definimos o método distancia() que calcula essa distância. O operador __ne__ retorna True se uma ou ambas coordenadas dos dois pontos testados forem diferentes. Para usar a função sqrt(), (raiz quadrada) temos que importar o módulo math.

» from math import sqrt
» class Ponto:
»     def __init__(self, x, y):
»         self.x, self.y = x, y
» 
»     def distancia(self):
»         return sqrt(self.x**2 + self.y**2)
»     
»     def __gt__(self, other):
»         return self.distancia() > other.distancia()
»     
»     def __ne__(self, other):
»         x, y, w, z = self.x, self.y, other.x, other.y
»         return x != w or y != z
»     
»     def __str__(self):
»         return 'Ponto com coordenadas (%d, %d)' % (self.x, self.y)

» p1 = Ponto(4,5)
» p2 = Ponto(1,2)

» p1 != p2
↳ True

» p1 > p2
↳ True

De forma análoga podemos fazer o overload e utilizar os aperadores __eq__, ==, __ge__, >=, __gt__, >, __le__, <=, __lt__, < e __ne__, !=.

Todas as comparações se baseiam no método __cmp __(self, other) que deve retornar um inteiro negativo se self < other, zero se self == other e um inteiro positivo se self > other.

Geralmente é melhor definir cada uma das comparações que serão utilizadas. Mesmo assim a definição do método __cmp__ pode ser uma boa maneira de economizar repetição e melhorar a clareza quando você precisa que todas as comparações sejam implementadas com critérios semelhantes.

Método __init__()

Já vimos na seção anterior o funcionamento do método __init__, e extendemos aqui a descrição de sua funcionalidade. Sabemos que podemos inicializar um objeto sem qualquer referência às propriedades de que ele necessita e inserir mais tarde, dinamicamente, essas propriedades.

» class Area:
»     ''' Área de um retângulo '''
» 
»     def area(self):
»         return self.altura * self.largura
»
» a = Area()
» a.altura = 25
» a.largura = 75
» a.area()
↳ 1875

Uma classe definida dessa forma não deixa claro quais são as propriedades que ele deve usar. Considerando que o método __init__() é acionado internamente, podemos tirar vantagem desse método fazendo seu overload e inicializando as propriedades explicitamente.

» class Area:
»     def __init__(self, altura, largura):
»         self.altura = altura
»         self.largura = largura
» 
»     def area(self):
»         return self.altura * self.largura
» 
» b = Area(123, 90)
» b.area()
↳ 11070

A segunda definição é considerada um melhor design uma vez que torna mais clara a leitura do código e seu uso durante a construção de um aplicativo. A própria definição da classe informa quais são os parâmetros usados pelos objetos dela derivados.

Para o próximo exemplo suponha um jogo do tipo RPG onde os jogadores são criados com uma determinada quantidade de energia e inteligência e que esses valores são incrementados ou decrementados de acordo com as escolhas feitas pelo jogador. A partir desses valores se calcula vida e poder, significando quanto tempo a personagem tem de vida e quanto poder de destruição ela possui em seus golpes.

Para representar as dois tipos possíveis de personagens no jogo definimos a superclasse Personagem com duas variáveis de classe (energia e inteligência) e dois atributos calculados: vida e poder. Duas subclasses Cientista e Estudante herdam da primeira, todas as variáveis e métodos, inclusive __init__(), mas sobreescrevem os métodos _estado(), usado na inicialização, e __str()__.

» class Personagem:
»     def __init__(self, energia, inteligencia):
»         self.energia = energia
»         self.inteligencia = inteligencia
»         self.vida, self.poder = self._estado()
» 
»     def _estado(self):
»         ''' Retorna uma tupla '''
»         return int(self.energia + self.inteligencia), int(self.energia * 10)
»     
»     def __str__(self):
»         return 'Vida = %d Poder = %d' % (self.energia , self.inteligencia)
» 
» class Cientista(Personagem):
»     def _estado(self):
»         return  int(self.energia + self.inteligencia *10), int(self.energia * 5)
»      
»     def __str__(self):
»         return 'Cientista: Vida = %d Poder = %d' %  (self.vida , self.poder)
» 
» class Estudante(Personagem):
»     def _estado(self):
»         return  int(self.energia + self.inteligencia * 5), int(self.energia * 10)
» 
»     def __str__(self):
»         return 'Estudante: Vida = %d Poder = %d' % (self.vida , self.poder)
» 
» p = Personagem(10,10)
» c = Cientista(10,10)
» e = Estudante(10,10)
» 
» print(p)
↳ Vida = 10 Poder = 10
» 
» print(c)
↳ Cientista: Vida = 110 Poder = 50
» 
» print(e)
↳ Estudante: Vida = 60 Poder = 100

Esse é um exemplo de polimorfismo pois cada subclasse possui seu próprio método _estado(). É importante lembrar que __init__() sempre retorna None pois não possui (nem admite) o comando return. A notação com sublinhado em _estado() sugere que o método é de uso interno na classe e seus objetos.

Exibindo um objeto, __str__, __repr__, __format__

Temos usado o método __str__ que retorna uma string com dados sobre o objeto que é exibida com print(objeto). Usando esse método esperamos obter uma descrição amigável e legível do objeto, contendo todos os dados ou que julgamos mais relevantes.

Outro método, __repr__, também retorna uma representação de string, mais técnica e em geral usando uma expressão completa que pode ser usada para reconstruir o objeto. Ele é acionado pela função repr(objeto). Essa representação, se passada como argumento para a função eval(), retorna um objeto com as mesmas características do original. A função eval() interpreta a string passada como argumento e a interpreta como código, executando esse código como uma linha de programação python. Outros exemplos são dados a seguir.

» # __repr__
» class Bicho:
»     def __init__(self, especie, habitat):
»         self.especie = especie
»         self.habitat = habitat
» 
»     def __repr__(self):
»         return 'Bicho("%s", "%s")' % (self.especie, self.habitat)
» 
»     def __str__(self):
»         return 'O bicho %s com habitat: %s' % (self.especie, self.habitat)
»     
» peixe = Bicho('peixe', 'rios')
» 
» # usando o método __repr__
» print(repr(peixe))
↳ Bicho("peixe", "rios")
» 
» # usando o método __str__
» print(peixe)
↳ O bicho peixe com habitat: rios
» 
» # usando eval()
» bagre = eval(repr(peixe))
» print(bagre)
↳ O bicho peixe com habitat: rios

O retorno de repr(peixe) pode ser usado para reconstruir o objeto peixe. Caso o método __str__ não tenha sido sobrescrito sua execução chama o método __repr__ e a saída é idêntica. A expressão bagre = eval(repr(peixe)) é idêntica à bagre = Bicho("peixe", "rios").

» # outros exemplos de uso de eval()
» txt = 'x+x**x'.replace('x','3')
» eval(txt)
↳ 30
» 
» txt = "'casa da mãe joana'.title()"
» eval(txt)
↳ Casa Da Mãe Joana
» 
» for t in [a + ' * ' + b for a in '67' for b in '89']:
»     print(t, '=', eval(t))
↳ 6 * 8 = 48
↳ 6 * 9 = 54
↳ 7 * 8 = 56
↳ 7 * 9 = 63


A função eval() não deve ser aplicada diretamente a dados digitados pelo usuário devido ao risco de que se digite uma expressão que delete dados ou cause qualquer outro dano ao computador ou rede onde o código é executado.

Função e método format

Já vimos o método de formatação de strings usando format(), que é uma funcionalidade mais moderna e poderosa que a notação de % para inserir campos. Recapitulando e expandindo um pouco vamos listas mais alguns exemplos desse método.

» # marcadores nomeados são alimentados por format
» txt1 = '1. Eu me chamo {nome} e tenho {idade} anos.'.format(nome = 'João', idade = 36)
» 
» # os campos podem ser marcados numericamente
» txt2 = '2. Moro em {0}, {1} há {2} anos.'.format('Brasília', 'DF', 20)
» txt3 = '3. Há {2} anos moro em {0}, {1}.'.format('Brasília', 'DF', 20)
» 
» # ou marcados apenas por sua ordem de aparecimento
» txt4 = "4. {} são convertidos em {}. Exemplo: {}.".format('Dígitos', 'strings', 100)
» 
» # controle do número de casas decimais
» txt5 = '5. Esse livro custa R$ {preco:.2f} com desconto!'.format(preco = 49.8)
» 
» print(txt1)
↳ 1. Eu me chamo João e tenho 36 anos.
» 
» print(txt2)
↳ 2. Moro em Brasília, DF há 20 anos.
» 
» print(txt3)
↳ 3. Há 20 anos moro em Brasília, DF.
» 
» print(txt4)
↳ 4. Dígitos são convertidos em strings. Exemplo: 100.
» 
» print(txt5)
↳ 5. Esse livro custa R$ 49.80 com desconto!
» 
» # argumento de format é "unpacking" de sequência
» print('{3}{2}{1}{0}-{3}{0}{1}{2}-{1}{2}{3}{0}-{0}{3}{2} '.format(*'amor'))
↳ roma-ramo-mora-aro
» 
» #indices podem ser repetidos
» print('{0}{1}{0}'.format('abra', 'cad'))
↳ abracadabra

padrao.format(objeto) pode conter um objeto com propriedades que serão lidas em {objeto.propriedade}. Por ex., o módulo sys contém os atributos sys.platform e sys.version.

» import sys
» # sys possui atibutos sys.platform e sys.version
» # no caso abaixo sys é o parâmetro 0
» print ('Platform: {0.platform}\nPython version: {0.version}'.format(sys))
↳ Platform: linux
↳ Python version: 3.8.5 (default, Sep  4 2020, 07:30:14) 
↳ [GCC 7.3.0]

O parâmetro pode ser um objeto construído pelo programador.

» class Nome:
»     nome='Silveirinha'
»     profissao='contador' 
» 
» n = Nome()
» idade = 45
» print('Empregado: {0.nome}\nProfissão: {0.profissao}\nIdade: {1} anos'.format(n, idade))
↳ Empregado: Silveirinha
↳ Profissão: contador
↳ Idade: 45 anos

Uma especificação de formato mais precisa pode ser incluída cim a sintaxe de vírgula seguida da especificação do formato.
{0:30} significa, campo 0 preenchido com 30 espaços, e {1:>4} campo 1 com 4 espaços, alinhado à direita. O alinhamento à esquerda é default.

» # Campo 0: justificado à esquerda (default), preenchendo o campo com 30 caracteres
» # Campo 1: justificado à direita preenchendo o campo com 4 caracteres
» linha = '{0:30} R${1:>4},00'
» 
» print(linha.format('Preço para alunos', 35))
» print(linha.format('Preço para professores', 115))
↳ Preço para alunos              R$  35,00
↳ Preço para professores         R$ 115,00

Os campos usados em format() podem ser aninhados (um campo dentro do outro). No caso abaixo a largura do texto é passada como campo 1, que está dentro do campo 0, como comprimento.

» # campos aninhados. campo 0 é o texto a ser formatado pelo padrão. Campo 1 é a largura do texto
» padrao = '|{0:{1}}|'
» largura = 30
» txt ='conteúdo de uma célula'
» print(padrao.format(txt, largura))
↳ |conteúdo de uma célula        |

Esse processo pode ser muito útil quando se monta um texto com formatação, como em html. No preencimento de texto delimitado por tags um padrão pode incluir trechos de início e fim. Na construção de um tabela, por ex., as tags de abertura e fechamento de uma célula de uma tabela são <td></td>.

» padrao = '{inicio}{0:{1}}{fim}'
» txt ='conteúdo da célula'
» larg = len(txt)
» print(padrao.format(txt, larg, inicio = '', fim = ''))
↳ conteúdo da célula

Lembrando: os parâmetros posicionais 0, 1 devem vir antes dos nomeados.

Outro exemplo é a montagem de uma tabela com valores organizados por colunas. O padrão abaixo estabelece que cada número deve ocupar 6 espaços. Observe que ‘{:6d} {:6d} {:6d} {:6d}’ é o mesmo que ‘{0:6d} {1:6d} {2:6d} {3:6d}’.

» for i in range (3, 8):
»     padrao = '{:6d} {:6d} {:6d} {:6d}'
»     print(padrao.format(i, i ** 2, i ** 3, i ** 4))
↳      3      9     27     81
↳      4     16     64    256
↳      5     25    125    625
↳      6     36    216   1296
↳      7     49    343   2401

Os seguintes sinais podem são usados para alinhamento:

Especificadores de alinhamento
< alinhado à esquerda (default),
> alinhado à direita,
^ centralizado,
= para tipos numéricos, preenchimento após o sinal.

Os seguintes sinais são usados para especificação de formato:

Especificadores de formato
b Binário. Exibe o número na base 2.
c Caracter. Converte inteiros em caractere Unicode.
d Inteiro decimal. Exibe o número na base 10.
o Octal. Exibe o número na base 8.
x Hexadecimal. Exibe número na base 16, usando letras minúsculas para os dígitos acima de 9.
e Expoente. Exibe o número em notação científica usando a letra ‘e’ para o expoente.
g Formato geral. Número com ponto fixo, exceto para números grandes, quando muda para a notação de expoente ‘e’.
n Número. É o mesmo que ‘g’ (para ponto flutuantes) ou ‘d’ (para inteiros), exceto que usa a configuração local para inserir separadores numéricos.
% Porcentagem. Multiplica número por 100 e exibe no formato fixo (‘f’), seguido de sinal %.

A função format() aciona o método __format__() interno ao objeto. Uma classe do programador pode ter esse método overloaded e customizado. Ele deve ser chamado como __format__(self, format_spec) onde format_spec são as especificações das opções de formatação.

As seguintes expressões são equivalentes:
format(obj,format_spec) <=> obj.__format__(obj,format_spec) <=> "{:format_spec}".format(obj)

No próximo exemplo construímos uma classe para representar datas com os atributos inteiros dia, mês e ano. O método __format() recebe um padrão e retorna a string de data formatada de acordo com esse padrão. Por exemplo, se padrao = 'dma' ela retorna '{d.dia}/{d.mes}/{d.ano}'.format(d=self). O método __str__() monta uma string diferente usando um dicionário que associa o número ao nome do meses.

» class Data:
»     def __init__(self, dia, mes, ano):
»         self.dia, self.mes, self.ano = dia, mes, ano
»     
»     def __format__(self, padrao):
»         p = '/'.join('{d.dia}' if t=='d' else ('{d.mes}' if t=='m' else '{d.ano}') for t in padrao)
»         return p.format(d=self)
»
»     def __str__(self):
»         mes = {1:'janeiro', 2:'fevereiro', 3:'março', 4:'abril', 5:'maio', 6:'junho',
»          7:'julho', 8:'agosto', 9:'setembro', 10:'outubro', 11:'novembro', 12:'dezembro'}
»         m = mes[self.mes]
»         return '{0} de {1} de {2}'. format(self.dia, m, self.ano)
» 
» d1, d2, d3 = Data(31,12,2019), Data(20,7,2021), Data(1,7,2047)
» 
» print('Usando função: format(objeto, especificacao)')
» print(format(d1,'dma'))
» print(format(d2,'mda'))
» print(format(d3,'amd'))
↳ Usando função: format(objeto, especificacao)
↳ 31/12/2019
↳ 7/20/2021
↳ 2047/7/1
»
» print('\nUsando formatação de string: padrao.format(objeto)')
» print('dia 1 = {:dma}'.format(d1))
» print('dia 2 = {:mda}'.format(d2))
» print('dia 3 = {:amd}'.format(d3))
↳ Usando formatação de string: padrao.format(objeto)
↳ dia 1 = 31/12/2019
↳ dia 2 = 7/20/2021
↳ dia 3 = 2047/7/1
» 
» print('\nUsando método __str__()')
» print(d3)
↳ Usando método __str__()
↳ 1 de julho de 2047

Função property() e decorador @property

Vimos anteriormente que pode ser útil isolar uma propriedade em uma classe e tratá-la como privada. Isso exige a existência de getters e setters, tratados na seção anterior.

No código abaixo definimos uma classe simples com métodos getter, setter e outro para apagar uma propriedade.

» class Pessoa:
»     def __init__(self):
»         self._nome = ''
»     def setNome(self,nome):
»         self._nome = nome.title()
»     def getNome(self):
»         return 'Não fornecido' if not self._nome else self._nome
»     def delNome(self):
»         self._nome = ''
»    
» # inicializamos um objeto da classe e atribuimos um nome
» p1 = Pessoa()
» p1.setNome('juma jurema')
» 
» print(p1.getNome())
↳ Juma Jurema
» 
» # apagando o nome
» p1.delNome()
» print(p1.getNome())
↳ Não fornecido

A função property facilita o uso desse conjunto de métodos. Ela tem a seguinte forma:

property(fget=None, fset=None, fdel=None, doc=None)

onde todos os parâmetros são opcionais. São esses os parâmetros:

  • fget representa o método de leitura,
  • fset método de atribuição,
  • fdel método de apagamento e
  • doc de uma docstring para o atributo. Se doc não for fornecido a função lê o docstring da função getter.

Ela é usada da seguinte forma:

» class Pessoa:
»     def __init__(self, nome=''):
»         self._nome = nome.title()
»
»     def setNome(self, nome):
»         self._nome = nome.title()
»
»     def getNome(self):
»         return 'Não informado' if self._nome=='' else self._nome
»
»     def delNome(self):
»         self._nome = ''
» 
»     nome = property(getNome, setNome, delNome, 'Propriedade de nome')
 
» p = Pessoa('juma jurema')
» print(p.nome)
↳ Juma Jurema
 
» p.nome = 'japira jaciara'
» print(p.nome)
↳ Japira Jaciara

» del p.nome
» print(p.nome)
↳ Não informado

Com a linha nome = property(getNome, setNome, delNome, 'Propriedade de nome') se adiciona um novo atributo nome associada aos métodos getNome, setNome, delNome. Fazendo isso os seguintes comandos são equivalentes:

  • p1.nomep1.getNome(),
  • p1.nome = 'novo nome'p1.setNome('novo nome') e
  • del p1.nomep1.delNome().

Ao invés de usar essa sintaxe também podemos usar o decorador @property.

» class Pessoa:
»     def __init__(self, nome):
»         self._nome = nome.title()
» 
»     @property
»     def nome(self):
»         return 'Não informado' if self._nome=='' else self._nome
» 
»     @nome.setter
»     def nome(self, nome):
»         self._nome = nome.title()
» 
»     @nome.deleter
»     def nome(self):
»         self._nome = ''

» p = Pessoa('adão adâmico')
» print(p.nome)
↳ Adão Adâmico

» p.nome = 'arthur artemis'
» print(p.nome)
↳ Arthur Artemis

» del p.nome
» print(p.nome)
↳ Não informado

Observer que o decorador @property define a propriedade nome e portanto deve aparecer antes de nome.setter e nome.deleter.

Se mais de um atributo deve ser decorado o processo deve ser repetido para cada atributo.

» class Pessoa:
»     def __init__(self):
»         self._nome = ''
»         self._cpf = ''
» 
»     @property
»     def nome(self):
»         return self._nome
» 
»     @property
»     def cpf(self):
»         return self._cpf
»  
»     @nome.setter
»     def nome(self, nome):
»         self._nome = nome
»         
»     @cpf.setter
»     def cpf(self, cpf):
»         self._cpf = cpf
        
» p = Pessoa()
» p.nome = 'Albert'
» p.nome
↳ 'Albert'

» p.cpf = '133.551.052-12'
» p.cpf
↳ '133.551.052-12'

O decorador @property permite que um método seja acessado como se fosse um atributo. Ele é particularmente útil quando já existe código usando um acesso direto à propriedade na forma de objeto.atributo. Se a classe é modificada para usar getters e setters o uso de @property dispensa que todo o restante do código seja alterado..

Método __getattr__

A função interna getattr é usada para ler o valor de um atributo dentro de um objeto. Além de realizar essa leitura ela permite que se retorne um valor especificado caso o atributo não exista. Ela tem a seguinte sintaxe:

getattr(objeto, atributo[, default])

onde os parâmetros são:

  • objeto (obrigatório), um objeto qualquer;
  • atributo (obrigatório), um atributo do objeto;
  • default (opcional), o valor a retornar caso o atributo não exista.

Ela retorna o valor de objeto.atributo.
Com funcionalidades associadas temos as seguintes funções, exemplificadas abaixo:

  • hasattr(objeto, atributo), que verifica se existe o atributo,
  • setattr(objeto, atributo), que insere um valor nesse atributo,
  • delattr(objeto, atributo), que remove o atributo desse objeto.
» class Pessoa:
»     nome = 'Einar Tandberg-Hanssen'
»     idade = 91
»     pais = 'Norway'

» p = Pessoa
» p.pais='Noruega'
» print(getattr(p, 'nome'))
↳ Einar Tandberg-Hanssen

» print(getattr(p, 'idade'))
↳ 91

» print(getattr(p, 'pais'))
↳ Noruega

» print(getattr(p,'profissao','não encontrado'))
↳ não encontrado

» # funções hasattr, setattr e delattr
» hasattr(p,'pais')
↳ True

» hasattr(p,'profissao')
↳ False

» setattr(p, 'idade', 28)
» p.idade
↳ 28

» delattr(p,'idade')
» hasattr(p,'idade')
↳ False

O método __getattr__ permite um overload da função getattr. Ele é particularmente útil quando se deseja retornar muitos atributos derivados dos dados fornecidos, seja por cálculo, por composição ou modificação.

» class Pessoa:
»     def __init__(self, nome, sobrenome):
»         self._nome = nome
»         self._sobrenome = sobrenome
» 
»     def __getattr__(self, atributo):
»         if atributo=='nomecompleto':
»             return '%s %s' % (self._nome, self._sobrenome)
»         elif atributo=='nomeinvertido':
»             return '%s, %s' % (self._sobrenome, self._nome)
»         elif atributo=='comprimento':
»             return len(self._nome) + len(self._sobrenome)
»         else:
»             return 'Não definido'

» p = Pessoa('Albert','Einsten')

» p.nomecompleto
↳ 'Albert Einsten'

» p.nomeinvertido
↳ 'Einsten, Albert'

» p.comprimento
↳ 13

» p.idade
↳ 'Não definido'

Função e método hash

O método __hash__ é invocado quando se aciona a função hash().

chave = hash(objeto)

Hash usa um algoritmo que transforma o objeto em um número inteiro único que identifica o objeto. Esses inteiros são gerados de forma aleatória (tanto quanto possível) de forma que diversos objetos no código não repitam o mesmo código. O hash é mantido até o fim da execução do código (ou se o objeto for reinicializado). Só existem hashes de objetos imutáveis, como inteiros, booleanos, strings e tuplas e essa propriedade pode ser usada para testar se um objeto é ou não imutável.

Essa chave serve como índice que agiliza a localização de objetos nas coleções e é usada internamente pelo Python em dicionários e conjuntos.

» # o hash de um inteiro é o  próprio inteiro
» hash(12345)
↳ 12345

» hash('12345')
↳ -918245046130431123

» # listas não possuem hashes
» hash([1,2,3,4,5])
↳ TypeError: unhashable type: 'list'

» # o hash de uma função
» def func(fim):
»     for t in range(fim):
»         print(t, end='')
» a = func
» print(hash(a))
↳ 8743614090882

» # todos os elementos de uma tupla devem ser imutáveis
» print(hash((1, 2, [1, 2])))
↳ TypeError: unhashable type: 'list'

Em uma classe podemos customizar __hash__ e __eq__ de forma a alterar o teste de igualdade, ou seja, definimos nosso próprio critério de comparação.

As implementações default de __eq__ e __hash__ nas classes usam id() para fazer comparações e calcular valores de hash, respectivamente. A regra principal para a implementação customizada de métodos __hash__ é que dois objetos iguais devem ter o mesmo valor de hash. Por isso se __eq__ for alterada para um teste diferente de igualdade, o que pode ser o caso dependendo de sua aplicação, o método __hash__ também deve ser alterado para ficar em acordo com essa escolha.

Métodos id, hash e operadores == e is

Três conceitos são necessários para entender id, hash e os operadores == e is que são, respectivamente: identidade, valor do objeto e valor de hash. Nem todos os objetos possuem os três.

Objetos têm uma identidade única, retornada pela função id(). Se dois objetos têm o mesmo id eles são duas referências ao mesmo objeto. O operador is compara itens por identidade: a is b é equivalente a id(a) == id(b).

Objetos também têm um valor: dois objetos a e b têm o mesmo valor se a igualdade pode ser testada e a == b. Objetos container (como listas) têm valor definido por seu conteúdo. Objetos do usuário tem valores baseados em seus atributos. Objetos de diferentes tipos podem ter os mesmos valores, como acontece com os números: 0 == 0.0 == 0j == decimal.Decimal ("0") == fraction.Fraction(0) == False.

Se o método __eq__ não estiver definido em uma classe (para implementar o operador ==), seus objetos herdarão da superclasse padrão e a comparação será feita entre seus ids.

Objetos distintos podem ter o mesmo hash mas objetos iguais devem ter o mesmo hash. Armazenar objetos com o mesmo hash em um dicionário é muito menos eficiente do que armazenar objetos com hashes distintos pois a colisão de hashes exige processamento extra. Objetos do usuário são “hashable” por padrão pois seu hash é seu id. Se um método __eq__ for overloaded em uma classe personalizada, o hash padrão será desativado e deve também ser overloaded se existe intenção de manter a classe imutável. Por outro lado se você deseja forçar uma classe a gerar objetos mutáveis (sem valor de hash) você pode definir seu método __hash__ para retornar None.

Exibiremos dois exemplos para demonstrar esses conceitos. Primeiro definimos uma classe com sua implementação default de __equal__ e __hash__. Em seguida alteramos hash e __eq__ fazendo o teste de igualdade concordar com o teste de hash. Observe que em nenhum caso dois objetos diferentes satisfazem b1 is b2 já que o id é fixo e não pode ser alterado pelo usuário.

» # ----------- Exemplo 1 ---------------    
» class Bar1:
»     def __init__(self, numero, lista):
»         self.numero = numero
»         self.lista = lista

» b1 = Bar1(123, [1,2,3])
» b2 = Bar1(123, [1,2,3])
» b3 = b1

» b1 == b2                      # (o mesmo que b1 is b2)
↳ False
» b1 == b3                      # (o mesmo que b1 is b3)
↳ True
» id(b1) == id(b2)
↳ False
» hash(b1) == hash(b2)
↳ False

» # ----------- Exemplo 2 ---------------
» class Bar2:
»     def __init__(self, numero, lista):
»         self.numero = numero
»         self.lista = lista
»     def __hash__(self):
»         return hash(self.numero)
»     def __eq__(self, other):
»         return self.numero == other.numero and self.lista == other.lista
        
» b1 = Bar2(123, [1,2,3])
» b2 = Bar2(123, [1,2,3])
» b3 = Bar2(123, [4,5,6])

» b1 == b2
↳ True
» id(b1) == id(b2)
↳ False
» hash(b1) == hash(b2)
↳ True
» b1 == b3
↳ False
» hash(b1) == hash(b3)
↳ True

No segundo caso a alteração de __hash__ faz com que dois objetos diferentes, de acordo com o teste is, possam ter o mesmo código hash, o que pode ser útil, dependendo da aplicação.

Uma tabela que associa objetos usando o hash de uma coluna como índice, agilizando a busca de cada par é chamada de hashtable. No Python os dicionários são exemplos dessas hashtables.

O processo de hashing é usado em encriptação e verificação de autenticidade. O Python oferece diversos módulos para isso, como hashlib, instalado por padrão, e cryptohash disponível em Pypi.org.

Métodos __new__ e __del__

Vimos que o método __init__ é adicionado automaticamente quando uma classe é instanciada e que podemos passar valores em seus parâmetros para inicializar o objeto. Antes dele outro método é acionado automaticamente, o método __new__, acessado durante a criação do objeto. Ambos são acionados automaticamente.

Além dele o método __del__ é ativado quando o objeto é destruído (quando a última referência é desfeita e o objeto fica disponível para ser coletado pela lixeira).

O parâmetro cls representa a superclasse que será instanciada e seu valor é provido pelo interpretador. Ele não tem acesso a nenhum dos atributos do objeto definidos em seguida.

» class A:
»     def __new__(cls):
»         print("Criando novo objeto") 
»         return super().__new__(cls)
»     def __init__(self): 
»         print("Dentro de __init__ ")
»     def __del__(self):
»         print("Dentro de __del__ ")

» # instanciando um objeto
» a = A()
↳ Criando novo objeto
↳ Dentro de __init__ 

» # finalizando a referência ao objeto
» del a
↳ Dentro de __del__ 

A função super(), que veremos como mais detalhes, é usada para acessar a superclasse de A, que no caso é object, a classe default da qual herdam todos os demais objetos. Nessa linha se retorna o método __new__ da superclasse e, sem ela, não teríamos acesso à A.__init__.

Como não há uma garantia de que o coletor de lixo destruirá o objeto imediatamente após a referência ser cortada (veja a seção sobre o coletor de lixo) método __del__ tem utilidade reduzida. Ele pode ser útil, no entanto e por ex., para desfazer outra referência que pode existir para o mesmo objeto. Também podem ser usadas em conjunto __new__ e __del__ para abir e fechar, respectivamente, uma conexão com um arquivo ou com um banco de dados.

Se __init__ demanda por valores de parâmetros esses valores devem ser passados para __new__. O exemplo abaixo demostra o uso de __new__ para decidir se cria ou não um objeto da classe Aluno. Se a nota < 5 nenhum objeto será criado.

» class Aluno:
»     def __new__(cls, nome, nota):
»         if nota >= 5:
»             return object.__new__(cls)
»         else:
»             return None
» 
»     def __init__(self, nome, nota):
»         self.nome = nome
»         self.nota = nota
» 
»     def getConceito(self):
»         return 'A' if self.nota >= 8.5 else 'B' if self.nota >= 7.5 else 'C'
» 
»     def __str__(self):
»         return 'Classe = %s\nDicionário(%s) Conceito: %s %s' % (self.__class__.__name__,
»                                                    str(self.__dict__),
»                                                    self.getConceito(),
»                                                    '-'*60 )
                                                   
» a1, a2, a3 = Aluno('Isaac Newton', 9), Aluno('Forrest Gump',4), Aluno('Mary Mediana',7)

» print(a1)
» print(a2, '<---- Objeto não criado' + '-'*60)
» print(a3)

↳ Classe = Aluno
↳ Dicionário({'nome': 'Isaac Newton', 'nota': 9}) Conceito: A
↳ ------------------------------------------------------------
↳ None <---- Objeto não criado
↳ ------------------------------------------------------------
↳ Classe = Aluno
↳ Dicionário({'nome': 'Mary Mediana', 'nota': 7}) Conceito: C
↳ ------------------------------------------------------------

A classe Aluno usa no método __str__ dois atributos das classes: __class__ e __dict__. __class__ retorna um objeto descritor, que é de leitura apenas (não pode ser modificado) que contém dados sobre a superclasse, entre eles o atributo __name__, o nome da classe.

» a1.__class__==Aluno
↳ True

» print(a1.__class__.__name__)
↳ Aluno

» a1.__dict__
↳ {'nome': 'Isaac Newton', 'nota': 9}

» print(dir(a1))
↳ ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
↳ '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
↳  '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
↳ '__weakref__', 'getConceito', 'nome', 'nota']

A função dir retorna todos os seus métodos e propriedades. Como todos os objetos do Python, a classe Aluno possui o atributo __dict__ que é um dicionário contendo suas propriedades. As propriedades podem ser apagadas, editadas ou inseridas diretamente nesse dicionário.

» # inserindo campo 'nascimento'
» a1.__dict__['nascimento'] = '21/03/2001'
» a1.nascimento
↳ '21/03/2001'

» # apagando campo 'nota'
» del(a1.__dict__)['nota']
» a1.nota
↳ AttributeError: 'Aluno' object has no attribute 'nota

Funções e Classes Fábrica (Factory Classes, Functions)


Na programação orientada a objetos uma fábrica é um objeto usado para a criação de outros objetos. Ela pode ser uma função ou método que retorna objetos derivados de um protótipo ou de uma classe e suas subclasses, à partir de parâmetros que permitem a decisão sobre qual objeto retornar.

Isso é particularmente útil quando um grande número de objetos devem ser criados. Esse construtor generalizado pode ter um processo de decisão sobre que subclasse usar. No exemplo seguinte usamos a classe Personagem e suas subclasses Cientista e Estudante definidas anteriormente para construir um exemplo que gera alguns objetos.

» # uma função "factory"
» def criaPersonagem(qual, energia, inteligencia):
»     if qual == 0:
»         return Personagem(energia, inteligencia)
»     elif qual ==1:
»         return Cientista(energia, inteligencia)
»     elif qual ==2:
»         return Estudante(energia, inteligencia)
»     else:
»         print('Personagem = 0, 1, 2')

» # Constroi 3 personagens de tipos diferentes e os armazena em um dicionário
» personagens = {}
» for t in range(3):
»     personagens[t] = criaPersonagem(t, 100, 100)

» # temos um dicionário com os personagens
» for t in range(3):
»     print(personagens[t])
↳ Vida = 100 Poder = 100
↳ Cientista: Vida = 1100 Poder = 500
↳ Estudante: Vida = 600 Poder = 1000

Um design interessante para evitar um código repleto de ifs e, ao mesmo tempo, forçar uma padronização da informação consiste em usar códigos associados ao dado que se quer informar. Para o exemplo seguinte suponha que estamos interessados em classificar livros em uma biblioteca. Para isso criamos um dicionário que associa um código a cada gênero de livros. Com isso obrigamos o preenchimento de dados a se conformar com a inserção correta do código e evitar erros tais como escrever de modo diferente o mesmo gênero (como Poesias e Poemas).

Digamos que em uma biblioteca existem livros dos seguintes gêneros:

  • Romance
  • Poesia
  • Ficção Científica
  • Divulgação Científica
  • Política
  • Biografias

Claro que em um caso mais realista teríamos muito mais gêneros e subgêneros, tipicamente armazenados por código em um banco de dados.

Iniciamos por criar um dicionário (categoria) usando código do gênero como chave. A definição da classe Livro testa na inicialização se o código fornecido existe e, caso afirmativo, substitui self.genero com o gênero correspondente. Se o código não existe fazemos self.genero = "Não Informado".

» # armazenamos a relação codigo - gênero
» categoria = {1 : 'Romance',
»              2 : 'Poesia',
»              3 : 'Ficção Científica',
»              4 : 'Divulgação Científica',
»              5 : 'Política',
»              6 : 'Biografias'
»              }
» 
» # definimos a classe livro
» class Livro:
»     def __init__(self, codigoGenero, titulo, autor):
»         if codigoGenero in categoria.keys():
»             self.genero = categoria[codigoGenero]
»         else:
»             self.genero = 'Não informado'
»         self.titulo = titulo
»         self.autor = autor
»         
»     def __str__(self):
»         txt = 'Gênero: %s\n' % self.genero
»         txt += 'Título: %s\n' % self.titulo
»         txt += 'Autor: %s\n' % self.autor
»         return txt

» # Inicializamos livro com código existente
» livro1 = Livro(1, 'Anna Karenina','Leo Tolstoy')
» print(livro1)
↳ Gênero: Romance
↳ Título: Anna Karenina
↳ Autor: Leo Tolstoy

» # Inicializamos livro com código inexistente
» livro2 = Livro(9, 'Cosmos','Carl Sagan')
» print(livro2)
↳ Gênero: Não informado
↳ Título: Cosmos
↳ Autor: Carl Sagan

Outra abordagem seria armazenar o próprio código transformando-o em texto apenas no momento de uma consulta ou impressão. Esse tipo de design é particularmente útil quando a quantidade de dados mapeados é grande e a consulta a eles é frequente.

Já vimos que uma subclasse pode fazer poucas modificações na classe base, aproveitando quase todo o seu conteúdo mas customizando alguns atributos.

» class Biografia(Livro):
»     def __init__(self, titulo, autor, biografado):
»         self.biografado = biografado
»         super().__init__(6, titulo, autor)
»     def __str__(self):
»         return '%sBiografado: %s' % (super().__str__(), self.biografado)

» bio1 = Biografia('Um Estranho ao Meu Lado','Ann Rule','Ted Bundy')
» print(bio1)
↳ Gênero: Biografias
↳ Título: Um Estranho ao Meu Lado
↳ Autor: Ann Rule
↳ Biografado: Ted Bundy

Suponha que tenhamos criado subclasses especializadas para cada gênero: Biografia, Politica, Poesia, …, etc. Uma factory de classes pode usar um dicionário que associa diretamente um código à classe. No exemplo temos, além da classe Biografia, criamos outras duas apenas para efeito de demonstração.

» class Politica(Livro):
»     pass
»
» class Poesia(Livro):
»     pass
    
» # uma factory de classe (retorna a classe apropriada)
» class FactoryLivro:
»     def __init__(self, i):
»         classe = {6: Biografia, 5: Politica, 2: Poesia}
»         self.essaClasse = classe.get(i,Livro)
»     def getClasse(self):
»         return self.essaClasse

» # Inicializa com livro do gênero 6 (biografia, único que defimos corretamente)
» fact = FactoryLivro(6)
» # classBio vai receber a classe Biografia
» ClassBio = fact.getClasse()

» # instancia um objeto de ClassBio
» bio2 = ClassBio('Vivendo na Pré-história','Ugah Bugah','Fred Flistone')
» print(bio2)
↳ Gênero: Biografias
↳ Título: Vivendo na Pré-história
↳ Autor: Ugah Bugah
↳ Biografado: Fred Flistone

Relembrando, usamos acima o método de dicionários dict.get(key, default), que retorna o valor correspondente à key se ela existe, ou default, se não existe.

Claro que, ao invés de criar subclasses para cada gênero, também poderíamos ampliar a generalidade da própria superclasse Livro inserindo campos flexíveis capazes de armazenar as especificidades de cada gênero, tal como as propriedades Livro.nomeDoCampo e Livro.valorDoCampo.

Classes servidoras de dados: Podemos definir uma classe que não recebe dados do usuário e apenas é usada para preparar e retornar dados em alguma forma específica. A classe Baralho abaixo representa um baralho construído na inicialização, juntando naipes com números de 2 até 10, adicionados de A, J, Q, K. Um coringa C é adicionado a cada baralho. Um objeto da classe tem acesso ao método Baralho.distribuir(n) que seleciona e retorna aleatoriamente n cartas. Para embaralhar as cartas usamos o método random.shuffle que mistura elementos de uma coleção (pseudo) aleatoriamente.

» import random
» class Baralho:
»     def __init__(self):
»         naipes = ['♣', '♠', '♥','♦']
»         numeros = list(range(1, 14))
»         numeros = ['A' if i==1
»                    else 'K' if i==13
»                    else 'Q' if i==12
»                    else 'J' if i==11
»                    else str(i) for i in numeros
»                   ]
»         deck = [a + b for a in numeros for b in naipes]
»         deck += ['C']
»         random.shuffle(deck)
»         self.deck = deck
» 
»     def distribuir(self, quantas):
»         if quantas > len(self.deck):
»             return 'Só restam %d cartas!' % len(self.deck)
»         mao = ''
»         for t in range(quantas):
»             mao += '[%s]' % self.deck.pop()
»         return mao

» jogo = Baralho()
» player1 = jogo.distribuir(11)
» player2 = jogo.distribuir(11)

» print(player1)
↳ [7♥][5♠][6♥][3♦][6♠][8♣][4♥][3♣][9♠][Q♦][7♣]

» print(player2)
↳ [4♦][A♦][J♣][J♥][8♠][9♦][8♦][10♦][10♥][5♥][3♥]


O método lista.pop() retorna o último elemento da lista, removendo o elemento retornado, como uma carta retirada de um baralho.

Para tornar esse processo ainda mais interessante poderíamos criar a classe Carta que gera objetos contendo separadamente seu naipe e valor para facilitar as interações com outras cartas durante o jogo. Esses objetos poderiam, inclusive, armazenar o endereço de imagens de cada carta do baralho, gerando melhores efeitos visuais. Com isso o método Baralho.distribuir() poderia retornar uma coleção de cartas e não apenas strings com uma represntação simples das cartas do baralho.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Funções, Decoradores e Exceções


Funções e decoradores

No Python tudo é um objeto, inclusive as funções. Isso significa que elas podem ser atribuidas a uma variável ou serem retornadas por outra função. Na programação em geral uma função é considerada um objeto de primeira classe se:

  • é uma instância do tipo Object,
  • pode pode ser armazenada em uma variável,
  • pode ser passada como um parâmetro para outra função,
  • pode ser obtida no retorno de outra função,
  • pode ser armazenada em estruturas de dados, como listas e dicionários.

No Python funções podem ser atribuídas a variáveis.

# uma variável pode armazenar uma função interna
» p = print
» p(1234)
↳ 1234

# ou uma do usuário
» def funcao():
»     print('Tô aqui!')

» a = funcao
# a é uma função
» print(a)
↳ <function __main__.funcao()>

# a função é executada com colchetes
» a()
↳ Tô aqui!

# outra função recebe uma string como parâmetro
» def funcao(texto):
»     print(texto)

» a = funcao

» a('Meu nome é Enéas!')
↳ 'Meu nome é Enéas!

Funções podem ter outras funções definidas dentro de seu corpo. No caso abaixo temos o cálculo da função composta \(f(x) = \sqrt(x^2+1)\).

» import math

» def funcaoComposta(x):
»     def funcaoInterna(i):
»         return i**2 + 1
»     return math.sqrt(funcaoInterna(x))

» funcaoComposta(7)
↳ 7.0710678118654755

Funções podem ser passadas como argumentos para outras funções. A função digaOla(arg) recebe outras duas funções como argumento.

# funções como argumento de outras funções
» def falaAlto(texto):
»     return texto.upper()

» def falaBaixo(texto):
»     return texto.lower()

» def digaOla(func):
»     # variável oi armazena o retorno (string) das funções no argumento func
»     oi = func('Olá, texto usado como argumento da função parâmetro!')
»     print (oi)

» digaOla(falaBaixo)
↳ olá, texto passado como argumento da função parâmetro!

» digaOla(falaAlto)
↳ OLÁ, TEXTO PASSADO COMO ARGUMENTO DA FUNÇÃO PARÂMETRO!

A função funcOla é chamada de decoradora. A função funcNome, que é passada como argumento para o decorador, é chamada de função decorada.

» # exemplo 1
» def funcOla(varFuncao):
»     def funcInterna():
»         print('Olá ', end='')
»         varFuncao()
»     return funcInterna

» def funcNome():
»     print('Assurbanipal, rei da Assíria')

» obj = funcOla(funcNome)
» obj()
↳ Olá Assurbanipal, rei da Assíria

# exemplo 2
» def func1(txt):
»     print(txt)

» def func2(funcao, txt):
»     funcao(txt)

» func2(func1, 'Libbali-sharrat, esposa de Assurbanipal')
↳ Libbali-sharrat, esposa de Assurbanipal

# exemplo 3
» def decoradora(func):
»     def interna():
»         print("Ocorre antes da função parâmetro ser executada.")
»         func()
»         print("Ocorre depois da função parâmetro ser executada.")
»     return interna

» def digaUau():
»     print("Uau!!!!")

» f = decoradora(digaUau)    #   <---- f é uma função composta

» f()                        #   <---- executar a função f
↳ Ocorre antes da função parâmetro ser executada.
↳ Uau!!!!
↳ Ocorre depois da função parâmetro ser executada.

Funções compostas são chamadas de objetos de segunda classe ou funções de ordem superior. Decoradores envolvem uma função, modificando seu comportamento. Quando executamos f = decoradora(digaUau) estamos executando interna() tendo em seu corpo func=digaUau().

O Python fornece uma forma simplificada de usar decoradores, usando o sinal @.

» def funcaoDecoradora(funcaoArg):
»     def interna():
»         # corpo de interna usando funcaoArg()
»     return interna

» @funcaoDecoradora
» def funcaoDecorada:
»     # corpo de decorada #

» # essa sintaxe é equivalente à
» funcaoComposta = funcaoDecoradora(funcaoDecorada)
» # para executá-la
» funcaoComposta()

No caso do último exemplo 3 podemos apenas fazer

» def decoradora(func):
»     def interna():
»         print("Ocorre antes da função decorada ser executada.")
»         func()
»         print("Ocorre depois da função decorada ser executada.")
»     return interna

» @decoradora
» def digaUau():
»     print("Uau!!!!")

» digaUau()
↳ Ocorre antes da função decorada ser executada.
↳ Uau!!!!
↳ Ocorre depois da função decorada ser executada.

Se a função a ser decorada possuir parâmetros, a função interna (que envolve a decorada) deve possuir os mesmos parâmetros, que devem ser fornecidos quando se invoca a função decorada.

» def produtoDecorado(func):
»     def interna(a,b):
»         print('%d x %d = ' % (a,b), end='')
»         return func(a,b)
» 
»     return interna

» @produtoDecorado
» def produto(a,b):
»     print(a * b)

» produto(55,33)
↳ 55 x 33 = 1815

Vale lembrar que se desejamos passar um número qualquer de parâmetros podemos usar *args e *kwargs, que representam respectivamente um número arbitrário de argumentos e de argumentos com palavras chaves.

» def produtoDecorado(func):
»     def interna(*args):
»         print('O produto %s = ' % str(args).replace(', ',' x '), end='')
»         return func(*args)
» 
»     return interna

» @produtoDecorado
» def produtorio(*args):
»     prod = 1
»     for t in args:
»         prod *= t
»     print(prod)

» produtorio(1,2,3,4,5,6,7,8,9)
↳ O produto de (1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9) = 362880
: time.time() retorna a hora em segundos, como um número de ponto flutuante, lida no relógio interno do computador. Ela é o número de segundos decorridos desde a época, 1 de Janeiro de 1970, 00:00:00 (UTC), também chamada de (Unix time).

Um uso comum para um decorador é o de medir o tempo de execução de um bloco de código qualquer. Isso pode ser útil na otimização de um programa. Para isso usamos o módulo time, e suas funções time.time(), que lê a hora em segundos , e time.sleep(n), que suspende a execução do código por n segundos.

Para isso envolvemos o bloco de código a ser medido, no caso a função que é decorada, com o contador. O instante inicial é armazenado e comparado com o final, após a execução, a a diferença é exibida.

» # decorador para medir o tempo de execução de um bloco de código
» import time

» def cronometro(func):
»     def interna(*arg):
»         inicio = time.time()
»         func(*arg)
»         print('O código levou %s segundos para rodar.' % str(time.time()-inicio))
»     return interna

» @cronometro
» def funcaoTeste(n):
»     time.sleep(n)

» funcaoTeste(1.5)
↳ O código levou 1.5015053749084473 segundos para rodar.

» # outro teste, com um laço for
» @cronometro
» def laco():
»     soma = 0
»     for t in range(10000):
»         soma += t
»     print('soma =',soma)

» laco()
↳ soma = 49995000
↳ O código levou 0.0010344982147216797 segundos para rodar.

Erros, Exceções e tratamento de erros

No Python existem dois tipos de erros que são denominados erros de sintaxe e exceções.

Erros de sintaxe são comandos escritos incorretamente, a ausência ou excesso de parênteses, chaves ou colchetes ((, {, [,), delimitadores incorretos de strings, vírgulas ausentes ou postas em local incorreto, etc. Quando encontra esses erros o interpretador interrompe a execução e retorna uma instrução de onde o erro ocorreu e, em alguns casos, uma sugestão de como consertá-lo. Nessas mensagens um sinal de ^ indica o local onde o erro foi notado. Se o código foi lido em um arquivo.py o nome do arquivo é indicado e a linha do erro é indicada. Essas mensagens são fornecidas pela função Traceback.

» print 'sem parênteses'
↳   File "<ipython-input-6-cfe4fc7e6b4d>", line 1
↳     print 'sem parênteses'
↳           ^
↳ SyntaxError: Missing parentheses in call to 'print'. Did you mean print('sem parênteses')?

» print('parênteses excessivos'))
↳  File "<ipython-input-7-1c97f0f5b744>", line 1
↳     print('parênteses excessivos'))
                                  ^
↳ SyntaxError: unmatched ')'

» dicionario = {1:'um', 2:'dois' 3:'três'}
↳   File "<ipython-input-12-60359adab8df>", line 1
↳     dicionario = {1:'um', 2:'dois' 3:'três'}
↳                                    ^
↳ SyntaxError: invalid syntax

Esses são, quase sempre, os erros mais fáceis de serem encontrados e corrigidos. Observe que, no Python 2, o comando print 'sem parênteses' estava correto. No Python 3 print() se tornou uma função e os parênteses passaram a ser obrigatórios.

Vimos que o Python usa indentações (que podem ser espaços ou tabs) para delimitar eus blocos de código. Erros desses tipos são capturados como IndentationError e TabError.

Excessões: Uma exceção é um evento que ocorre durante a execução de um programa que interrompe o fluxo das instruções, além dos erros de sintaxe. Quando o interpretador encontra uma situação com esse tipo de erro ele levanta uma exceção, instanciando uma das classes derivadas da superclasse exception. Exceções levantadas devem ser tratadas para que a execução do código não termine de forma indesejada. Uma lista completa de exceções pode ser encontrada no artigo Python, Resumo.

Um exemplo de exceção é a tentativa de dividir por zero.

» for i in range(4):
»     v = 10/(2-i)
»     print(v)
↳ 5.0
↳ 10.0
↳ ---------------------------------------------------------------------------
↳ ZeroDivisionError                         Traceback (most recent call last)
↳ <ipython-input-14-b8aab2286d16> in <module>
↳             1 for i in range(4):
↳ ---->       2     v = 10/(2-i)
↳             3     print(v)
↳ ZeroDivisionError: division by zero

No exemplo é claro que quando i = 2 o denominador será nulo e a divisão por 0 não é definida. Por isso ZeroDivisionError foi lançada. Podemos corrigir esse erro simplesmente testando o denomidor e pulando o valor problemático. Mas denominadores nulos podem surgir de forma inesperada de muitas formas, tais como em dados lidos automaticamente ou inseridos pelo usuário. Por isso precisamos de um tratamento de erros. Para esse fim temos os blocos try, except e finally ou else.

  • try: verifica se há um erro no bloco seguinte de código,
  • except 1: recebe fluxo de execução em caso de exceção 1,
  • … : (podem existir várias capturas de exceções),
  • except n: recebe fluxo de execução em caso de exceção n,
  • else: código executado se nenhum erro for encontrado,
  • finally: código executado em ambos os casos.

Portanto, se suspeitamos que há possibilidade de um erro ser lançado envolvemos partes do código nesses blocos.

» for i in range(4):
»     try:
»         v = 10/(2-i)
»         print('i = %d, v = %d' % (i,v))
»     except:
»         print('Erro em i = %d' % i)

» # no caso de i=2 o primeiro comando print não é executado
↳ i = 0, v = 5
↳ i = 1, v = 10
↳ Erro em i = 2
↳ i = 3, v = -10

No caso acima except captura qualquer erro que tenha acontecido. Blocos grandes de código podem estar dentro de um try com captura genérica. Isso não é muito bom em muitos casos pois não saberíamos que tipo de de erro foi lançado. Ao invés disso podemos capturar um erro específico.

» # supondo que a variável w não está definida
» try:
»     print(w)
» except NameError:
»     print("A variável w não está definida")
» except:
»     print("Outro erro ocorreu")
» A variável w não está definida

O opção else ocorre se nenhuma exceção foi capturada. finally ocorre em ambos os casos e pode ser útil para a execução de alguma finalização ou limpeza.

Suponha que existe o arquivo arquivoTeste.txt na pasta de trabalho atual mas ele está marcado como read only (somente de leitura).

» try:
»     f = open('arquivoTeste.txt')
»     f.write('Lorum Ipsum')
» except:
»     print('Aconteceu alguma coisa errada com esse arquivo!')
» else:
»     print('Operação bem sucedida!')
» finally:
»     f.close()
»     print('* conexão fechada')
↳ Aconteceu alguma coisa errada com esse arquivo!
↳ * conexão fechada

Se o arquivo arquivoTeste2.txt não existe na pasta de trabalho outro erro será lançado:

» try:
»     f = open('arquivoTeste2.txt')
» except FileNotFoundError:
»     print('Esse arquivo não existe!')
» except:
»     print('Aconteceu alguma coisa errada com esse arquivo!')
» finally:
»     f.close()
↳ Esse arquivo não existe!

Suponha que na atual pasta de trabalho existe uma subpasta dados. Se tentarmos abrir essa pasta como se fosse um arquivo teremos uma exceção.

» try:
»     arq = 'dados'
»     f = open(arq)
» except FileNotFoundError:
»     print('Esse arquivo não existe!')
» except IsADirectoryError:
»     print('"%s" é uma pasta e não um arquivo!' % arq)
» else:
»     f.close()
↳ "dados" é uma pasta e não um arquivo!

As exceções FileNotFoundError e IsADirectoryError são ambas subclasses de OSError. As duas exceções são capturadas por essa superclasse.

» try:
»     arq = 'dados'
»     f = open(arq)
» except OSError:
»     print('"%s" é uma pasta e não um arquivo!' % arq)
↳ "dados" é uma pasta e não um arquivo!

» try:
»     arq = 'arquivoNaoExistente'
»     f = open(arq)
» except OSError:
»     print('"%s" não existe!' % arq)
↳ "arquivoNaoExistente" não existe! 

Diversos erros podem ser capturados em um bloco.

» try:
»     lunch()
» except SyntaxError:
»     print('Fix your syntax')
» except TypeError:
»     print('Oh no! A TypeError has occured')
» except ValueError:
»     print('A ValueError occured!')
» except ZeroDivisionError:
»     print('Did by zero?')
» else:
»     print('No exception')
» finally:
»     print('Ok then')

Segue uma lista parcial de erros e sua descrição. Uma lista completa de exceções pode ser encontrada no artigo Python, Resumo.

Exceção Ocorre quando
AsserationError na falha de uma instrução assert
AttributeError em erro de atribuição de atributo
FloatingPointError erro em operação de ponto flutuante
MemoryError ocorre falta de memória para realizar a operação
IndexError há uma chamada à índice fora do intervalo existente
NotImplementedError erro em métodos abstratos
NameError não existe uma variável com o nome no escopo local ou global
KeyError chave não encontrada no dicionário
ImportError tentativa de importar módulo não existente
ZeroDivisorError tentativa de divisão por 0 (zero)
GeneratorExit um gerador é abandonado antes de seu final
OverFlowError uma operação aritmética resulta em número muito grande
IndentationError indentação incorreta
EOFError uma função como input() ou raw_input() retorna end-of-file (EOF, fim de arquivo)
SyntaxError um erro de sintaxe é levantado
TabError espaço ou tabulações inconsistentes
ValueError uma função recebe um argumento com valor incorreto
TypeError tentativa de operação entre tipos incompatíveis
SystemError o interpretador detecta erro interno

É possível capturar o erro lançado com a expressão except Exception as varExcecao: de forma a exibir a mensagem embutida no objeto.

» x, y = 2, '3'
» try:
»     y + x
» except TypeError as t:
»     print(t)
↳ can only concatenate str (not "int") to str

Vários tipos de exceções podem ser capturadas simultaneamente.

try:
    <código que pode conter as exceções>
    ......................
except(Exception1[, Exception2[,...ExceptionN]]]):
    <tratamento das exceções, caso ocorram>
    ......................
else:
    <código executado caso nenhuma das exceções ocorra>
    ......................    

Além das diversas exceções built-in lançadas automaticamente o usuário pode lançar suas próprias exceções. Isso é feito com raise.

» x = 'um'
» if not isinstance(x, int):
»     raise ValueError("Tipo incorreto")
» else:
»     print(34/x)
↳ ValueError: Tipo incorreto

No exemplo acima isinstance(x, int) testa se x é uma instância de int, ou seja, se x é um inteiro.

O usuário pode definir suas próprias exceções, lembrando que devem ser todas derivadas da classe Exception. No exemplo as classes ValorMuitoBaixoError e ValorMuitoAltoError herdam todos os atributos da superclasse, sem acrescentar nenhuma cacterística própria.

» class ValorMuitoBaixoError(Exception):
»     """Erro lançado quando a tentativa é um valor muito baixo"""
»     pass

» class ValorMuitoAltoError(Exception):
»     """Erro lançado quando a tentativa é um valor muito alto"""
»     pass

» # Você deve adivinhar esse número
» numero = 10

» # Loop enquanto o número não for correto
» while True:
»     try:
»         num = int(input("Digite um número: "))
»         if num < numero:
»             raise ValorMuitoBaixoError
»         elif num > numero:
»             raise ValorMuitoAltoError
»         else:
»             print('Acertou!')
»             break
»     except ValorMuitoBaixoError:
»         print("Valor muito pequeno. Tente de novo!\n")
»     except ValorMuitoAltoError:
»         print("Valor muito alto. Tente de novo!\n")

» # ao ser executado o código abre um diálogo para input do usuário
» # suponha que as tentativas feitas são: 2, 55, 10
↳ Digite um número: 2
↳ Valor muito pequeno. Tente de novo!

↳ Digite um número: 55
↳ Valor muito alto. Tente de novo!

↳ Digite um número: 10
↳ Acertou!        

Além de simplesmente herdar da superclasse as classes de erros customizadas podem fazer o overload de seus métodos para realizar tarefas específicas. No caso abaixo usamos apenas uma classe indicativa de erro e alteramos a propriedade message da classe e da superclasse para informar se o erro foi para mais ou menos. Por default o método __str__ retorna essa mensagem.

No trecho abaixo fazemos o overload também de __str__ para incluir uma mensagem mais completa, mantendo igual todo o restante do código.

» # Você deve adivinhar esse número, entre 0 e 100
» numero = 50

» class ValorIncorretoError(Exception):
»     """Exceção lançada para erro de valor """
» 
»     def __init__(self, valor):
»         message='Valor %d é muito %s' % (valor,'baixo' if valor < numero else 'alto')
»         self.message = message
»         super().__init__(self.message)

» # Loop enquanto o número não for correto
» while True:
»     try:
»         num = int(input('Digite um número entre 0 e 100: '))
»         if num != numero:
»             raise ValorIncorretoError(num)
»         else:
»             print('Acertou!')
»             break
»     except ValorIncorretoError as vi:
»         print('%s. Tente de novo!\n' % str(vi))

↳ Digite um número entre 0 e 100: 34
↳ 34. Tente de novo!

↳ Digite um número entre 0 e 100: 89
↳ 89. Tente de novo!

↳ Digite um número entre 0 e 100: 50
↳ Acertou!

Assert e AssertionError

A instrução assert fornece um teste de uma condição. Se a condição é verdadeira o código continua normalmente sua execução. Se for falsa a exceção AssertionError é lançada, com uma mensagem de erro opcional. Ela deve ser usada como um auxiliar na depuração do código, informando o desenvolvedor sobre erros irrecuperáveis ​​em um programa. Asserções são autoverificações internas do programa e funcionam através da declaração de condições que não deveriam ocorrer de forma alguma. O lançamento de uma exceção AssertionError deve indicar que há um bug no código e sua ocorrência deve informar qual condição inaceitável foi violada.

» # forçando o levantamento de AssertionError
» a, b = 2, 3
» assert a==b
↳ AssertionError

Suponha que uma loja monta um sistema para gerenciar suas vendas. Em algum momento o vendedor pode oferecer um desconto na compra mas o gerente determinou que o desconto não pode ser superior a 50%. Definimos uma função de cálculo do valor final da venda que impede que o preço final seja menor que metade do preço original, o maior que ele.

» def precoComDesconto(preco, desconto):
»     try:
»         precoFinal = preco * (1-desconto/100)
»         assert .5 <= precoFinal/preco <= 1
»     except AssertionError:
»         return 'Desconto inválido!'
»     else:    
»         return precoFinal

» print(precoComDesconto(120,50))
↳ 60.0

» print(precoComDesconto(120,55))
↳ Desconto inválido!

O último exemplo mostra que um AssertionError pode ser capturado como qualquer outra exceção lançada.

Exceções do tipo AssertionError não devem ser usadas em produtos finais, no código depois de todos os testes de erros foram executados. Parcialmente porque é possível executar o código desabilitando todas as instruções assert. Suponha que um desenvolvedor quer evitar que um usuário, que não o administrador do sistema, apague registros em um banco de dados.

» # não faça isso!
» def apagarRegistros(usr):
»     assert usr.isAdmin()
»     < código de apagamento >

Se o sistema for executado com desabilitação de assert qualquer usuário tem acesso ao apagamento de dados!

Erros lógicos

Outro tipo de erro são os erros lógicos que, provavelmente, ocupam a maior parte do tempo de debugging dos desenvolvedores. Eles ocorrem quando o código não tem erros de sintaxe nem exceções de tempo de execução mas foram escritos de forma que o resultado da execução é incorreto. Um exemplo simples seria de uma lista que começa a ler os elementos com índice i=1, o que faz com que o primeiro elemento seja ignorado. Esses erros podem ser complexos e difíceis de serem encontrados e corrigidos pois não causam a interrupção do programa nem lançam mensagens de advertência.

Os três exemplos abaixo mostram casos de erros lógicos.

» # 1) queremos o produto dos 9 primeiros números
» produto = 1
» for i in range(10):
»     produto *= i
» print(produto)
↳ 0

» # 2) queremos soma dos 9 primeiros números
» num = 0
» for num in range(10):
»     num += num
» print(num)
↳ 18

» # 3) queremos a soma dos quadrados dos 9 primeiros números
» soma_quadrados = 0
» for i in range(10):
»     iquad = i**2
» soma_quadrados += iquad
» print(soma_quadrados)
↳ 81

É muito difícil ou impossível escrever um codigo mais complexo sem cometer erros de lógica. Algumas sugestões podem ajudar a minorar esse problema:

  • Planeje antes de começar a escrever código:
    • Faça diagramas deixando claro quais são os dados de entrada do código e o que se espera obter. Tenha clareza sobre o objetivo de seu projeto.
    • Fluxogramas e pseudocódigo ajudam nesse aspecto.
  • Comente o código e use docstrings corretos para suas funções e classes:
    • Um código bem documentado é mais fácil de ser compreendido não só por outros programadores que talvez trabalhem em seu projeto como para você mesmo, quando tiver que rever um bloco algum tempo após tê-lo idealizado.
    • Docstrings podem ser acessados facilmente pelo desenvolvedor e usado por várias IDEs para facilitar seu acesso.
  • Escreva primeiro blocos de código de funcionamento geral, depois os detalhes, testando sempre cada etapa.
  • Teste o produto final com dados válidos e dados inválidos:
    • Faça testes usando valores esperados. É particularmente importante testar o código com valores limítrofes, tais como o mais baixo e o mais alto aceitável. Teste também usando valores incorretos que podem ser, inadvertidamente, pelo usuário final. Um exemplo comum, o usuário pode digitar a letra l ou invés do dígito 1. Valores fora da faixa esperada e de tipos diferentes devem ser experimentados.
    • Para aplicativos usados por muitos usuários finais, particularmente os executados na internet, use testadores que não aqueles que desenvolveram o código.

Instrumentos de depuração (debugging)

Uma das formas simples de encontrar erros no código consiste em escrever instruções print() nas partes onde desejamos observar um valor intermediário de alguma variável. Existem IDEs que permitem o acompanhamento em tempo real de cada valor, na medida em que ocorrem na execução do código. E, finalmente, existem programas auxiliares para a localização de erros. Algumas dessas ferramentas verificam a sintaxe do código escrito marcando os erros e sugerindo melhor estilo de programação. Outras nos permitem analisar o programa enquanto ele está em execução.

Pyflakes, pylint, PyChecker e pep8

Descritos na documentação do Python esses quatro utilitários recebem os arquivos *.py como input e analisam o código em busca de erros de sintaxe e alguns de erros de tempo de execução. Ao final eles imprimem avisos sugerindo melhor estilo de codificação e códigos ineficientes e potencialmente incorretos como, por exemplo, variáveis ​​e módulos importados que nunca são usados.

Pyflakes analisa as linhas de código sem importá-lo. Ele detecta menos erros que os demais aplicativos mas é mais seguro pois não há risco de executar código defeituoso. Ele também é mais rápido que as demais ferramentas aqui descritas.

Pylint e PyChecker importam o código e produzem listas mais extensas de erros e advertências. Eles são especialmente importantes quando se considera a funcionalidade de pyflakes muito básica.

Pep8 faz uma análise do código procurando por trechos com estilo de codificação ruim, tendo como padrão o estilo proposto na Pep 8 que é o documento de especificação para um bom estilo de codificação no Python.

Todos eles são usados com os comandos de comando, no prompt do sistema:

> pyflakes meuCodigo.py
> pylint meuCodigo.py
> pychecker meuCodigo.py
> pep8 meuCodigo.py

pdb


pdb é um módulo built-in, usado para depurar código enquanto ele está em execução. É possível usá-lo invocando-o como um script enquanto o código é executado ou importar o módulo e usar suas funções junto com o código do desenvolvedor. pdb permite que o código seja executado uma linha de cada vez, ou em blocos, inspeccionando a cada passo o estado do programa. Ele também emite um relatório de problemas que causam o término da execução por erros.

> import pdb
» def funcaoComErro(x):
»     ideia_ruim = x + '4'

» pdb.run('funcaoComErro(3)')
↳ > <string>(1)>module>()

Como script pdb pode ser executado da seguinte forma:
python3 -m pdb myscript.py.

Uma descrição mais completa de pdb pode ser encontrada em Python Docs: Pdb.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Strings


Strings

Strings são variáveis usadas para armazenar texto. No Python elas são sequências, coleções de caracteres ordenada por posição. A manipulação de strings é um dos pontos fortes do python e existem diversas funções para isso.

Strings são definidas com aspas simples (') ou aspas duplas ("). Aspas triplas (''' ou """) podem ser usadas para definir strings longas. Strings dentro de parentêses são concatenadas, o que é útil para se escrever uma string longa sem perder a legibilidade.

» string_1 = 'casa'
» string_2 = ' da mãe Joana'
» print(string_1 + string_2)
↳ casa da mãe joana

» string_longa = '''
             este é um
             exemplo de uma
             string longa!
             '''
» print(string_longa)
↳              este é um
               exemplo de uma
               string longa!

# string concatenada com parênteses (quebras de linhas são ignoradas)
» outra_longa = (
     'https://phylos.net/'
     '2020-12-16/'
     'pacote-factoranalyser/'
     )

» print(outra_longa)
↳  'https://phylos.net/2020-12-16/pacote-factoranalyser/'

Quando se usa aspas triplas, (''' ou """), os espaços e quebras de linha são mantidos na string final. Esse recurso é muito utilizado da documentação de módulos e funções, como veremos.  Já nas strings concatenadas com parênteses espaços e quebras de linha são removidos.

Vamos ver alguns exemplos de strings e de suas manipulações. Aqui usamos print(string, end=' ') para exibir a valor de string sem a quebra de linha que é default para essa função. Nesse caso a quebra é substituída por end=’ ‘, um espaço em branco.

# a sequência pode ser percorrida
» for letra in "banana":
»     print(letra, end=' ')
↳ b a n a n a 

# definimos uma variável de string
» palavra = 'Pindamonhangaba!'

# len() fornece seu comprimento (quantos caracteres)
» len(palavra)
↳ 16
# um caracter pode ser obtido por seu índice
» print(palavra[0], palavra[15])
↳ P !
# índices negativos contam do fim para o início
» print(palavra[-1])
↳ !
» print(palavra[-2])
↳ a

# palavra[-n] equivale a palavra[len(palavra)-n]  (desde que n < len(palavra))
» palavra[len(palavra)-3]   # palavra[-3]
↳ 'b'

# uma 'fatia' (slice)
» palavra[1:4]
↳ 'ind'

# se o segundo índice for omitido a fatia vai até o fim
» palavra[5:]
↳ 'monhangaba!'

# se o primeiro índice for omitido a fatia começa no início
» palavra[:5]
↳ 'Pinda'

» palavra[:]
↳ 'Pindamonhangaba!'

# len(palavra[i:j]) = j-i
» len(palavra[1:4])
↳ 3

Vimos nos exemplos acima que uma fatia (ou slice) palavra[i:f] se inicia em i e termina em f, exclusive. Os índices, e todas as contagens em python se iniciam em 0. A posição final não é incluida de forma que len(palavra[i:j]) = j-i.

Os operadores + e * (adição e multiplicação) tem significado especial quando os operandos são strings. + promove a concatenação de strings e * retorna uma repetição.

# Operações com palavras
» p1 = 'casa'
» p2 = 'da mãe Joana'

# '+' faz uma concatenação
» p1 + ' ' + p2
↳ 'casa da mãe Joana'

# '*' repete a string
» p1 * 5
↳ 'casacasacasacasacasa'

Alguns operadores lógicos também são permitidos.

» palavra1 = 'teste'
» palavra2 = 'Teste'

» palavra1 == palavra2   # 2 sinais de =
↳ False

» palavra1 != palavra2
↳ True

» 't' in palavra1
↳ True

» 'y' not in palavra1
↳ True

Observe que nenhuma das operações realizadas até agora afetam o conteúdo da variável. Ao contrário, cada uma delas produz uma nova string. Dizemos que strings são imutáveis no python. Como são imutáveis, strings não podem ser alteradas in loco. No exemplo abaixo, para alterar ‘rato’ para ‘pato’ é necessário construir nova string e atribuí-la à variável.

# Strings são imutáveis
» str = 'rato'
» str[0]
↳ 'r'

# a tentativa de alterar a string resulta em erro
» str[0] = 'p'
↳ TypeError: 'str' object does not support item assignment

# para trocar 'r' por 'p'
» str = 'p' + str[1:]
» str
↳ 'pato'

Métodos das strings

Claro que o leitor não precisa se preocupar em memorizar todas essas funções. Uma primeira leitura serve para se ter conhecimento das possibilidades.

Como objetos, as strings possuem métodos e propriedades (coisas que veremos com detalhes mais tarde). A lista abaixo contém alguns métodos.

Método descrição
capitalize() converte 1º caracter em maiúsculo
casefold() converte string em minúsculas
center() retorna string centralizada
count() retorna número de ocorrências de um valor especificado na string
endswith() retorna True se string termina com valor especificado
find() busca por valor especificado na string e retorna a posição se encontrado
format() Formata de acordo com valores especificados (detalhes abaixo)
index() busca por valor especificado na string e retorna the posição se encontrado
isalnum() retorna True se todos os caracteres são alfa-numéricos
isalpha() retorna True se todos os caracteres são alfabéticos
isdecimal() retorna True se todos os caracteres são decimais
isdigit() retorna True se todos os caracteres são dígitos
islower() retorna True se todos os caracteres são minúsculos
isnumeric() retorna True se todos os caracteres são numéricos
isspace() retorna True se todos os caracteres são espaços
istitle() retorna True se a string segue regra de títulos
isupper() retorna True se todos os caracteres são maiúsculos
join() reune elementos de um iterável no final da string
ljust() retorna a string justificada à esquerda
lower() converte a string para minúsculas
lstrip() retorna a string sem espaços à esquerda
partition() retorna tuple partindo a string em 3 partes
replace() substitui trecho da string por outro especificado
rfind() busca trecho especificado value e retorna última posição
rindex() busca trecho especificado value e retorna última posição
rjust() retorna string justificada à direita
rsplit() quebra a string no separador especificado, retornando lista
rstrip() retorna a string sem espaços à direita
split() quebra a string no separador especificado, retornando lista
splitlines() quebra a string nas quebras de linha, retornando lista
startswith() retorna True se string começa com valor especificado
strip() retorna a string sem espaços laterais
swapcase() inverte minúsculas e maiúsculas
title() converte em maiúscula o 1º caracter de cada palavra
upper() converte a string em maiúsculas
zfill() preencha com número de zeros especificado, no início

Alguns exemplos de uso são apresentados a seguir.

» str = 'uma palavra qualquer'
# tornar a primeira letra maiúscula
» str.capitalize()
↳ 'Uma palavra qualquer'

# contar quantas letras 'a' existem na string
» str.count('a')
↳ 5

# a string termina com 'r'?
» str.endswith('r')
↳ True

# em que posição 'qu' está na string
» str.find('qu')
↳ 12

# se o trecho procurado não existe na string
» str.find('y')
↳ -1

» str.isnumeric()
↳ False

» '1234567890'.isnumeric()
↳ True

» str.replace('palavra','coisa')
'uma coisa qualquer'

» str.startswith('un')
↳ False

# join é usada para concatenar strings
» uma_tupla = ('Carro', 'Casa', 'Gado')
» x = '-'.join(uma_tupla)
» print(x)
↳ Carro-Casa-Gado

# exemplo de uso para join, construindo uma tabela html
» html = '<table><tr><td>'
» html += '</td><td>'.join(uma_tupla)
» html += '</td></tr></table>'
» print(html)
↳ <table><tr><td>Carro</td><td>Casa</td><td>Gado</td></tr></table>

# lembrando que x = 'Carro-Casa-Gado'
» for txt in x.split('-'):
»     print(txt)
↳ Carro
  Casa
  Gado

# quebrando uma frase em palavras
» frase = 'A segunda palavra da frase'
» frase.split(' ')[1]
↳ 'segunda'

No exemplo abaixo str é quebrada nos espaços, pegando cada palavra em separado. O primeiro caso imprime a palavra que começa com ‘r’ e termina com ‘a’. O segundo imprime a palavra que começa com ‘p’ e não termina com ‘a’.

» str = 'pato pata rato rata'

» for palavra in str.split(' '):
»     if palavra.startswith('r') and palavra.endswith('a'):
»         print(palavra)
↳ rata

» for palavra in str.split(' '):
»     if palavra.startswith('p') and not palavra.endswith('a'):
»         print(palavra)
↳ pato

Existem alguns mecanismos de apoio à formatação de strings, particularmente útil na impressão de resultados. Os sinais especiais %d e %s podem ser usados na montagem de strings, o primeiro como substituto de um dígito, o segundo de texto. %f informa a substituição por número de ponto flutuante, sendo %.nf  um número de ponto flutuante com n casas decimais. %E indica número com notação científica (usando potências de 10).

» s1 = 'Uma string com um dígito e texto: dígito: %d; texto: %s' % (10, 'dez')
» print(s1)
↳ Uma string com um dígito e texto: dígito: 10; texto: dez

print('Um inteiro convertido em ponto flutuante: %f' % 5)
↳ Um inteiro convertido em ponto flutuante: 5.000000

print('Exibir o número %.02f com 2 casas decimais' % 25.123456)
↳ Exibir o número 25.12 com 2 casas decimais

print('Um número com notação científica: %E' % 6.789E7)
↳ Um número com notação científica: 6.789000E+07

Um método mais moderno de formatação consiste um usar string.format(), que permite código mais legível.

# marcadores nomeados são supridos por format
» txt1 = "Eu me chamo {nome} e tenho {idade} anos.".format(nome = 'João', idade = 36)
» print(txt1)
↳ Eu me chamo João e tenho 36 anos.

# os lugares podem ser marcados numericamente
» txt2 = "Moro em {0}, {1} há {2} anos.".format('Brasília', 'DF', 20)
» print(txt2)
↳ Moro em Brasília, DF há 20 anos.

# ou marcados apenas por sua ordem de aparecimento
» txt3 = "{} são convertidos em {}. Exemplo: {}.".format('Dígitos', 'strings', 100)
» print(txt3)
↳ Dígitos são convertidos em strings. Exemplo: 100.

# podemos controlar do número de casas decimais exibidos
» txt4 = 'Esse livro custa R$ {preco:.2f} com desconto!'.format(preco = 49.8)
» print(txt4) 
↳ Esse livro custa R$ 49.80 com desconto!

Também existe o chamado método de interpolação de strings que usa o prefixo f para que a string possa incorporar variáveis.

» nome = 'Albert'
» sobrenome = 'Einstein'
» ano = 1879
» print(f'O físico {nome} {sobrenome} nasceu em {ano}')
↳ O físico Albert Einstein nasceu em 1879

# podemos inserir operações dentro da string de formatação
» a = 214
» b = 3
» print(f'{a} multiplicado por {b} é {a * b}.')
↳ 214 multiplicado por 3 é 642.

Observe que strings justapostas são concatenadas. No entanto isso gera um código menos claro. O método de interpolação torna o código de mais fácil entendimento.

print('Paul ''Adrien ''Maurice ''Dirac' ' nascido em ' '1902')
Paul Adrien Maurice Dirac nascido em 1902

fisico = 'P. A. M. Dirac'
ano = 1902
print(f'O físico {fisico} nascem em {ano}' )
O físico P. A. M. Dirac nascem em 1902

Outros exemplos com métodos de strings

Seguem mais alguns exemplos de uso. Os outputs aparecem como comentários.

# um string de teste
original = 'Pedro, Paulo,   Jonas'

# split retorna uma lista com as partes
original.split(',')                      # ['Pedro', ' Paulo', '   Jonas']

# a compreensão de lista com strip para eliminar espaços
partes = [x.strip() for x in original.split(',')]
partes                                   # ['Pedro', 'Paulo', 'Jonas']

# join juntas as partes
' : '.join(partes)                       # 'Pedro : Paulo : Jonas'

# testes de pertencimento
'Jonas' in original     # True
'Jo' in original        # True
'X' in original         # False

original.index('lo')    # 10
original.index('X')     # ValueError substring not found

original.find('Pa')     # 7
original.find('Pu')     # -1

original.count('P')     # 2

original.replace('o','a')                # 'Pedra, Paula,   Janas'

Outras operações mais sofisticadas com strings podem ser realizadas com expressões regulares. Sobre esse assunto você pode ler:

Caracteres de escape

Caracteres especiais podem ser inseridos em uma string por meio de “escapes”. Por exemplo, para usar aspas em uma string podemos fazer o seguinte:

# inserindo aspas simples em string
» print('O cara se chama O\'Maley')
↳ O cara se chama O'Maley

# se aspas duplas são usadas como delimitador a aspa simples pode ser usada
» print("O cara se chama O'Maley")
↳ O cara se chama O'Maley

» print("E seu primeiro nome é \"George\"")
↳ E seu primeiro nome é "George"

# quebra de linha
» print('linha1\nlinha2')
↳ linha1
↳ linha2
código significado
\’ aspas simples
\” aspas duplas
\\ barra invertida
\n nova linha
\r retorno de “carro”
\t tab, tabulação
\b backspace, volta 1 espaço
\f form feed
\ooo valor octal
\xhh valor hexadecimal
🔺Início do artigo

Continue a leituira: Python, testes, laços e funções

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Introdução

Programação

Um computador é uma máquina eletrônica com capacidade para armazenar valores numéricos em formato binário e instruções de manipulação desses valores armazenados. Informações não numéricas, como letras, palavras e imagens, precisam ser convertidas em binários para que possam ser tratados. Nos primeiros tempos da computação a interação com as máquinas era muito difícil e um programador tinha que escrever códigos muito complexos, como diretamente em linguagem de máquina ou assembly. Hoje, após muito aperfeiçoamento, as linguagens modernas abstraem as camadas mais complexas de interação com o computador. O código tem a aparência de uma linguagem natural humana e pode ser lido quase como um texto comum.

Um exemplo pode ilustrar essa descrição. Considere que uma pessoa possui uma caixa cheia de bolas pretas e brancas. Ela quer saber qual é a porcentagem de bolas pretas, no total. Uma possível solução seria o seguinte procedimento, usando um bloco de notas para registrar bolas pretas e brancas:

  1. tire uma bola da caixa,
  2. verifique se é preta,
  3. se for preta faça uma marca para bolas pretas, caso contrário para as brancas,
  4. sem retornar a bola, verifique se ainda restam bolas na caixa,
  5. se ainda restam bolas, volte para a etapa (1),
  6. se acabaram as bolas, conte quantas marcas foram feitas para cada cor,
  7. calcule porcentagem = pretas/(pretas+brancas)*100 e anote esse resultado.

Esse procedimento é chamado de algoritmo. A lista de tarefas é análoga à um programa de computador. As marcas feitas para contagem das bolas brancas e pretas são análogas às variáveis do programa.

Jupyter

O Python pode ser executado de várias formas diferentes. Existem bons editores e IDEs (Integrated Devolopment Environment) tais como o VSCode, Pycharm, Geany ou Spyder. Para esse tutorial usaremos o Jupyter Notebook. Para maiores informações sobre o Jupyter leia nesse site sobre sua instalação e execução.

Resumindo, sua instalação pode ser feita instalando-se o Anaconda:

  • Baixe a versão mais recente do Anaconda aqui. Existem instaladores gráficos para Linux, Windows e MacOS.
  • Instale seguindo as instruções na página de download ou contidas no arquivo instalador executável.
  • ou … usuários que conhecem Python e que preferem gerenciar seus pacotes manualmente, pode apenas usar:
    pip3 install jupyter.

Python

Python é uma linguagem de programação de alto nível, interpretada e de propósito geral, criada por Guido van Rossum em 1985 e em franco desenvolvimento desde então. Ela está disponível sob a licença GPL (GNU General Public License). Ela permite o uso interativo, com o usuário digitando as linhas de código e obtendo o resultado imediatamente, ou através de lotes (batches), com as linhas de código armazenadas em arquivos e executadas em grupo. Apesar de ser chamada de linguagem de script é possível criar aplicativos completos, na web ou para desktop, com interfaces gráficas modernas e eficientes. Além disso existe a possibilidade de gerar arquivos compilados e executáveis usando Cython.

Python Package Index

Uma grande quantidade de bibliotecas ou módulos que podem ser importados e executados nas sessões do python o tornam atraente para o gerenciamento de bancos de dados, de imagens, a mineração de dados, análise e plotagem sofisticada, inteligência artificial e aprendizado de máquina, além de várias outras aplicações. Você pode procurar por bibliotecas em The Python Package Index (PyPI).

Python é uma linguagem orientada a objetos (um conceito a ser estudado mais tarde) e todos de seus elementos são objetos. Seus conceitos básicos são de rápido aprendizado, apesar da existência das várias bibliotecas para uso específico que podem ser um pouco mais complexas, como ocorre com o pandas.

Variáveis e operações básicas

Convenção: Usaremos nesse texto a seguinte convenção: linhas iniciadas com » marcam o código inserido para execução. No Jupyter Notebook são as linhas de código dentro das células. marca os outputs, as linhas de resposta do código. Linhas, ou trechos de linha, iniciadas com # são comentários não executados.

# Essa linha é um comentário    
» x = 15        # inserindo um inteiro
» print(x)
↳ 15

Você pode usar o python para fazer operações matemáticas como um calculadora, embora isso raramente seja algo muito útil. Mais interessante é a possibilidade de criar variáveis que armazenam dados. No python uma variável é criada no momento em que se atribui a ela um valor, sem necessidade de informar de que tipo será (o que chamamos de tipagem dinâmica).

# inicializamos duas variáveis numéricas e uma com uma palavra
» x = 15           # um número inteiro
» y = 4.2          # um número de ponto flutuante
» z = 'Pedro'      # uma palavra ou 'string'

# para ver o conteúdo das variáveis podemos usar o método print
» print(x)
↳ 15
» print(y)
↳ 4.2
» print(z)
↳ Pedro

# podemos ver de que tipo são essas variáveis
» print(type(x))
↳ <class 'int'>

» print(type(y))
↳ <class 'float'>

» print(type(z))
↳ <class 'str'>

# variáveis podem ser apagadas com
» del x
» print(x)
↳ NameError: name 'x' is not defined 

O comando print() é uma função interna do python, e serve para exibir o conteúdo de seu parâmetro, no caso as variáveis x, y, z. Quando usamos o Jupyter Notebook uma variável ou expressão avaliada na última linha de uma célula é exibida quando a célula é executada, sem a necessidade de print.

Usamos type() para ver o tipo de conteúdo armazendo. Os tipos básicos do Python são:

Tipo Exemplos Nome no Python
Inteiros 1, -1, 67, -9900 int
Inteiros longos 123L, -999L long integer
Ponto flutuante 1.0, 3.14, 345.6, -99.9 double float
Complexos complex(1, 3), complex(“1+2j”), 1 + j complex
Booleano True, False bool
String ‘casa’, “teste de string” string, object

Com esses tipos básicos se constroi objetos mais complexos, como

Tipo Exemplos Nome no Python
Lista [‘a’, ‘b’, ‘c’, ‘d’], [1, 2, 3] list
Tupla (‘a’, ‘b’, ‘c’, ‘d’), (1, 2, 3) tuple
Dicionário {1: ‘um’, 2: ‘dois’} dictionary

Qualquer objeto do python pode ser associado a uma variável, que passa a ser uma representante ou “referência de acesso” a esse objeto. Os nomes de variáveis devem obedecer a algumas regras:

  • só podem conter números, letras e sublinhados (underscores): A até z, 0 até 9, _,
  • deve começar com uma letra ou sublinhado,
  • maiúsculas e minúsculas são diferenciadas.

A diferenciação de maiúsculas de minúsculas é válida em toda parte do Python. As variáveis idade, Idade, IDADE são três variáveis diferentes. A instrução if, para definir um ponto de bifurcação condicional no código, não ser escrita como If ou IF. Além disso existem as palavras reservadas (keywords) que são nomes que fazem parte da sintaxe da linguagem e não podem ser utilizadas para nomes de variáveis:

Palavras reservadas do python
and del for lambda raise
as elif from None return
assert else global nonlocal True
break except if not try
Class exec import or while
continue False in pass with
def finally is print yield

Como assumem o lugar dos objetos as variáveis podem ser usadas em lugar deles. Por ex., a operação abaixo faz um cálculo usando 2 variáveis e atribuindo o valor à uma terceira.

» largura = 2.20
» altura = 1.10
» area_retangulo = largura * altura
» print("A área de retângulo é: " , area_retangulo)
↳ A área de retângulo é: 2.42

Para facilitar a leitura posterior de um código os nomes de variáveis devem ser escolhidos de forma concisa e elucidativa de seu uso.

Os seguintes operadores matemáticos formam a base para as operações numéricas usuais.

Operador significado exemplo numérico
+ Adição x + y 24 + 36 = 60
Subtração x – y 36 – 24 = 12
* Multiplicação x * y 15 * 3 = 45
** Exponentiação x ** y 8 ** 3 = 512
/ Divisão x / y 15 / 8 = 1.875
// Divisão inteira x // y 15 // 8 = 1
% Módulo (resto da divisão inteira) x % y 15 % 8 = 7

Para verificar seu comportamento vamos fazer alguns testes.

# no python se pode declarar 2 variáveis (ou mais) simultaneamente †
» x, y = 3, 8
» x + y
↳ 11
» x - y
↳ -5
» x * y
↳ 24
» y ** x
↳ 512
» x / y
↳ 0.375
# 3 dividido por 8 é 0, com resto 3
» x % y
↳ 0
» x // y
↳ 3
Estritamente dizendo, a atribuição x, y = 3, 8 é feita como em uma atribuição de tupla,
(x, y) = (3, 8). Leia Sequências e Coleções.

Terminologia: Um comando ou declaração (statement, em inglês) é uma linha com uma instrução única. Uma atribuição do tipo x = 1 armazena o valor 1 na variável x. No código abaixo temos 4 declarações. As 3 primeiras são atribuições e print() é uma função interna (predefinida) do Python:

» incremento = 0.2
» valor = 10
» valor = valor + incremento
» print(valor)
↳ 10.20

Várias declarações podem ser dispostas em uma única linha, separadas por ;. Por outro lado uma declaração longa pode ser quebrada em várias linhas.

# várias declarações em uma linha
» a=10; b=20; c=a*b; print(c)
↳ 200

# Uma string longa em várias linhas
» mensagem = "Olá.\nVocê está aqui para aprender a programar em Python.\n" \
»            "Leia o material e faça seus testes." \
»            "Indicar esse site para os seus amigos."

# Uma declaração longa em várias linhas 
» soma = 1 + 2 + 3 + 4 + \
»        5 + 6 + 7 + 8 + \
»        9 + 10
 
» print(mensagem)
↳ Olá.
  Você está aqui para aprender a programar em Python.
  Leia o material e faça seus testes. Indicar esse site para os seus amigos.

» print(soma)
↳ 55
# No texto acima usamos \n que é um código para quebra de linha. 

Diferente das operações matemáticas usuais, x = x + 1 significa some 1 ao valor de x e substitua o resultado em x.
Um atalho ou shortcut possível e muito usado é o seguinte:

# ao invés de escrever
» i = i + 1                  # podemos escrever, como o mesmo efeito
» i += 1                     # portanto
» valor = valor + incremento # pode ser escrito como
» valor += incremento

Outros operadores análogos podem ser usados.

Operador Exemplo Equivale a
+= Soma a += 2 a = a + 2
-= Subtração a -= 2 a = a – 2
*= Multiplicação a *= 2 a = a * 2
/= Divisão a /= 2 a = a / 2
%= Módulo a %= 2 a = a % 2
**= Expoente a **= 2 a = a ** 2
//= Divisão inteira a //= 2 a = a // 2

Além dos operadores matemáticos temos os operadores lógicos. Os resultados de operações lógicas são booleanos (True ou False) e eles são a base das decisões feitas em códigos. Alguns desses operadores podem ser usados, como veremos, em outros objetos que não apenas números.

Operador Significado Exemplo
> maior que x > y
>= maior ou igual a x ≥ y
< menor que x < y
>= menor ou igual a x ≤ y
== igual a x == y
!= diferente de x != y

Exemplos de uso de operações lógicas:

» num1 = 12
» num2 = 25

» print(num1 > num2)
↳ False

» print(num2 == num1 *2 +1)
↳ True

» string1 = 'isso'
» string2 = 'esse'
» print(string1 != string2)
↳ True

Execução do código em python


Foi sugerido o uso do Jupyter Notebook para o aprendizado do conteúdo nesse artigo. Isso permite interatividade e facilidade de instalação dos pacotes, bem como uma boa visualização das saídas de código. No entanto muitas vezes é necessário rodar blocos grandes de código sem preocupação com resultados e saídas intermediárias.

Arquivos contendo comandos python e gravados com a extensão *.py podem ser executados em qualquer máquina que tenha o interpretador instalado. Por exemplo, suponha que temos na pasta de trabalho ativa o arquivo teste.csv e queremos alterar o seu nome. Usando um editor ASCII qualquer, como o Notepad do Windows, Geany ou Gedit no Linux e TextEdit no Mac, gravamos um arquivo com o nome de alterarNome.py, com o seguinte conteúdo:

#!/usr/bin/env python
import os
for arq in os.listdir():
    print(arq)
nome_antigo = 'teste.csv'
nome_novo = 'teste_novo.csv'
os.rename(nome_antigo, nome_novo)

Para executar esse arquivo abrimos uma sessão do terminal e digitamos:

» python alterarNome.py

A primeira linha contém o shebang (#!) com a instrução de onde está instalado o python em seu computador. Depois é importada a biblioteca os que contém comandos de interação com o sistema operacional. Seu método os.listdir() retorna uma lista com os arquivos na pasta atual, que são impressos dento do laço for. Se existir na pasta um arquivo teste.csv ele será renomeado para teste_novo.csv. Uma mensagem de erro será exibida se o arquivo teste.csv não existir.


Strings do Python

🔺Início do artigo

Bibliografia

Python.org

Livros

  • Barry, Paul: Head First Python, O’Reilly, Sebastopol, 2011.
  • Ceder, Vernon: The Quick Python Book, 2 ed., Manning Publications Co., Greenwich, 2010.
  • Downey, Allen: How to Think Like a Computer Scientist, Cambridge University Press, Nova Iorque, 2009.
  • Hall, Tim; Stacey, J-P: Python 3 for Absolute Beginners, Apress, Nova Iorque, 2009.
  • Hetland, Magnus Lie: Beginning Python, From Novice to Professional, Apress, Nova Iorque, 2005.
  • Lambert, Kenneth: Fundamentals of Python: From First Programs Through Data Structures, Cengage, Boston, 2010.
  • Lee, Kent D.: Python Programming Fundamentals, Springer-Verlag, Londres, 2011.
  • Lutz, Mark:Learning Python, 4ª Edição, O’Reilly, Sebastopol, 2005.
  • Payne, James: Beginning Python, Using Python 2.6 and Python 3.1, Wiley, Indianoplis, 2010.
  • Summerfield, Mark: Programming in Python 3, a complete introduction to the Python language, Pearson, Boston, 2010.

Recursos na internet

Introdução ao Pandas – Series

🔻Final do artigo

O que é pandas?

Pandas é uma biblioteca do Python, de código aberto e com licença BSD, desenvolvida e mantida pelo PуDаtа Dеvеlорmеnt Tеаm. Ela fornece ferramentas de manipulação e análise estatística de dados, capacidade de exibição gráfica E extração de dados análogos (mas não idênticos) aos de consultas sql.

A biblioteca foi construída com Cython e, por isso, é bastante rápida. Ela se destinava inicialmente ao uso no setor financeiro para análise de séries temporiais, tendo se tornado uma ferramenta de uso comum na manipulação de dados, particularmente em data science e machine learning. Ela tem sido usada para substituir as planilhas do Excel, para processar dados sob forma tabular, importando com facilidade dados de arquivos csv ou json.

Os experimentos abaixo foram realizados no Jupyter Notebook. Você encontra nesse site um artigo sobre instalação e uso do Jupyter Notebook. As linhas de código e suas respostas, quando existirem, serão representadas da seguinte forma:

» # Comentários (não são lidos ou executados pelo interpretador)
» Linha de input (entrada de comandos)
↳ Linha de output (resposta do código)

NumPy e matplotlib

NumPy é a abreviação de Numerical Python, a biblioteca base da computação numérica em Python. Ela fornece as estruturas de dados e algoritmos necessários para a maioria das aplicações científicas e de engenharia utilizando cálculo numérico. Entre outros objetos NumPy NumPy fornece

  • o objeto multidimensional ndarray onde se pode aplicar operações vetorializadas rápidas e eficientes,
  • um conjunto de funções para cálculos elementares com vetores e matrizes,
  • ferramentas de leitura e gravação de dados,
  • operações da álgebra linear, transformada de Fourier e geração de números aleatórios,
  • interação com C e C++.


Para dados numéricos as matrizes do NumPy são armazenadas e manipuladas de modo mais eficiente do que as demais estruturas do Python. Além disso códigos escritos em linguagens de baixo nível, como C ou Fortran, podem operar diretamente nos dados armazenados com o NumPy. Por isso muitas ferramentas de computação numérica do Python usam as matrizes NumPy como um estrutura de dados primária.

matplotlib é a biblioteca Python mais popular usada para a produção de gráficos e visualização de dados. Ela pode ser usada na geração de gráficos estáticos ou animados e visualização interativa.

Pandas

O pandas se utiliza basicamente de 3 objetos de armazenamento de dados com as seguintes estruturas:
Estrutura de dados dos objetos do pandas:

Nome dimensões tabela
Series 1D coluna (vetor)
DataFrame 2D tabela (matriz)
Panel 3D várias tabelas (matriz multidimensional)

As series e os dataframes são utilizados com maior frequência.

Como a manipulação de dados usando séries e dataframes frequentemente envolvem operações encontradas no módulo numpy é frequente sua importação junto com pandas.

» import pandas as pd
» import numpy as np

Series

Uma series é um objeto unidimensional, tipo um vetor, que contém uma sequência de objetos do mesmo tipo. A essa sequência está associado um outro vetor de labels chamado de index (índice). O método básico de criação de séries é da seguinte forma:
serie = pd.Series(data, index=index)
onde data pode ser um dict (um dicionário do Python), uma lista ou ndarray do numPy, ou um escalar. index é uma lista de índices que, se omitida, é preenchida com inteiros iniciando em 0.

» serie1 = pd.Series([-1, 9, 0, 2, 5])
↳  0   -1
   1    9
   2    0
   3    2
   4    5

À esquerda estão listados os índices que, por default, são uma lista de inteiros começando por 0. Os valores podem ser listados com .values e os índices com .index.

» serie1.values
↳ array([-1,  9,  0,  2,  5])

» serie1.index
↳ RangeIndex(start=0, stop=5, step=1)

Os índices podem ser inseridos manualmente e não precisam ser inteiros. O valor correspondente ao índice i pode ser acessado com serie[i], como mostrado abaixo, onde os índices são strings.

» serie2 = pd.Series([4, 7, -5, 3], index=['a', 'b', 'c', 'd'])
» serie2
↳ a    4
  b    7
  c   -5
  d    3
  dtype: int64

» serie2['c']
↳ -5

Uma série pode ser filtrada passando como índice outra série de elementos boolenos, (True, False). Além disso operações vetorializadas podem ser realizadas sobre todos os elementos da série.

# O teste seguinte gera uma série de booleanos
» serie2 > 3
↳ a     True
  b     True
  c    False
  d    False
  dtype: bool

# Essa serie de True e False filtra a serie original
» serie2[serie2 > 3]
↳ a    4
  b    7
  dtype: int64

# Operações podem, ser realizadas sobre todos os elementos
» serie2 * 3
↳ a    12
  b    21
  c   -15
  d     9
  dtype: int64

# o módulo Numpy possui a função exponencial
» np.exp(serie2)
↳ a      54.598150
  b    1096.633158
  c       0.006738
  d      20.085537
  dtype: float64

Séries se comportam, em muitos sentidos, como um dicionário. Uma série pode ser criada passando-se um dicionário como argumento para pd.Series().

» populacao = {
           'Sudeste': 89012240, 
           'Nordeste': 57374243,
           'Sul': 30192315,
           'Norte': 18672591,
           'Centro-Oeste':16504303
         }

» serie3 = pd.Series(populacao)

» serie3
↳ Sudeste         89012240
  Nordeste        57374243
  Sul             30192315
  Norte           18672591
  Centro-Oeste    16504303
  dtype: int64 


A ordem dos itens na série pode ser alterada através do fornecimento de uma lista com o ordenamento desejado para o argumento index. A elementos não presentes no dicionário serão atribuídos o valor NaN, Not a Number (não número). O método pd.isnull(serie) permite a avalição de quais elementos estão nulos ou NaN.

# fornecendo uma lista para o argumento index:
» ordem_alfabetica = ['Brasil', 'Centro-Oeste', 'Nordeste', 'Norte', 'Sudeste', 'Sul']
» serie4 = pd.Series(populacao, index=ordem_alfabetica)
» serie4
↳ Brasil               NaN
  Centro-Oeste    16504303
  Nordeste        57374243
  Norte           18672591
  Sudeste         89012240
  Sul             30192315
  dtype: int64

# para verificar quais valores são nulos (NaN)
» pd.isnull(serie4)
↳ Brasil           True
  Centro-Oeste    False
  Nordeste        False
  Norte           False
  Sudeste         False
  Sul             False
  dtype: bool

# os seguintes registros são NaN
» serie4[pd.isnull(serie4)]
↳ Brasil   NaN
  dtype: float64

» serie4[pd.notnull(serie4)]
↳ Centro-Oeste    16504303.0
  Nordeste        57374243.0
  Norte           18672591.0
  Sudeste         89012240.0
  Sul             30192315.0
  dtype: float64

» 'Brasil' in serie4
↳ True
» 'EUA' in serie4
↳ False
# uma excessão KeyError é lançada se o indice não existe
» serie4['EUA']
↳ KeyError

Como não existe no dicionário um valor para o índice Brasil a série atribuiu o valor NaN (Not a Number) para essa chave, a forma de NumPy e pandas indicar a ausência de um valor. O método retorna True ou False para cada item da série e pd.notnull() o seu inverso booleano. Alternativamente se pode usar o método das séries serie4.isnull().

Ainda sobre a semelhança entre séries e dicionários, podemos testar a existência de uma chave usando o operador in, como em 'Brasil' in serie4. A tentativa de recuperar um valor com índice não existente gera uma exceção (um erro do Python).

Observe que uma series tem propriedades de numpy ndarray, mas é um objeto de tipo diferente. Se um ndarray é necessário use series.to_numpy().

» type(serie4)
↳ pandas.core.series.Series

» type(serie4.to_numpy())
↳ numpy.ndarray

Series podem ser fatiadas com a notação serie[i:f] onde serão retornadas a i-ésima linha até a f-ésima, exclusive. Se i for omitido a lista se inicia em 0, se f for omitido ela termina no final da series.

» serie4
↳ Brasil                 NaN
  Centro-Oeste    16504303.0
  Nordeste        57374243.0
  Norte           18672591.0
  Sudeste         89012240.0
  Sul             30192315.0
  dtype: float64

» serie4[2:5]
↳ Nordeste        57374243.0
  Norte           18672591.0
  Sudeste         89012240.0
  dtype: float64

» serie4[:2]
↳ Brasil                 NaN
  Centro-Oeste    16504303.0
  dtype: float64

» serie4[4:]
↳ Sudeste    89012240.0
  Sul        30192315.0
  dtype: float64

Series podem ser exibidas com o método display(serie_0, serie_1, ..., serie_n). O resultado de operações envolvendo mais de uma serie, como a soma, alinha os valores por chaves (como uma UNION). Valores não presentes em um dos operandos terá NaN como resultado.

» serie5 = pd.Series([2, -1, -2, 1], index=['a', 'b', 'c', 'd'])
» serie6 = pd.Series([3, 4, 7, -1], index=['e', 'c', 'b', 'f'])
» display(serie5, serie6)
↳ a    2
  b   -1
  c   -2
  d    1
  dtype: int64

↳ e    3
  c    4
  b    7
  f   -1
  dtype: int64

» serie5 + serie6
↳ a    NaN
  b    6.0
  c    2.0
  d    NaN
  e    NaN
  f    NaN
  dtype: float64

Series possuem a propriedade name que pode ser atribuída na construção ou posteriormente com o método .rename(). No exemplo usamos o método np.random.randn(n) de numpy para fornecer um numpy.ndarray com n números aleatórios. Damos inicialmente a essa série o nome ‘randomica’, depois a renomeamos para ‘aleatoria’.

» serie7 = pd.Series(np.random.randn(5), name='randomica')
» serie7
↳ 0   -1.703662
  1    1.406167
  2    0.966557
  3   -0.557846
  4   -0.264914
  Name: randomica, dtype: float64

» serie7.name
↳ 'randomica'

» serie7= serie7.rename('aleatoria')
» serie7.name
↳ 'aleatoria'

O nome de uma série se torna seu índice ou nome de coluna caso ela seja usada para formar um DataFrame.

Atributos e Métodos das Series

Os atributos e métodos mais comuns e úteis das Series estão listados abaixo. Para uma lista completa consulte pandas.Series: API Reference.

Atributos

Atributo Descrição
at[n] Acesso ao valor na posição n
attrs Retorna ditionario de atributos globais da series
axes Retorna lista de labels do eixo das linhas
dtype Retorna o tipo (dtype) dos objetos armazenados
flags Lista as propriedades do objeto
hasnans Informa se existem NaNs
iat[n] Acesso ao valor na posição n inteiro
iloc[n] Acesso ao valor na posição n inteiro
index Retorna lista de índices
index[n] Retorna índice na n-ésima posição
is_monotonic Booleano: True se valores crescem de forma monotônica
is_monotonic_decreasing Booleano: True se valores decrescem de forma monotônica
is_unique Booleano: True se valores na series são únicos
loc Acessa linhas e colunas por labels em array booleano
name O nome da Series
nbytes Número de bytes nos dados armazenados
shape Retorna uma tuple com forma (dimensões) dos dados
size Número de elementos nos dados
values Retorna series como ndarray

Alguns casos de acessos à essas propriedades. Os outputs são exibidos como comentários:

» import pandas as pd
» import numpy as np 
» serie = pd.Series([1,-1, 2, 2, 6, 63])
» serie.size              # 6, o mesmo que len(serie)
» serie.at[5]             # 63
» serie.iloc[0]           # 1
» serie.index             # RangeIndex(start=0, stop=6, step=1)
» serie.is_monotonic      # False
» serie.is_unique         # False
» serie.shape             # (6,) 

Métodos

Nas tabelas abaixo a expressão “elemento a elemento” é abreviada para “e/e”. Estas operações são repetidas a cada elemento da, ou das, séries envolvidas. Por exemplo, o método serie.add(s2) é feita “elemento a elemento” (e/e):

» import pd	
» serie_a = pd.Series([-1,9,0,2, 5, -8])
» serie_b = pd.Series([1,-9,0, -2,-5, 8])
» serie_a.add(serie_b)
» # é o mesmo que
» serie_a + serie_b
» # que resulta em serie nula 
» # pd.Series([0,0,0,0,0,0])

Manipulação e gerenciamento das Series

Método (sobre série s, outra s2) Descrição
s.align(s2) Alinha 2 objetos em seus eixos usando método especificado
s.append(to_append[, ignore_index, …]) Concatena 2 ou mais Series
s.asof(where[, subset]) Último elemento antes da ocorrência de NaNs após ‘where’
s.backfill([axis, inplace, limit, downcast]) Aliás para DataFrame.fillna() usando method=’bfill’
s.bfill([axis, inplace, limit, downcast]) Alias para DataFrame.fillna() usando method=’bfill’
s.clip([min, max, axis, inplace]) Inclui apenas valores no intervalo
s.combine(s2, func[, fill_value]) Combina a s com s2 ou escalar, usando func
s.copy([deep]) Cópia do objeto s, índices e valores
s.drop_duplicates([keep, inplace]) Remove elementos duplicados de s
s.dropna() Remove valores faltantes de s
s.duplicated([keep]) Exibe valores duplicados na s
s.explode([ignore_index]) Transforma cada elemento de um objeto tipo lista em uma linha
s.fillna([value, method, axis, inplace, …]) Substitui valores NA/NaN usando método especificado
s.get(key) Retorna item correspondente à key
s.groupby([by, axis, level, as_index, sort, …]) Agrupa a s
s.head([n]) Retorna os n primeiros valores
s.interpolate([method, axis, limit, inplace, …]) Preenche valores NaN usando metodo de interpolação
s.item() Primeiro elemento dos dados como escalar do Python
s.items() Iteração (lazy) sobre a tupla (index, value)
s.iteritems() Iteração (lazy) sobre a tupla (index, value)
s.mask(cond[, s2, inplace, axis, level, …]) Substitui valores sob condição dada
s.max([axis, skipna, level, numeric_only]) Valor máximo
s.memory_usage([index, deep]) Memória usada pela s
s.min([axis, skipna, level, numeric_only]) Menor dos valores da s
s.nlargest([n, keep]) Retorna os n maiores elementos
s.nsmallest([n, keep]) Retorna os n menores elementos
s.nunique([dropna]) Retorna quantos elementos únicos existem na s
s.pad([axis, inplace, limit, downcast]) O mesmo que DataFrame.fillna() usando method=’ffill’
s.plot() O mesmo que pandas.plotting._core.PlotAccessor
s.pop(i) Remove s[i] de s e retorna s[i]
s.repeat(repeats[, axis]) Repete elementos da s
s.replace([to_replace, value, inplace, limit, …]) Substitui valores em to_replace por value
s.sort_values([axis, ascending, inplace, …]) Reorganiza s usando seus valores
s.str Usa funções de string sobre s (se string). Ex. s.str.split(“-“)
s.tail([n]) Últimos n elementos
s.unique() Retorna os valores da s, sem repetições
s.update(s2) Modifica s usando valores de s2, usando índices iguais
s.view([dtype]) Cria uma nova “view” da s
s.where(cond[, serie, inplace, axis, level, …]) Substitui valores se a condição cond = True

Operações matemáticas básicas:

s.ewm([com, span, halflife, alpha, …])Calcula exponencial com peso

s.abs() Retorna s com valor absoluto, e/e
s.add(s2) Soma s com s2, e/e
s.agg([func, axis]) Agrega usando func sobre o eixo especificado
s.apply(func[, convert_dtype, args]) Aplica func sobre os valores de s, e/e
s.div(s2) Divisão (float) de s por s2, e/e
s.divmod(s2) Divisão inteira e módulo de s por s2, e/e
s.dot(s2) Produto interno entre a s e s2
s.floordiv(s2) Divisão inteira da s por s2, e/e
s.mod(s2[, level, fill_value, axis]) Módulo de s por s2, e/e
s.rfloordiv(s2[, level, fill_value, axis]) Divisão inteira de s por s2, e/e
s.rmod(s2[, level, fill_value, axis]) Modulo da divisão da s por s2, e/e
s.rmul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.round([n]) Arredonda valores da s para n casas decimais.
s.rpow(s2[, level, fill_value, axis]) Exponential de s por s2, e/e
s.rsub(s2[, level, fill_value, axis]) Subtração da s por s2, e/e
s.rtruediv(serie[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.sub(s2) Subtração de s por s2, e/e
s.subtract(serie) Idem
s.sum([axis, skipna, level, numeric_only, …]) Soma dos valores da s
s.transform(func[, axis]) Executa func sobre elementos de s
s.truediv(s2) Divisão (float) de s por s2, e/e
s.truncate([before, after, axis, copy]) Trunca a s antes e após índices dados
s.mul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.multiply(s2[, level, fill_value, axis]) Idem
s.pow(s2) Exponential de s por s2, e/e
s.prod([axis, skipna, level, numeric_only, …]) Produto dos elementos da s
s.product([axis, skipna, level, numeric_only, …]) Idem
s.rdiv(s2[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.rdivmod(s2) Divisão inteira e módulo de s por s2, e/e

Operações estatísticas:

Método (sobre série s, outra s2) Descrição
s.corr(s2) Correlação de s com s2, excluindo NaNs
s.count([level]) Número de observações na s, excluindo NaN/nulls
s.cov(s2[, min_periods, ddof]) Covariância da s, excluindo NaN/nulls
s.cummax([axis, skipna]) Máximo cumulativo
s.cummin([axis, skipna]) Mínimo cumulativo
s.cumprod([axis, skipna]) Produto cumulativo
s.cumsum([axis, skipna]) Soma cumulativa
s.describe([percentiles, include, exclude, …]) Gera descrição estatística
s.kurt([axis, skipna, level, numeric_only]) Kurtosis imparcial
s.kurtosis([axis, skipna, level, numeric_only]) Idem
s.hist() Plota histograma da s usando matplotlib.
s.mad([axis, skipna, level]) Desvio médio absoluto dos valores de s
s.mean([axis, skipna, level, numeric_only]) Média dos valores
s.median([axis, skipna, level, numeric_only]) Mediana dos valores
s.mode([dropna]) Moda da s
s.quantile([q, interpolation]) Valor no quantil dado
s.ravel([order]) Retorna dados como um ndarray
s.sample([n, frac, replace, weights, …]) Amostra randomizada de items da s
s.sem([axis, skipna, level, ddof, numeric_only]) Erro padrão imparcial da média
s.skew([axis, skipna, level, numeric_only]) Inclinação imparcial
s.std([axis, skipna, level, ddof, numeric_only]) Desvio padrão da amostra
s.value_counts([normalize, sort, ascending, …]) Retorna s com a contagem de valores únicos
s.var([axis, skipna, level, ddof, numeric_only]) Variância imparcial dos valores da s

Operações com índices:

s.add_prefix('prefixo') Adiciona prefixo aos labels com string ‘prefixo’
s.add_suffix('sufixo') Adiciona sufixo aos labels com string ‘sufixo’
s.argmax([axis, skipna]) Posição (índice inteiro) do valor mais alto de s
s.argmin([axis, skipna]) Posição (índice inteiro) do menor valor de s
s.argsort([axis, kind, order]) Índices inteiros que ordenam valores da s
s.drop([labels]) Retorna s com labels removidos
s.first_valid_index() Índice do primeiro valor não NA/null
s.idxmax([axis, skipna]) Label do item de maior valor
s.idxmin([axis, skipna]) Label do item de menor valor
s.keys() Alias de index
s.last_valid_index() Índice do último valor não NA/null
s.reindex([index]) Ajusta a s ao novo índice
s.reindex_like(s2[, method, copy, limit, …]) Série com índices em acordo com s2
s.rename([index, axis, copy, inplace, level, …]) Altera o nome (labels) dos índices
s.reorder_levels(order) Reajusta níveis de índices usando order
s.reset_index([level, drop, name, inplace]) Reinicializa índices
s.searchsorted(value[, side, sorter]) Índices onde elementos devem ser inseridos para manter ordem
s.sort_index([axis, level, ascending, …]) Reorganiza s usando os índices

Testes, com retorno booleanos e comparações:

Método (sobre série s, outra s2) Descrição
s.all([axis, bool_only, skipna, level]) Booleano: se todos os elementos são True
s.any([axis, bool_only, skipna, level]) Booleano: se algum elemento é True
s.equals(s2) Booleano: True se s contém os mesmos elementos que s2
s.between(min, max) Booleano: satisfazendo min <= s <= max, e/e
s.compare(s2[, align_axis, keep_shape, …]) Compara s com s2 exibindo diferenças
s.eq(s2) Boleano: igualdade entre s e s2, e/e
s.ge(s2) Booleana: maior ou igual entre s e s2, e/e
s.gt(s2[, level, fill_value, axis]) Booleana: se s é maior que s2, e/e
s.isin(valores) Booleano: se elementos da s estão contidos em valores
s.isna() Booleano: se existem valores ausentes
s.isnull() Booleano: se existem valores nulos
s.le(s2) Booleana: se s é menor ou igual a s2, e/e
s.lt(s2[, level, fill_value, axis]) Booleana: se s é menor que s2, e/e
s.ne(s2[, level, fill_value, axis]) Booleana: se s é diferente de s2, e/e
s.notna() Booleana: se existem valores não faltantes ou nulos
s.notnull() Idem

Transformações para outros formatos e tipos:

s.astype(dtype[, copy, errors]) Transforma (cast) para dtype
s.to_clipboard([excel, sep]) Copia o object para o clipboard do sistema
s.to_csv([path_or_buf, sep, na_rep, …]) Grava a s como arquivo csv
s.to_dict() Converte s para dict {label ⟶ value}
s.to_excel(excel_writer[, sheet_name, na_rep, …]) Grava s como uma planilha Excel
s.to_frame([name]) Converte s em DataFrame
s.to_hdf(path_or_buf, key[, mode, complevel, …]) Grava s em arquivo HDF5 usando HDFStore
s.to_json([path_or_buf, orient, date_format, …]) Converte s em string JSON
s.to_latex([buf, columns, col_space, header, …]) Renderiza objeto para LaTeX
s.to_markdown([buf, mode, index, storage_options]) Escreve a s em formato Markdown (leia)
s.to_numpy([dtype, copy, na_value]) Converte s em NumPy ndarray
s.to_pickle(path[, compression, protocol, …]) Grava objeto serializado em arquivo Pickle
s.to_sql(name, con[, schema, if_exists, …]) Grava elementos em forma de um database SQL
s.to_string([buf, na_rep, float_format, …]) Constroi uma representação string da s
s.tolist() Retorna uma lista dos valores
s.to_list() idem

Operações com séries temporais:

s.asfreq(freq) Converte TimeSeries para frequência especificada.
s.at_time(time[, asof, axis]) Seleciona valores em determinada hora (ex., 9:30AM)
s.between_time(inicio, fim) Seleciona valores com tempo entre inicio e fim
s.first(offset) Seleciona período inicial de uma série temporal usando offset.
s.last(offset) Seleciona período final de uma série temporal usando offset

Alguns exemplos de uso dos métodos de pandas.Series:

» import pandas as pd
» import numpy as np
» serie = pd.Series([1,-1, 2, 2, 6, 63])
» serie.abs()
» # retorna  pd.Series([1, 1, 2, 2, 6, 63])

» # Muitos métodos não alteram a series inplace.
» s2 = serie.rename('NovoNome')  # não altera nome de serie
» s2.name
↳ 'NovoNome'
» # para alterar o nome usamos
» serie.rename('NovoNome', inplace=True)  # altera nome de serie inplace
» serie.name
↳ 'NovoNome'

» # um resumo estatístico pode ser visto com describe
» serie.describe()
↳ count    15.000000
  mean      4.866667
  std       3.044120
  min       0.000000
  25%       3.500000
  50%       5.000000
  75%       7.000000
  max       9.000000
  dtype: float64

» # gerando outra series
» data = np.random.randint(0, 10,size=15)
» serie2 = pd.Series(data)
» data
↳ array([2, 2, 6, 3, 1, 4, 3, 4, 3, 0, 8, 3, 8, 2, 7])

» serie.div(serie2)
↳ 0     2.500000
  1     0.000000
  ...
  9          inf
  13    0.000000
  14    1.000000
  dtype: float64


Observe que a divisão por 0 não gera erro mas é representada por inf.

Alguma habilidade gráfica pode ser encontrada entre os métodos das series. Um exemplo é o serie.hist() que traça o histograma da series usando matplotlib. Veremos com mais detalhes as funcionalidades dessa biblioteca.

» data = np.random.randint(0, 10,size=15)
» data
array([5, 0, 7, 7, 0, 9, 6, 5, 6, 9, 5, 5, 2, 0, 7])
» serie = pd.Series(data)
» serie.hist()

Objetos do pandas possuem métodos poderosos e eficientes. O método serie.str() permite operações de strings sobre os elementos da serie, se esses forem strings.

» str = ['-mercado','-tensão','-plasia']
» serie = pd.Series(str)
» serie
↳ 0    -mercado
  1     -tensão
  2     -plasia

» serie = serie.str.replace('-','hiper-')
» serie
↳ 0    hiper-mercado
  1     hiper-tensão
  2     hiper-plasia

» serie.str.split('-')
↳ 0    [hiper, mercado]
  1     [hiper, tensão]
  2     [hiper, plasia]

» # elementos não strings resultam em NaN
» serie = pd.Series([123,'-tensão','-plasia'])
» serie.str.replace('-','hiper-')
↳ 0             NaN
  1    hiper-tensão
  2    hiper-plasia

Series podem ser usadas na construção de dataframes, que serão vistos a seguir. Em particular o método to_frame() transforma uma series em um dataframe com uma coluna, onde cada valor ocupa uma linha.

» # uma series pode ser transformada em um dataframe
» df = serie.to_frame()
» # um dataframe é gerado e armazenado em df

Como a maioria dos métodos de series são análogos àqueles de dataframes faremos uma exploração mais extensa desses na sessão referente aos dataframes.

🔺Início do artigo

Bibliografia

  • Blair,Steve: Python Data Science, The Ultimate Handbook for Beginners on How to Explore NumPy for Numerical Data, Pandas for Data Analysis, IPython, Scikit-Learn and Tensorflow for Machine Learning and Business, edição do autor disponível na Amazon, 2019.
  • Harrison, Matt: Learning Pandas, Python Tools for Data Munging, Data Analysis, and Visualization,
    edição do autor, 2016.
  • McKinney, Wes: pandas: powerful Python data analysistoolkit, Release 1.2.1
    publicação do Pandas Development Team, revisado em 2021.
  • McKinney, Wes: Python for Data Analysis, Data Wrangling with Pandas, NumPy,and IPython
    O’Reilly Media, 2018.
  • Pandas: página oficial, acessada em janeiro de 2021.
  • Pandas User Guide, acessada em fevereiro de 2021.
  • Miller, Curtis: On Data Analysis with NumPy and pandas, Packt Publishing, Birmingham, 2018.

Marcação de Texto com Markdown

“O poeta é um fingidor. Finge tão completamente que chega a fingir que é dor a dor que deveras sente.”
Fernando Pessoa, Autopsicografia.

O que é Markdown

Markdown é uma linguagem de marcação de texto. Ele permite a insercão de marcas ou códigos em texto simples que são representados na exibição final em formatos sofisticados. Além disso ela permite a conversão para textos em HTML, que podem ser distribuídos pela web (ou localmente) e lidos por browsers. A marcação foi concebida para deixar o texto simples e legível. Markdown é software livre, disponível sob uma licença BSD de código aberto.

Títulos (Headings)

Para criar títulos use sinais # em frente ao texto (um para cada nível).

Markdown html Output obtido
# Título de nível 1 <h1>Título de nível 1</h1> (título de nível 1)
## Título de nível 2 <h2>Título de nível 2</h2> (título de nível 2)
### Título de nível 3 <h3>Título de nível 3</h3> (título de nível 3)
#### Título de nível 4 <h4>Título de nível 4</h4> (título de nível 4)
##### Título de nível 5 <h5>Título de nível 5</h5> (título de nível 5)
###### Título de nível 6 <h6>Título de nível 6</h6> (título de nível 6)

Alternativamente, é possível sublinhar com sinais de igual (===) para gerar um título de nível 1 e com traços (---) para gerar um título de nível 2:

Markdown Output
Título de nível 1
=================
(Título de nível 1)
Título de nível 2
—————–
(Título de nível 2)

Bastam dois sinais == ou -- para a representação de título.

Parágrafos

Parágrafos são linhas de texto separadas por linhas vazias.

Markdown html Output obtido
Markdown é simples.
Vou usá-lo sempre!
<p>Markdown é simples.</p>
<p>Vou usá-lo sempre!</p>
Markdown é simples.
Vou usá-lo sempre!

Quebra de linhas

Para quebrar uma linha termine a linha com 2 ou mais espaços (⎵ ⎵) seguidos de return ().

Markdown html Output obtido
Essa é a primeira linha. ⎵⎵↲
Essa é a segunda linha.
<p>Essa é a primeira linha.<br>
Essa é a segunda linha.</p>
Essa é a primeira linha.
Essa é a segunda linha.

Negrito e itálico

Texto em negrito é obtido com asteriscos ou traços baixos (underscores) envolvendo o texto a negritar. Letras dentro da palavra também podem ser negritadas com duplos asteriscos.

Markdown html Output obtido
Precisamos de **negrito**. Precisamos de <strong>negrito</strong>. Precisamos de negrito.
Precisamos de __negrito__. Precisamos de <b>negrito</b>. Precisamos de negrito.
abcdef**GH**ij abcdef<strong>GH</strong>ij abdcefGHij

Textos em itálico são obtidos com um asterisco ou uma traço baixo (underscores) envolvendo o texto. Letras dentro da palavra também podem ser tornadas itálicos com um asterisco.

Markdown html Output obtido
Texto *em itálico*. Texto <em>em itálico</em>. Texto em itálico.
Texto _em itálico_. Texto <i>em itálico</i>. Texto em itálico.
a*bcd*ef a<em>bcd</em>ef abcdef

Texto em negrito e itálico simultâneos são obtidos com três asteriscos ou traços baixos (underscores) envolvendo o texto. Letras dentro da palavra também podem ser negritadas com três asteriscos.

Markdown html Output obtido
Um texto ***muito importante***. Um texto <strong><em>muito importante</em></strong>. Um texto muito importante.
Um texto ___muito importante___. Um texto <b><i>muito importante</i></b>. Um texto muito importante.
Um texto __*muito importante*__. Um texto <i><b>muito importante</b></i>. Um texto muito importante.
Um texto **_muito importante_**. …idem… Um texto muito importante.
Texto***muito***importante. Texto<strong><em>muito</em></strong>importante. Textomuitoimportante.

Textos riscados são obtidos com dois “tils” ˜˜ anterior e posterior ao texto.

Markdown html Output obtido
Texto ˜˜riscado˜˜. Texto <del>riscado</del>. Texto riscado.

Linha horizontal

Linhas horizontais são inseridas com três ou mais asteriscos (***), ‘dashes’ (---), ou ‘underscores’ (___) em uma linha.

Markdown html Output obtido
*** <hr>

Blocos

Para criar blocos destacados (citações) coloque > na frente do parágrafo.

> “O poeta é um fingidor. Finge tão completamente que chega a fingir que é dor a dor que deveras sente.”
> ― Fernando Pessoa, Autopsicografia.

“O poeta é um fingidor. Finge tão completamente que chega a fingir que é dor a dor que deveras sente.”
― Fernando Pessoa, Autopsicografia.

Blocos destacados podem conter muitos parágrafos. Para isso basta inserir > em cada linha branca entre as parágrafos.

> “Nós, os mortais, alcançamos a imortalidade nas coisas que criamos em comum e que estão atrás de nós.”
>
> “A coisa mais bela que podemos experimentar é o mistério. Essa é a fonte de toda a arte e ciências verdadeiras.”
> ― Albert Einstein.

“Nós, os mortais, alcançamos a imortalidade nas coisas que criamos em comum e que estão atrás de nós.”
“A coisa mais bela que podemos experimentar é o mistério. Essa é a fonte de toda a arte e ciências verdadeiras”.
― Albert Einstein.

Blocos ou citações podem ser aninhadas. Para isso coloque >> na frente do parágrafo com segunda indentação.

> “Eu jamais iria para a fogueira por uma opinião minha pois não tenho certeza alguma. Porém, eu iria pelo
direito de ter e mudar de opinião, quantas vezes quisesse.”
>
>> ”Sem música a vida seria um erro.”
> ― Friedrich Nietzsche

“Eu jamais iria para a fogueira por uma opinião minha pois não tenho certeza alguma. Porém, eu iria pelo direito de ter e mudar de opinião, quantas vezes quisesse.”

“Sem música a vida seria um erro.”

― Friedrich Nietzsche

Listas

Listas ordenadas (numeradas) são criadas numerando-se as linhas, cada número seguido de ponto. Qualquer número serve para isso, embora o número da primeira linha marque o número usado na lista:

Markdown html Output obtido
1. Primeiro item
2. Segundo item
3. Terceiro item
4. Quarto item
<ol>
<li>Primeiro item</li>
<li>Segundo item</li>
<li>Terceiro item</li>
<li>Quarto item</li>
</ol>
  1. Primeiro item
  2. Segundo item
  3. Terceiro item
  4. Quarto item
1. Primeiro item
1. Segundo item
1. Terceiro item
  1. Indentado item 1
  1. Indentado item 2
1. Quarto item
<ol>
<li>Primeiro item</li>
<li>Segundo item</li>
<li>Terceiro item
<ol>
<li>Indentado item 1</li>
<li>Indentado item 2</li>
</ol>
</li>
<li>Quarto item</li>
</ol>
  1. Primeiro item
  2. Segundo item
  3. Terceiro item
    1. Indentado item 1
    2. Indentado item 2
  4. Quarto item

Observe que não é necessário usar a numeração correta nas listas. Alguns editores completam automaticamente esses números. A segunda indentação (ou lista dentro de outra lista) é gerada por espaços ou tab antes do item.

Listas não numeradas são criadas com os sinais *, + ou - antes dos itens.

Markdown html Output obtido
– Primeiro item
– Segundo item
– Terceiro item
– Quarto item
<ul>
<li>Primeiro item</li>
<li>Segundo item</li>
<li>Terceiro item</li>
<li>Quarto item</li>
</ul>
  • Primeiro item
  • Segundo item
  • Terceiro item
  • Quarto item
– Primeiro item
– Segundo item
– Terceiro item
  - Indentado item 1
  - Indentado item 2
– Quarto item
<ul>
<li>Primeiro item</li>
<li>Segundo item</li>
<li>Terceiro item
<ul>
<li>Indentado item 1</li>
<li>Indentado item 2</li>
</ul>
</li>
<li>Quarto item</li>
</ul>
  • Primeiro item
  • Segundo item
  • Terceiro item
    • Indentado item 1
    • Indentado item 2
  • Quarto item

Blocos de código

Uma anotação apropriada para código, principalmente de programação, é uma característica muito útil no markdown. A indentação correta para blocos de código pode ser conseguida com linhas precedidas por 4 espaços ( ) ou 1 tab. Dentro de uma lista essas linhas são precedidas por 8 espaços ou 2 tabs. Nesse contexto usamos o sinal para representar um espaço:

Markdown html Output obtido
⎵⎵⎵⎵def soma(a,b):
⎵⎵⎵⎵⎵⎵⎵⎵return a + b
⎵⎵⎵⎵print (soma(5, 7))
<pre>
  def soma(a,b):
    return a + b
  print (soma(5, 7))
</pre>
  def soma(a,b):
    return a + b
  print(soma(5, 7))

Alternativamente, blocos de código também podem ser colocados entre três acentos graves, backticks, ```:

Markdown Output obtido
```
function factorial(n) {
  if (n === 0) return 1; // 0! = 1
  return n * factorial(n – 1);
}
factorial(3); // returns 6}
```
function factorial(n) {
  if (n === 0) return 1; // 0! = 1
  return n * factorial(n – 1);
}
factorial(3); // returns 6}

Nesse formato de bloco de código é possível marcar o código com cores, de acordo com a linguagem selecionada (syntax highlighting). Essa característica depende do processador usado. No exemplo seguinte o código é marcado como JSON, o que é informado após o primeiro grupo de acentos graves.

Markdown Output obtido
``` json
{
  ”nome”: “José”,
  ”sobrenome”: “Garcia”,
  ”idade”: 45
}
```
{
  “nome”: “José”,
  “sobrenome”: “Garcia”,
  “idade”: 45
}

Para denotar texto em código, uma ou mais palavras, coloque-as entre acentos graves (backticks, `).

Markdown html Output obtido
Digite no prompt de comando: `nano`. Digite no prompt de comando: <code>nano</code>. Digite no prompt de comando: nano.

Imagens

Para acrescentar uma imagem use a seguinte sintaxe:


![Imagem 1](caminho/images/tux.png “Título a ser exibido”)

Tabelas

Tabelas são criadas usando-se a barra vertical | e hífens. Por exemplo, as três tabelas abaixo mostram o uso do código markdown a seguir

Tabela (1)

| Título 1 | Título 2 | T3 |
| ‐‐‐‐‐‐‐‐‐‐‐‐| ‐‐‐‐‐‐‐‐‐‐‐‐|‐‐-‐|
| célula 1, 1 | célula 1, 2 | 1 |
| célula 2, 1 | célula 2, 2 | 2 |

Tabela (2)

| Alinhamento | Alinhamento | Alinhamento |
| :‐‐‐‐‐‐‐‐‐‐ | :‐‐‐‐‐‐‐‐‐: | ‐‐‐‐‐‐‐‐‐‐: |
| Esquerda | Centro | Direita |
| 1 | 2 | 3 |

Tabela (3)

Título | N
-|-
a|b

são representadas como as tabelas (1), (2) e (3) abaixo:

A tabela (2) ilustra o uso dos sinais de alinhamento: à esquerda (:‐‐), centralizado (:‐‐:) (centralizado) e à direita (‐‐:). Os itens da tabela não precisam estar bem alinhados no markdown e as barras laterais são dispensáveis, como é mostrado na tabela (3). Dentro das tabelas o texto pode conter links, negrito e itálico e código.

Links

Links para outras páginas, locais ou na web, podem ser inseridos dentro de colchetes (ex.: [Duck Duck Go]) seguidos da URL entre parênteses. Por ex. (https://duckduckgo.com)).

Markdown html Output obtido
Recomendo o mecanismo de busca_ _
[Duck Duck Go](https://duckduckgo.com).
Recomendo o mecanismo de busca <br>
<a href=”https://duckduckgo.com”>Duck Duck Go</a>.
Recomendo o mecanismo de busca
Duck Duck Go.

Notas de Pés de Página

Segue um exemplo de texto com notas de pé de página.

Use a seguinte notação para criar pés de página[^1], que também pode conter textos mais longos e várias parágrafos[^2].
Os marcadores não precisam ser números[^t].[^1]: O pé de página fica no final do texto.
[^2]: Um pé de página pode ter vários parágrafos.
Todos os parágrafos dentro da mesma nota devem ser indentados.
`{ function teste(a, b): # pode conter código e outras formatações. }`
[^t]: Outro pé de página.

Esse código é apresentado em forma final da seguinte forma (com os links ativos):


Use a seguinte notação para criar pés de página[1], que também pode conter textos mais longos e várias parágrafos[2].
Os marcadores não precisam ser números[3].


1. O pé de página fica no final do texto.
2. Um pé de página pode ter vários parágrafos.
Todo o texto dentro do mesma anotação deve ser indentado.
{ function teste(a, b): # pode conter código e outras formatações. }
3. Outro pé de página.

Lista de tarefas

O código abaixo gera a lista mostrada à direita.

## Lista de Tarefas
- [x] Tarefa primeira (completa)
- [ ] Tarefa segunda
- [ ] Arranjar outras **tarefas**

Lista de Tarefas
☑ Tarefa primeira (completa)
Tarefa segunda
Arranjar outras tarefas

HTML

Vários aplicativos que usam Markdown, entre eles o Jupyter Notebook, também podem representar corretamente diversas tags de marcação html. Isso é útil para usuários familiarizados com html, principalmente quando se pretende inserir texto com formatos mais sofisticados, tais como como texto colorido, com variação de tamanho, caixas coloridas, etc.

Como exemplo, o código abaixo produzirá uma barra vermelha, útil para marcar o ponto de parada dentro de um notebook (Jupyter). Outro exemplo consiste na produção de texto sublinhado. Como não há código markdown específico para isso você pode conseguir texto sublinhado com as tags html.

<u>Texto 1 sublinhado</u> ou <ins>Texto 2 sublinhado</ins><div style=”border:1px solid black; padding:10px; color:white; background-color:red;”>parei aqui &curarr;</div>


Texto 1 sublinhado ou Texto 2 sublinhado

parei aqui ↷

Latex

LaTeX é uma linguagem de composição de texto para a produção de documentos científicos. Vários editores de Markdown podem renderizar equações matemáticas e símbolos Latex para reproduzir notação matemática. Em particular o Jupyter Notebook reconhece código LaTeX escrito nas células de Markdown e os representa corretamente no navegador usando a biblioteca JavaScript MathJax. Para inserir uma fórmula matemática em uma linha use o símbolo $expressão$. Para equações isoladas em uma linha e centralizadas use $$expressão$$.

A identidade de Euler é $e^{i \pi} + 1 = 0$.
As equações de Einstein são:
$$
R_{\mu\nu}-\frac{1}{2} Rg_{\mu\nu} +\Lambda g_{\mu\nu} = \frac{8 \pi G}{c^4} T_{\mu\nu}.
$$
A definição da derivada de primeira ordem é:
$$
f'(x) = \lim\limits_{h \rightarrow 0} \frac{f(x+h) – f(x)}{h}
$$

A identidade de Euler é \(e^{i \pi} + 1 = 0\).
As equações de Einstein são:
$$
R_{\mu\nu}-\frac{1}{2} Rg_{\mu\nu} +\Lambda g_{\mu\nu} = \frac{8 \pi G}{c^4} T_{\mu\nu}.
$$
A definição da derivada de primeira ordem é:
$$
f'(x) = \lim\limits_{h \rightarrow 0} \frac{f(x+h) – f(x)}{h}
$$

Editores de Markdown

Existem muitos bons editores de markdown. Algumas sugestões:

Remarkable
Haroopad
UberWriter
Macdown
Ghostwriter
Marcor

Editores online:

São excelentes aplicativos de anotações, permitindo o uso de plugins para calendários, ToDos, customização de aparência por css. Permitem a sincronização entre diversos aparelhos Mac, Windows, Linux e Androide:

SimpleNote, Joplin, Obsidian

Bibliografia

O Futuro da Inteligência Artificial

Expectativa e Desafio Futuro

No campo das expectativas para um futuro muito próximo podemos mencionar as interfaces entre cérebro e computador mediadas por IAs, conectados à máquinas externas tais como exoesqueletos. Esses sistemas estarão disponíveis para pacientes com acidentes cérebro-vasculares, com traumas, doenças neurológicas, ou simplesmente como uma extensão de habilidades de uma pessoa saudável.

Nenhuma nova tecnologia é introduzida sem apresentar simultaneamente problemas e desafios em seu uso. Estima-se que um número relevante de empregos será perdido para a automação inteligente. Diferente das máquinas mecânicas, que substituíram o trabalhador braçal, agora é razoável considerar que computadores farão o trabalho de profissionais com níveis mais elevados de qualificação. Investimento em equipamentos, e não em pessoas, provavelmente aumentará o problema do desemprego e da concentração de renda.

O uso das máquinas para recomendações de conteúdo, por exemplo, tem se mostrado problemático em algumas plataformas. Buscando atrair a atenção do usuário e mantê-lo por mais tempo conectado e fidelizado as redes apresentam como sugestões conteúdos cada vez mais contundentes, muitas vezes envolvendo violência e intolerância. A indicação de conteúdo de fácil aceitação, em geral aprovado por muitos usuários, tende a esconder aqueles de menor circulação ou de gosto mais elaborado. Como resultado se observa a tendência de uniformização e formação de guetos, com a consequente inclinação à radicalização e intolerância. Junta-se a isso a facilidade para a geração inteligente de imagens, áudios e vídeos falsos que torna as campanhas de desinformação ainda mais nocivas e de difícil deteção.

Os problemas envolvendo a indústria da propaganda nos meios virtuais ficam exacerbados pelas práticas ilegais e imorais do roubo de dados. Dados se tornaram um produto valioso e a ausência de uma legislação atualizada estimula a prática da invasão de computadores e telefones pessoais e corporativos. Redes sociais importantes já foram flagradas vendendo informações sobre seus usuários que são usadas para o mero estímulo ao consumo ou para o atingimento de metas políticas muitas vezes obscuras e antidemocráticas.

A habilidade das IAs de reconhecer texto escrito e falado e extrair conteúdo das linguagens naturais agrava a ameaça à privacidade. Uma espionagem feita pelo microfone de um celular pode não apenas indicar que tipo de propaganda deve ser oferecida ao usuário mas também revelar traços que essa pessoa gostaria de manter privados, tais como traços de comportamentos íntimos ou a presença de uma doença.

Questões éticas, usualmente difíceis, ficam mais complexas na presença de IAs. Quem deve ser responsabilizado se um médico autômato comete um erro em seu diagnóstico ou procedimento? Quanta autonomia se pode atribuir à um juiz máquina ou a um automóvel auto-guiado? É aceitável permitir que um drone armado dispare contra um grupo que ele considera perigoso?

Provavelmente a afirmação do falecido físico Stephen Hawkings de que “O desenvolvimento de uma inteligência artificial completa pode determinar o fim da raça humana” seja pessimista em excesso.

É inegável, no entanto, que vigilância e responsabilidade devem ser elementos comuns no uso de toda nova tecnologia.

Referências

CHOLLET, F.. Deep Learning with Python. Nova Iorque: Manning Publications Co., 2018.

GOODFELLOW, I.; BENGIO, Y.; COURVILLE, A. Deep Learning: Cambridge, MA : MIT Press, 2017.

UNESCO. Artificial Intelligence in Education: Challenges and Opportunities for Sustainable Development, United Nations Educational, Scientific and Cultural Organization, Paris, 2019.

Inteligência Artificial

Onde o Aprendizado de Máquina é usado?

Máquinas treinadas podem ser empacotadas em aplicativos para computadores, telefones celulares ou circuitos com processadores que podem ser inseridos em outros equipamentos maiores, como automóveis e aviões, ou simplesmente serem carregados com o usuário.

Bons exemplos são os programas de busca e remoção de vírus de computadores e programas espiões que visam o roubo de dados pessoais ou corporativos. Novos vírus são disseminados a todo momento mas a maior parte de seu código replica o de vírus já detectados e combatidos. Sistemas inteligentes podem prever com boa exatidão quando um código representa um ataque ou, caso contrário, informar sobre possíveis riscos que ele representa. Da mesma forma um servidor de correio eletrônico (email) pode processar as mensagem recebidas e seus anexos em busca de spams, vírus, mensagens enviadas por remetentes cadastrados como perigosos ou prever outras anomalias.


Procedimentos de segurança de pessoas ou grupos podem ser automatizados. Filas de aeroportos ou de ingresso a um evento público de grande comparecimento podem ser varridas por meio de câmeras que localizam faces cadastradas em listas de criminosos ou encontrar outras características suspeitas que seriam de difícil identificação por funcionários humanos.

A análise de séries temporais, dados coletados ao longo do tempo, submetidas ao treinamento de máquina pode, em muitos casos, permitir a previsão de eventos futuros ou a inferência retrógrada de períodos passados que não foram documentados. Dois casos de interesse evidente são as previsões meteorológicas e da flutuação de mercados e bolsas de valores. Em ambos os casos as previsões automatizadas estão sendo rapidamente aprimoradas e os resultados cada vez mais úteis. A previsão meteorológica, por exemplo, permite o planejamento de ações de defesa ou de emergência em casos de tempestades. Empresas voltadas para as aplicações financeiras têm conseguido sucesso relevante nessa previsão, o que se reflete em lucro. Bancos e outros agentes financeiros usam a IA para previsão de comportamento de clientes, para sugestões de investimentos e prevenção de fraudes.

Um exemplo não óbvio da análise de séries temporais está nos aplicativos de reconhecimento de voz. Conversão direta de voz em texto ou vice-versa, de tradução ou de uso nos chamados assistentes pessoais, tais como o Alexa do Google e Siri da Apple são aplicações deste tecnologia. Através desses assistentes o usuário pode acionar outros aplicativos em seu aparelho pessoal, escolher músicas, enviar emails, entre outras funcionalidades.

A análise automática de textos já é usada nos tribunais brasileiros para triagem e encaminhamento de processos. Muitas questões estão em aberto no que se refere ao uso desses sistemas e se torna necessário discutir a inserção do assunto nos currículos de ensino de Direito.

O reconhecimento de significado de texto representa uma parte importante dos aplicativos de IA. Chatbots são aplicativos que simulam uma conversação com o usuário, fazendo pesquisas de opinião e preferência, sugerindo um procedimento para o usuário ou escolhendo o atendente humano que melhor responderá a suas demandas. A análise automática de um texto pode discriminar o estado de humor de quem o escreveu e sugerir a ele uma página na web de seu interesse ou um produto que ele esteja inclinado a consumir.

Muitas desta aplicações podem, e de fato o fazem, se beneficiar de um acesso direto à informação obtida por meio de sensores de natureza diversa, principalmente quando estão conectados à internet e transmitem dados em tempos real. Exemplos disso são as leituras de GPS (Global Positioning System ou Sistema Global de Posicionamento) que permitem a exibição de mapas de trânsito atualizados a cada instante, exibindo rotas de menor tráfego ou pontos de congestionamentos. Usando estes sistemas associados a diversos outros sensores, todos analisados por IA, automóveis autônomos podem circular pelas ruas das cidades provocando um número de acidentes inferior àqueles provocados por motoristas humanos. Da mesma forma um drone pode entregar produtos adquiridos remotamente diretamente nas mãos do consumidor.

Baseados na interação que fazemos com as páginas das chamadas rede sociais é possível a captura muito precisa de personalidades, gostos e tendências de um usuário, o que é usado pelos agentes de mídia para veicular propaganda de consumo, de comportamento ou política. Os provedores de conteúdo áudio-visuais, tais como o Netflix, o Youtube e Spotify, usam IAs para acompanhamento dos hábitos de seus usuários para indicar novos acessos à filmes, músicas ou outros produtos de qualquer natureza.

As aplicações de máquinas inteligentes são particularmente animadoras no campo da saúde, em especial para diagnósticos por imagem. Doenças de pele ou dos olhos, ou células cancerosas podem ser detectadas por meio da análise de imagens, muitas vezes com precisão superior à obtida por um técnico humano. O estudo do registro médico pormenorizado de um paciente pode indicar tendências e sugerir formas viáveis de tratamento.

Usuários dos jogos de computadores também são expostos à decisões de máquinas rotineiramente. A industria dos jogos foi uma das primeiras a se aproveitar desta tecnologia e muitos jogos lançam mão de IA para tomadas de decisões e para a animação de personagens virtuais que povoam os mundos de fantasia dos jogos.

Telescópio Espacial kepler – representação artística

A inteligência artificial, com sua habilidade de análise de grande volume de dados, é um recurso poderoso na pesquisa científica. Um exemplo brilhante dessa faceta foi a descoberta anunciada pela NASA em março de 2019 de dois exoplanetas por meio de um algoritmo chamado AstroNet-K2, uma rede neural modificada para o estudo dos dados colhidos pelo telescópio espacial Kepler.

IA e Educação

A Educação é um setor tradicionalmente refratário às novas tecnologias e as novidades demoram para entrar na sala de aula. Apesar disso muitas propostas envolvendo IA têm surgido, algumas delas já em aplicação.

O professor hoje compete na atenção dos alunos com uma variedade de estímulos eletrônicos, jogos, páginas da web interativas e ambientes de interação social. Qualquer sistema educacional no presente e futuro próximo deve oferecer estímulo similar ao encontrado nesses meios, buscando dialogar com os alunos no ambiente de interatividade digital com que estão bem familiarizados.

Ainda que os computadores já sejam, há algum tempo, utilizados no manejo da burocracia escolar, sistemas inteligentes podem agilizar esse processo. Professores gastam boa parte de seu tempo em atividades burocráticas, não relacionadas com o ensino em si. A avaliação de exames e o preenchimento de formulários, o acompanhamento de frequências às aulas, o contato permanente com pais e responsáveis e o controle de estoque de material escolar, todas são tarefas que podem ser facilitadas com o uso de IA. O mesmo ocorre com o gerenciamento de matrículas, formulação de calendários e manejo de pessoal. Para o aluno um assistente escolar pode agendar compromissos virtuais ou não, acompanhar a rotina de estudos e contato com professores.

É no campo puramente acadêmico, no entanto, que se pode esperar os melhores resultados. Sistemas inteligentes tem sido treinados para a personalização com ajuste super fino de ementas e fluxos de estudo para o indivíduo, levando em conta suas habilidades e deficiências. Com a adoção de textos eletrônicos a informação antes contida em grandes volumes de papel pode ser, de modo simples e de baixo custo, fragmentada em guias menores de estudos, contendo blocos lógicos completos com referência a recursos multimídia e conteúdo expandido. Sistemas de IA podem fornecer uma interface interativa capaz de responder perguntas ou indicar referências visando esclarecer pontos pouco compreendidos. Uma IA pode auxiliar o professor inclusive por meio de diálogos falados, resolvendo dúvidas e ajudando na solução de exercícios, enquanto a análise de imagens capturadas por câmeras pode indicar o nível de concentração ou dispersão dos estudantes.

Associada ao uso da apresentação dinâmica de conteúdo um sistema treinado por IA e subsidiado por avaliações permanentes e automáticas de desempenho pode indicar o melhor roteiro, as necessárias revisões e ritmo do aprendizado. Tutores automáticos podem acompanhar e sugerir o ritmo de estudo de um estudante. A avaliação permanente, além de tornar desnecessária a temida temporada de provas, avaliará o nível atual de conhecimento do aluno, insistindo em exemplos e exercícios caso um conceito não esteja bem assimilado através da apresentação de questões com níveis crescentes de dificuldade, sugerindo o retorno para níveis mais básicos ou a progressão para tópicos mais avançados. Ele pode identificar lacunas no entendimento e apresentar as intervenções corretas para preencher essas deficiências.

Simultaneamente com a avaliação de testes e exercícios podem ser inseridos mecanismos para a detecção de dificuldades outras que não as puramente acadêmicas, tais como deficiências de visão e concentração para alunos mais jovens.

Evidentemente nenhum sistema, por mais arrojado que seja, poderá se furtar ao estímulo de aspectos humanos básicos tais como a criatividade, cooperação entre indivíduos e desenvolvimento ético. Principalmente nas primeiras fases de implantação dos sistemas inteligentes nas escolas, visto que o efeito completo da exposição a sistemas artificiais não é plenamente conhecido, não se pode permitir uma dependência excessiva nas máquinas. Alunos devem receber estímulo para realizar pesquisas tradicionais em bibliotecas e usar livros físicos. A interação entre alunos e educadores e colegas deve ser uma prioridade. Além disso, em um contexto onde a informação, como acesso aos dados, e a capacidade de processamento destes dados estão super facilitados pela presença de computadores, a criatividade e a habilidade para a solução de problemas, individualmente ou em grupo, deve ser o foco do processo educativo.

Grande parte do desafio das instituições de ensino em face da explosão da IA consiste em treinar profissionais para o uso e desenvolvimento dos próprios sistemas inteligentes. Um usuário não especialista em computação deve adquirir compreensão básica do mecanismo de funcionamento das máquinas, com conhecimento mínimo da arquitetura destes sistemas e da análise estatística utilizada por eles. Caso contrário não saberá discernir que tipo de demandas são adequadas para as IAs nem terá competência para interpretar os resultados destes processos.

O treinamento de especialistas com competência para desenvolver sistemas inteligentes não é desafio menor. A inteligência de máquina e os sistemas de aprendizado dependem profundamente de matemática avançada, em particular a álgebra linear e a estatística, e de programação e lógica. Existe risco evidente do uso das ferramentas mais modernas como caixas pretas onde um programador pode colocar em funcionamento sistemas complexos sem ter um bom domínio da tecnologia envolvida. Além da dependência cultural e tecnológica dos centros desenvolvedores isso cria restrição severa sobre a habilidade para tratar de novos desafios, especialmente aqueles de interesse restrito à comunidade local, tais como problemas dos países em desenvolvimento que não foram devidamente tratados pelos grandes centros de pesquisa.


Expectativa e Desafio Futuro