Você está na página 1de 264

José Unpingco

Python
Programming
for Data
Analysis
Python Programming for Data Analysis
José Unpingco

Python Programming
for Data Analysis
José Unpingco
University of California
San Diego
CA, USA

ISBN 978-3-030-68951-3 ISBN 978-3-030-68952-0 (eBook)


https://doi.org/10.1007/978-3-030-68952-0

© The Editor(s) (if applicable) and The Author(s), under exclusive license to Springer Nature Switzerland
AG 2021
This work is subject to copyright. All rights are solely and exclusively licensed by the Publisher, whether
the whole or part of the material is concerned, specifically the rights of translation, reprinting, reuse
of illustrations, recitation, broadcasting, reproduction on microfilms or in any other physical way, and
transmission or information storage and retrieval, electronic adaptation, computer software, or by similar
or dissimilar methodology now known or hereafter developed.
The use of general descriptive names, registered names, trademarks, service marks, etc. in this publication
does not imply, even in the absence of a specific statement, that such names are exempt from the relevant
protective laws and regulations and therefore free for general use.
The publisher, the authors, and the editors are safe to assume that the advice and information in this book
are believed to be true and accurate at the date of publication. Neither the publisher nor the authors or
the editors give a warranty, expressed or implied, with respect to the material contained herein or for any
errors or omissions that may have been made. The publisher remains neutral with regard to jurisdictional
claims in published maps and institutional affiliations.

This Springer imprint is published by the registered company Springer Nature Switzerland AG
The registered company address is: Gewerbestrasse 11, 6330 Cham, Switzerland
To Irene, Nicholas, and Daniella, for all their
patient support.
1
Prefácio

Este livro surgiu de notas para o curso de Programação ECE143 para Análise de
Dados que tenho ensinado na Universidade da Califórnia, San Diego, que é um
requisito para pós-graduação e graduação em Aprendizado de Máquina e Ciência de
Dados. Presume-se que o leitor tenha algum conhecimento básico de programação e
experiência no uso de outra linguagem, como Matlab ou Java. Os idiomas e métodos
do Python que discutimos aqui se concentram na análise de dados, apesar do uso do
Python em muitos outros tópicos. Especificamente, como os dados brutos costumam
ser uma bagunça e exigem muito trabalho para serem preparados, este texto se
concentra em recursos específicos da linguagem Python para facilitar essa limpeza,
em vez de focar apenas em módulos Python específicos para isso.
Assim como acontece com ECE143, aqui discutimos por que as coisas são como
são em Python, em vez de apenas serem assim. Descobri que fornecer esse tipo de
contexto ajuda os alunos a fazer melhores escolhas de projeto de engenharia em seus
códigos, o que é especialmente útil para iniciantes em Python e análise de dados. O
texto é polvilhado com pequenos truques do comércio para torná-lo mais fácil de criar
código legível e sustentável adequado para uso em produção e desenvolvimento.
O texto se concentra no uso da própria linguagem Python de forma eficaz e, em
seguida, passa para os principais módulos de terceiros. Essa abordagem melhora a
eficácia em diferentes ambientes, que podem ou não ter acesso a esses módulos de
terceiros. O módulo de array numérico Numpy é abordado em profundidade porque
é a base de toda ciência de dados e aprendizado de máquina em Python. Discutimos
a estrutura de dados do array Numpy em detalhes, especialmente seus aspectos de
memória. Em seguida, passamos para o Pandas e desenvolvemos seus muitos
recursos para um processamento de dados eficiente e fluido. Como a visualização de
dados é fundamental para a ciência de dados e o aprendizado de máquina, módulos
de terceiros, como Matplotlib, são desenvolvidos em profundidade, bem como
módulos baseados na web, como Bokeh, Holoviews, Plotly e Altair.
Por outro lado, eu não recomendaria este livro para alguém sem experiência em
programação, mas se você já pode fazer um pouco de Python e quer melhorar
entendendo como e por que o Python funciona dessa maneira, então este é um bom
livro para você.
2 Prefácio
Para obter o máximo deste livro, abra um interpretador Python e digite junto com
os vários exemplos de código. Trabalhei muito para garantir que todos os exemplos
de código fornecidos funcionassem conforme anunciado.

Agradecimentos Gostaria de agradecer a ajuda de Brian Granger e Fernando Perez, dois dos
criadores do Jupyter Notebook, por todo seu excelente trabalho, bem como à comunidade Python
como um todo, por todas as contribuições que tornaram este livro possível. Hans Petter Langtangen
foi o autor do sistema de preparação de documentos Doconce [1] que foi usado para escrever este
texto. Obrigado a Geoffrey Poore [2] por seu trabalho com PythonTeX e L ATEX, ambas as
tecnologias-chave usadas para produzir este livro.

San Diego, CA, EUA José Unpingco


Fevereiro de 2020

Referências

1. HP Langtangen, DocOnce markup language. https://github.com/hplgit/doconce


2. GM Poore, Pythontex: documentos reproduzíveis com latex, python e muito mais. Comput. Sci.
Discov. 8(1), 014010 (2015)
3
Capítulo 1
Programação Básica

1.1 Linguagem Básica

Antes de entrarmos nos detalhes, é uma boa idéia obter uma orientação de alto nível
para Python. Isso melhorará sua tomada de decisão posterior com relação ao
desenvolvimento de software para seus próprios projetos, especialmente à medida
que eles se tornam maiores e mais complexos. Python surgiu de uma linguagem
chamada ABC, que foi desenvolvida na Holanda na década de 1980 como uma
alternativa ao BASIC para fazer os cientistas utilizarem microcomputadores, que
eram novos na época. O impulso importante foi tornar os cientistas não
especializados capazes de utilizar de forma produtiva esses novos computadores. Na
verdade, essa abordagem pragmática continua até hoje em Python, que é um
descendente direto da linguagem ABC.
Existe um ditado em Python -venha para a linguagem, fique para a comunidade.
Python é um projeto de código aberto conduzido pela comunidade, portanto, não há
nenhuma entidade de negócios corporativa tomando decisões de cima para baixo para
a linguagem. Parece que tal abordagem levaria ao caos, mas Python se beneficiou por
muitos anos da liderança paciente e pragmática de Guido van Rossum, o criador da
linguagem. Hoje em dia, existe um comitê de governança separado que assumiu essa
função desde a aposentadoria de Guido. O design aberto da linguagem e a qualidade
do código-fonte possibilitaram ao Python desfrutar de muitas contribuições de
desenvolvedores talentosos em todo o mundo ao longo de muitos anos, incorporadas
pela riqueza da biblioteca padrão. Python também é lendário por ter uma comunidade
acolhedora para recém-chegados, portanto, é fácil encontrar ajuda online para
começar a usá-lo.
O pragmatismo da linguagem e a generosidade da comunidade há muito fazem do
Python uma ótima maneira de desenvolver aplicativos da web. Antes do advento da
ciência de dados e do aprendizado de máquina, mais de 80% da comunidade Python
eram desenvolvedores da web. Nos últimos cinco anos (no momento em que este
artigo foi escrito), o equilíbrio se inclinou para uma divisão quase uniforme entre
desenvolvedores da web e cientistas de dados. Esta é a razão pela qual você encontra
muitos protocolos e tecnologias da Web na biblioteca padrão.
Python é uma linguagem interpretada em oposição a uma linguagem compilada
como C ou FORTRAN.
4 Capítulo 1
Embora ambos os casos comecem com um arquivo de código-fonte, o compilador
examina o código-fonte de ponta a ponta e produz um executável que está vinculado
a arquivos de biblioteca específicos do sistema. Uma vez que o executável é criado,
não há mais necessidade do compilador. Você pode simplesmente executar o
executável no sistema. Por outro lado, em uma linguagem interpretada como Python,
você deve sempre ter um processo Python em execução para executar o código. Isso
ocorre porque o processo Python é uma abstração na plataforma em que está sendo
executado e, portanto, deve interpretar as instruções no código-fonte para executá-las
na plataforma. Como intermediário entre o código-fonte na plataforma, o
interpretador Python é responsável pelos problemas específicos da plataforma. A
vantagem disso é que o código-fonte pode ser executado em plataformas diferentes,
desde que haja um interpretador Python funcionando em cada plataforma. Isso torna
os códigos-fonte Python portáteis entre plataformas porque os detalhes específicos
da plataforma são tratados pelos respectivos interpretadores Python. A portabilidade
entre plataformas era uma vantagem importante do Python, especialmente nos
primeiros dias. Voltando às linguagens compiladas, como os detalhes específicos da
plataforma estão embutidos no executável, o executável está vinculado a uma
plataforma específica e às bibliotecas específicas que foram vinculadas ao
executável. Isso faz com que esses códigos sejam menos portáveis que o Python, mas
como o compilador é capaz de se vincular à plataforma específica, ele tem a opção
de explorar otimizações e bibliotecas de nível específico da plataforma. Além disso,
o compilador pode estudar o arquivo de código-fonte e aplicar otimizações no nível
do compilador que aceleram o executável resultante. Em linhas gerais, essas são as
principais diferenças entre as linguagens interpretadas e compiladas. Veremos mais
tarde que existem muitos compromissos entre esses dois extremos para acelerar os
códigos Python.
Às vezes se diz que o Python é lento em comparação com as linguagens
compiladas e, de acordo com as diferenças que discutimos acima, isso pode ser
esperado, mas é realmente uma questão de onde o relógio começa. Se você iniciar o
relógio para contabilizar o tempo do desenvolvedor, não apenas o tempo de execução
do código, o Python será claramente mais rápido, apenas porque o ciclo de iteração
de desenvolvimento não requer uma compilação tediosa e etapa de link. Além disso,
Python é apenas mais simples de usar do que linguagens compiladas porque muitos
elementos complicados, como gerenciamento de memória, são tratados
automaticamente. O tempo de resposta rápido do Pythons é uma grande vantagem
para o desenvolvimento de produtos, que requer iteração rápida. Por outro lado, os
códigos que são limitados por computação e devem ser executados em hardware
especializado não são bons casos de uso para Python. Isso inclui a solução de sistemas
de equações diferenciais paralelas que simulam a mecânica dos fluidos em grande
escala ou outros cálculos físicos em grande escala. Observe que o Python é usado em
tais configurações, mas principalmente para preparar esses cálculos ou pós-
processamento dos dados resultantes.

1.1.1 Primeiros passos


Sua interface principal para o interpretador Python é a linha de comando. Você pode
digitar python em seu terminal você deve ver algo como o seguinte,
Python 3.7.3 (default, Mar 27 2019, 22:11:17)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Capítulo 1 5
Type "help", "copyright", "credits" or "license" for more
e→ informações.
>>>

Há muitas informações úteis, incluindo a versão do Python e sua proveniência.


Isso é importante porque às vezes o interpretador Python é compilado para permitir
acesso rápido a certos módulos preferidos (ou seja, módulo math). Discutiremos
mais isso quando falarmos sobre os módulos Python.

1.1.2 Palavras-chave reservadas

Embora o Python não o impeça, não as use como nomes de variáveis ou funções.
and del from not while
as elif global or with
assert else if pass yield
break except import print
class exec in raise
continue finally is return
def for lambda try

nem estes nem


abs all any ascii bin bool breakpoint bytearray bytes callable
chr classmethod compile complex copyright credits delattr
dict dir display divmod enumerate eval exec filter float
format frozenset getattr globals hasattr hash help hex id
input int isinstance issubclass iter len list locals map max
memoryview min next object oct open ord pow print property
range repr reversed round set setattr slice sorted staticmethod str
sum super tuple type vars zip

Por exemplo, um erro comum é atribuir sum = 10, sem perceber que agora a função
Python sum() não está mais disponível.

1.1.3 Números

Python tem recursos de controle de números de bom senso. O caractere de comentário


é o símbolo hash (#).
>>> 2+2
4
>>> 2+2 # and a comment on the same line as code
4
>>> (50-5*6)/4
5.0

Observe que a divisão em Python 2 é inteira e em Python 3 é a divisão de ponto


flutuante com o símbolo // fornecendo divisão inteira.
6 Capítulo 1
Python é dinamicamente tipado, então podemos fazer as seguintes atribuições sem
declarar os tipos de width e height.
>>> width = 20
>>> height = 5*9
>>> width * height
900
>>> x = y = z = 0 # assign x, y and z to zero
>>> x
0
>>> y
0
>>> z
0
>>> 3 * 3.75 / 1.5
7.5
>>> 7.0 / 2 # float numerator
3.5
>>> 7/2
3.5
>>> 7 // 2 # double slash gives integer division
3

É melhor pensar em atribuições como valores de rotulagem na memória. Portanto,


width é um rótulo para o número 20. Isso fará mais sentido mais tarde. 1Desde
Python 3.8, o operador morsa de atribuição permite que a própria atribuição tenha o
valor do destinatário, como a seguir,
>>> print(x:=10)
10
>>> print(x)
10

O operador tem muitos outros usos sutis e foi introduzido para melhorar a legibilidade
em certas situações. Você também pode converter entre os tipos numéricos de uma
forma de bom senso:
>>> int(1.33333)
1
>>> float(1)
1.0
>>> type(1)
<class 'int'>
>>> type(float(1))
<class 'float'>

É importante ressaltar que os inteiros do Python são de comprimento arbitrário e


podem lidar com inteiros extremamente grandes. Isso ocorre porque eles são
armazenados como uma lista de dígitos internamente. Isso significa que eles são mais
lentos para manipular do que inteiros Numpy que têm comprimentos de bit fixos,
desde que o inteiro possa caber na memória alocada.

1
Nota http://www.pythontutor.com é um ótimo recurso para explorar como as variáveis são
atribuídas em Python.
Capítulo 1 7

Dica de programação: IPython


O interpretador Python padrão é útil e você deve estar familiarizado com ele,
mas o intérpretea IPython é uma extensão útil que fornece recursos como
preenchimento de tabulação sofisticado e muitas outras conveniências que
tornam mais fácil trabalhar com Python no terminal.
a
Veja http://github.com/ipython/ipython para saber mais sobre como começar a usar o IPython.

1.1.4 Números complexos

Python tem suporte rudimentar para números complexos.


>>> 1j * 1J
(-1+0j)
>>> 1j * complex(0,1)
(-1+0j)
>>> 3+1j*3
(3+3j)
>>> (3+1j)*3
(9+3j)
>>> (1+2j)/(1+1j)
(1.5+0.5j)
>>> a=1.5+0.5j
>>> a.real # the dot notation gets an attribute
1.5
>>> a.imag
0.5
>>> a=3.0+4.0j
>>> float(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't convert complex to float
>>> a.real
3.0
>>> a.imag
4.0
>>> abs(a) # sqrt(a.real**2 + a.imag**2)
5.0
>>> tax = 12.5 / 100
>>> price = 100.50
>>> price * tax
12.5625
>>> price + _
113.0625
>>> # the underscore character is the last evaluated result
>>> round(_, 2) # the underscore character is the last evaluated
e→ result
113.06
8 Capítulo 1
Normalmente usamos Numpy para números complexos, embora.

1.1.5 Strings

O manuseio de strings é muito bem desenvolvido e altamente otimizado em Python.


Acabamos de cobrir os pontos principais aqui. Primeiro, aspas simples ou duplas
definem uma string e não há relação de precedência entre aspas simples e duplas.
>>> 'spam eggs'
'spam eggs'
>>> 'doesn\'t' # backslash defends single quote
"doesn't"
>>> "doesn't"
"doesn't"
>>> '"Yes," he said.'
'"Yes," he said.'
>>> "\"Yes,\" he said."
'"Yes," he said.'
>>> '"Isn\'t," she said.'
'"Isn\'t," she said.'

Strings Python têm caracteres de escape no estilo C para newlinewsnewlines, tabs,


etc. Literais de string definidos desta forma são codificados por padrão usando UTF-
8 em vez de ASCII, como em Python 2. As simples triplas (') ou aspas duplas
triplas (") denota um bloco com novas linhas incorporadas ou outras aspas. Isso é
particularmente importante para a documentação da função docstrings que
discutiremos mais tarde.
>>> print( '''Usage: thingy [OPTIONS]
... and more lines
... here and
... here
... ''')
Usage: thingy [OPTIONS]
and more lines
here and here

Strings podem ser controlado com um caractere antes das aspas simples ou duplas.
Por exemplo, veja os comentários ( #) abaixo para cada etapa,
>>> # the 'r' makes this a 'raw' string
>>> hello = r"This long string contains newline characters \n, as
e→ in C"
>>> print(hello)
This long string contains newline characters \n, as in C
>>> # otherwise, you get the newline \n acting as such
>>> hello = "This long string contains newline characters \n, as
e→ in C"
>>> print(hello)
This long string contains newline characters, as in C
Capítulo 1 9
>>> u'this a unicode string μ ±' # the 'u' makes it a unicode
e→ string for Python2
'this a unicode string μ ±'
>>> 'this a unicode string μ ±' # no 'u' in Python3 is still
e→ unicode string
'this a unicode string μ ±'
>>> u'this a unicode string \xb5 \xb1' # using hex-codes
'this a unicode string μ ±'

Observe que uma f-string avalia (ou seja, interpola) as variáveis Python no
escopo atual,
>>> x = 10
>>> s = f'{x}'
>>> type(s)
<class 'str'>
>>> s '10'

Esteja ciente de que uma f-string não é resolvida até o tempo de execução porque ela
precisa resolver as variáveis incorporadas. Isso significa que você não pode usar
strings f como docstrings. É importante ressaltar que as strings do Python são
imutáveis, o que significa que, uma vez que uma string é criada, ela não pode ser
alterada no local. Por exemplo,
>>> x = 'strings are immutable '
>>> x[0] = 'S' # not allowed!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

Isso significa que você deve criar novas strings para fazer esse tipo de alteração.
Strings vs Bytes Em Python 3, a codificação de string padrão para strings literais é
UTF-8. O principal a se ter em mente é que bytes e strings são objetos distintos, em
oposição a ambos derivados de basestring em Python 2. Por exemplo, dada a
seguinte string Unicode,
>>> x='Ø'
>>> isinstance(x,str) # True
True
>>> isinstance(x,bytes) # False
False
>>> x.encode('utf8') # convert to bytes with encode
b'\xc3\x98'

Observe a distinção entre bytes e strings. Podemos converter bytes em strings usando
decode,
>>> x=b'\xc3\x98'
>>> isinstance(x,bytes) # True
True
>>> isinstance(x,str) # False
False
>>> x.decode('utf8')
'Ø'
10 Capítulo 1
Uma consequência importante é que você não pode acrescentar strings e bytes como
no seguinte: u"hello"+b"goodbye". Isso costumava funcionar bem no Python
2 porque os bytes seriam decodificados automaticamente usando ASCII, mas não
funciona mais no Python 3. Para obter esse comportamento, você deve explicitamente
decode/encode. Por exemplo,
>>> x=b'\xc3\x98'
>>> isinstance(x,bytes) # True
True
>>> y='banana'
>>> isinstance(y,str) # True
True
>>> x+y.encode()
b'\xc3\x98banana'
>>> x.decode()+y
'Øbanana'
Slicing Strings Python é uma linguagem de indexação zero (como C). O caractere
dois pontos(:).
>>> word = 'Help' + 'A'
>>> word
'HelpA'
>>> '<' + word*5 + '>'
'<HelpAHelpAHelpAHelpAHelpA>'
>>> word[4]
'A'
>>> word[0:2]
'He'
>>> word[2:4]
'lp'
>>> word[-1] # The last character
'A'
>>> word[-2] # The last-but-one character
'p'
>>> word[-2:] # The last two characters
'pA'
>>> word[:-2] # Everything except the last two characters
'Hel'
Operações de string Algumas operações numéricas básicas funcionam com strings.
>>> 'hey '+'you' # concatenate with plus operator
'hey you'
>>> 'hey '*3 # integer multiplication duplicates strings
'hey hey hey '
>>> ('hey ' 'you') # using parentheses without separating comma
'hey you'

Python tem um módulo de expressão regular embutido e muito poderoso (re) para
manipulação de strings. A substituição de string cria novas strings.
>>> x = 'This is a string'
>>> x.replace('string','newstring')
'This is a newstring'
>>> x # x hasn't changed
'This is a string'
Capítulo 1 11
Formatação Strings Há tantas maneiras de cadeias de formato em Python, mas aqui
é o mais simples que segue a linguagem C convenções sprintf em conjunto com
o operador de módulo %.
>>> 'this is a decimal number %d'%(10)
'this is a decimal number 10'
>>> 'this is a float %3.2f'%(10.33)
'this is a float 10.33'
>>> x = 1.03
>>> 'this is a variable %e' % (x) # exponential format
'this is a variable 1.030000e+00'

Alternativamente, você pode apenas juntá-las usando +,


>>> x = 10
>>> 'The value of x = '+str(x) 'The value of x = 10'

Você pode formatar usando dicionários como a seguir,


>>> data = {'x': 10, 'y':20.3}
>>> 'The value of x = %(x)d and y = %(y)f'%(data) 'The value of x = 10
and y = 20.300000'

Você pode usar o método format na string,


>>> x = 10
>>> y = 20
>>> 'x = {0}, y = {1}'.format(x,y)
'x = 10, y = 20'

A vantagem do format é que você pode reutilizar os marcadores de posição como


a seguir,
>>> 'x = {0},{1},{0}; y = {1}'.format(x,y)
'x = 10,20,10; y = 20'

E também o método da string f que discutimos acima.

Dica de programação: Strings do Python 2


No Python 2, a codificação de string padrão era ASCII de 7 bits. Não havia
distinção entre bytes e strings. Por exemplo, você pode ler de um arquivo JPG com
codificação binária como a seguir,
with open('hour_1a.jpg','r') as f:
x = f.read()

Isso funciona bem no Python 2, mas gera um erro UnicodeDecodeError no


Python 3. Para corrigir isso no Python 3, você deve ler usando o rb modo binário
em vez de apenas r modo de arquivo.

Estruturas de dados básicas Python fornece muitas estruturas de dados poderosas.


Os dois mais poderosos e fundamentais são a lista e o dicionário. Estruturas de dados
e algoritmos andam de mãos dadas.
12 Capítulo 1
Se você não entender as estruturas de dados, não poderá escrever algoritmos com
eficácia e vice-versa. Fundamentalmente, as estruturas de dados fornecem garantias
ao programador que serão satisfeitas se as estruturas de dados forem usadas de
maneira combinada. Essas garantias são conhecidas como invariantes da estrutura de
dados.
Listas A list é um contêiner geral que preserva a ordem que implementa a
sequência estrutura de dados de. A invariante para a lista é que a indexação de uma
lista não vazia sempre fornecerá o próximo elemento válido na ordem. Na verdade, a
lista é a estrutura de dados ordenada primária do Python. Isso significa que, se você
tiver um problema em que a ordem é importante, você deve pensar na estrutura de
dados da lista. Isso fará sentido com os exemplos a seguir.
>>> mix = [3,'tree',5.678,[8,4,2]] # can contain sublists
>>> mix
[3, 'tree', 5.678, [8, 4, 2]]
>>> mix[0] # zero-indexed Python
3
>>> mix[1] # indexing individual elements
'tree'
>>> mix[-2] # indexing from the right, same as strings
5.678
>>> mix[3] # get sublist
[8, 4, 2]
>>> mix[3][1] # last element is sublist
4
>>> mix[0] = 666 # lists are mutable
>>> mix
[666, 'tree', 5.678, [8, 4, 2]]
>>> submix = mix[0:3] # creating sublist
>>> submix
[666, 'tree', 5.678]
>>> switch = mix[3] + submix # append two lists with plus
>>> switch
[8, 4, 2, 666, 'tree', 5.678]
>>> len(switch) # length of list is built-in function
6
>>> resize=[6.45,'SOFIA',3,8.2E6,15,14]
>>> len(resize)
6
>>> resize[1:4] = [55] # assign slices
>>> resize
[6.45, 55, 15, 14]
>>> len(resize) # shrink a sublist
4
>>> resize[3]=['all','for','one']
>>> resize
[6.45, 55, 15, ['all', 'for', 'one']]
>>> len(resize)
4
>>> resize[4]=2.87 # cannot append this way!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list assignment index out of range
>>> temp = resize[:3]
Capítulo 1 13
>>> resize = resize + [2.87] # add to list
>>> resize
[6.45, 55, 15, ['all', 'for', 'one'], 2.87]
>>> len(resize)
5
>>> del resize[3] # delete item
>>> resize
[6.45, 55, 15, 2.87]
>>> len(resize) # shorter now
4
>>> del resize[1:3] # delete a sublist
>>> resize
[6.45, 2.87]
>>> len(resize) # shorter now
2

Dica de programação: Listas de classificação


A função integrada sorted classifica listas,
>>> sorted([1,9,8,2])
[1, 2, 8, 9]
As listas também podem ser classificadas no local usando o método de lista
sort(),
>>> x = [1,9,8,2]
>>> x.sort()
>>> x
[1, 2, 8, 9]

Ambos usam o poderoso algoritmo Timsort. Posteriormente, veremos mais


variações e usos para essas funções de classificação.

Agora que temos uma ideia de como indexar e usar listas, vamos falar sobre a
invariante que ela fornece: contanto que você indexe uma lista dentro de seus limites,
ela fornece o próximo elemento ordenado da lista. Por exemplo,
>>> x = ['a',10,'c']
>>> x[1] # return 10
10
>>> x.remove(10)
>>> x[1] # next element
'c'

Observe que a estrutura de dados da lista preencheu a lacuna após a remoção de 10.
Este é um trabalho extra que a estrutura de dados da lista fez para você sem
programação explícita. Além disso, os elementos da lista são acessíveis por meio de
índices inteiros e os inteiros têm uma ordem natural e, portanto, a lista também. O
trabalho de manter o invariante não vem de graça, entretanto. Considere o seguinte,
>>> x = [1,3,'a']
>>> x.insert(0,10) # insert at beginning
14 Capítulo 1
>>> x
[10, 1, 3, 'a']

Parece inofensivo? Claro, para listas pequenas, mas não para listas grandes. Isso
ocorre porque, para manter a invariante, a lista tem que deslocar (ou seja, copiar na
memória) os elementos restantes para a direita para acomodar o novo elemento
adicionado no início. Em uma grande lista com milhões de elementos e em um loop,
isso pode levar uma quantidade substancial de tempo. É por isso que os métodos de
lista padrão append() e pop() funcionam no final da lista, onde não há
necessidade de deslocar os itens para a direita.
Tuplas Tuplas são outro contêiner sequencial de uso geral em Python, muito
semelhante a listas, mas são imutáveis. As tuplas são delimitadas por vírgulas (os
parênteses são símbolos de agrupamento). Aqui estão alguns exemplos,
>>> a = 1,2,3 # no parenthesis needed!
>>> type(a)
<class 'tuple'>
>>> pets=('dog','cat','bird')
>>> pets[0]
'dog'
>>> pets + pets # addition
('dog', 'cat', 'bird', 'dog', 'cat', 'bird')
>>> pets*3
('dog', 'cat', 'bird', 'dog', 'cat', 'bird', 'dog', 'cat', 'bird')
>>> pets[0]='rat' # assignment not work!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

Pode parecer redundante ter tuplas que se comportam em termos de suas listas de
indexação, mas a principal diferença é que as tuplas são imutáveis, como mostra a
última linha acima. A principal vantagem da imutabilidade é que ela vem com menos
sobrecarga para o gerenciamento de memória do Python. Nesse sentido, eles são mais
leves e fornece estabilidade para códigos que passam por tuplas. Posteriormente,
veremos isso para assinaturas de função, que é onde surgem as principais vantagens
das tuplas.

Dica de programação: Compreendendo a memória de listas


Função do Python id mostra um inteiro correspondente à referência interna para
uma determinada variável. Anteriormente, sugerimos considerar a atribuição de
variável como rotulagem porque internamente o Python funciona com uma
variável id, não com seu nome / rótulo de variável.

>>> x = y = z = 10.1100
>>> id(x) # different labels for
same id
140271927806352
>>> id(y)
140271927806352
>>> id(z)
140271927806352 (continuação)
Capítulo 1 15

Isso é mais importante para dados mutáveis estruturas como listas. Considere o
seguinte,
>>> x = y = [1,3,4]
>>> x[0] = 'a'
>>> x
['a', 3, 4]
>>> y
['a', 3, 4]
>>> id(x),id(y)
(140271930505344, 140271930505344)

Porque x e y são apenas dois rótulos para a mesma lista subjacente, as alterações
em um dos rótulos afetam ambas as listas. Python é inerentemente mesquinho na
alocação de nova memória, então se você quiser ter duas listas diferentes com o
mesmo conteúdo, você pode forçar uma cópia como a seguir,
>>> x = [1,3,4]
>>> y = x[:] # force copy
>>> id(x),id(y) # different ids now!
(140271930004160, 140271929640448)
>>> x[1] = 99
>>> x
[1, 99, 4]
>>> y # retains original data
[1, 3, 4]

Descompactando Tupla As tuplas descompactam as atribuições na seguinte ordem


:,
>>> a,b,c = 1,2,3
>>> a
1
>>> b
2
>>> c3

Python 3 pode desempacotar tuplas em blocos usando o operador *,


>>> x,y,*z = 1,2,3,4,5
>>> x
1
>>> y
2
>>> z
[3, 4, 5]

Observe como a variável z coletou os itens restantes na atribuição. Você também


pode alterar a ordem da fragmentação,
>>> x,*y, z = 1,2,3,4,5
>>> x
1
16 Capítulo 1
>>> y
[2, 3, 4]
>>> z
5

Essa descompactação às vezes é chamada de desestruturação, ou splat, caso você


tenha lido este termo em outro lugar.
Dicionários Os dicionários Python são fundamentais para Python porque muitos
outros elementos (por exemplo, funções, classes) são construídos em torno deles.
Programar Python de forma eficaz significa usar dicionários de forma eficaz.
Dicionários são contêineres gerais que implementam a estrutura de dados de
mapeamento, que às vezes é chamada de tabela hash ou matriz associativa. Os
dicionários exigem um par chave / valor, que mapeia a chave para o valor.
>>> x = {'key': 'value'}

As chaves e os dois pontos formam o dicionário. Para recuperar o valor do dicionário


x, você deve indexá-lo com a chave conforme mostrado,
>>> x['key']
'value'

Vamos começar com alguma sintaxe básica.


>>> x={'play':'Shakespeare','actor':'Wayne','direct':'Kubrick',
... 'author':'Hemmingway','bio':'Watson'}

>>> len(x) # number of key/value pairs


5
>>> x['pres']='F.D.R.' # assignment to key 'pres'
>>> x
{'play': 'Shakespeare', 'actor': 'Wayne', 'direct': 'Kubrick',
e→ 'author': 'Hemmingway', 'bio': 'Watson', 'pres': 'F.D.R.'}
>>> x['bio']='Darwin' # reassignment for 'bio' key
>>> x
{'play': 'Shakespeare', 'actor': 'Wayne', 'direct': 'Kubrick',
e→ 'author': 'Hemmingway', 'bio': 'Darwin', 'pres': 'F.D.R.'}
>>> del x['actor'] # delete key/value pair
>>> x
{'play': 'Shakespeare', 'direct': 'Kubrick', 'author':
e→ 'Hemmingway', 'bio': 'Darwin', 'pres': 'F.D.R.'}
Os dicionários também podem ser criados com a função dict embutida,
>>> # another way of creating a dict
>>> x=dict(key='value',
... another_key=1.333,
... more_keys=[1,3,4,'one'])
>>> x
{'key': 'value', 'another_key': 1.333, 'more_keys': [1, 3, 4,
e→ 'one']}
>>> x={(1,3):'value'} # any immutable type can be a valid key
>>> x
{(1, 3): 'value'}
>>> x[(1,3)]='immutables can be keys'
Capítulo 1 17
Como contêineres generalizados , os dicionários podem conter outros dicionários ou
listas ou outros tipos de Python.

Dica de programação: União de dicionários


E se você quiser criar uma união de dicionários em uma linha?
>>> d1 = {'a':1, 'b':2, 'c':3}
>>> d2 = {'A':1, 'B':2, 'C':3}
>>> dict(d1,**d2) # combo of d1 and d2
{'a': 1, 'b': 2, 'c': 3, 'A': 1, 'B': 2, 'C': 3}
>>> {**d1,**d2} # without dict function
{'a': 1, 'b': 2, 'c': 3, 'A': 1, 'B': 2, 'C': 3}
Muito habilidoso.

A invariante que o dicionário fornece é que, contanto que você forneça uma chave
válida, ele sempre recuperará o valor correspondente; ou, no caso de cessão,
armazene o valor de forma confiável. Lembre-se de que as listas são estruturas de
dados ordenadas no sentido de que, quando os elementos são indexados, o próximo
elemento pode ser encontrado por um deslocamento relativo do anterior. Isso
significa que esses elementos são dispostos de forma contígua na memória. Os
dicionários não têm essa propriedade porque colocam os valores onde quer que
encontrem memória, contígua ou não. Isso ocorre porque os dicionários não
dependem de deslocamentos relativos para indexação, mas sim de uma função hash.
Considere o seguinte,
>>> x = {0: 'zero', 1: 'one'}
>>> y = ['zero','one']
>>> x[1] # dictionary
'one'
>>> y[1] # list
'one'

A indexação de ambas as variáveis parece notacionalmente a mesma em ambos os


casos, mas o processo é diferente. Quando fornecida uma chave, o dicionário calcula
uma função hash e armazena o valor em um local da memória com base na função
hash. O que é uma função hash? Uma função hash recebe uma entrada e é projetada
para retornar, com probabilidade muito alta, um valor que é exclusivo para a chave.
Em particular, isso significa que duas chaves não podem ter o mesmo hash ou, de
maneira equivalente, não podem armazenar valores diferentes no mesmo local de
memória. Aqui estão duas chaves que são quase idênticas, mas têm hashes muito
diferentes.
>>> hash('12345')
3973217705519425393
>>> hash('12346')
3824627720283660249

Porém, tudo isso diz respeito à probabilidade. Como a memória é finita, pode
acontecer que a função hash produza valores iguais. Isso é conhecido como uma
colisão de hash e Python implementa algoritmos de fallback para lidar com esse caso.
18 Capítulo 1
No entanto, à medida que a memória se torna escassa, especialmente em uma
plataforma pequena, a dificuldade para encontrar blocos de memória adequados pode
ser perceptível se o seu código usar muitos dicionários grandes.
Como discutimos antes, inserir / remover elementos do meio de uma lista causa
movimento extra de memória, pois a lista mantém sua invariante, mas isso não
acontece com os dicionários. Isso significa que os elementos podem ser adicionados
ou removidos sem qualquer sobrecarga de memória extra além do custo de calcular
a função hash (ou seja, pesquisa de tempo constante). Assim, os dicionários são ideais
para códigos que não precisam de pedido. Observe que, desde o Python 3.6+, os
dicionários são ordenados de acordo com a ordem em que os itens foram inseridos
no dicionário. No Python 2.7, isso era conhecido como
Collections.OrderedDict, mas desde então se tornou o padrão no Python
3.6+.
Agora que temos uma boa ideia de como os dicionários funcionam, considere as
entradas para a função hash: as chaves. Usamos principalmente inteiros e strings para
chaves, mas qualquer tipo imutável também pode ser usado, como uma tupla,
>>> x= {(1,3):10, (3,4,5,8):10}

No entanto, se você tentar usar um tipo mutável como uma chave,


>>> a = [1,2]
>>> x[a]= 10
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Vamos pensar sobre por que isso acontece. Lembre-se de que a função hash garante
que, ao receber uma chave, ela sempre poderá recuperar o valor. Suponha que fosse
possível usar chaves mutáveis em dicionários. No código acima, teríamos hash(a)
-> 132334, por exemplo, e vamos supor que o valor 10 está inserido nesse slot
de memória. Posteriormente no código, poderíamos alterar o conteúdo de a como
em a[0]=3. Agora, como a função hash tem garantia de produzir saídas diferentes
para entradas diferentes, a saída da função hash seria diferente de 132334 e,
portanto, o dicionário não pôde recuperar o valor correspondente, o que violaria seu
invariante. Assim, chegamos a uma contradição que explica por que as chaves do
dicionário devem ser imutáveis.
Conjuntos Python fornece conjuntos matemáticos e operações correspondentes com
a estrutura de dados set(), que são basicamente dicionários sem valores.
>>> set([1,2,11,1]) # union-izes elements
{1, 2, 11}
>>> set([1,2,3]) & set([2,3,4]) # bitwise intersection
{2, 3}
>>> set([1,2,3]) and set([2,3,4])
{2, 3, 4}
>>> set([1,2,3]) ^ set([2,3,4]) # bitwise exclusive OR
{1, 4}
>>> set([1,2,3]) | set([2,3,4]) # OR
{1, 2, 3, 4}
>>> set([ [1,2,3],[2,3,4] ]) # no sets of lists
Capítulo 1 19
(without more work)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
Note-se que desde Python 3.6+, chaves podem ser usadas como objetos set,como
a seguir,
>>> d = dict(one=1,two=2)
>>> {'one','two'} & d.keys() # intersection
{'one', 'two'}
>>> {'one','three'} | d.keys() # union
{'one', 'two', 'three'}
Isso também funciona para o dicionário items se os valores forem hashable,
>>> d = dict(one='ball',two='play')
>>> {'ball','play'} | d.items()
{'ball', 'play', ('one', 'ball'), ('two', 'play')}
Depois de criar um conjunto, você pode adicionar elementos individuais ou removê-
los da seguinte maneira :,
>>> s = {'one',1,3,'10'}
>>> s.add('11')
>>> s
{1, 3, 'one', '11', '10'}
>>> s.discard(3)
>>> s
{1, 'one', '11', '10'}
Lembre-se de que os conjuntos não são ordenados e você não pode indexar
diretamente nenhum dos itens constituintes. Além disso, o método subset()é para
um subconjunto adequado, não um subconjunto parcial. Por exemplo,
>>> a = {1,3,4,5}
>>> b = {1,3,4,5,6}
>>> a.issubset(b)
True
>>> a.add(1000)
>>> a.issubset(b)
False
E da mesma forma para issuperset. Os conjuntos são ideais para pesquisas
rápidas em Python, como a seguir,
>>> a = {1,3,4,5,6}
>>> 1 in a
True
>>> 11 in a
False
que funciona muito rápido, mesmo para grandes conjuntos.

1.1.6 Loops e condicionais

Existem duas construções de loop primárias em Python: o loop for e o loop


while. A sintaxe do loop for é direto:
20 Capítulo 1
>>> for i in range(3):
... print(i)
...
0
1
2

Observe o caractere de dois pontos no final. Esta é a sua dica de que a próxima linha
deve ser recuada. Em Python, os blocos são denotados por recuo de espaço em branco
(quatro espaços são recomendados), o que torna o código mais legível de qualquer
maneira. O laço for itera sobre os itens fornecidos pelo iterator, que é o
range(3) lista no exemplo acima. Python abstrai a ideia de iterable fora da
construção de loop para que alguns objetos Python sejam iteráveis por si próprios e
estejam apenas esperando por um provedor de iteração como um loop for ou
while para colocá-los em movimento. Curiosamente, o Python tem uma cláusula
else, que é usada para determinar se o loop foi encerrado ou não com um break 3.
>>> for i in [1,2,3]:
... if i>20:
... break # won't happen
... else:
... print('no break here!')
...
no break here!

O bloco else só é executado quando o loop termina sem ser interrompido.


O loop while tem uma construção direta semelhante:
>>> i = 0
>>> while i < 3:
... i += 1
... print(i)
...
1
2
3

Isso também tem um bloco opcional correspondente else. Novamente, observe que
a presença do caractere de dois pontos sugere o recuo da linha a seguir. O loop while
continuará até que a expressão booleana (isto é, i<10) avalia False.Vamos
considerar booleano e associação em Python.
Lógica e Membership Python é uma linguagem verdadeira no sentido de que as
coisas são verdadeiras, exceto o seguinte:
• None
• False
• zero, de qualquer tipo numérico, por exemplo, 0, 0L, 0.0, 0j.
• qualquer sequência vazia, por exemplo, ”, (), [].

3
Há também uma continue . declaração que vai saltar para o topo da for ou while
Capítulo 1 21
• qualquer mapeamento vazio, por exemplo, {}.
• instâncias de classes definidas pelo usuário, se a classe define um método
nonzero () or len (), quando esse método retorna o inteiro zero ou valor
booleano False.
Vamos tentar alguns exemplos,
>>> bool(1)
True
>>> bool([]) # empty list
False
>>> bool({}) # empty dictionary
False
>>> bool(0)
False
>>> bool([0,]) # list is not empty!
True

Python é sintaticamente limpo em relação aos intervalos numéricos!


>>> 3.2 < 10 < 20
True
>>> True
True

Você também pode usar disjunções (or), negações (not) e conjunções (and).
>>> 1 < 2 and 2 < 3 or 3 < 1
True
>>> 1 < 2 and not 2 > 3 or 1<3
True

Use parênteses de agrupamento para facilitar a leitura. Você pode usar a lógica entre
iteráveis da seguinte forma:
>>> (1,2,3) < (4,5,6) # at least one True
True
>>> (1,2,3) < (0,1,2) # all False
False

Não use comparação relativa para strings Python (ou seja, 'a' < 'b') que é obtuso
de ler. Em vez disso, use operações de correspondência de string (ou seja, ==). O
teste de associação usa a palavra-chave in.
>>> 'on' in [22,['one','too','throw']]
False
>>> 'one' in [22,['one','too','throw']] # no recursion
False
>>> 'one' in [22,'one',['too','throw']]
True
>>> ['too','throw'] not in [22,'one',['too','throw']]
False

Se você está testando a associação em milhões de elementos, é muito mais rápido


usar set() em vez de lista. Por exemplo,
22 Capítulo 1
>>> 'one' in {'one','two','three'}
True

A palavra-chave is é mais forte que a igualdade, uma vez que verifica se dois
objetos são os mesmos.
>>> x = 'this string'
>>> y = 'this string'
>>> x is y
False
>>> x==y
True
No entanto, is verifica o id de cada um dos itens:
>>> x=y='this string'
>>> id(x),id(y)
(140271930045360, 140271930045360)
>>> x is y True

Em virtude disso, os seguintes idiomas são comum: x is True, x is None.


Observe que None é o singleton do Python.
Condicionais Agora que entendemos as expressões booleanas, podemos construir
declarações condicionais usando if.
>>> if 1 < 2:
... print('one less than two')
...
one less than two

Há um else e elif, mas sem interruptor demonstração.


>>> a = 10
>>> if a < 10:
... print('a less than 10')
... elif a < 20:
... print('a less than 20')
... else:
... print('a otherwise')
...
a less than 20
Há uma linha única rara para condicionais
>>> x = 1 if (1> 2) else 3 # 1 linha condicional
>>> x
3

Compreensões de listas Coletar itens em um loop é tão comum que é seu próprio
idioma em Python. Ou seja,
>>> out=[] # initialize container
>>> for i in range(10):
... out.append(i**2)
...
>>> out
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Capítulo 1 23
Isso pode ser abreviado como uma compreensão de lista.
>>> [i**2 for i in range(10)] # squares of integers
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Elementos condicionais podem ser incorporados na compreensão da lista .


>>> [i**2 for i in range(10) if i % 2] # embedded conditional
[1, 9, 25, 49, 81]

que é equivalente ao seguinte,


>>> out = []
>>> for i in range(10):
... if i % 2:
... out.append(i**2)
...
>>> out
[1, 9, 25, 49, 81]

Essas compreensões também funcionam com dicionários e conjuntos.


>>> {i:i**2 for i in range(5)} # dictionary
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
>>> {i**2 for i in range(5)} # set
{0, 1, 4, 9, 16}

1.1.7 Funções

Existem duas maneiras comuns de definir funções. Você pode usar a palavra-chave
def como a seguir:
>>> def foo():
... return 'Eu disse foo'
...
>>> foo ()
'Eu disse foo'

Observe que você precisa de uma instrução return e o parêntese final para invocar
a função. Sem a instrução return, as funções retornam o singleton None. Funções
são objetos de primeira classe.
>>> foo # apenas outro objeto Python
<function foo at 0x7f939a6ecc10>

Nos primeiros dias do Python, esse era um recurso importante porque, de outra forma,
apenas ponteiros para funções podiam ser passados e eles exigiam tratamento
especial. Na prática, os objetos de primeira classe podem ser manipulados como
qualquer outro objeto Python - eles podem ser colocados em contêineres e passados
adiante sem nenhum tratamento especial. Naturalmente, queremos fornecer
argumentos para nossas funções. Existem dois tipos de argumentos de função
posicional e palavra-chave.
24 Capítulo 1
>>> def foo(x): # argumento posicional
... return x*2
...
>>> foo (10)
20

Argumentos posicionais podem ser especificados com seus nomes,


>>> def foo(x, y):
... print('x =', x,'y =', y)
...

As variáveis x e y podem ser especificadas por suas posições da seguinte forma:,


>>> foo (1,2)
x=1y=2

Eles também podem ser especificados usando os nomes em vez das posições como
segue:,
>>> foo (y= 2, x= 1)
x=1y=2

Argumentos de palavra-chave permitem que você especifique os padrões.


>>> def foo(x= 20): # palavra-chave nomeada argumento
... return 2*x
...
>>> foo (1)
2
>>> foo ()
40
>>> foo (x= 30)
60

Você pode ter vários padrões especificados,


>>> def foo(x= 20, y= 30):
... return x+y
...
>>> foo (20,)
50
>>> foo (1,1)
2
>>> foo (y= 12)
32
>>> help (foo)
Help on function foo:

foo(x=20, y=30)

Dica de programação: funções de documentação


Sempre que possível, forneça nomes de variáveis significativos e padrões para
suas funções junto com strings de documentação. Isso torna seu código fácil de
navegar, entender e usar.
Capítulo 1 25
Python facilita a inclusão de documentação para suas funções usando docstrings. Isso
torna a função help mais útil para funções.
>>> def compute_this(position= 20, velocity= 30):
... '' 'position in m
... speed in m / s
... ' ''
... return x+y
...
>> > help (compute_this)
Ajuda na função compute_this:

compute_this(position=20, velocity=30)
position in m
velocity in m/s

Assim, usando argumentos e nomes de função significativos e incluindo


documentação básica nas docstrings, você pode melhorar muito a usabilidade de suas
funções Python. Além disso, é recomendado fazer nomes de funções semelhantes a
verbos (por exemplo, get_this, compute_field).
Além de usar a instrução def, você também pode criar funções de uma linha
usando lambda. Às vezes, são chamadas de funções anônimas.
>>> f = lambda x: x**2 # anonymous functions
>>> f(10) 100

Como objetos de primeira classe, as funções podem ser colocadas em listas como
qualquer outro objeto Python,
>>> [lambda x: x, lambda x:x**2] # list of functions
[<function <lambda> at 0x7f939a6ba700>, <function <lambda> at
e→ 0x7f939a6ba790>]
>>> for i in [lambda x: x, lambda x:x**2]:
... print(i(10))
...
10
100

Até agora, não fizemos um bom uso da tuple, mas essas estruturas de dados se
tornam muito poderosas quando usadas com funções. Isso ocorre porque eles
permitem que você separe os argumentos da função da própria função. Isso significa
que você pode passá-los e construir argumentos de função e depois executá-los com
uma ou mais funções. Considere a seguinte função,
>>> def foo(x, y, z):
... return x+y+z
...
>>> foo (1,2,3)
6
>>> args = (1,2,3)
>>> foo (*args) # splat tupla em argumentos
6

A notação em estrela na frente da tupla descompacta a tupla na assinatura da função.


Já vimos esse tipo de atribuição de desempacotamento com tuplas.
26 Capítulo 1
>>> x, y, z = args
>>> x
1
>>> y
2
>>> y
2

Este é o mesmo comportamento para descompactar na assinatura da função. A


notação de asterisco duplo faz a descompactação correspondente para argumentos de
palavras-chave,
>>> def foo(x= 1, y= 1, z= 1):
... return x+y+z
...
>>> kwds = dict( x= 1, y= 2, z= 3)
>>> kwds
{'x': 1, 'y': 2, 'z': 3}
>>> foo (**kwds) # asteriscos duplos para splat de palavra-chave
6

Você pode usar os dois ao mesmo tempo:


>>> def foo(x, y, w= 10, z= 1):
... return (x, y, w, z)
...
>>> args = (1,2)
>>> kwds = dict(w= 100, z= 11)
>>> foo (*args,**kwds)
(1, 2, 100, 11)

Escopo de Variável de Função Variáveis dentro de funções ou subfunções são locais


para o respectivo escopo. Variáveis globais requerem tratamento especial se forem
alteradas dentro do corpo da função.
>>> x=10 # outside function
>>> def foo():
... return x
...
>>> foo()
10
>>> print('x = %d is not changed'%x)
x = 10 is not changed

>>> def foo():


... x=1 # defined inside function
... return x
...
>>> foo()
1
>>> print('x = %d is not changed'%x)
x = 10 is not changed

>>> def foo():


... global x # define as global
... x=20 # assign inside function scope
... return x
Capítulo 1 27
...
>>> foo()
20
>>> print('x = %d IS changed!'%x)
x = 20 IS changed!

Filtragem de palavras-chave de função Usando **kwds no final permite que uma


função para ignorar palavras-chave não utilizados ao filtrar para fora (usando a
assinatura de função) a palavra-chave entradas que ele faz uso.
>>> def foo(x= 1, y= 2, z= 3,**kwds):
... print('in foo, kwds = % s'%(kwds))
... return x+y+z
...
>>> def goo(x= 10,**kwds):
... print('in goo, kwds = % s'%(kwds))
... return foo (x= 2*x,**kwds)
...
>>> def moo(y= 1, z= 1,**kwds):
... print('in moo, kwds = % s'%(kwds))
... return goo (x=z+y, z=z+1, q= 10,**kwds)
...

Isso significa que você pode chamar qualquer um deles com uma palavra-chave não
especificada como em
>>> moo(y=91,z=11,zeta_variable = 10)
in moo, kwds = {'zeta_variable': 10}
in goo, kwds = {'z': 12, 'q': 10, 'zeta_variable': 10}
in foo, kwds = {'q': 10, 'zeta_variable': 10}
218

e a zeta_variable será passada sem uso porque nenhuma função a usa. Assim, você
pode injetar alguma outra função na seqüência de chamamento que faz usar essa
variável, sem ter que mudar as assinaturas de chamadas de qualquer das outras
funções. Usar argumentos de palavra-chave dessa forma é muito comum ao usar
Python para envolver outros códigos.
Como esse é um recurso incrível e útil do Python, aqui está outro exemplo onde
podemos rastrear como cada uma das assinaturas de função é satisfeita e o resto dos
argumentos de palavra-chave são passados.
>>> def foo(x= 1, y= 2,**kwds):
... print('foo: x = %d, y = %d, kwds =%r'%(x, y, kwds) )
... print('\t',)
... goo (x=x,**kwds)
...
>>> def goo(x= 10,**kwds):
... print('goo : x = %d, kwds =%r'%(x, kwds))
... print('\t\t',)
... moo (x=x,**kwds)
...
>>> def moo(z= 20,**kwds):
... print('moo: z =%d, kwds =%r'%(z, kwds))
...

Então,
28 Capítulo 1
>>> foo (x= 1, y= 2, z= 3, k= 20)
foo: x = 1, y = 2, kwds = {'z': 3, 'k': 20}

goo: x = 1, kwds = {'z' : 3, 'k': 20}

moo: z = 3, kwds = {'x': 1, 'k': 20}

Observe como as assinaturas de função de cada uma das funções são satisfeitas e o
resto dos argumentos de palavra-chave são passou através.
Python 3 tem a capacidade de forçar os usuários a fornecer argumentos de palavras-
chave usando o símbolo * na assinatura da função,
>>> def foo(*, x, y, z):
... return x*y*y
...

Então,
>>> foo(1,2,3) # no required keyword arguments?
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() takes 0 positional arguments but 3 were given
>>> foo(x=1,y=2,z=3) # must use keywords
4

Usar *args e **kwargs fornece argumentos de função de interface geral, mas eles
não funcionam bem com ferramentas de desenvolvimento de código integradas
porque a introspecção de variável não funciona para essas assinaturas de função. Por
exemplo,
>>> def foo(*args,**kwargs):
... return args, kwargs
...
>>> foo (1,2,2, x= 12, y= 2, q='a ')
((1, 2, 2), {' x ': 12,' y ': 2,' q ':' a '})

Isso deixa para a função processar os argumentos, o que torna um pouco claro a
assinatura da função. Você deve evitar isso sempre que possível.
Idiomas de programação funcional Embora não seja uma linguagem de
programação funcional real como Haskell, Python tem idiomas funcionais úteis.
Esses idiomas se tornam importantes em estruturas de computação paralela como o
PySpark.
>>> map(lambda x: x**2 , intervalo(10))
<objeto do mapa em 0x7f939a6a6fa0>

Isso aplica a função (lambda) dada a cada um dos iteráveis no range(10), mas
você deve convertê-lo em uma lista para obter a saída.
>>> list(map(lambda x: x**2 , range(10)))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Isso dá a mesma saída que o correspondente compreensão de lista,


>>> list(map(lambda x: x**2, range(10)))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> [i**2 for i in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Capítulo 1 29
Há também uma função reduce,
>>> from functools import reduce
>>> reduce(lambda x,y:x+2*y,[0,1,2,3],0)
12
>>> [i for i in range(10) if i %2 ] [1, 3, 5, 7, 9]

Preste atenção ao problema recursivo que functools.reduce resolve porque


functools.reduce é super-rápido em Python. Por exemplo, o algoritmo múltiplo
menos comum pode ser efetivamente implementado usando functools.reduce,
como mostrado:
>>> from functools import reduce
>>> def gcd(a, b):
... 'Return greatest common divisor using Euclids Algorithm.'
... while b:
... a, b = b, a % b
... return a
...
>>> def lcm(a, b):
... 'Return lowest common multiple.'
... return a * b // gcd(a, b)
...
>>> def lcmm(*args):
... 'Return lcm of args.'
... return reduce(lcm, args)
...

Dica de programação: cuidado com os contêineres padrão em funções


>>> def foo(x=[]): # using empty list as default
... x.append(10)
... return x
...
>>> foo() # maybe you expected this...
[10]
>>> foo() # ... but did you expect this...
[10, 10]
>>> foo() # ... or this? What's going on here?
[10, 10, 10]

Argumentos não especificados são geralmente tratados com None na assinatura


da função,
>>> def foo(x=None):
... if x is None:
... x = 10
...

A lógica de o código resolve os itens ausentes.


30 Capítulo 1
Mergulho profundo da função Vamos ver como o Python constrói o objeto de
função com um exemplo:
>>> def foo(x):
... return x
...

O atributo code da função foo contém os elementos internos da função. Por


exemplo, foo. code .co_argcount mostra o número de argumentos para a função
foo.
>>> foo. code .co_argcount
1

O atributo co_varnames fornece o nome do argumento como uma tupla de strings,


>>> foo. code .co_varnames
('x',)

Variáveis locais também estão contidas no objeto de função.


>>> def foo(x):
... y= 2*x
... return y
...
>>> foo. code .co_varnames ('x', 'y')

Lembre-se de que as funções também podem usar *args para entradas arbitrárias
não especificadas no momento da definição da função.
>>> def foo(x,*args):
... return x+sum(args)
...
>>> foo. code .co_argcount # same as before?
1

A contagem de argumentos não aumentou porque *args é tratado com o atributo


co_flags do objeto de função. Esta é uma máscara de bits que indica outros aspectos
do objeto de função.
>>> print('{0: b}'.format (foo. code .co_flags)) 1000111

Observe que o terceiro bit (isto é, coeficiente 2 ^ 2) é 1 que indica que a assinatura
da função contém uma entrada *args. Em hexadecimal, a máscara 0x01
corresponde a co_optimized (use locais rápidos), 0x02 a co_newlocals (novo
dicionário para bloco de código), 0x04 a co_varags (tem *args), 0x08 a
co_varkeywords (tem **kwds na assinatura de função), 0x10 a co_nested (escopo
de função aninhada) e, finalmente, 0x20 para co_generator (a função é um
gerador).
O módulo dis pode ajudar a desempacotar o objeto de função.
Capítulo 1 31
>>> def foo(x):
... y= 2*x
... return y
...
>>> import dis
>>> dis.show_code(foo)
Name: foo
Filename: <stdin>
Argument count: 1
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals: 2
Stack size: 2
Flags: OPTIMIZED, NEWLOCALS, NOFREE
Constants:
0: None
1: 2
Variable names:
0: x
1: y

Observe que as constantes não são compiladas no código de bytes, mas são
armazenadas no objeto de função e referenciadas posteriormente no código de bytes.
Por exemplo,
>>> def foo(x):
... a,b = 1,2
... return x*a*b
...
>>> print(foo. code .co_varnames)
('x', 'a', 'b')
>>> print(foo. code .co_consts) (None, (1, 2))

onde o singleton None está sempre disponível aqui para uso na função. Agora,
podemos examinar o byte-code no atributo co_code em mais detalhes usando
dis.dis
>>> print(foo. code .co_code) # raw byte-code
b'd\x01\\\x02}\x01}\x02|\x00|\x01\x14\x00|\x02\x14\x00S\x00'
>>> dis.dis(foo)
2 0 LOAD_CONST 1 ((1, 2))
2 UNPACK_SEQUENCE 2
4 STORE_FAST 1 (a)
6 STORE_FAST 2 (b)

3 8 LOAD_FAST 0 (x)
10 LOAD_FAST 1 (a)
12 BINARY_MULTIPLY
14 LOAD_FAST 2 (b)
16 BINARY_MULTIPLY
18 RETURN_VALUE

onde LOAD_CONST pega as constantes previamente armazenadas na função e


LOAD_FAST significa usar uma variável local e o resto é autoexplicativo. Funções
armazenam valores padrão na tupla padrões. Por exemplo,
>>> def foo(x= 10):
... return x*10
32 Capítulo 1
...
>>> print(foo. defaults )
(10,)

As regras de escopo para funções seguem a ordem Local, Envolvente (ou seja ,
fechamento), Global, Integrados (LEGB). No corpo da função, quando o Python
encontra uma variável, ele primeiro verifica se é uma variável local (co_varnames) e,
em seguida, verifica o resto se não for. Aqui está um exemplo interessante,
>>> def foo():
... print('x=',x)
... x = 10
...
>>> foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment

Por que isso? Vejamos os internos da função:


>>> foo. code .co_varnames
('x',)

Quando Python tenta resolver x, ele verifica se é uma variável local, e é porque ela
aparece em co_varnames, mas não houve nenhuma atribuição a ela e, portanto, gera
o UnboundLocalError.
O fechamento para o escopo da função é mais complexo. Considere o seguinte
código,
>>> def outer():
... a,b = 0,0
... def inner():
... a += 1
... b += 1
... print(f'{a},{b}')
... return inner
...
>>> f = outer()
>>> f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in inner
UnboundLocalError: local variable 'a' referenced before assignment

Vimos este erro acima. Vamos examinar o objeto de função,


>>> f. code .co_varnames
('a', 'b')

Isso significa que a função interna pensa que essas variáveis são locais para ela quando
na verdade existem no escopo da função envolvente. Podemos corrigir isso com a
palavra-chave nonlocal.
>>> def outer():
... a, b = 0,0
Capítulo 1 33
... def inner():
... nonlocal a,b # use nonlocal
... a+=1
... b+=1
... print(f'{a},{b}')
... return inner
...
>>> f = outer()
>>> f() # this works now
1,1

Se você voltar e verifique o atributo co_varnames, você verá que está vazio. O
co_freevars na função interna contém as informações para as variáveis nonlocal,

>>> f. code .co_freevars


('a', 'b')

para que a função interna saiba o que estão no escopo delimitador. A função externa
também sabe quais variáveis estão sendo usadas na função incorporada por meio do
atributo co_cellvars.
>>> outer. code .co_cellvars
('a', 'b')

Portanto, essa relação de fechamento é em ambos os sentidos.


Quadros de pilha de funções As funções Python aninhadas são colocadas em uma
pilha. Por exemplo,
>>> def foo():
... return 1
...
>>> def goo():
... return 1+foo ()
...

Então, quando goo é chamado, ele chama foo para que foo está no topo da goo na
pilha. O frame da pilha é a estrutura de dados que mantém o escopo do programa e
as informações sobre o estado da execução. Portanto, neste exemplo, há dois quadros
para cada função com foo no nível mais alto da pilha.
Podemos inspecionar o estado de execução interna por meio do frame da pilha
usando o seguinte,
>>> import sys
>>> depth = 0 # top of the stack
>>> frame = sys._getframe(depth)
>>> frame
<frame at 0x7f939a6bcba0, file '<stdin>', line 1, code <module>>

onde depth é o número de chamadas abaixo do topo da pilha e 0 corresponde ao


quadro atual. Os quadros contêm variáveis locais (frame.f_locals) no escopo atual
e variáveis globais (frame.f_globals) no módulo atual. Observe que as variáveis
locais e globais também podem ser acessadas usando as funções locals() e
internasglobals() .
34 Capítulo 1
O objeto de quadro também tem o f_lineno (número da linha atual), f_trace
(função de rastreamento) e f_back (referência ao quadro anterior). Durante uma
exceção não tratada, o Python navega para trás para exibir os frames da pilha usando
f_back. É importante excluir o objeto de quadro, caso contrário, existe o perigo de
criar um ciclo de referência.
O frame da pilha também contém frame.f_code, que é um código de bytes
executável. Isso é diferente do objeto de função porque não contém as mesmas
referências ao ambiente de execução global. Por exemplo, um objeto de código pode
ser criado usando a compile interna.
>>> c = compile('1 + a*b','tmp.py','eval')
>>> print(c)
<code object <module> at 0x7f939a6a5df0, file "tmp.py", line 1>
>>> eval(c,dict(a=1,b=2))
3

Observe que eval avalia a expressão para o objeto de código. O objeto de código
contém os atributos co_filename (nome do arquivo onde foi criado), co_name
(nome da função / módulo), co_varnames (nomes de variáveis) e co_code (bytecode
compilado).

Dica de programação: afirmações em funções


Escrever funções limpas e reutilizáveis é fundamental para uma programação
Python eficaz. A maneira mais fácil de melhorar muito a confiabilidade e a
capacidade de reutilização de suas funções são as declarações polvilhadas
assert em seu código. Essas instruções levantam um AssertionError se
False e fornecem uma boa maneira de garantir que sua função está se
comportando conforme o esperado. Considere o seguinte exemplo,
>>> def foo(x):
... return 2*x
...

>>> foo (10)


20
>>> foo ('x')
'xx'

Se a intenção da função é trabalhar com entradas numéricas, então o foo('x')


passou direto, mas ainda é Python válido. Isso é chamado de failover e é o
subproduto mais insidioso da tipagem dinâmica. Uma maneira rápida de corrigir
isso é assert a entrada como a seguir,
>>> def foo(x):
... assert isinstance(x,int)
... return 2*x
...
>>> foo('x')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>

(continuação)
Capítulo 1 35

File "<stdin>", line 2, in foo


AssertionError

Agora, a função está restrita ao trabalho com entradas inteiras e aumentará caso
contrário AssertionError. Além de verificar os tipos, as declarações assert
podem garantir a lógica de negócios de suas funções, verificando se os cálculos
intermediários são sempre positivos, somam um ou o que quer que seja. Além
disso, há uma opção de linha de comando no python que permitirá que você
desative as instruções assert, se necessário; mas o seu declarações assert não
deve ser excessivamente complicado, mas deve fornecer uma posição de recuo
para usos inesperados. Pense em declarações assert e pré-posicionam os pontos
de interrupção do depurador que você pode usar mais tarde.

Funções claras e expressivas são a marca registrada da programação Python


sólida. Usar nomes de variáveis e funções que sejam claros, com docstrings
detalhadas correspondentes significa que os usuários de suas funções (incluindo
versões posteriores de você) irão agradecer. Uma boa regra geral é que sua docstring
deve ser mais longa do que o corpo de sua função. Caso contrário, divida sua função
em pedaços menores. Para obter exemplos de código Python excelente, verifique o
projeto networkx3.
Avaliação preguiçosa e assinaturas de função Considere a seguinte função,
>>> def foo(x):
... return a*x
...
Observe que o interpretador não reclama quando você define esta função, embora a
variável a não seja definida. Ele só reclamará quando você tentar executar a função.
Isso ocorre porque as funções do Python são avaliadas lentamente, o que significa
que os corpos das funções são processados apenas quando chamados. Portanto, a
função não sabe sobre a variável ausente a até que tente procurá-la no namespace.
Embora os corpos das funções sejam avaliados lentamente, a assinatura da função é
avaliada avidamente. Considere o seguinte experimento,
>>> def report():
... print('Fui chamado!')
... return 10
...
>>> def foo(x = report ()):
... return x
...
fui chamado!

Observe que foo não foi chamado, mas report() foi porque ele aparece na
assinatura da função de foo.

3
Consulte https://networkx.org/.
36 Capítulo 1
1.1.8 Entrada / saída de arquivos

É simples ler e gravar arquivos usando Python. O mesmo padrão se aplica ao gravar
em outros objetos, como soquetes. A seguir está a maneira tradicional de obter E/S
de arquivo em Python.
>>> f=open('myfile.txt','w') # write mode
>>> f.write('this is line 1')
14
>>> f.close()
>>> f=open('myfile.txt','a') # append mode
>>> f.write('this is line 2')
14
>>> f.close()
>>> f=open('myfile.txt','a') # append mode
>>> f.write('\nthis is line 3\n') # put in newlines
16
>>> f.close()
>>> f=open('myfile.txt','a') # append mode
>>> f.writelines([ 'this is line 4\n', 'this is line 5\n']) #
e→ put in newlines
>>> f=open('myfile.txt','r') # read mode
>>> print(f.readlines())
['this is line 1this is line 2\n', 'this is line 3\n', 'this is
e→ line 4\n', 'this is line 5\n']
>>> ['this is line 1this is line 2\n', 'this is line 3\n', 'this
e→ is line 4\n', 'this is line 5\n']
['this is line 1this is line 2\n', 'this is line 3\n', 'this is
e→ line 4\n', 'this is line 5\n']

que tiver o identificador do arquivo, você pode usar métodos como search() para
mover o ponteiro ao redor do arquivo. Em vez de fechar explicitamente o arquivo, a
instrução with lida com isso automaticamente,
>>> with open('myfile.txt','r') as f:
... print(f.readlines())
...
['this is line 1this is line 2\n', 'this is line 3\n', 'this is
e→ line 4\n', 'this is line 5\n']

A principal vantagem é que o arquivo será automaticamente fechado no final do


bloco, o que significa que você não precisa se lembrar de fechá-lo posteriormente
usando f.close. Resumidamente, quando o bloco with é inserido, o Python
executa o método f. enter para abrir o arquivo e no final do bloco executa o método
f.exit. A instrução with funciona com outros objetos conformáveis que respeitam
4
este protocolo e com gerenciadores de contexto.
Observe que para escrever arquivos não-texto, você deve usar o rb read-binary e
o wb equivalentes binários de gravação do acima. Você também pode especificar uma
codificação de arquivo com a função open.

4
Consulte o contextlib módulo integrado.
Capítulo 1 37

Dica de programação: Módulos Outras E/S


Python tem muitas ferramentas para lidar com E/S de arquivos em diferentes
níveis de granularidade. O módulo struct é boa para leitura e escrita binárias
puras. O módulo mmap é útil para contornar o sistema de arquivos e usar memória
virtual para acesso rápido aos arquivos. O módulo StringIO permite ler e escrever
strings como arquivos.

Serialização: salvando objetos complexos Serialização significa empacotar objetos


Python para serem enviados entre processos Python separados ou computadores
separados, digamos, por meio de um soquete de rede. A natureza multiplataforma do
Python significa que não se pode ter certeza de que os atributos de baixo nível dos
objetos Python (digamos, entre os tipos de plataforma ou versões Python)
permanecerão consistentes.
Para a grande maioria das situações, o seguinte funcionará.
>>> import pickle
>>> mylist = ["This", "is", 4, 13327]
>>> f=open('myfile.dat','wb') # binary-mode
>>> pickle.dump(mylist, f)
>>> f.close()
>>> f=open('myfile.dat','rb') # write mode
>>> print(pickle.load(f))['This', 'is', 4, 13327]

Dica de programação : Serialização via Sockets


Você também pode serializar strings e transportá-los com a criação de arquivo
intermediário, usando sockets, ou algum outro protocolo.

Retirando uma função O estado interno de uma função e como ela está ligada ao
processo Python no qual foi criada torna complicado separar funções. Como em tudo
em Python, existem maneiras de contornar isso, dependendo do seu caso de uso. Uma
ideia é usar o módulo marshal para despejar o objeto de função em um formato
binário, gravá-lo em um arquivo e, em seguida, reconstruí-lo na outra extremidade
usando types.FunctionType. A desvantagem dessa técnica é que ela pode não
ser compatível nas diferentes versões principais do Python, mesmo se forem todas
implementações CPython.
>>> import marshal
>>> def foo(x):
... return x*x
...
>>> code_string = marshal.dumps(foo. code )

Então, no processo remoto (após transferir code_string):


>>> import marshal, types
>>> code = marshal.loads(code_string)
38 Capítulo 1
>>> func = types.FunctionType(code, globals(), "some_func_name")
>>> func(10) # gives 100
100

Dica de programação: Dill Pickles


O módulo dill pode selecionar funções e lidar com todas essas complicações.
No entanto, a import dill sequestra toda a decapagem daquele ponto em
diante. Para um controle mais refinado da serialização usando dill sem esse
sequestro, execute dill.extend(False) após importar dill.
import dill
def foo(x):
return x*x

dill.dumps(foo)

1.1.9 Lidando com erros

É aqui que o Python realmente brilha. A abordagem é pedir perdão em vez de


permissão. Este é o modelo básico.
try:
# try something
except:
# fix something

O bloco acima exceto irá capturar e processar qualquer tipo de exceção que seja
lançada no bloco try. Python fornece uma longa lista de exceções integradas que
você pode capturar e a capacidade de criar suas próprias exceções, se necessário.
Além de capturar exceções, você pode lançar sua própria exceção usando a instrução
raise. Há também uma declaração assert que pode lançar exceções se certas
declarações não forem True após a declaração (mais sobre isso mais tarde).
A seguir estão alguns exemplos dos poderes de tratamento de exceções do Python.
>>> def some_function():
... try:
... # Divisão por zero gera uma exceção
... 10/0
... except ZeroDivisionError:
... print("Oops, invalid.")
... else:
... # Não ocorreu exceção, estamos bem.
... pass
... finally:
... # Isso é executado depois que o bloco de código é executado
... # e todas as exceções foram tratadas, mesmo
... # se uma nova exceção for levantada durante o tratamento.
Capítulo 1 39
... print("We're done with that.")
...
>>> some_function()
Oops, invalid.
We're done with that.
>>> out = list(range(3))

Exceções podem ser muito específico,


>>> try:
... 10 / 0
... except ZeroDivisionError:
... print('I caught an attempt to divide by zero')
...
I caught an attempt to divide by zero
>>> try:
... out[999] # raises IndexError
... except ZeroDivisionError:
... print('I caught an attempt to divide by zero')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range

As exceções capturadas alteram o fluxo de código,


>>> try:
... 1/0
... out[999]
... except ZeroDivisionError:
... print('I caught an attempt to divide by zero but I did not
e→ try out[999]')
...
I caught an attempt to divide by zero but I did not try out[999]

A ordem das exceções no bloco try é importante,


>>> try:
... 1/0 # raises ZeroDivisionError
... out[999] # never gets this far
... except IndexError:
... print('I caught an attempt to index something out of
e→ range')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module> ZeroDivisionError: division by
zero

Os blocos podem ser aninhados, mas se eles tiverem mais de duas camadas de
profundidade, é um mau presságio para o código geral.
>>> try: #nested exceptions
... try: # inner scope
... 1/0
... except IndexError:
... print('caught index error inside')
... except ZeroDivisionError as e:
40 Capítulo 1
... print('I caught an attempt to divide by zero inside
e→ somewhere')
...
I caught an attempt to divide by zero inside somewhere

A cláusula finalmente sempre é executada.


>>> try: #nested exceptions with finally clause
... try:
... 1/0
... except IndexError:
... print('caught index error inside')
... finally:
... print("I am working in inner scope")
... except ZeroDivisionError as e:
... print('I caught an attempt to divide by zero inside
e→ somewhere')
...
I am working in inner scope
I caught an attempt to divide by zero inside somewhere

As exceções podem ser agrupadas em tuplas,


>>> try:
... 1/0
... except (IndexError,ZeroDivisionError) as e:
... if isinstance(e,ZeroDivisionError):
... print('I caught an attempt to divide by zero inside
e→ somewhere')
... elif isinstance(e,IndexError):
... print('I caught an attempt to index something out of
e→ range')
...
I caught an attempt to divide by zero inside somewhere

Embora você possa pegar qualquer exceção com uma linha não qualificada except,
que não iria dizer o que exceção foi lançada, mas você pode usar Exception para
revelar isso.
>>> try: # more detailed arbitrary exception catching
... 1/0
... except Exception as e:
... print(type(e))
...
<class 'ZeroDivisionError'>

Dica de programação: Usando exceções para Recursão da função de


controle
Conforme discutido anteriormente, as chamadas de função aninhadas resultam em
mais quadros de pilha que eventualmente atingem um limite de recursão. Por
exemplo, a seguinte função recursiva irá eventualmente falhar com grande o
suficiente n.

(continuação)
Capítulo 1 41

>>> def factorial(n):


... if (n == 0): return 1
... return n * factorial(n-1)
...

Exceções Python podem ser usadas para parar o empilhamento de frames


adicionais na pilha. O seguinte Recurse é uma subclasse de Exception e
faz pouco mais do que salvar os argumentos. A função recurse levanta a
exceção Recurse, que para de aumentar a pilha quando chamada. O objetivo
dessas duas definições é criar o decorador tail_recursive dentro do qual o
trabalho real ocorre. O decorador retorna uma função que incorpora um loop
while infinito que pode sair apenas quando a função assim decorada termina
sem acionar uma etapa recursiva adicional. Caso contrário, os argumentos de
entrada para a função decorada são transmitidos e os argumentos de entrada
realmente contêm os valores intermediários do cálculo recursivo. Essa técnica
ignora o limite de recursão Python embutido e funcionará como um decorador
para qualquer função recursiva que usa argumentos de entrada para armazenar
valores intermediários.a
>>> class Recurse(Exception):
... def init (self, *args, **kwargs):
... self.args, self.kwargs = args, kwargs
...
>>> def recurse(*args, **kwargs):
... raise Recurse(*args, **kwargs)
...
>>> def tail_recursive(f):
... def decorated(*args, **kwargs):
... while True:
... try:
... return f(*args, **kwargs)
... except Recurse as r:
... args, kwargs = r.args, r.kwargs
... return decorated
...
>>> @tail_recursive
... def factorial(n, accumulator=1):
... if n == 0: return accumulator
... recurse(n-1, accumulator=accumulator*n)
...

a
Veja https://chrispenner.ca/posts/python-tail-recursion para uma discussão aprofundada
desta técnica.
42 Capítulo 1
1.1.10 Recursos do Power Python para dominar

Usar esses recursos integrados do Python indica maturidade em suas habilidades de


codificação em Python.
A Função zip Python tem uma função embutida zip que pode combinar iteráveis
em pares.
>>> zip(range(3),'abc')
<zip object at 0x7f939a91d200>
>>> list(zip(range(3),'abc'))
[(0, 'a'), (1, 'b'), (2, 'c')]
>>> list(zip(range(3),'abc',range(1,4))) [(0, 'a', 1), (1, 'b', 2),
(2, 'c', 3)]

A parte lisa é inverter esta operação usando a operação *,


>>> x = zip(range(3),'abc')
>>> i,j = list(zip(*x))
>>> i
(0, 1, 2)
>>> j
('a', 'b', 'c')

Quando combinado com dict, zip fornece uma maneira poderosa de construir
dicionários Python,
>>> k = range(5)
>>> v = range(5,10)
>>> dict(zip(k,v))
{0: 5, 1: 6, 2: 7, 3: 8, 4: 9}

A Função max A função max pega o valor máximo de uma sequência.


>>> max([1,3,4])
4

Se os itens na sequência forem tuplas, o primeiro item na tupla será usado para a
classificação
>>> max([(1,2),(3,4)])
(3, 4)

Esta função leva um argumento key que controla como os itens na sequência são
avaliados. Por exemplo, podemos classificar com base no segundo elemento da tupla,
>>> max([(1,4),(3,2)], key=lambda i:i[1])
(1, 4)

O argumento key também funciona com as funções min e sorted.


A Instrução with Configure um contexto para o código subsequente
>>> class ControlledExecution:
... def enter (self):
... #set things up
Capítulo 1 43
... print('I am setting things up for you!')
... return 'something to use in the with-block'
... def __exit__(self, type, value, traceback):
... #tear things down
... print('I am tearing things down for you!')
...
>>> with ControlledExecution() as thing:
... # some code
... pass
...
I am setting things up for you!
I am tearing things down for you!

Mais comumente, as instruções with são usadas com arquivos:


f = open("sample1.txt") # file handle
f. __enter_ _ ()
f.read(1)
f. __exit__(None, None, None)
f.read(1)

Esta é a melhor maneira de abrir arquivos fechados, o que torna mais difícil esquecer
de fechá-los quando terminar.
with open("x.txt") as f:
data = f.read()
#do something with data

contextlib para construção de contexto rápido O módulo contextlib torna


muito fácil criar gerenciadores de contexto rapidamente.
>>> import contextlib
>>> @contextlib.contextmanager
... def my_context():
... print('setting up ')
... try:
... yield {} # can yield object if necessary for 'as' part
... except:
... print('catch some errors here')
... finally:
... print('tearing down')
...
>>> with my_context():
... print('I am in the context')
...
setting up
I am in the context
tearing down
>>> with my_context():
... raise RuntimeError ('I am an error')
...
setting up
catch some errors here tearing down
44 Capítulo 1
Python Memory Management Anteriormente, usamos a função id para obter o
identificador exclusivo de cada objeto Python. Na implementação do CPython, esta
é realmente a localização da memória do objeto. Qualquer objeto em Python tem um
contador de referência que rastreia os rótulos que apontam para ele. Vimos isso
anteriormente em nossa discussão sobre listas com rótulos diferentes (ou seja, nomes
de variáveis). Quando não há mais rótulos apontando para um determinado objeto, a
memória para esse objeto é liberada. Isso funciona bem, exceto para objetos de
contêiner que podem gerar referências cíclicas. Por exemplo,
>>> x = [1,2]
>>> x.append(x) # cyclic reference
>>> x
[1, 2, [...]]

Nesta situação, o contador de referência nunca fará a contagem regressiva até zero e
será liberado. Para defender isso, Python implementa um coletor de lixo, que
interrompe periodicamente o thread principal de execução para localizar e remover
tais referências. O código a seguir usa o poderoso módulo ctypes para obter acesso
direto ao campo do contador de referência na estrutura C em object.h para
CPython.
>>> def get_refs(obj_id):
... from ctypes import Structure, c_long
... class PyObject(Structure):
... _fields_ = [("reference_cnt", c_long)]
... obj = PyObject.from_address(obj_id)
... return obj.reference_cnt
...

Vamos retornar ao nosso exemplo e ver o número de referências que apontam para a
lista rotulada x.
>>> x = [1,2]
>>> idx = id(x)
>>> get_refs (idx)
1

Agora, depois de criarmos a referência circular,


>>> x.append (x)
>>> get_refs (idx)
2

Mesmo excluir x não ajuda,


>>> del x
>>> get_refs (idx)
1

Podemos forçar o coletor de lixo a trabalhar com o seguinte,


>>> import gc
> >> gc.collect () 216
>>> get_refs (idx) # finalmente removido!
0
Capítulo 1 45
Assim, podemos finalmente nos livrar dessa x lista. É importante ressaltar que você
não deve ter que disparar manualmente o coletor de lixo porque o Python decide
quando fazer isso de forma eficiente. Um aspecto importante desses algoritmos é se
o contêiner é ou não mutável. Portanto, do ponto de vista da coleta de lixo, é melhor
usar tuplas em vez de listas porque as tuplas não precisam ser policiadas pelo coletor
de lixo como as listas regulares.

1.1.11 Geradores

Os geradores fornecem contêineres com eficiência de memória just-in-time.


• Produz um fluxo de valores sob demanda
• Só executa em next().
• A função yield() produz um valor, mas salva o estado da função para mais
tarde
• Usável exatamente uma vez (ou seja, não reutilizável depois de usado pela
primeira vez)

>>> def generate_ints(N):


... for i in range(N):
... yield i # the yield makes the function a generator
...
>>> x=generate_ints(3)
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

Esvaziar o gerador levanta a exceção StopIteration. O que está acontecendo no


próximo bloco?
>>> next(generate_ints (3))
0
>>> next(generate_ints (3))
0
>>> next(generate_ints (3))
0

O problema é que o gerador não é salvo em uma variável em que o estado atual de o
gerador pode ser armazenado. Assim, o código acima cria um novo gerador a cada
linha, que inicia a iteração no início de cada linha. Você também pode iterar
diretamente:
>>> for i in generate_ints(5): # no assignment necessary here
... print(i)
...
0
46 Capítulo 1
1
2
3
4

Os geradores mantêm um estado interno que pode retornar após o yield. Isso
significa que os geradores podem continuar de onde pararam.
>>> def foo():
... print('hello')
... yield 1
... print('world')
... yield 2
...
>>> x = foo()
>>> next(x)
hello
1

>>> # do some other stuff here


>>> next(x) # pick up where I left off
World
2

Os geradores podem implementar algoritmos que possuem loops infinitos.


>>> def pi_series(): # infinite series converges to pi
... sm = 0
... i = 1.0; j = 1
... while True: # loops forever!
... sm = sm + j/i
... yield 4*sm
... i = i + 2; j = j * -1
...
>>> x = pi_series()
>>> next(x)
4.0
>>> next(x)
2.666666666666667
>>> next(x)
3.466666666666667
>>> next(x)
2.8952380952380956
>>> next(x)
3.3396825396825403
>>> next(x)
2.9760461760461765
>>> next(x)
3.2837384837384844
>>> gen = (i for i in range(3)) # List comprehension style

Você também pode send() para um gerador existente, colocando o yield no lado
direito do sinal de igual,
>>> def foo():
... while True:
... line=(yield)
Capítulo 1 47
... print(line)
...
>>> x= foo()
>>> next(x) # get it going
>>> x.send('I sent this to you')
I sent this to you ê

Estes podem ser encadeados também:


>>> def goo(target):
... while True:
... line=(yield)
... target.send(line.upper()+'---')
...
>>> x= foo()
>>> y= goo(x)
>>> next(x) # get it going
>>> next(y) # get it going
>>> y.send('from goo to you')
FROM GOO TO YOU---

Os geradores também podem ser criados como compreensões de lista, alterando a


notação de colchetes para parênteses.
>>> x= (i for i in range(10))
>>> print(type(x))
<class 'generator'>

Agora, x é um gerador. O módulo itertools são essenciais para o uso eficaz de


geradores. Por exemplo, pode-se clonar geradores,
>>> x = (i for i in range(10))
>>> import itertools as it
>>> y, = it.tee(x,1) # clone generator
>>> next(y) # step this one
0
>>> list(zip(x,y))
[(1, 2), (3, 4), (5, 6), (7, 8)]

Este método de execução atrasada torna-se particularmente útil ao trabalhar com


grandes conjuntos de dados.
>>> x = (i for i in range(10))
>>> y = map(lambda i:i**2,x)
>>> y
<map object at 0x7f939af36fd0>

Observe que y também é um gerador e que nada foi calculado ainda. Você também
pode mapear funções em sequências usando it.starmap.
Rendendo a partir de Geradores O seguinte idioma é comum para iterar em um
gerador,
>>> def foo(x):
... for i in x:
... yield i
...
48 Capítulo 1
Então você pode alimentar o gerador x em foo,
>>> x = (i**2 for i in range(3)) # create generator
>>> list(foo(x)) [0, 1, 4]

Com o yield from, isso pode ser feito em uma linha


>>> def foo(x):
... yield from x
...

Então,
>>> x = (i**2 for i in range(3)) # recreate generator
>>> list(foo(x))
[0, 1, 4]

Há muito mais que o yield from pode fazer, no entanto. Suponha que temos um
gerador / co-rotina que recebe dados.
>>> def accumulate():
... sm = 0
... while True:
... n = yield # receive from send
... print(f'I got {n} in accumulate')
... sm+=n
...

Vamos ver como isso funciona,


>>> x = accumulate()
>>> x.send(None) # kickstart coroutine
>>> x.send(1)
I got 1 in accumulate
>>> x.send(2)
I got 2 in accumulate

E se você tiver uma composição de funções e quiser passar os valores enviados para
a co-rotina incorporada?
>>> def wrapper(coroutine):
... coroutine.send(None) # kickstart
... while True:
... try:
... x = yield # capture what is sent...
... coroutine.send(x) # ... and pass it thru
... except StopIteration:
... pass
...

Então, poderíamos fazer algo como isto,


>>> w = wrapper(accumulate())
>>> w.send(None)
>>> w.send(1)
I got 1 in accumulate
>>> w.send(2)
I got 2 in accumulate
Capítulo 1 49
Observe como os valores enviados passam diretamente para a co-rotina incorporada.
Por diversão, podemos embrulhar isso duas vezes,
>>> w = wrapper(wrapper(accumulate()))
>>> w.send(None)
>>> w.send(1)
I got 1 in accumulate
>>> w.send(2)
I got 2 in accumulate

Agora que sabemos como isso funciona, o wrapper pode ser abreviado como o
seguinte
>>> def wrapper(coroutine):
... yield from coroutine
...

e tudo funcionaria da mesma forma ( tente!). Além disso, trata automaticamente os


erros incorporados com a mesma transparência. Um exemplo útil é o caso em que
você deseja nivelar uma lista de contêineres incorporados como
>>> x = [1,[1,2],[1,[3]]]
>>> def flatten(seq):
... for item in seq:
... if hasattr(item,'__iter__'):
... yield from flatten(item)
... else:
... yield item
...
>>> list(flatten(x))
[1, 1, 2, 1, 3]

Há outra sintaxe que é usada para geradores que permite enviar / receber
simultaneamente. Um problema óbvio com nossa função anterior accumulate é
que você não recebe o valor acumulado. Isso é remediado alterando uma linha do
código anterior,
>>> def accumulate():
... sm = 0
... while True:
... n = yield sm # receive from send and emit sm
... print (f'I got {n} in accumulate and sm ={sm}')
... sm+=n
...

Então, podemos fazer,


>>> x = accumulate()
>>> x.send(None) # still need to kickstart
0

Observe que ele retornou zero.


>>> x.send (1)
Eu tenho 1 em acumular e sm = 0
1
50 Capítulo 1
>>> x.send(2)
Eu tenho 2 em acumular e sm = 1
3
>>> x.send (3)
Eu tenho 3 em acumular e sm = 3
6

1.1.12 Decoradores

Decoradores são funções que transformam funções em funções. Parece redundante,


mas acaba sendo muito útil para combinar conceitos díspares. No código abaixo,
observe que a entrada para my_decorator é uma função. Como não sabemos os
argumentos de entrada para essa função, temos que passá-los para a new_function
que é definida no corpo usando args e kwargs. Então, dentro da new_function,
chamamos explicitamente a função de entrada fn e passamos esses argumentos. A
linha mais importante é a última linha onde retornamos à função definida dentro do
corpo de my_decorator. Como essa função inclui a chamada explícita para fn, ela
replica a funcionalidade de fn, mas estamos livres para fazer outras tarefas dentro do
corpo de new_function.
>>> def my_decorator(fn): # note that function as input
... def new_function(*args,**kwargs):
... print('this runs before function')
... return fn(*args,**kwargs) # return a function
... return new_function
...
>>> def foo(x):
... return 2*x
...
>>> goo = my_decorator(foo)
>>> foo(3)
6
>>> goo(3)
this runs before function
6

Na saída acima, observe que goo reproduz fielmente, goo mas com a saída extra que
colocamos no corpo de new_function. A ideia importante é que qualquer que seja a
nova funcionalidade que construímos no decorador, ela deve ser ortogonal à lógica
de negócios da função de entrada. Caso contrário, a identidade da função de entrada
é misturada ao decorador, o que se torna difícil de depurar e entender posteriormente.
O seguinte decorador log_arguments é um bom exemplo desse princípio. Suponha
que queremos monitorar os argumentos de entrada de uma função. O decorador
log_arguments adicionalmente imprime os argumentos de entrada para a função de
entrada, mas não interfere na lógica de negócios dessa função subjacente.
>>> def log_arguments(fn): # note that function as input
... def new_function(*args,**kwargs):
... print('positional arguments:')
Capítulo 1 51
... print(args)
... print('keyword arguments:')
... print(kwargs)
... return fn(*args,**kwargs) # return a function
... return new_function
...

Você pode empilhar um decorador em cima de uma função definição usando a sintaxe
@. A vantagem é que você pode manter o nome da função original, o que significa
que os usuários posteriores não precisam acompanhar outra versão decorada da
função.
>>> @log_arguments # these are stackable also
... def foo(x,y=20):
... return x*y
...
>>> foo(1,y=3)
positional arguments:
(1,)
keyword arguments:
{'y': 3}
3

Decoradores são muito úteis para caches, que evitam recalcular valores de funções
caros,
>>> def simple_cache(fn):
... cache = {}
... def new_fn(n):
... if n in cache:
... print('FOUND IN CACHE; RETURNING')
... return cache[n]
... # otherwise, call function
... # & record value
... val = fn(n)
... cache[n] = val
... return val
... return new_fn
...
>>> def foo(x):
... return 2*x
...
>>> goo = simple_cache(foo)
>>> [goo(i) for i in range(5)]
[0, 2, 4, 6, 8]
>>> [goo(i) for i in range(8)]
FOUND IN CACHE; RETURNING
FOUND IN CACHE; RETURNING
FOUND IN CACHE; RETURNING
FOUND IN CACHE; RETURNING
FOUND IN CACHE; RETURNING
[0, 2, 4, 6, 8, 10, 12, 14]

O decorador simple_cache executa a função de entrada, mas depois armazena cada


saída no dicionário cache com chaves correspondentes às entradas de função.
52 Capítulo 1
Então, se a função for chamada novamente com a mesma entrada, o valor da função
correspondente não é recalculado, mas sim recuperado de cache, que pode ser muito
eficiente se a função de entrada levar muito tempo para calcular. Este padrão é tão
comum que agora está em functools.lru_cache na biblioteca padrão do Python.

Dica de programação: Módulos de decoradores


Alguns módulos Python são distribuídos como decoradores (por exemplo, do
módulo click para criar interfaces de linha de comando) para facilitar a inserção
de novas funcionalidades sem alterar o código-fonte. A ideia é qualquer que seja
a nova funcionalidade fornecida pelo decorador, ela deve ser diferente da lógica
de negócios da função que está sendo decorada. Isso separa as preocupações entre
o decorador e a função decorada.

Decoradores também são úteis para executar certas funções em threads. Lembre-
se de que um thread é um conjunto de instruções que a CPU pode executar
separadamente do processo pai. O decorador a seguir envolve uma função para ser
executada em um thread separado.
>>> def run_async(func):
... from threading import Thread
... from functools import wraps
... @wraps(func)
... def async_func(*args, **kwargs):
... func_hl = Thread(target = func,
... args = args,
... kwargs = kwargs)
... func_hl.start()
... return func_hl
... return async_func
...

A função wrap do módulo functools corrige a assinatura da função. Este


decorador é útil quando você tem um pequeno trabalho paralelo (como uma
notificação) que deseja executar fora do thread principal de execução. Vamos
escrever uma função simples que faz algum trabalho falso.
>>> from time import sleep
>>> def sleepy(n=1,id=1):
... print('item %d sleeping for %d seconds...'%(id,n))
... sleep(n)
... print('item %d done sleeping for %d seconds'%(id,n))
...

Considere o seguinte bloco de código:


>>> sleepy(1,1)
item 1 sleeping for 1 seconds...
item 1 done sleeping for 1 seconds
>>> print('I am here!')
I am here!
>>> sleepy(2,2)
Capítulo 1 53
item 2 sleeping for 2 seconds...
item 2 done sleeping for 2 seconds
>>> print('I am now here!') I am now here!

Usando o decorador, podemos fazer versões assíncronas desta função:


@run_async
def sleepy(n=1,id=1):
print('item %d sleeping for %d seconds...'%(id,n))
sleep(n)
print('item %d done sleeping for %d seconds'%(id,n))

E com o mesmo bloco de afirmações acima, obtemos a seguinte sequência de


resultados impressos:
sleepy(1,1)
print('I am here!')
sleepy(2,2)
print('I am now here!')
I am here!
item 1 sleeping for 1 seconds...
item 2 sleeping for 2 seconds...
I am now here!
item 1 done sleeping for 1 seconds
item 2 done sleeping for 2 seconds

Observe que a última instrução de impressão no bloco realmente executada antes que
as funções individuais fossem concluídas. Isso ocorre porque o encadeamento
principal de execução lida com essas instruções de impressão enquanto os
encadeamentos separados estão adormecidos por diferentes períodos de tempo. Em
outras palavras, no primeiro exemplo, a última instrução é bloqueada pelas instruções
anteriores e tem que esperar que elas terminem antes de fazer a impressão final. No
segundo caso, não há bloqueio, portanto, ele pode chegar à última instrução
imediatamente, enquanto o outro trabalho continua em threads separadas.
Outro uso comum de decoradores é criar fechamentos. Por exemplo,
>>> def foo(a=1):
... def goo(x):
... return a*x # uses `a` from outer scope
... return goo
...
>>> foo(1)(10) # version with a=1
10
>>> foo(2)(10) # version with a=2
20
>>> foo(3)(10) # version with a=2
30

Neste caso , observe que a função incorporada goo requer um parâmetro a que não
está definido em sua assinatura de função. Portanto, a função foo fabrica diferentes
versões da função incorporada goo, conforme mostrado. Por exemplo, suponha que
você tenha muitos usuários com certificados diferentes para acessar dados acessíveis
via goo. Em seguida, foo pode fechar os certificados sobre goo para que cada
usuário efetivamente tenha sua própria versão da função goo com o certificado
integrado correspondente.
54 Capítulo 1
Isso simplifica o código porque a lógica de negócios e a assinatura de função do goo
não precisam ser alteradas e o certificado desaparecerá automaticamente quando o
goo sair do escopo.

Dica de programação: Threads Python Threads


em Python são usados principalmente para tornar os aplicativos responsivos. Por
exemplo, se você tem uma GUI com muitos botões e deseja que o aplicativo reaja
a cada botão quando clicado, mesmo que o próprio aplicativo esteja ocupado
renderizando a exibição, então os threads são a ferramenta certa. Como outro
exemplo, suponha que você esteja baixando arquivos de vários sites e, em seguida,
fazer cada site usando um thread separado faz sentido porque a disponibilidade do
conteúdo em cada um dos sites irá variar. Isso é algo que você não saberia antes
do tempo.
A principal diferença entre threads e processos é que os processos têm seus
próprios recursos compartimentados. A linguagem C Python (ou seja, CPython)
implementa um Global Interpreter Lock (GIL) que evita que os threads lutem por
estruturas de dados internas. Assim, o GIL emprega um mecanismo de bloqueio
de granularidade de curso onde apenas um thread tem acesso aos recursos a
qualquer momento. O GIL, portanto, simplifica a programação de threads porque
a execução de várias threads simultaneamente requer uma contabilidade
complicada. A desvantagem do GIL é que você não pode executar vários threads
simultaneamente para acelerar tarefas de computação restrita. Observe que certas
implementações alternativas de Python, como IronPython, usam um design de
threading mais refinado, em vez da abordagem GIL.
Como um comentário final, em sistemas modernos com vários núcleos, pode
ser que vários threads realmente deixem as coisas mais lentas porque o sistema
operacional pode ter que alternar threads entre diferentes núcleos. Isso cria
sobrecargas adicionais no mecanismo de troca de thread que, em última análise,
torna as coisas mais lentas. O CPython implementa o GIL no nível do código de
byte, o que significa que as instruções do código de byte em diferentes threads são
proibidas de execução simultânea.

1.1.13 Iteração e iteráveis

Os iteradores permitem um controle mais preciso para construções em loop.


>>> a = range(3)
>>> hasattr(a,'__iter_ _')
True
>>> # generally speaking, returns object that supports iteration
>>> iter(a)
Capítulo 1 55
<range_iterator object at 0x7f939a6a6630>
>>> hasattr(_,'__iter_ _')
True
>>> for i in a: #use iterables in loops
... print(i)
...
0
1
2

A verificação acima hasattr é a maneira tradicional de verificar se um determinado


objeto é iterável. Aqui está a maneira moderna,
>>> from collections.abc import Iterable
>>> isinstance(a,Iterable)
True

Você também pode usar iter() com funções para criar sentinelas,
>>> x=1
>>> def sentinel():
... global x
... x+=1
... return x
...
>>> for k in iter(sentinel,5): # stops when x = 5
... print(k)
...
2
3
4
>>> x5

Você pode usar isso com um objeto de arquivo f como em iter(f.readline,


”) que lerá as linhas do arquivo até o final.
Enumeração A biblioteca padrão possui um módulo enum que oferece suporte à
enumeração. Enumeração significa vincular nomes simbólicos a valores constantes e
únicos. Por exemplo,
>>> from enum import Enum
>>> class Codes(Enum):
... START = 1
... STOP = 2
... ERROR = 3
...
>>> Codes.START
<Codes.START: 1>
>>> Codes.ERROR
<Codes.ERROR: 3>

Uma vez definidos, tentar alterá-los irá gerar um AttributeError. Nomes e


valores podem ser extraídos,
56 Capítulo 1
>>> Codes.START.name
'START'
>>> Codes.START.value
1

Enumerações podem ser iteradas,


>>> [i for i in Codes]
[<Codes.START: 1>, <Codes.STOP: 2>, <Codes.ERROR: 3>]

Enumerações podem ser criadas diretamente com sensível padrões,


>>> Codes = Enum('Lookup','START,STOP,ERROR')
>>> Codes.START
<Lookup.START: 1>
>>> Codes.ERROR
<Lookup.ERROR: 3>

Você pode pesquisar os nomes correspondentes a um valor,


>>> Codes(1)
<Lookup.START: 1>
>>> Codes['START']
<Lookup.START: 1>

Você pode usar o decorador unique para garantir que não haja valores duplicados,
>>> from enum import unique

Isso gerará um ValueError


>>> @unique
... class UniqueCodes(Enum):
... START = 1
... END = 1 # same value as START
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "/mnt/e/miniconda3/lib/python3.8/enum.py", line 860, in
c→ unique
raise ValueError('duplicate values found in %r: %s' %
ValueError: duplicate values found in <enum 'UniqueCodes'>: END
c→ -> START

Tipos de Anotações Python 3 permite tipos de anotações, que é uma maneira de


fornecer informações de tipagem de variável para funções para que outras
ferramentas, como mypy possam analisar grandes bases de código para verificar se
há conflitos de digitação. Isso não afeta a digitação dinâmica do Python. Isso significa
que, além de criar testes de unidade, as anotações de tipo fornecem Como forma
complementar de melhorar a qualidade do código, descobrindo defeitos distintos
daqueles revelados por testes de unidade.
# filename: type_hinted_001.py

def foo(fname:str) -> str:


return fname+'.txt'

foo(10) # called with integer argument instead of string


Capítulo 1 57
Executando isto através de mypy na linha de comando do terminal fazendo
% mypy type_hinted_001.py

produzirá o seguinte erro:


type_hinted_001.py:6: error: Argument 1 to "foo" has incompatible
c→ type "int"; expected "str"

Funções que não são anotadas (isto é, digitadas dinamicamente) são permitidas no
mesmo módulo que aquelas que são anotadas. O mypy pode tentar chamar erros de
digitação nessas funções digitadas dinamicamente, mas esse comportamento é
considerado instável. Você pode fornecer anotações de tipo e valores padrão, como a
seguir:
>>> def foo(fname:str = 'some_default_filename') -> str:
... return fname+'.txt'
...
Os tipos não são inferidos dos tipos dos valores padrão. O módulo embutido typing
tem definições que podem ser usadas para dicas de tipo,
>>> from typing import Iterable
>>> def foo(fname: Iterable[str]) -> str:
... return "".join(fname)
...

A declaração acima diz que a entrada é uma iterável (por exemplo, list) de strings
e a função retorna uma única string como saída. Desde Python 3.6, você também pode
usar anotações de tipo para variáveis, como no seguinte,
>>> from typing import List
>>> x: str = 'foo'
>>> y: bool = True
>>> z: List[int] = [1]

Lembre-se de que essas adições são ignoradas no intérprete e processadas por mypy
separadamente. As anotações de tipo também funcionam com classes, como
mostrado abaixo,
>>> from typing import ClassVar
>>> class Foo:
... count: ClassVar[int] = 4
...

Porque a sugestão de tipo pode se tornar elaborada, especialmente com objetos


complexos padrões, anotações de tipo podem ser segregadas em arquivos pyi. Neste
ponto, entretanto, meu sentimento é que entramos em um ponto de rendimentos
decrescentes, já que o fardo da complexidade provavelmente superará os benefícios
de manter tal verificação de tipo. No caso de código crítico que é mantido como
funções separadas utilizando principalmente tipos Python integrados, então essa
carga adicional pode ser garantida, mas de outra forma, e pelo menos até que as
ferramentas necessárias para auxiliar o desenvolvimento e manutenção de anotações
de tipo amadureçam ainda mais , provavelmente é melhor deixar isso de lado.
Pathlib Python 3 tem um novo módulo pathlib que torna mais fácil trabalhar com
sistemas de arquivos.
58 Capítulo 1
Antes do pathlib, você tinha que usar o os.walk ou outra combinação de
ferramentas de busca de diretório para trabalhar com diretórios e caminhos. Para
começar, importamos o Path do módulo,
>>> do pathlib import Path

O próprio objeto Path fornece informações úteis,


>>> Path.cwd() # gets current directory
PosixPath('/mnt/d/class_notes_pub')
>>> Path.home() # gets users home directory
PosixPath('/home/unpingco')

Você pode dar ao objeto um caminho inicial,


>>> p = Path('./') # points to current directory

Então, você pode pesquisar o diretório caminho usando seus métodos,


>>> p.rglob('*.log') # searches for log file extensions
<generator object Path.rglob at 0x7f939a5ecc80>

que retorna um gerador através do qual você pode iterar para obter todos os arquivos
de log no diretório nomeado. Cada elemento retornado da iteração é um objeto
PosixPath com seus próprios métodos
>>> item = list(p.rglob('*.log'))[0]
>>> item
PosixPath('altair.log')

Por exemplo, o método stat fornece os metadados do arquivo,


>>> item.stat()
os.stat_result(st_mode=33279, st_ino=3659174697287897, st_dev=15,
c→ st_nlink=1, st_uid=1000, st_gid=1000, st_size=52602,
c→ st_atime=1608339360, st_mtime=1608339360,
c→ st_ctime=1608339360)
O módulo pathlib pode fazer muito mais, como criar arquivos ou processar
elementos de caminhos individuais.
Asyncio: Vimos que geradores / co-rotinas podem trocar e agir sobre os dados, desde
que conduzamos manualmente esse processo. Na verdade, uma maneira de pensar
sobre as corrotinas é como objetos que requerem gerenciamento externo para realizar
o trabalho. Para geradores, a lógica de negócios do código geralmente gerencia o
fluxo, mas async simplifica esse trabalho e oculta os detalhes de implementação
complicados para fazer isso em grande escala.
Considere a seguinte função definida com a palavra-chave async,
>>> async def sleepy(n=1):
... print(f'n = {n} seconds')
... return n
...

Podemos começar assim teríamos um gerador regular


>>> x = sleepy(3)
>>> type(x)
<class 'coroutine'>
Capítulo 1 59
Agora, temos um loop de evento para conduzir isso,
>>> import asyncio
>>> loop = asyncio.get_event_loop ()

Agora o loop pode conduzir a co-rotina,


>>> loop.run_until_complete(x)
n = 3 seconds
3

Vamos colocar isso em um loop de bloqueio síncrono.


>>> from time import perf_counter
>>> tic = perf_counter()
>>> for i in range(5):
... sleepy(0.1)
...
<coroutine object sleepy at 0x7f939a5f7ac0>
<coroutine object sleepy at 0x7f939a4027c0>
<coroutine object sleepy at 0x7f939a5f7ac0>
<coroutine object sleepy at 0x7f939a4027c0>
<coroutine object sleepy at 0x7f939a5f7ac0>
>>> print(f'elapsed time = {perf_counter()-tic}') elapsed time =
0.0009255999993911246

Nada aconteceu! Se quisermos usar a função sleepy dentro de outro código,


precisamos da palavra-chave await,
>>> async def naptime(n=3):
... for i in range(n):
... print(await sleepy(i))
...

Grosso modo, as palavras-chave await significa que a função de chamada deve ser
suspensa até que o destino de await seja concluído e o controle deve ser passado de
volta para o loop de eventos nesse meio tempo. Em seguida, conduzimos como antes,
com o loop de eventos.
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(naptime(4))
n = 0 seconds
0
n = 1 seconds
1
n = 2 seconds
2
n = 3 seconds
3

Sem a palavra-chave await, a função naptime iria apenas retornar os objetos


sleepy em vez das saídas desses objetos. Os corpos das funções devem ter código
assíncrono ou serão bloqueados. Estão em desenvolvimento módulos Python que
podem brincar com esta estrutura, mas isso está em andamento (por exemplo,
aiohttp para acesso assíncrono à web) no momento.
60 Capítulo 1
Vamos considerar outro exemplo que mostra como o loop de eventos recebe o
controle de cada uma das funções assíncronas.
>>> async def task1():
... print('entering task 1')
... await asyncio.sleep(0)
... print('entering task 1 again')
... print('exiting task 1')
...
>>> async def task2():
... print('passed into task2 from task1')
... await asyncio.sleep(0)
... print('leaving task 2')
...
>>> async def main():
... await asyncio.gather(task1(),task2())
...

Com tudo isso configurado, temos que começar com o loop de eventos,
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(main())
entering task 1
passed into task2 from task1
entering task 1 again
exiting task 1
leaving task 2

A instrução await asyncio.sleep(0) informa o controle de passagem do


loop de evento para o próximo item (ou seja, futuro ou co-rotina) porque o atual ficará
ocupado esperando, então o loop de eventos pode muito bem executar outra coisa
nesse meio tempo. Isso significa que as tarefas podem terminar fora de ordem,
dependendo de quanto tempo demoram até que o loop de eventos as retorne. Este
próximo bloco demonstra isso.
>>> import random
>>> async def asynchronous_task(pid):
... await asyncio.sleep(random.randint(0, 2)*0.01)
... print('Task %s done' % pid)
...
>>> async def main():
... await asyncio.gather(*[asynchronous_task(i) for i in
c→ range(5)])
...

>>> loop = asyncio.get_event_loop()


>>> loop.run_until_complete(main())
Task 0 done
Task 1 done
Task 2 done
Task 3 done
Task 4 done

Tudo isso é muito bom dentro do ecossistema de asyncio, mas o que fazer com os
códigos existentes que não estão configurados dessa forma? Podemos usar
concurrent.futures para fornecer futuros, podemos nos envolver na estrutura.
Capítulo 1 61
>>> from functools import wraps
>>> from time import sleep
>>> from concurrent.futures import ThreadPoolExecutor
>>> executor = ThreadPoolExecutor(5)
>>> def threadpool(f):
... @wraps(f)
... def wrap(*args, **kwargs):
... return asyncio.wrap_future(executor.submit(f,
... *args,
... **kwargs))
... return wrap
...

Podemos decorar uma versão de bloqueio de sleepy como mostrado a seguir e use
asyncio.wrap_future para dobrar o tópico na estrutura asyncio,
>>> @threadpool
... def synchronous_task(pid):
... sleep(random.randint(0, 1)) # blocking!
... print('synchronous task %s done' % pid)
...
>>> async def main():
... await asyncio.gather(synchronous_task(1),
... synchronous_task(2),
... synchronous_task(3),
... synchronous_task(4),
... synchronous_task(5))
...

Com a seguinte saída. Observe a não ordenação dos resultados.


>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(main())
synchronous task 3 done
synchronous task 5 done
synchronous task 1 done
synchronous task 2 done
synchronous task 4 done

Python de depuração e registro A maneira mais fácil é depurar o Python na linha


de comando,
% python -m pdb filename.py

e você entrará automaticamente no depurador. Caso contrário, você pode colocar


import pdb; pdb.set_trace() arquivo de origem onde você deseja breakpoints. A
vantagem de usar o ponto e vírgula para colocar tudo em uma linha é que torna mais
fácil localizar e excluir essa linha posteriormente em seu editor.
Um truque legal que fornecerá um shell Python totalmente interativo em qualquer
lugar do código é fazer o seguinte:
import code; code.interact(local=locals());

Desde o Python 3.7, temos a função breakpoint, que é essencialmente o mesmo


que a linha import pdb; pdb.set_trace() acima, mas que permite usar a
PYTHONBREAKPOINT variável de ambiente para ligar ou desligar o ponto de
interrupção, como em
62 Capítulo 1
% export PYTHONBREAKPOINT=0 python foo.py

Nesse caso, como a variável de ambiente é definida como zero, da linha


breakpoint no código-fonte de foo.py é ignorado. Definir a variável de
ambiente como um fará o oposto. Você também pode escolher seu depurador usando
a variável de ambiente. Por exemplo, se você preferir o depurador IPython ipdb,
pode fazer o seguinte,
export PYTHONBREAKPOINT=ipdb.set_trace python foo.py

Usando esta variável de ambiente, você também pode fazer o breakpoint executar
código personalizado quando chamado. Dada a seguinte função,
# filename break_code.py
def do_this_at_breakpoint():
print ('I am here in do_this_at_breakpoint')

Então, dado que temos um breakpoint definido no arquivo foo.py, podemos


fazer o seguinte,
% export PYTHONBREAKPOINT=break_code.do_this_at_breakpoint python
c→ foo.py

e então o código será executado. Observe que, como esse código não está chamando
um depurador, a execução não será interrompida no breakpoint. A função
breakpoint também pode receber argumentos,
breakpoint('a','b')

em seu código-fonte e, em seguida, a função de chamada irá processar essas entradas,


como a seguir,
# filename break_code.py
def do_this_at_breakpoint(a,b):
print ('I am here in do_this_at_breakpoint')
print (f'argument a = {a}')
print (f'argument b = {b}')

Então, o valor dessas variáveis em tempo de execução será impresso. Observe que
você também pode chamar explicitamente o depurador de dentro de sua função de
ponto de interrupção personalizado, incluindo o usual import pdb;
pdb.set_trace() que irá parar o código com o depurador embutido.

1.1.14 Uso de asserções Python para pré-depurar código

Asserts são uma ótima maneira de fornecer verificações de integridade para seu
código. Usar isso é a maneira mais rápida e fácil de aumentar a confiabilidade do seu
código! Observe que você pode desativá-los executando python na linha de comando
com a opção -O.
Capítulo 1 63
>>> import math
>>> def foo(x):
... assert x>=0 # entry condition
... return math.sqrt(x)
...
>>> foo(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
AssertionError

Considere o seguinte código,


>>> def foo(x):
... return x*2
...

Qualquer entrada x que entenda o operador de multiplicação passará. Por exemplo,


>>> foo('string')
'stringstring'
>>> foo([1,3,'a list'])
[1, 3, 'a list', 1, 3, 'a list']

que pode não ser o que você deseja. Para defender foo desse efeito e impor a entrada
numérica, você pode fazer o seguinte:
def foo(x):
assert isinstance(x,(float,int,complex))
return x*2

Ou, ainda melhor, usando tipos de dados abstratos,


import numbers
def foo(x):
assert isinstance(x,numbers.Number)
return x*2

Há um argumento filosófico sobre o uso de assert nesta situação porque a digitação


de pato do Python deve reconciliar isso, mas às vezes esperando pelo rastreio para
reclamar não é viável para seu aplicativo. Asserções fazem muito mais do que
verificação de tipo de entrada. Por exemplo, suponha que você tenha um problema
em que a soma de todos os itens em uma lista intermediária seja um. Você pode usar
assert no código para verificar se isso é verdadeiro e, em seguida, assert gerará
automaticamente AssertionError se não for.

1.1.15 Rastreamento de pilha com sys.settrace

Embora o depurador pdb seja ótimo para trabalhos de propósito geral, às vezes é
muito trabalhoso percorrer um programa longo e complicado para encontrar bugs.
Python tem uma função de rastreamento poderosa que torna possível relatar cada
linha de código que
64 Capítulo 1
Python executa e também filtrar essas linhas. Salve o código a seguir em um arquivo
e execute-o.
# filename: tracer_demo.py
# demo tracing in python

def foo(x=10,y=10):
return x*y

def goo(x,y=10):
y= foo(x,y)
return x*y

if __name =="__main__":

import sys
def tracer(frame,event,arg):
if event=='line': # line about to be executed
filename, lineno = frame.f_code.co_filename,
c→ frame.f_lineno
print(filename,end='\t') # filename
print(frame.f_code.co_name,end='\t')# function name
print(lineno,end='\t') # line number in
c→ filename
print(frame.f_locals,end='\t') # local variables
argnames =
c→ frame.f_code.co_varnames[:frame.f_code.co_argcount]
print(' arguments:',end='\t')
print(str.join(', ',['%s:%r' % (i,frame.f_locals[i]) for
c→ i in argnames]))
return tracer # pass function along for next time

sys.settrace(tracer)
foo(10,30)
foo(20,30)
goo(33)

A etapa principal é alimentar a função tracer em sys.settrace que executará


A função tracer nos quadros de pilha e relatará os elementos especificados. Você
também pode usar o rastreador embutido como
% python -m trace --ignore-module=sys --trace filename.py >
c→ output.txt

Isto irá despejar muito material, então você provavelmente vai querer usar os outros
sinalizadores para filtrar.

Dica de programação: Pysnooper


O código de rastreamento acima é útil e um bom lugar para começar, mas o
módulo pynsooper disponível no Pypi torna o rastreamento extremamente fácil
de implementar e monitorar e é altamente recomendado!
Capítulo 1 65
1.1.16 Depuração usando IPython

Você também pode usar IPython a partir da linha de comando como em


% ipython --pdb <filename>

Você também pode fazer isso no código-fonte se quiser usar o depurador IPython em
vez do padrão.
from IPython.core.debugger import Pdb
pdb=Pdb() # create instance
for i in range(10):
pdb.set_trace() # set breakpoint here
print (i)

Isto fornece a introspecção dinâmica IPython usual. Isso é compartimentado no


pacote ipdb do PyPI. Você também pode invocar um shell IPython embutido
fazendo:
import IPython
for i in range(10):
if i > 7:
IPython.embed() # this will stop with an embedded IPython
c→ shell
print (i)

Isso é útil ao embutir Python em uma GUI. Sua milhagem pode variar de outra forma,
mas é um bom truque na pitada!

1.1.17 Log do Python

Há um poderoso pacote de log integrado que é melhor do que colocar instruções de


impressão em todo o código, mas requer algumas configurações.
import logging, sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.debug("debug message") # no output
logging.info("info message") # output
logging.error("error") # output

Observe que os valores numéricos dos níveis são definidos como


>>> import logging
>>> logging.DEBUG
10
>>> logging.INFO
20

portanto, quando o nível de registro é definido como INFO, apenas INFO e


mensagens acima são relatadas. Você também pode formatar a saída usando
formatadores.
66 Capítulo 1
import logging, sys
logging.basicConfig(stream=sys.stdout,
level=logging.INFO,
format="%(asctime)s - %(name)s -
c→ %(levelname)s - %(message)s")

logging.debug("debug message") # no output


logging.info("info message")
logging.error("error")

Até agora, temos usado o root logger, mas você pode ter muitas camadas de log
organizado com base no nome do logger. Tente executar demo_log1.py em um
console e veja o que acontece
# top level program

import logging, sys


from demo_log2 import foo, goo

log = logging.getLogger('main') #name of logger


log.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)

filehandler = logging.FileHandler('mylog.log')
formatter = logging.Formatter("%(asctime)s - %(name)s -
c→ %(funcName)s - %(levelname)s - %(message)s") # set format

handler.setFormatter(formatter) # setup format


filehandler.setFormatter(formatter) # setup format
log.addHandler(handler) # read to go
log.addHandler(filehandler) # read to go

def main(n=5):
log.info('main called')
[(foo(i),goo(i)) for i in range(n)]

if __name == '__main__':
main()

# subordinate to demo_log1

import logging
log = logging.getLogger('main.demo_log2')

def foo(x):
log.info('x=%r'%x)
return 3*x

def goo(x):
log = logging.getLogger('main.demo_log2.goo')
log.info('x=%r'%x)
log.debug('x=%r'%x)
return 5*x**2

def hoo(x):
log = logging.getLogger('main.demo_log2.hoo')
Capítulo 1 67
log.info('x=%r'%x)
return 5*x**2

Agora, tente mudar o nome do logger main em demo_log1.py e veja o que acontece.
O registro no demo_log2.py será não registrado a menos que seja subordinado ao
registrador main. Você pode ver como incorporar isso em seu código facilita a
ativação de vários níveis de diagnóstico de código. Você também pode ter vários
manipuladores para diferentes níveis de log.
68
Capítulo 2
Programação orientada a objetos

Python é uma linguagem orientada a objetos. A programação orientada a objetos


facilita o encapsulamento de variáveis e funções e separa as várias questões do
programa. Isso melhora a confiabilidade porque a funcionalidade comum pode ser
concentrada no mesmo código. Em contraste com C++ ou Java, você não precisa
escrever classes personalizadas para interagir com os recursos integrados do Python
porque a programação orientada a objetos não é o único estilo de programação que o
Python suporta. Na verdade, eu diria que você deseja conectar os objetos e classes
integrados que o Python já fornece, em vez de escrever para as próprias classes do
zero. Nesta seção, construímos o plano de fundo para que você possa escrever suas
próprias classes personalizadas, se necessário.

2.1 Propriedades / Atributos

As variáveis encapsuladas por um objeto são chamadas de propriedades ou atributos.


Tudo em Python é um objeto. Por exemplo,
>>> f = lambda x:2*x
>>> f.x = 30 # attach to x function object
>>> f.x 30

Acabamos de pendurar um atributo no objeto de função. Podemos até referenciá-lo


de dentro da função, se quisermos.
>>> f = lambda x:2*x*f.x
>>> f.x = 30 # attach to x function object
>>> f.x
30
>>> f(3)
180
69 Capítulo 2
Por razões de segurança, esta anexação arbitrária de atributos a objetos está bloqueada
para certos objetos embutidos (cf, __slots__ ), mas você entendeu. Você pode criar
seus próprios objetos com a palavra-chave class. A seguir está o objeto
personalizado mais simples possível,
>>> class Foo:
... pass
...

Podemos instanciar nosso objeto Foo chamando-o com parênteses como uma função
a seguir,
>>> f = Foo() # need parenthesis
>>> f.x = 30 # tack on attribute
>>> f.x
30

Observe que podemos anexar nossas propriedades uma a uma como fizemos
anteriormente com o objeto de função embutido, mas podemos usar o método
__init__ para fazer isso para todas as instâncias desta classe.
>>> class Foo:
... def __init__ (self): # note the double underscores
... self.x = 30
...

A palavra-chave self refere à instância criada. Podemos instanciar este objeto da


seguinte forma,
>>> f = Foo ()
>>> f.x
30

O construtor __init__ constrói os atributos em todos os objetos criados,


>>> g = Foo ()
>>> g.x
30

Você pode fornecer argumentos para a função __init__ que são chamados na
instanciação,
>>> classe Foo:
... def __nit__ (self, x= 30):
... self.x = x
...
>>> f = Foo (99)
>>> f.x
99

Lembre-se de que a função __init__ é apenas outra função Python e segue a mesma
sintaxe. Os sublinhados duplos circundantes indicam que a função tem um status
especial de baixo nível.
Capítulo 2 70

Dica de programação: atributos privados versus públicos


Ao contrário de muitas linguagens orientadas a objetos, o Python não precisa
implementar atributos privados versus públicos como parte da linguagem. Em vez
disso, eles são gerenciados por convenção. Por exemplo, atributos que começam
com um único caractere de sublinhado são (apenas por convenção) considerados
privados, embora nada no idioma forneça a eles um status especial.

2.2 Métodos

Métodos são funções anexadas a objetos e têm acesso aos atributos internos do objeto.
Eles são definidos dentro do corpo da definição de classe,
>>> class Foo:
... def __init__ (self,x=30):
... self.x = x
... def foo(self,y=30):
... return self.x*y
...
>>> f = Foo(9)
>>> f.foo(10)
90

Observe que você pode acessar as variáveis que foram anexadas em self.x de
dentro do corpo da função de foo com as variáveis self. Uma prática comum na
codificação Python é empacotar todas as variáveis não mutáveis nos atributos no
__init__ e configurar o método de forma que as variáveis que mudam com
frequência sejam, então, variáveis de função, a serem fornecidas pelo usuário na
chamada. Além disso, self pode manter o estado entre as chamadas de método para
que o objeto possa manter um histórico interno e alterar o comportamento
correspondente dos métodos do objeto.
É importante ressaltar que os métodos sempre têm pelo menos um argumento (ou
seja, self). Por exemplo, consulte o seguinte erro,
>>> f.foo(10) # this works fine
90
>>> f.foo(10,3) # this gives an error
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() takes from 1 to 2 positional arguments but 3
were given

Parece que o método leva legitimamente um argumento para a primeira linha, mas
por que a mensagem de erro diz que esse método leva dois argumentos? A razão é
que Python vê f.foo (10) como Foo.foo (f, 10), então o primeiro argumento
é a instância f que referenciamos como self na definição do método. Portanto,
existem dois argumentos da perspectiva do Python. Isso pode ser confuso na primeira
vez que você vê.
71 Capítulo 2

Dica de programação: funções e métodos


O acesso aos atributos do objeto é a distinção entre métodos e funções. Por
exemplo, podemos criar uma função e anexá-la a um objeto existente, como a
seguir,
>>> f = Foo ()
>>> f.func = lambda i: i*2

Esta é uma função legítima e pode ser chamada como um método f.func (10)
mas essa função tem não acesso a nenhum dos atributos internos de f e deve obter
todos os seus argumentos da invocação.

Não surpreendentemente, os métodos podem chamar outros métodos no mesmo


objeto, desde que sejam referenciados com o prefixo self. Operações como o mais
(operador + ) podem ser especificadas como métodos.
>>> class Foo:
... def __init__(self,x=10):
... self.x = x
... def __add__(self,y): # overloading "addition" operator
... return self.x + y.x
...
>>> a=Foo(x=20)
>>> b=Foo()
>>> a+b 30

O módulo operators possui uma lista de operadores implementados.

Dica de programação: chamando objetos como funções


Funções são objetos em Python e você pode tornar sua classe chamável como uma
função adicionando um método __call__ à sua classe. Por exemplo,
>>> class Foo:
... def __call__(self,x):
... return x*10
...

Agora podemos fazer algo como,


>>> f = Foo ()
>>> f (10)
100

A vantagem desta técnica é que agora você pode fornecer variáveis adicionais na
função __init__ e então apenas usar o objeto como qualquer outra função.
Capítulo 2 72
2.3 Herança

A herança facilita a reutilização de código. Considere o seguinte,


>>> class Foo:
... def __init__(self, x= 10):
... self.x = x
... def compute_this(self, y= 20):
... return self.x*y
...

Agora, vamos supor que Foo funcione bem, exceto que queremos mudar a maneira
como funciona compute_this para uma nova classe. Não precisamos reescrever a
classe, podemos simplesmente herdar dela e alterar (ou seja, sobrescrever) as partes
de que não gostamos.
>>> class Goo(Foo): # inherit from Foo
... def compute_this(self,y=20):
... return self.x*y*1000
...

Agora, isso nos dá tudo em Foo, exceto para a atualizada função compute_this.
>>> g = Goo ()
>>> g.compute_this (20)
200000

A ideia é reutilizar seus próprios códigos (ou, melhor ainda, de outra pessoa) com
herança. Python também oferece suporte a herança e delegação múltiplas (por meio da
palavra-chave super).
Como exemplo, considere a herança do objeto embutido list onde queremos
implementar uma função __repr__ especial.
>>> class MyList(list): # herda do objeto de lista embutido
... def __repr__(self):
... list_string = list.__repr__(self)
... return list_string.substituir ('','')
...
>>> MyList ([1,3]) # sem espaços na saída
[1,3]
>>> lista([1,3]) # espaços na saída
[1, 3]

Dica de programação: as vantagens de uma boa __repr__


A função repr embutida aciona o método __repr__, que é como o objeto é
representado como uma string. Estritamente falando, repr deve retornar uma
string, quando avaliada com a função embutida eval(), retorna o objeto dado de
uma instância. Na prática, repr retorna uma representação de string do objeto e
é uma excelente oportunidade para adicionar notação psicologicamente vantajosa
aos seus objetos, o que torna mais fácil raciocinar sobre seus objetos no
interpretador interativo ou depurador.

(continuação)
73 Capítulo 2

Aqui está um exemplo de um objeto que representa um intervalo na linha real, que
pode ser aberta ou fechada.
>>> class I:
... def __init__(self,left,right,isopen=True):
... self.left, self.right = left, right # edges of interval
... self.isopen = isopen
... def __repr__(self):
... if self.isopen:
... return '(%d,%d)'%(self.left,self.right)
... else:
... return '[%d,%d]'%(self.left,self.right)
...
>>> a = I(1,3) # open-interval representation?
>>> a
(1,3)
>>> b = I(11,13,False) # closed interval representation?
>>> b
[11,13]

Agora é visualmente óbvio se o intervalo dado está ou não aberto ou fechado pelos
parênteses ou colchetes. Fornecer esse tipo de dica psicológica a si mesmo tornará
muito mais fácil raciocinar sobre esses objetos.

Depois de escrever suas próprias classes, você pode reproduzir o comportamento de


outros objetos Python, como iteráveis, por exemplo,
>>> class Foo:
... def __init__(self,size=10):
... self.size = size
... def __iter__(self): # produces iterable
... self.counter = list(range(self.size))
... return self # return object that has next() method
... def __next__(self): # does iteration
... if self.counter:
... return self.counter.pop()
... else:
... raise StopIteration
...
>>> f = Foo()
>>> list(f)
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> for i in Foo(3): # iterate over
... print(i)
...
2
1
0
Capítulo 2 74
2.4 Variáveis de classe

Até agora, estivemos discutindo propriedades e métodos de objetos como instâncias.


Você pode especificar variáveis vinculadas a uma classe em vez de uma instância
usando variáveis de classe.
>>> class Foo:
... class_variable = 10 # variables defined here are tied to
c→ the class not the particular instance
...

>>> f = Foo()
>>> g = Foo()
>>> f.class_variable
10
>>> g.class_variable
10
>>> f.class_variable = 20
>>> f.class_variable # change here
20
>>> g.class_variable # no change here
10
>>> Foo.class_variable # no change here
10
>>> Foo.class_variable = 100 # change this
>>> h = Foo()
>>> f.class_variable # no change here
20
>>> g.class_variable # change here even if pre-existing!
100
>>> h.class_variable # change here (naturally)
100

Isso também funciona com funções, não apenas variáveis, mas apenas com o
decorador @classmethod. Observe que a existência de variáveis de classe não as
torna conhecidas para o resto da definição de classe automaticamente. Por exemplo,
>>> class Foo:
... x = 10
... def __init__(self):
... self.fx = x**2 # x variable is not known
...

resultará no seguinte erro: NameError: global name ’x’ is not defined.


Isso pode ser corrigido fornecendo a referência de classe completa ax como a seguir,
>>> class Foo:
... x = 10
... def __init__(self):
... self.fx = Foo.x**2 # full reference to x
...

Embora, provavelmente seja melhor evitar embutir o nome da classe no código, o que
torna a herança downstream frágil.
75 Capítulo 2
2.5 Funções de classe

As funções podem ser ligados às classes também usando o decorador


classmethod.
>>> class Foo:
... @classmethod
... def class_function(cls,x=10):
... return x*10
...
>>> f = Foo()
>>> f.class_function(20)
200
>>> Foo.class_function(20) # don't need the instance
200
>>> class Foo:
... class_variable = 10
... @classmethod
... def class_function(cls,x=10):
... return x*cls.class_variable #using class_variable
...
>>> Foo.class_function(20) # don't need the instance, using the
c→
200

Isso pode ser útil se você quiser transmitir um parâmetro para todas as instâncias de
classe após a construção. Por exemplo,
>>> class Foo:
... x=10
... @classmethod
... def foo(cls):
... return cls.x**2
...
>>> f = Foo()
>>> f.foo()
100
>>> g = Foo()
>>> g.foo()
100
>>> Foo.x = 100 # change class variable
>>> f.foo() # now the instances pickup the change
10000
>>> g.foo() # now the instances pickup the change
10000

Isso pode ser difícil de controlar porque a própria classe contém a variável de classe.
Por exemplo:
>>> class Foo:
... class_list = []
... @classmethod
... def append_one(cls):
... cls.class_list.append(1)
...
>>> f = Foo()
>>> f.class_list
Capítulo 2 76
[]
>>> f.append_one()
>>> f.append_one()
>>> f.append_one()
>>> g = Foo()
>>> g.class_list
[1, 1, 1]

Observe como o novo objeto g obteve as mudanças na variável de classe que fizemos
pela instância f. Agora, se fizermos o seguinte:
>>> del f, g
>>> Foo.class_list
[1, 1, 1]

Observe que a variável de classe está anexada à definição de classe, portanto, a


exclusão das instâncias de classe não a afetou. Certifique-se de que este é o
comportamento que você espera ao configurar variáveis de classe desta forma!
Variáveis de classe e métodos não são avaliados preguiçosamente. Isso ficará
aparente quando chegarmos a dataclasses. Por exemplo,
>>> class Foo:
... print('I am here!')
...
I am here!

Observe que não precisamos instanciar uma instância dessa classe para executar a
instrução print. Isso tem sutilezas importantes para o design orientado a objetos.
Às vezes, os parâmetros específicos da plataforma são inseridos como variáveis de
classe para que sejam configurados no momento em que qualquer instância da classe
for instanciada. Por exemplo,
>>> class Foo:
... _setup_const = 10 # get platform-specific info
... def some_function(self,x=_setup_const):
... return 2*x
...
>>> f = Foo()
>>> f.some_function()
20

2.6 métodos estáticos

Ao contrário de métodos de classe, um staticmethod está ligado à definição de


classe mas não precisam de ter acesso às variáveis internas. Considere o seguinte:
>>> class Foo:
... @staticmethod
... def mystatic(x,y):
... return x*y
...
77 Capítulo 2
O staticmethod não tem acesso à interna auto ou cls que uma instância de
método regular ou classmethod tem, respectivamente. É apenas uma forma de
anexar uma função a uma classe. Assim,
>>> f = Foo ()
>>> f.mystatic (mistos1,3)
3

Às vezes você os encontrará em objetos, que são objetos projetados para não tocar
em nenhuma das variáveis internas self ou cls.

2.7 O hash oculta as variáveis pai dos filhos

Por convenção, os métodos e atributos que começam com um único caractere de


sublinhado são considerados privados, mas aqueles que começam com um
sublinhado duplo são misturados internamente com o nome da classe,
>>> class Foo:
... def __init__(self):
... self._ _ x=10
... def count(self):
... return self.__x*30
...

Observe que a função count utiliza a variável sublinhado duplo self.__x.


>>> class Goo(Foo): # child with own . x attribute
... def __init__(self,x):
... self.__x=x
...
>>> g=Goo(11)
>>> g.count() # won't work
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in count
AttributeError: 'Goo' object has no attribute '_Foo_ _ x'

Isso significa que a variável x na declaração Foo está anexada à classe Foo. Isso
evita que uma subclasse potencial use a função Foo.count() com a variável de
uma subclasse (digamos, self._x, sem o sublinhado duplo).

2.8 Delegando funções

A seguir, tente pensar de onde a função abs se origina na cadeia de herança abaixo.
>>> class Foo:
... x = 10
Capítulo 2 78
... def abs(self):
... return abs(self.x)
...
>>> class Goo(Foo):
... def abs(self):
... return abs(self.x)*2
...
>>> classe Moo(Goo):
... pass
...
>>> m = Moo ()
>>> m.abs ()
20

Quando o Python vê m.abs(), ele primeiro verifica se a classe Moo implementa a


função abs(). Então, como isso não acontece, ele lê da esquerda para a direita na
herança e encontra Goo. Porque Goo faz implementar a função abs() que utiliza
um presente, mas requer self.x que Goo recebe de seu pai Foo, a fim de concluir
o cálculo. A função abs() no Goo depende da função Python integrada abs().

2.9 Usando super para Delegação

O método do Python super é uma maneira de executar funções junto com a Ordem
de Resolução de Método (MRO) da classe. Um nome melhor para super seria
próximo método em MRO. A seguir, ambas as classes A e B herdam de Base,
>>> class Base:
... def abs(self):
... print('in Base')
... return 1
...
>>> class A(Base):
... def abs(self):
... print('in A.abs')
... oldval=super(A,self).abs()
... return abs(self.x)+oldval
...
>>> class B(Base):
... def abs(self):
... print('in B.abs')
... oldval=super(B,self).abs()
... return oldval*2
...

Com tudo isso configurado, vamos criar uma nova classe que herda de A e B com a
árvore de herança na Fig. 2.1,
>>> class C(A,B):
... x=10
... def abs(self):
79 Capítulo 2
... return super(C,self).abs()
...
>>> c=C() # create an instance of class C
>>> c.abs()
in A.abs
in B.abs
in Base
1

O que aconteceu? Conforme mostrado na Fig. 2.1, a resolução do método procura


por uma função abs na classe C, encontra-o e, em seguida, passa para o próximo
método na ordem da herança (classe A). Ele encontra uma função abs e a executa e
então passa para a próxima classe no MRO (classe B) e então também encontra uma
função abs lá e subsequentemente a executa. Assim, super basicamente encadeia
as funções em série.
Agora vamos ver o que acontece quando nós mudamos a ordem de resolução
método, iniciando na classe A como no seguinte:
>>> class C(A,B):
... x=10
... def abs(self):
... return super(A,self).abs()
...
>>> c=C() # create an instance of class C
>>> c.abs()
in B.abs
in Base 2

Fig. 2.1 Ordem de resolução do método

Fig. 2.2 Ordem de resolução do método


Capítulo 2 80
Observe que ele pega a resolução do método após a classe A (ver Fig. 2.2). Podemos
alterar a ordem de herança e ver como isso afeta a ordem de resolução de super.
>>> class C(B,A): # change MRO
... x=10
... def abs(self):
... return super(B,self).abs()
...
>>> c=C()
>>> c.abs()
in A.abs
in Base
11

>>> class C(B,A): # same MRO, different super


... x=10
... def abs(self):
... return super(C,self).abs()
...
>>> c=C()
>>> c.abs()
in B.abs
in A.abs
in Base
22

Para resumir, super permite que você misture e combine objetos para chegar a
diferentes usos com base em como a resolução do método é resolvida para métodos
específicos. Isso adiciona outro grau de liberdade, mas também outra camada de
complexidade ao seu código.

2.10 Metaprogramação: Monkey Patching

Monkey Patching significa adicionar métodos a classes ou instâncias fora da


definição formal de classe. Isso torna muito difícil depurar, porque você não sabe
exatamente onde as funções foram sequestradas. Embora altamente desaconselhável,
você também pode métodos de classe de patch:
>>> import types
>>> class Foo:
... class_variable = 10
...
>>> def my_class_function(cls,x=10):
... return x*10
...
>>> Foo.my_class_function = types.MethodType(my_class_function,
c→ Foo)
>>> Foo.my_class_function(10)
100
>>> f = Foo()
>>> f.my_class_function(100)
1000
81 Capítulo 2
Isso pode ser útil para depuração de última hora
>>> import types
>>> class Foo:
... @classmethod
... def class_function(cls,x=10):
... return x*10
...
>>> def reported_class_function():
... # hide original function in a closure
... orig_function = Foo.class_function
... # define replacement function
... def new_class_function(cls,x=10):
... print('x=%r' % x)
... return orig_function(x)# return using original function
... return new_class_function # return a FUNCTION!
...
>>> Foo.class_function(10) # original method
100
>>> Foo.class_function = types.MethodType(reported_class_
function(), Foo)
>>> Foo.class_function(10) # new verbose method
x=10
100

Dica de programação: mantenha a simplicidade


É fácil se fantasiar com as classes Python, mas é melhor usar classes e
programação orientada a objetos, na qual simplificam e unificam conceitualmente
os códigos, evitam a redundância de código e mantêm a simplicidade
organizacional. Para ver uma aplicação perfeita de projeto orientado a objetos
Python, estude o módulo de gráfico Networkx [2].

2.11 Classes de base abstratas

O módulo collections contêm classes de base abstratas que atendem a duas


funções principais. Primeiro, eles fornecem uma maneira de verificar se um
determinado objeto personalizado tem uma interface desejada usando isinstance
ou issubclass. Em segundo lugar, eles fornecem um conjunto mínimo de
requisitos para novos objetos a fim de satisfazer padrões de software específicos.
Vamos considerar uma função como g = lambda x: x ** 2. Suponha que queremos
testar se g é um callable. Uma maneira de verificar isso é usar a função que pode ser
callable como em
>>> g = lambda x:x**2
>>> callable(g)
True
Alternativamente, usando classes abstratas de base, podemos fazer o mesmo teste
como o seguinte:
Capítulo 2 82
>>> from Collections.abc import Callable
>>> isinstance(g, Callable)
True

As classes abstratas de base estendem esse recurso às interfaces fornecidas, conforme


descrito na documentação principal do Python. Por exemplo, para verificar se um
objeto personalizado é iterável ou não, podemos fazer o seguinte:
>>> from Collections.abc import Iterable
>>> isinstance(g, Iterable)
False

Além desse tipo de verificação de interface, as classes abstratas de base também


permitem que os projetistas de objetos especifiquem um conjunto mínimo de métodos
e obtenham o restante dos métodos que caracterizam uma determinada classe de base
abstrata. Por exemplo, se quisermos escrever um objeto semelhante a um dicionário,
podemos herdar da Classe Base Abstrata MutableMapping e, em seguida, escreva
os métodos __getitem__, __setitem__ , __delitem__ , __Iter__, __len__. Então,
obtemos os outros métodos MutableMapping como clear(), update(), etc.
gratuitamente como parte da herança.
Programação ABCMeta O maquinário que implementa as metaclasses acima
também está disponível diretamente por meio do módulo abc. As classes abstratas
de base também podem impor métodos de subclasse usando o decorador
abc.abstractmethod. Por exemplo,
>>> import abc
>>> class Dog(metaclasse=abc.ABCMeta):
... @ abc.abstractmethod
... def bark(self):
... pass
...

Isso significa que todas as subclasses de Dog tem que implementar um método bark
ou TypeError será lançado. O decorador marca o método como abstrato.
>>> class Pug(Dog):
... pass
...
>>> p = Pug() # throws a TypeError
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Pug with abstract
c→ methods bark
Portanto, você deve implementar o método bark na subclasse,
>>> class Pug(Dog):
... def bark(self):
... print('Yap!')
...
>>> p = Pug()

Então,
>>> p.bark()
Yap!
83 Capítulo 2
Além de criar subclasses da classe base, você também pode usar o método
register criar uma subclasse para pegar outra classe e torná-la uma subclasse,
assumindo que ela implemente os métodos abstratos desejados, como a seguir:
>>> class Bulldog:
... def bark(self):
... print('Bulldog!')
...
>>> Dog.register(Bulldog)
<class ' __onsole__ .Bulldog'>

Então, embora Bulldog não seja escrito como uma subclasse de Dog, ele ainda
atuará dessa forma:
>>> issubclass(Bulldog, Dog)
True
>>> isinstance(Bulldog(), Dog)
True

Observe que sem o método bark, não obteriamos um erro se tentássemos instanciar
a classe Bulldog.
Mesmo que a implementação concreta do método abstrato seja de
responsabilidade do escritor da subclasse, você ainda pode usar o super para
executar a definição principal na classe pai. Por exemplo,
>>> class Dog(metaclass=abc.ABCMeta):
... @abc.abstractmethod
... def bark(self):
... print('Dog bark!')
...
>>> class Pug(Dog):
... def bark(self):
... print('Yap!')
... super(Pug,self).bark()
...

Então,
>>> p= Pug()
>>> p.bark()
Yap!
Dog bark!

2.12 Descritores

Os descritores expõem as abstrações internas dentro da tecnologia de criação de


objetos Python para uso mais geral. A maneira mais fácil de começar com os
descritores é ocultar a validação de entrada em um objeto do usuário. Por exemplo:
>>> class Foo:
... def __init__(self, x):
... self.x = x
...
Capítulo 2 84
Este objeto possui um atributo simples denominado x. Não há nada que impeça o
usuário de fazer o seguinte:
>>> f = Foo(10)
>>> f.x = 999
>>> f.x = 'some string'
>>> f.x = [1,3,'some list']

Em outras palavras, nada impede que o atributo x seja atribuído a todos esses tipos
diferentes. Isso pode não ser o que você deseja fazer. Se você quiser garantir que esse
atributo seja atribuído apenas a um número inteiro, por exemplo, você precisa de uma
maneira de impor isso. É aí que entram os descritores. Veja como:
>>> class Foo:
... def init (self,x):
... assert isinstance(x,int) # enforce here
... self._x = x
... @property
... def x(self):
... return self._x
... @x.setter
... def x(self,value):
... assert isinstance(value,int) # enforce here
... self._x = value
...

A parte interessante acontece com o decorator property. É aqui que mudamos


explicitamente como o Python lida com o atributo x. A partir de agora, quando
tentarmos definir o valor deste atributo em vez de ir diretamente para o Foo.dict ,
que é o dicionário de objetos que contém todos os atributos do objeto Foo, a função
x.setter será chamada em seu lugar e a atribuição será tratada lá. Analogamente,
isso também funciona para recuperar o valor do atributo, conforme mostrado abaixo
com o decorador x.getter:
>>> class Foo:
... def init (self,x):
... assert isinstance(x,int) # enforce here
... self._x = x
... @property
... def x(self):
... return self._x
... @x.setter
... def x(self,value):
... assert isinstance(value,int) # enforce here
... self._x = value
... @x.getter
... def x(self):
... print('using getter!')
... return self._x
...

A vantagem desta técnica @getter é que agora podemos retornar um valor


calculado toda vez que o atributo é acessado. Por exemplo, poderíamos fazer algo
como o seguinte:
85 Capítulo 2
@ x.getter
def x(self):
return self._x * 30

Portanto, agora, sem o conhecimento do usuário, o atributo é calculado


dinamicamente. Agora, aí vem a piada: todo o mecanismo do descritor pode ser
abstraído da definição da classe. Isso é realmente útil quando o mesmo conjunto de
descritores precisa ser reutilizado várias vezes na mesma definição de classe e
reescrevê-los um por um para cada atributo seria uma tarefa tediosa e sujeita a erros.
Por exemplo, suponha que temos a seguinte classe para FloatDescriptor e outra
classe que descreve um Car.
>>> class FloatDescriptor:
... def init (self):
... self.data = dict()
... def get (self, instance, owner):
... return self.data[instance]
... def set (self, instance, value):
... assert isinstance(value,float)
... self.data[instance] = value
...
>>> class Car:
... speed = FloatDescriptor()
... weight = FloatDescriptor()
... def init (self,speed,weight):
... self.speed = speed
... self.weight = weight
...

Observe que FloatDescriptor aparece como variáveis de classe na definição de classe


de Car. Isso terá ramificações importantes mais tarde, mas por enquanto vamos ver
como isso funciona.
>>> f = Car(1,2) # raises AssertionError
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in __init__
File "<stdin>", line 7, in __set__
AssertionError

Isso ocorre porque o descritor exige parâmetros flutuantes.


>>> f = Car(1.0,2.3) # no AssertionError because FloatDescriptor
c→ is satisfied
>>> f.speed = 10 # raises AssertionError
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __set
AssertionError
>>> f.speed = 10.0 # no AssertionError

Agora que abstraímos a gestão e validação dos atributos da classe Car usando
FloatDescriptor e podemos reutilizar FloatDescriptor em outras classes.
No entanto, há uma grande advertência aqui porque tivemos que usar
FloatDescriptor no nível de classe para obter os descritores conectados
corretamente.
Capítulo 2 86
Isso significa que temos que garantir que a atribuição dos atributos da instância seja
colocada na instância correta. É por isso que self.data é um dicionário no
construtor FloatDescriptor. Estamos usando a própria instância como a chave
para este dicionário a fim de garantir que os atributos sejam colocados na instância
correta, como a seguir: self.data[instance] = value. Isso pode falhar para
classes que não são hashble e que, portanto, não podem ser usadas como chaves de
dicionário. A razão de __get__ ter um argumento owner é que esses problemas
podem ser resolvidos usando metaclasses, mas isso está muito fora de nosso escopo.
Resumindo, os descritores são o mecanismo de baixo nível que as classes Python
usam internamente para gerenciar métodos e atributos. Eles também fornecem uma
maneira de abstrair o gerenciamento de atributos de classe em classes descritoras
separadas que podem ser compartilhadas entre as classes. Os descritores podem ser
complicados para classes sem hash e há outros problemas em estender esse padrão
além do que discutimos aqui. O livro Python Essential Reference [1] é uma excelente
referência para Python avançado.

2.13 Tuplas nomeadas e classes de dados

As tuplas nomeadas permitem um acesso mais fácil e legível às tuplas. Por exemplo,
>>> from collections import namedtuple
>>> Produce = namedtuple('Produce','color shape weight')

Acabamos de criar uma nova classe chamada Produce que tem os atributos color,
shape e weight. Observe que você não pode ter palavras-chave Python ou nomes
duplicados na especificação de atributos. Para usar esta nova classe, apenas
instanciamos como qualquer outra classe,
>>> mango = Produce(color='g',shape='oval',weight=1)
>>> print (mango)
Produce(color='g', shape='oval', weight=1)

Observe que podemos obter os elementos da tupla usando a indexação usual,


>>> mango[0]
'g'
>>> mango[1]
'oval'
>>> mango[2]
1

Podemos obter o mesmo usando os atributos nomeados,


>>> mango.color
'g'
>>> mango.shape
'oval'
>>> mango.weight
1

A descompactação de tupla funciona como tupla regular,


87 Capítulo 2
>>> i,j,k = mango
>>> i
'g'
>>> j
'oval'
>>> k 1

Você também pode obter os nomes dos atributos,


>>> mango._fields
('color', 'shape', 'weight')

Podemos criar novos objetos nomeados com duplas substituindo os valores dos
atributos existentes pelo método _replace, como a seguir,
>>> mango._replace(color='r')
Produce(color='r', shape='oval', weight=1)

Sob o capô, namedtuple automaticamente gera código para implementar a classe


correspondente (Produce neste caso). Esta ideia de geração automática de código
para implementar classes específicas é estendida com dataclasses no Python
+
3.7 .
+
Classes de dados No Python 3.7 , as dataclasses estendem a ideia de geração
de código além do namedtuple para objetos semelhantes a dados mais genéricos.
>>> from dataclasses import dataclass
>>> @dataclass
... class Produce:
... color: str
... shape: str
... weight: float
...
>>> p = Produce('apple','round',2.3)
>>> p
Produce(color='apple', shape='round', weight=2.3)

Não se iluda pensando que os tipos dados são aplicados,


>>> p = Produto (1,2,3)
>>> p
Produce (color = 1, shape = 2, weight = 3)

Obtemos muitos métodos adicionais gratuitamente usando o decorador dataclass,


>>> dir(Produce)
['__annotations__', '__class__', '__dataclass_fields__',
'__dataclass_params__', '__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__']

O __hash__ () e __eq __ () são particularmente úteis para permitir que esses objetos
sejam usados como chaves em um dicionário, mas você tem que usar o argumento de
palavra-chave frozen = True como mostrado,
Capítulo 2 88
>>> @dataclass(frozen=True)
... class Produce:
... color: str
... shape: str
... weight: float
...
>>> p = Produce('apple','round',2.3)
>>> d = {p: 10} # instance as key
>>> d
{Produce(color='apple', shape='round', weight=2.3): 10}

Você também pode usar order = True se quiser que a classe seja ordenada com
base na tupla de entradas. Os valores padrão podem ser atribuídos como a seguir,
>>> @dataclass
... class Produce:
... color: str = 'red'
... shape: str = 'round'
... weight: float = 1.0
. ..

Ao contrário namedtuple, você pode ter métodos personalizados,


>>> @dataclass
... class Produce:
... color : str
... shape : str
... weight : float
... def price(self):
... return 0 if self.color=='green' else self.weight*10
...

Ao contrário de namedtuple, dataclass não é iterável. Existem funções


auxiliares que podem ser usadas. A função field permite que você especifique
como certos atributos declarados são criados por padrão. O exemplo abaixo usa uma
fábrica list para evitar que todas as instâncias da classe compartilhem a mesma
list que uma variável de classe.
>>> from dataclasses import field
>>> @dataclass
... class Produce:
... color : str = 'green'
... shape : str = 'flat'
... weight : float = 1.0
... track : list = field(default_factory=list)
...

Assim, duas instâncias diferentes de Produce têm mutáveis diferentes listas de


track. Isso evita o problema de usar um objeto mutável no inicializador. Outros
argumentos para dataclass permitem definir automaticamente uma ordem em
seus objetos ou torná-los imutáveis, como a seguir.
>>> @dataclass(order=True,frozen=True)
... class Coor:
... x: float = 0
... y: float = 0
89 Capítulo 2
...
>>> c = Coor(1,2)
>>> d = Coor(2,3)
>>> c < d
True
>>> c.x = 10
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'x'

A função asdict converte facilmente suas classes de dados em dicionários Python


regulares, o que é útil para serialização. Observe que isso apenas converterá os
atributos da instância.
>>> from dataclasses import asdict
>>> asdict (c)
{'x': 1, 'y': 2}

Se você tem variáveis que dependem de outras variáveis inicializadas, mas não deseja
criá-las automaticamente com cada nova instância, você pode usar a função field,
como a seguir,
>>> @dataclass
... class Coor:
... x : float = 0
... y : float = field(init=False)
...
>>> c = Coor(1) # y is not specified on init
>>> c.y = 2*c.x # added later
>>> c
Coor(x=1, y=2)

Isso é um pouco complicado e é a razão para o método __post_init__. Lembre-se


de que o método __init__ é gerado automaticamente pelo dataclass.
>>> @dataclass
... class Coor:
... x : float = 0
... y : float = field(init=False)
... def __post_init__(self):
... self.y = 2*self.x
...
>>> c = Coor(1) # y is not specified on init
>>> c
Coor(x=1, y=2)

Resumindo, as classes de dados são novas e ainda não se sabe como elas se
encaixarão em fluxos de trabalho comuns. Essas classes de dados são inspiradas no
módulo de terceiros attrs portanto, leia esse módulo para entender se os casos de
uso se aplicam aos seus problemas.
Capítulo 2 90
2.14 Funções genéricas

Funções genéricas são aquelas que mudam suas implementações com base nos tipos
de entradas. Por exemplo, você pode realizar a mesma coisa usando a seguinte
instrução condicional no início de uma função, conforme mostrado a seguir,
>>> def foo(x):
... if isinstance(x,int):
... return 2*x
... elif isinstance(x,list):
... return [i*2 for i in x]
... else:
... raise NotImplementedError
...

Com o seguinte uso,


>>> foo (1)
2
>>> foo ([1,2])
[2, 4]

Nesse caso, você pode pensar em foo como uma genérica função. Para colocar mais
confiabilidade por trás desse padrão, desde o Python 3.3, temos
functools.singledispatch. Para começar, precisamos definir a função de
nível superior que modelará as implementações individuais com base no tipo do
primeiro argumento.
>>> from functools import singledispatch
>>> @singledispatch
... def foo(x):
... print('I am done with type(x): %s'%(str(type(x))))
...

Com a saída correspondente,


>>> foo(1)
I am done with type(x): <class 'int'>

Para fazer o despacho funcionar, temos que register as novas implementações


com foo usando o tipo do entrada como o argumento para o decorador. Podemos
nomear a função _ porque não precisamos de um nome separado para ela.
>>> @foo.register(int)
... def _(x):
... return 2*x
...

Agora, vamos tentar a saída novamente e notar que a nova versão int da função foi
executada .
>>> foo (1)
2

Podemos adicionar mais implementações baseadas em tipo usando register


novamente com diferentes argumentos de tipo,
91 Capítulo 2
>>> @ foo.register(float)
... def _(x):
... return 3*x
...
>>> @ foo.register(list)
... def _(x):
... return [3*i for i em x]
...

Com a saída correspondente,


>>> foo (1.3)
3.9000000000000004
>>> foo ([1,2,3])
[3, 6, 9]

As funções existentes podem ser anexadas usando a forma funcional do decorador,


>>> def existing_function(x):
... print('I am the existing_function with %s'%(str(type(x))))
...
>>> foo.register(dict,existing_function)
<function existing_function at 0x7f9398354a60>

Com a saída correspondente,


>>> foo({1:0,2:3})
I am the existing_function with <class 'dict'>

Você pode ver os despachos implementados usando foo.registry.keys(),


como a seguir,
>>> foo.registry.keys()
dict_keys([<class 'object'>, <class 'int'>, <class 'float'>,
<class 'list'>, <class 'dict'>])

Você pode escolher as funções individuais por acessando o despacho, como a seguir,
>>> foo.dispatch(int)
<function _ at 0x7f939a66b5e0>

Esses decoradores register também podem ser empilhados e usados com classes
abstratas de base.

Dica de programação: usando slots para reduzir a memória


A seguinte definição de classe permite a adição arbitrária de atributos.
>>> class Foo:
... def __init__(self,x):
... self.x=x
...

(continuação)
Capítulo 2 92

>>> f = Foo(10)
>>> f.y = 20
>>> f.z = ['some stuff', 10,10]
>>> f.__dict__
{'x': 10, 'y': 20, 'z': ['some stuff', 10, 10]}

Isso ocorre porque há um dicionário dentro de Foo, que cria sobrecarga de


memória, especialmente para muitos desses objetos (consulte discussão anterior
sobre coleta de lixo). Esta sobrecarga pode ser removida adicionando __slots__
como a seguir,
>>> class Foo:
... __slots__ = ['x']
... def __init__(self,x):
... self.x=x
...
>>> f = Foo(10)
>>> f.y = 20
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'y'

Isso gera AttributeError porque __slots__ impede o Python de criar um


dicionário interno para cada instância de Foo.

2.15 Padrões de design

Os padrões de design não são tão populares em Python quanto em oposição a Java ou
C ++ porque Python tem uma biblioteca padrão ampla e útil. Os padrões de projeto
representam soluções canônicas para problemas comuns. A terminologia deriva da
arquitetura. Por exemplo, suponha que você tenha uma casa e seu problema seja como
entrar nela carregando uma sacola de mantimentos. A solução para este problema é
o padrão da porta, mas este não especifica a forma ou o tamanho da porta, sua cor,
ou se tem ou não uma fechadura, etc. Estes são conhecidos como detalhes de
implementação. A ideia principal é que existem soluções canônicas para problemas
comuns.

2.15.1 Template

O Template Method é um padrão de design comportamental que define o esqueleto


de um algoritmo na classe base, mas permite que as subclasses substituam etapas
específicas do algoritmo sem alterar sua estrutura. A motivação é dividir um
algoritmo em uma série de etapas em que cada etapa possui uma implementação
abstrata.
93 Capítulo 2
Especificamente, isso significa que os detalhes de implementação do algoritmo são
deixados para a subclasse, enquanto a classe base orquestra as etapas individuais do
algoritmo.
>>> from abc import ABC, abstractmethod
>>> # ensures it must be subclassed not directly instantiated
>>> class Algorithm(ABC):
... # base class method
... def compute(self):
... self.step1()
... self.step2()
... # subclasses must implement these abstractmethods
... @abstractmethod
... def step1(self):
... 'step 1 implementation details in subclass'
... pass
... @abstractmethod
... def step2(self):
... 'step 2 implementation details in subclass'
... pass
...

Python lança um TypeError se você tentar instanciar este objeto diretamente. Para
usar a classe, temos que subclassificá-la como no seguinte,
>>> class ConcreteAlgorithm(Algorithm):
... def step1(self):
... print('in step 1')
... def step2(self):
... print('in step 2')
...
>>> c = ConcreteAlgorithm()
>>> c.compute() # compute is defined in base class
in step 1
in step 2

A vantagem do padrão de modelo é que ele deixa claro que a classe base coordena os
detalhes que são implementados pelas subclasses. Isso separa as preocupações de
forma clara e torna flexível a implantação do mesmo algoritmo em diferentes
situações.

2.15.2 Singleton

Singleton é um padrão de design criativo para garantir que uma classe tenha apenas
uma instância. Por exemplo, pode haver muitas impressoras, mas apenas um spooler
de impressora. O código a seguir oculta a instância singular na variável de classe e
usa o método __new__ para personalizar a criação do objeto antes que __init__ seja
chamado.
>>> class Singleton:
... # class variable contains singular _instance
... # __new__ method returns object of specified class and is
... # called before __init__
Capítulo 2 94
... def __new__(cls, *args, **kwds):
... if not hasattr(cls, '_instance'):
... cls._instance = super().__new__(cls, *args, **kwds)
... return cls._instance
...
>>> s = Singleton()
>>> t = Singleton()
>>> t is s # there can be only one!
True

Observe que há muitas maneiras de implementar esse padrão em Python, mas esta é
uma das mais simples.

2.15.3 Observer

Observer é um padrão de design comportamental que define a comunicação entre


objetos de forma que quando um objeto (ou seja, editor) muda de estado, todos os seus
assinantes são notificados e atualizados automaticamente. O módulo traitlets
implementam esse padrão de design.
>>> from traitlets import HasTraits, Unicode, Int
>>> class Item(HasTraits):
... count = Int() # publisher for integer
... name = Unicode() # publisher for unicode string
...
>>> def func(change):
... print('old value of count = ',change.old)
... print('new value of count = ',change.new)
...
>>> a = Item()
>>> # func subscribes to changes in `count`
>>> a.observe(func, names=['count'])
>>> a.count = 1
old value of count = 0
new value of count = 1
>>> a.name = 'abc' # prints nothing but not watching name

Com toda essa configuração, podemos ter vários assinantes para atributos publicados
>>> def another_func(change):
... print('another_func is subscribed')
... print('old value of count = ',change.old)
... print('new value of count = ',change.new)
...
>>> a.observe(another_func, names=['count'])
>>> a.count = 2
old value of count = 1
new value of count = 2
another_func is subscribed
old value of count = 1
new value of count = 2
95 Capítulo 2
Além disso, o módulo traitlets faz a verificação de tipo dos atributos do objeto
que gerará uma exceção se o tipo errado for definido para o atributo, implementando
o padrão descriptor. O módulo traitlets é fundamental para os recursos
interativos baseados na web do ecossistema Jupyter ipywidgets.

2.15.4 Adaptador

Os padrões do adaptador facilitam a reutilização do código existente, personificando


as interfaces relevantes usando classes. Isso permite que as classes interoperem que,
de outra forma, não poderiam devido a incompatibilidades em suas interfaces.
Considere a seguinte classe que pega uma lista,
>>> class EvenFilter:
... def __init__(self,seq):
... self._seq = seq
... def report(self):
... return [i for i in self._seq if i%2==0]
...
Isso retorna apenas os termos pares como mostrado abaixo,
>>> EvenFilter([1,3,4,5,8]).report()
[4, 8]
Mas agora queremos usar a mesma classe onde a entrada seq agora é um gerador em
vez de uma lista. Podemos fazer isso com a seguinte classe GeneratorAdapter,
>>> class GeneratorAdapter:
... def __init__(self,gen):
... self._seq = list(gen)
... def __iter__(self):
... return iter(self._seq)
...
Agora podemos voltar e usar isso com o EvenFilter seguinte maneira:
>>> g = (i for i in range(10)) # create generator with
c→ comprehension
>>> EvenFilter(GeneratorAdapter(g)).report()
[0, 2, 4, 6, 8]
A ideia principal para o padrão do adaptador é isolar as interfaces relevantes e
personificá-las com a classe do adaptador.

Referências

1. DM Beazley, Python Essential Reference (Addison-Wesley, Boston, 2009)


2. AA Hagberg, DA Schult, PJ Swart, Exploring network structure, dynamics, and function using
NetworkX, in Proceedings of the 7th Python in Science Conference (SciPy2008), Pasadena , CA,
agosto de 2008, pp. 11-15
96
Capítulo 3
Usando módulos

Os módulos permitem a reutilização de código e a portabilidade. Em geral, é melhor


usar um código amplamente usado e amplamente testado do que escrever seu próprio
código do zero. A declaração import é como os módulos são carregados no
namespace atual. Nos bastidores, a importação é um processo complexo. Considere
a seguinte instrução de importação:
import some_module

Para importar este módulo, o Python pesquisará por um módulo Python válido na
ordem das entradas na sys.path do diretório lista. Os itens no PYTHONPATH
variáveis de ambiente são adicionadas a este caminho de pesquisa. A forma como o
Python foi compilado afeta o processo de importação. De modo geral, o Python
pesquisa módulos no sistema de arquivos, mas certos módulos podem ser compilados
diretamente no Python, o que significa que ele sabe onde carregá-los sem pesquisar
no sistema de arquivos. Isso pode ter consequências massivas no desempenho ao
iniciar milhares de processos Python em um sistema de arquivos compartilhado,
porque o sistema de arquivos pode causar atrasos significativos na inicialização, pois
é prejudicado durante a pesquisa.

3.1 Biblioteca padrão

Python é uma linguagem com baterias incluídas, o que significa que muitos módulos
excelentes já estão incluídos na linguagem base. Devido ao seu legado como
linguagem de programação web, a maioria das bibliotecas padrão lidam com
protocolos de rede e outros tópicos importantes para o desenvolvimento web. Os
módulos da biblioteca padrão estão documentados no site principal do Python.
Vejamos o módulo integrado math,
>>> import math # Importando módulo matemático
>>> dir(math) # Fornece uma lista de atributos do módulo
['__doc__', '__file__', '__loader__', '__name__', '__package__',
'__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2',
'atanh', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees',
97 Capítulo 3
'dist', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial',
'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf',
'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'ldexp',
'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'perm',
'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh',
'sqrt', 'tan', 'tanh', 'tau', 'trunc']
>>> help(math.sqrt)
Help on built-in function sqrt in module math:

sqrt(x, /)
Return the square root of x.

>>> radius = 14.2


>>> area = math.pi*(radius**2)
>>> area # Using a module variable
633.4707426698459
>>> a = 14.5; b = 12.7
>>> c = math.sqrt(a**2+b**2) # Using a module function
>>> c
19.275372888740698

Depois que um módulo é importado, ele é incluído no dicionário sys.modules. A


primeira etapa da import é verificar este dicionário para o módulo desejado e não
importá-lo novamente. Isso significa que fazer import somemodule várias vezes
em seu interpretador não está recarregando o módulo. Isso ocorre porque a primeira
etapa no protocolo de resolução import é verificar se o módulo desejado já está no
dicionário sys.modules e, em seguida, não importá-lo se já estiver lá.
Às vezes, você só precisa de uma função específica de um determinado módulo. A
sintaxe from <module> import <name> trata desse caso,
>>> # will overwrite existing definitions in the namespace!
>>> from math import sqrt
>>> sqrt(a) # shorter to use
3.8078865529319543

Observe que você deve usar a função importlib.reload para reimportar


módulos para a área de trabalho. Mais importante, importlib.reload que não
funcionará com a sintaxe from usada acima. Portanto, se você estiver desenvolvendo
código e recarregando-o constantemente, é melhor manter o nome do módulo de nível
superior para que possa continuar recarregando-o com importlib.reload.
Existem muitos lugares para sequestrar o processo de importação. Eles
possibilitam a criação de ambientes virtuais e a personalização da execução, mas é
melhor ficar com as soluções bem desenvolvidas para esses casos (veja abaixo o
conda).

3.2 Escrevendo e usando seus próprios módulos

Além de usar a biblioteca padrão surpreendentemente excelente, você pode querer


compartilhar seus códigos com seus colegas. A maneira mais fácil de fazer isso é
colocar seu código em um arquivo e enviá-lo para eles.
Capítulo 3 98
Ao desenvolver interativamente o código a ser distribuído dessa forma, você deve
entender como o seu módulo é atualizado (ou não) no interpretador ativo. Por
exemplo, coloque o seguinte código em um arquivo separado denominado
mystuff.py:
def sqrt(x):
return x*x

e, em seguida, retorne à sessão interativa


>>> from mystuff import sqrt
>>> sqrt(3)
9
>>> import mystuff
>>> dir(mystuff)
[' builtins ', ' cached ', ' doc ',
C→ ' file ',' loader ',' name ', ' package ',
C→ ' spec ', 'sqrt']
>>> mystuff.sqrt(3)
9

Agora, adicione a seguinte função ao seu arquivo mystuff.py:


def poly_func(x):
return 1+x+x*x

E mude sua função anterior no arquivo:


def sqrt(x):
return x/ 2

e, em seguida, retorne a sessão interativa


mystuff da.sqrt (3)

Você obteve o que esperava?


mystuff.poly_func (3)

Você tem que importlib.reload para obter novas alterações em seu arquivo
para o interpretador. Um diretório chamado __ pycache__ aparecerá
automaticamente no mesmo diretório que mystuff.py. É aqui que o Python
armazena os códigos de byte compilados para o módulo, de forma que o Python não
precise recompilá-lo do zero toda vez que você importar mystuff.py. Este
diretório será atualizado automaticamente sempre que você fizer alterações em
mystuff.py. É importante nunca incluir o diretório __pycache__ em seu
repositório Git porque quando outros clonam seu repositório, se o sistema de arquivos
obtiver os carimbos de data / hora errados, pode ser que __pycache__ saia de
sincronia com o código-fonte. Este é um bug doloroso porque outros podem fazer
alterações no arquivo mystuff.py e essas alterações não serão implementadas
quando o módulo mystuff for importado porque o Python ainda está usando a
versão em __pycache__. Se você estiver trabalhando com Python 2.x, os códigos de
bytes Python compilados são armazenados no mesmo diretório sem __ pycache__
como arquivos .pyc. Eles também nunca devem ser incluídos em seu repositório Git
pelo mesmo motivo.
99 Capítulo 3

Dica de programação: recarregamento automatizado de IPython


IPython fornece algum carregamento automático por meio da magia
%autoreload, mas vem com muitas advertências. Melhor usar explicitamente
importlib.reload.

3.2.1 Usando um diretório como um módulo

Além de colar todo o seu código Python em um único arquivo, você pode usar um
diretório para organizar seu código em arquivos separados. O truque é colocar um
arquivo __init__.py no nível superior do diretório do qual você deseja importar. O
arquivo pode estar vazio. Por exemplo,
package/
__init__.py
moduleA.py

Então, se o package está em seu caminho, você pode import package. Se


__init__.py está vazio, então isso não faz nada. Para obter qualquer parte do código
em moduleA.py, você deve importá-lo explicitamente como import
package.moduleA, e então você pode obter o conteúdo desse módulo. Por
exemplo,
package.moduleA.foo()

executa a função foo no moduleA.py arquivo. Se você quiser fazer foo disponível
ao importar o package, então você tem que colocar from .moduleA import foo
no arquivo __init__.py. A notação de importação relativa é necessária no Python 3.
Então, você pode import package e executar a função como package.foo().
Você também pode fazer from package import foo para obter foo diretamente.
Ao desenvolver seus próprios módulos, você pode ter um controle refinado de quais
pacotes são importados usando importações relativas.

3.3 Importação dinâmica

Caso você não saiba os nomes dos módulos que precisa importar antecipadamente, a
função __import__ pode carregar módulos de uma lista especificada de nomes de
módulo.
>>> sys = __import__('sys') # import module from string argument
>>> sys.version
'3.8.3 (default, May 19 2020, 18:47:26) \n[GCC 7.3.0]'
Capítulo 3 100

Dica de programação: usando __main__


Os namespaces distinguem entre importar e executar um script Python.
if _name__== '__main__':
# essas instruções não são executadas durante a importação
# execute as instruções aqui.

Também existe __file__ que é o nome do arquivo importado. Além do arquivo


__init__.py, você pode colocar um arquivo __main__.py no nível superior do
diretório do módulo, se você quiser chamar o seu módulo usando o -m ligue a
linha de comando. Fazer isso significa que a função __init__.py também será
executada.

3.4 Obtendo módulos da Web

O pacote Python melhorou muito nos últimos anos. Isso sempre foi um ponto
sensível, mas agora é muito mais fácil implantar e manter códigos Python que
dependem de bibliotecas vinculadas em várias plataformas. Pacotes Python no
principal de suporte do índice de pacotes Python pip.
% pip install name_of_module

Isso descobre todas as dependências e as instala também. Existem muitos


sinalizadores que controlam como e onde os pacotes são instalados. Você não precisa
de acesso root para usar isso. Veja o sinalizador --user para acesso não-root. O
empacotamento moderno do Python depende dos chamados arquivos wheel, que
incluem fragmentos de bibliotecas compiladas das quais o módulo depende. Isso
geralmente funciona, mas se você tiver problemas na plataforma Windows, há um
tesouro de módulos para Windows na forma de arquivos de roda no laboratório de
Christoph Gohlke na UCI.1

3.5 Gerenciamento de pacotes Conda

Você realmente deve usar conda sempre que possível. Ele alivia muitas dores de
cabeça com o gerenciamento de pacotes e você não precisa de direitos de
administrador / root para usá-lo com eficácia. O anaconda conjunto de ferramentas é
uma lista com curadoria de pacotes científicos que são suportados pela empresa
Anaconda. Isso tem quase todos os pacotes científicos que você deseja. Fora desse
suporte, a comunidade também oferece suporte a uma lista mais longa de pacotes
científicos como conda-forge. Você pode adicionar conda-forge à sua lista de
repositórios usual com o seguinte:

1Veja
https://www.l.uci.edu/~gohlke/pythonlibs..
101 Capítulo 3
Terminal> conda config --add channels conda-forge

E então você pode instalar novos módulos como a seguir:


Terminal> conda install name_of_module

Além disso, conda também facilita sub-ambientes independentes que são uma ótima
maneira de experimentar com segurança códigos e até mesmo versões diferentes do
Python. Isso pode ser fundamental para o provisionamento automatizado de
máquinas virtuais em um ambiente de computação em nuvem. Por exemplo, o
seguinte criará um ambiente denominado my_test_env com o Python versão 3.7.
Terminal> conda create -n my_test_env python= 3.7

A diferença entre pip e conda é que pip usará os requisitos do pacote desejado
para garantir a instalação de quaisquer módulos ausentes. O pacote conda manager
fará o mesmo, mas determinará adicionalmente se há algum conflito nas versões dos
pacotes desejados e suas dependências em relação à instalação existente e fornecerá
um aviso com antecedência.2 Isso evita o problema de sobrescrever as dependências
de um pacote pré-existente para satisfazer uma nova instalação. A prática
recomendada é preferir conda ao lidar com códigos científicos com muitas
bibliotecas vinculadas e, em seguida, contar com pip para os códigos Python puros.
Às vezes, é necessário usar ambos porque certos módulos Python desejados podem
ainda não ser suportados pelo conda. Isso pode se tornar um problema quando o
conda não sabe como integrar os novos pacotes pip instalados pelo para
gerenciamento interno. A documentação conda tem mais informações e lembre-se
que o conda também está em constante desenvolvimento.
Outra forma de criar ambientes virtuais é com o venv (ou virtualenv), que
vem com o próprio Python. Novamente, essa é uma boa ideia para pacotes pip
instalados come particularmente para códigos Python puros, mas conda é uma
alternativa melhor para programas científicos. No entanto, ambientes virtuais criados
com venv ou virtualenv são particularmente úteis para segregar programas de
linha de comando que podem ter dependências estranhas que você não deseja carregar
ou interferir em outras instalações.

Dica de programação: Gerenciador de pacotes Mamba


O gerenciador de pacote pacote mamba é várias vezes mais rápido do que conda
por causa de seu solucionador de satisfatibilidade mais eficiente e é um excelente
substituto para o conda.

Os seguintes estão listados na Bibliografia como excelentes livros para aprender


mais sobre Python: [1-4, 4-13, 13-15].

2Para
resolver conflitos, o conda implementa um solucionador de satisfazibilidade (SAT), que é
um problema combinatório clássico.
Capítulo 3 102
Referências

1. DM Beazley, Python Essential Reference (Addison-Wesley, Boston, 2009)


2. D. Beazley, BK Jones, Python Cookbook: Recipes for Mastering Python 3 (O'Reilly Media,
Newton, 2013)
3. N. Ceder, The Quick Python Book. (Manning Publications, Shelter Island, 2018)
4. D. Hellmann, The Python 3 Standard Library by Example Developer's Library (Pearson
Education, Londres, 2017)
5. C. Hill, Learning Scientific Programming With Python (Cambridge University Press,
Cambridge, 2020)
6. D. Kuhlman, A Python Book: Beginning Python, Advanced Python, and Python Exercises
(Platypus Global Media, Washington, 2011)
7. HP Langtangen, A Primer on Scientific Programming With Python. Textos em Ciência e
Engenharia Computacional (Springer, Berlin, Heidelberg, 2016)
8. M. Lutz, Learning Python: Powerful Object-Oriented Programming. Safari Books Online
(O'Reilly Media, Newton, 2013)
9. M. Pilgrim, Dive Into Python 3. Books for Professionals by Professionals (Apress, Nova York,
2010)
10. K. Reitz, T. Schlusser, The Hitchhiker's Guide to Python: Best Practices for Development
(O'Reilly Media, Newton, 2016)
11. C. Rossant, Learning IPython for Interactive Computing and Data Visualization (Packt
Publishing, Birmingham, 2015)
12. ZA Shaw, Learn Python the Hard Way: Release 2.0. Lulu.com (2012)
13. M. Summerfield, Python in Practice: Create Better Programs Using Concurrency, Libraries, and
Patterns (Pearson Education, Londres, 2013)
14. J. Unpingco, Python for Signal Processing: Com IPython Notebooks (Springer International
Publishing , Cham, 2016)
15. J. Unpingco, Python for Probability, Statistics, and Machine Learning, 2nd edn. (Springer
International Publishing, Cham, 2019)
103
Capítulo 4
Numpy

Numpy fornece uma maneira unificada de gerenciar matrizes numéricas em Python.


Ele consolidou as melhores ideias de muitas abordagens anteriores para matrizes
numéricas em Python. É a base de muitos outros módulos Python de computação
científica. Para entender e ser eficaz com computação científica e Python, um
domínio sólido sobre Numpy é essencial!
>>> import numpy as np # naming convention

4.1 Dtypes

Embora o Python seja tipado dinamicamente, o Numpy permite a especificação


refinada de tipos de número usando dtypes,
>>> a = np.array([0],np.int16) # 16-bit integer
>>> a.itemsize # in 8-bit bytes
2
>>> a.nbytes
2
>>> a = np.array([0],np.int64) # 64-bit integer
>>> a.itemsize
8

Matrizes numéricas seguem o mesmo padrão,


>>> a = np.array([0,1,23,4],np.int64) # 64-bit integer
>>> a.shape
(4,)
>>> a.nbytes
32

Observe que você não pode adicionar elementos extras a uma matriz Numpy após a
criação,
>>> a = np.array ([1,2])
>>> a [2] = 32
104 Capítulo 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: index 2 is out of bounds for axis 0 with size 2

Isso ocorre porque o bloco de memória já foi delineado e o Numpy não alocará nova
memória e copiará os dados sem instrução explícita. Além disso, depois de criar a
matriz com um tipo de específico d, a atribuição a essa matriz fará o cast para aquele
tipo. Por exemplo,
>>> x = np.array(range(5), dtype=int)
>>> x[0] = 1.33 # float assignment does not match dtype=int
>>> x
array([1, 1, 2, 3, 4])
>>> x[0] = 'this is a string'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'this is a
e→ string'

Isso é diferente de sistemas como o Matlab porque a semântica de copiar / visualizar


array é fundamentalmente diferente.

4.2 Arrays multidimensionais

Arrays multidimensionais seguem o mesmo padrão,


>>> a = np.array([[1,3],[4,5]]) # omitting dtype picks default
>>> a
array([[1, 3],
[4, 5]])
>>> a.dtype
dtype('int64')
>>> a.shape
(2, 2)
>>> a.nbytes
32
>>> a.flatten()
array([1, 3, 4, 5])

O limite máximo no número de dimensões depende de como ele é configurado


durante a compilação do Numpy (geralmente trinta e dois). O Numpy oferece muitas
maneiras de construir arrays automaticamente,
>>> a = np.arange(10) # analogous to range()
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a = np.ones((2,2))
>>> a
array([[1., 1.],
[1., 1.]])
>>> a = np.linspace(0,1,5)
>>> a
Capítulo 4 105
array([0. , 0.25, 0.5 , 0.75, 1. ])
>>> X,Y = np.meshgrid([1,2,3],[5,6])
>>> X
array([[1, 2, 3],
[1, 2, 3]])
>>> Y
array([[5, 5, 5],
[6, 6, 6]])
>>> a = np.zeros((2,2))
>>> a
array([[0., 0.],
[0., 0.]])
Você também pode criar matrizes Numpy usando funções,
>>> np.fromfunction(lambda i,j: abs(i-j)<=1, (4,4))
array([[ True, True, False, False],
[ True, True, True, False],
[False, True, True, True],
[False, False, True, True]])
Matrizes Numpy também podem ter nomes de campo,
>>> a = np.zeros((2,2), dtype=[('x','f4')])
>>> a['x']
array([[0., 0.],
[0., 0.]], dtype=float32)
>>> x = np.array([(1,2)], dtype=[('value','f4'),
... ('amount','c8')])
>>> x['value']
array([1.], dtype=float32)
>>> x['amount']
array([2.+0.j], dtype=complex64)
>>> x = np.array([(1,9),(2,10),(3,11),(4,14)],
... dtype=[('value','f4'),
... ('amount','c8')])
>>> x['value']
array([1., 2., 3., 4.], dtype=float32)
>>> x['amount']
array([ 9.+0.j, 10.+0.j, 11.+0.j, 14.+0.j], dtype=complex64)
Numpy arrays can also be accessed by their attributes using recarray,
>>> y = x.view(np.recarray)
>>> y.amount # access as attribute
array([ 9.+0.j, 10.+0.j, 11.+0.j, 14.+0.j], dtype=complex64)
>>> y.value # access as attribute
array([1., 2., 3., 4.], dtype=float32)

4.3 Remodelando e Empilhando Arrays Numpy

Os arrays podem ser empilhados horizontalmente e verticalmente,


>>> x = np.arange(5)
>>> y = np.array([9,10,11,12,13])
106 Capítulo 4
>>> np.hstack([x,y]) # stack horizontally
array([ 0, 1, 2, 3, 4, 9, 10, 11, 12, 13])
>>> np.vstack([x,y]) # stack vertically
array([[ 0, 1, 2, 3, 4],
[ 9, 10, 11, 12, 13]])

Há também um método dstack se você deseja empilhar na terceira profundidade


dimensão de. Numpy np.concatenate lida com o caso geral de dimensão
arbitrária. Em alguns códigos (por exemplo, scikit-learn), você pode encontrar
os np.c_ and np.r_ usado para empilhar matrizes em colunas e linhas:
>>> np.c_[x,x] # column-wise
array([[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4]])
>>> np.r_[x,x] # row-wise
array([0, 1, 2, 3, 4, 0, 1, 2, 3, 4])

4.4 Duplicando Numpy Arrays

Numpy tem uma função de repeat para duplicar elementos e uma versão mais
generalizada em tile que apresenta uma matriz de bloco da forma especificada,
>>> x=np.arange(4)
>>> np.repeat(x,2)
array([0, 0, 1, 1, 2, 2, 3, 3])
>>> np.tile(x,(2,1))
array([[0, 1, 2, 3],
[0, 1, 2, 3]])
>>> np.tile(x,(2,2))
array([[0, 1, 2, 3, 0, 1, 2, 3],
[0, 1, 2, 3, 0, 1, 2, 3]])

Você também pode ter itens não numéricos, como strings, como itens no array
>>> np.array(['a','b','cow','deep'])
array(['a', 'b', 'cow', 'deep'], dtype='<U4')

Observe que o 'U4' refere-se à sequência de comprimento 4, que é a sequência mais


longa da sequência.
Remodelando matrizes Numpy Matrizes Numpy podem ser remodeladas após a
criação,
>>> a = np.arange(10).reshape(2,5)
>>> a
array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])

Para os verdadeiramente preguiçosos, você pode substituir uma das dimensões acima
por um negativo (ou seja, reshape(-1,5) ), e o Numpy descobrirá a outra
dimensão em conformidade. O array transpose operação do método é igual ao
atributo .T,
Capítulo 4 107
>>> a.transpose()
array([[0, 5],
[1, 6],
[2, 7],
[3, 8],
[4, 9]])
>>> a.T
array([[0, 5],
[1, 6],
[2, 7],
[3, 8],
[4, 9]])

A transposta conjugada (isto é, transposta Hermitiana) é o atributo .H.

4.5 Fatiamento, operações lógicas de array

Os arrays numpy seguem a mesma lógica de fatiamento com índice zero que as listas
e strings do Python:
>>> x = np.arange(50).reshape(5,10)
>>> x
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]])

O caractere de dois pontos significa levar tudo ao longo da dimensão indicada


>>> x[:,0]
array([ 0, 10, 20, 30, 40])
>>> x[0,:]
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> x = np.arange(50).reshape(5,10) # reshaping arrays
>>> x
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]])
>>> x[:,0] # any row, 0th column
array([ 0, 10, 20, 30, 40])
>>> x[0,:] # any column, 0th row
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> x[1:3,4:6]
array([[14, 15],
[24, 25]])
>>> x = np.arange(2*3*4).reshape(2,3,4) # reshaping arrays
>>> x
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
108 Capítulo 4
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]])
>>> x[:,1,[2,1]] # index each dimension
array([[ 6, 5],
[18, 17]])

Função de Numpy where pode encontrar elementos de array de acordo com critérios
lógicos específicos. Observe que np.where retorna uma tupla de índices Numpy,
>>> np.where(x % 2 == 0)
(array([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]),
array([0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2]),
array([0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2]))
>>> x[np.where(x % 2 == 0)]
array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22])
>>> x[np.where(np.logical_and(x % 2 == 0,x < 9))] # also
e→ logical_or, etc.
array([0, 2, 4, 6, 8])

Além disso, matrizes Numpy podem ser indexadas por matrizes Numpy lógicas onde
apenas o correspondente entradas True são selecionadas,
>>> a = np.arange(9).reshape((3,3))
>>> a
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
>>> b = np.fromfunction(lambda i,j: abs(i-j) <= 1, (3,3))
>>> b
array([[ True, True, False],
[ True, True, True],
[False, True, True]])
>>> a[b]
array([0, 1, 3, 4, 5, 7, 8])
>>> b = (a>4)
>>> b
array([[False, False, False],
[False, False, True],
[ True, True, True]])
>>> a[b]
array([5, 6, 7, 8])

4.6 Numpy Arrays e Memory

Numpy usa passagem semântica de referência para que as operações de fatia sejam
visualizadas na matriz sem cópia implícita, o que é consistente com a semântica do
Python. Isso é particularmente útil com matrizes grandes que já sobrecarregam a
memória disponível. Na terminologia do Numpy, o fatiamento cria visualizações
(sem cópia) e a indexação avançada cria cópias. Vamos começar com a indexação
avançada.
Se o objeto de indexação (ou seja, o item entre os colchetes) é um objeto de
sequência não tupla, outro array Numpy (do tipo inteiro ou booleano), ou uma tupla
com pelo menos um objeto de sequência ou array Numpy, então a indexação cria
cópias.
Capítulo 4 109
Para o exemplo acima, para estender e copiar um array existente em Numpy, você
deve fazer algo como o seguinte:
>>> x = np.ones((3,3))
>>> x
array([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
>>> x[:,[0,1,2,2]] # notice duplicated last dimension
array([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
>>> y=x[:,[0,1,2,2]] # same as above, but do assign it to y

devido ao avançado indexação, a variável y tem sua própria memória porque as partes
relevantes de x foram copiadas. Para provar isso, atribuímos um novo elemento a x
e vemos que y não é atualizado:
>>> x[0,0]=999 # change element in x
>>> x # changed
array([[999., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.]])
>>> y # not changed!
array([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])

No entanto, se nós recomeçar e construir y por corte (o que torna uma vista), como
mostrado abaixo, em seguida, a alteração que fizemos afecta y porque um ponto de
vista é apenas uma janela para a mesma memória:
>>> x = np.ones((3,3))
>>> y = x[:2,:2] # view of upper left piece
>>> x[0,0] = 999 # change value
>>> x # see the change?
array([[999., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.]])
>>> y
array([[999., 1.],
[ 1., 1.]])

Observe que se você deseja forçar explicitamente uma cópia sem nenhum truque de
indexação, você pode fazer y = x.copy(). O código a seguir funciona por meio de
outro exemplo de indexação avançada versus fracionamento:
>>> x = np.arange(5) # create array
>>> x
array([0, 1, 2, 3, 4])
>>> y=x[[0,1,2]] # index by integer list to force copy
>>> y
array([0, 1, 2])
>>> z=x[:3] # slice creates view
110 Capítulo 4
>>> z # note y and z have same entries
array([0, 1, 2])
>>> x[0]=999 # change element of x
>>> x
array([999, 1, 2, 3, 4])
>>> y # note y is unaffected,
array([0, 1, 2])
>>> z # but z is (it's a view).
array([999, 1, 2])

Neste exemplo, y é uma cópia, não uma visualização, porque foi criado usando
indexação avançada, enquanto z foi criado usando fatiamento. Portanto, embora y e
z tenham as mesmas entradas, apenas z é afetado pelas mudanças em x. Observe que
a propriedade flags.ownsdata de matrizes Numpy pode ajudar a resolver isso
até você se acostumar com isso.
Sobreposição de matrizes Numpy Manipular a memória usando visualizações é
particularmente poderoso para algoritmos de processamento de sinal e imagem que
requerem fragmentos de memória sobrepostos. A seguir está um exemplo de como
usar Numpy avançado para criar blocos sobrepostos que não consomem memória
adicional,
>>> from numpy.lib.stride_tricks import as_strided
>>> x = np.arange(16).astype(np.int32)
>>> y=as_strided(x,(7,4),(8,4)) # overlapped entries
>>> y
array([[ 0, 1, 2, 3],
[ 2, 3, 4, 5],
[ 4, 5, 6, 7],
[ 6, 7, 8, 9],
[ 8, 9, 10, 11],
[10, 11, 12, 13],
[12, 13, 14, 15]], dtype=int32)

O código acima cria um intervalo de inteiros e então sobrepõe as entradas para criar
uma matriz Numpy 7x4. O argumento final na função as_strided são os strides,
que são os passos em bytes para mover nas dimensões de linha e coluna,
respectivamente. Assim, o array resultante alcança quatro bytes na dimensão da
coluna e oito bytes na dimensão da linha. Como os elementos inteiros na matriz
Numpy têm quatro bytes, isso é equivalente a se mover por um elemento na dimensão
da coluna e por dois elementos na dimensão da linha. A segunda linha na matriz
Numpy começa com oito bytes (dois elementos) a partir da primeira entrada (ou seja,
2) e prossegue por quatro bytes (por um elemento) na dimensão da coluna (ou seja,
2,3,4,5) . A parte importante é que a memória seja reutilizada na resultante matriz
Numpy 7x4. O código a seguir demonstra isso atribuindo elementos na matriz
original x. As alterações aparecem no array y porque apontam para a mesma memória
alocada:
>>> x[::2] = 99 # assign every other value
>>> x
array([99, 1, 99, 3, 99, 5, 99, 7, 99, 9, 99, 11, 99, 13, 99,
e→ 15],
dtype=int32)
>>> y # the changes appear because y is a view
array([[99, 1, 99, 3],
Capítulo 4 111
[99, 3, 99, 5],
[99, 5, 99, 7],
[99, 7, 99, 9],
[99, 9, 99, 11],
[99, 11, 99, 13],
[99, 13, 99, 15]], dtype=int32)

Tenha em mente que as_strided não verifica que você fique dentro dos limites de
blocos de memória. Portanto, se o tamanho da matriz de destino não for preenchido
pelos dados disponíveis, os elementos restantes virão de quaisquer bytes que estejam
naquele local da memória. Em outras palavras, não há preenchimento padrão por
zeros ou outra estratégia que defenda os limites do bloco de memória. Uma defesa é
controlar explicitamente as dimensões como no código a seguir:
>>> n = 8 # number of elements
>>> x = np.arange(n) # create array
>>> k = 5 # desired number of rows
>>> y = as_strided(x,(k,n-k+1),(x.itemsize,)*2)
>>> y
array([[0, 1, 2, 3],
[1, 2, 3, 4],
[2, 3, 4, 5],
[3, 4, 5, 6],
[4, 5, 6, 7]])

4.7 Estruturas de dados da memória Numpy

Vamos examinar a seguinte estrutura de dados typedef no código-fonte do Numpy:


typedef struct PyArrayObject {
PyObject_HEAD

/* Block of memory */
char *data;

/* Data type descriptor */


PyArray_Descr *descr;

/* Indexing scheme */
int nd;
npy_intp *dimensions;
npy_intp *strides;

/* Other stuff */
PyObject *base;
int flags;
PyObject *weakreflist;
} PyArrayObject;0

Vamos criar um array Numpy de inteiros de 16 bits e explorá-lo:


>>> x = np.array([1], dtype=np.int16)
112 Capítulo 4
Podemos ver os dados brutos usando o atributo x.data,
>>> bytes(x.data)
b '\ x01 \ x00'

Observe a orientação dos bytes. Agora, mude o dtype para um inteiro big-endian de
dois bytes sem sinal,
>>> x = np.array([1], dtype='>u2')
>>> bytes(x.data)
b'\x00\x01'

Observe novamente a orientação dos bytes. Isso é o que little/big endian significa para
dados na memória. Podemos criar matrizes Numpy de bytes diretamente usando
frombuffer, como a seguir. Observe o efeito do uso de diferentes dtypes,
>>> np.frombuffer(b'\x00\x01',dtype=np.int8)
array([0, 1], dtype=int8)
>>> np.frombuffer(b'\x00\x01',dtype=np.int16)
array([256], dtype=int16)
>>> np.frombuffer(b'\x00\x01',dtype='>u2') array([1], dtype=uint16)

Uma vez que um ndarray é criado, você pode relançá-lo para um tipo diferente ou
alterar o tipo de view. Grosso modo, o casting copia dados. Por exemplo,
>>> x = np.frombuffer(b'\x00\x01',dtype=np.int8)
>>> x
array([0, 1], dtype=int8)
>>> y = x.astype(np.int16)
>>> y
array([0, 1], dtype=int16)
>>> y.flags['OWNDATA'] # y is a copy
True

Alternativamente, podemos reinterpretar os dados usando uma view,


>>> y = x.view(np.int16)
>>> y
array([256], dtype=int16)
>>> y.flags['OWNDATA']
False

Observe que y não é uma memória nova, ela apenas faz referência à memória existente
e a reinterpreta usando um diferente dtype.
Numpy Memory Strides Os avanços do typedef acima referem-se a como o Numpy
se move entre os arrays. Uma stride é o número de bytes para alcançar o próximo
elemento consecutivo da matriz. Existe um passo por dimensão. Considere a seguinte
matriz Numpy:
>>> x = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.int8)
>>> bytes(x.data)
b'\x01\x02\x03\x04\x05\x06\x07\x08\t'
>>> x.strides (3, 1)
Capítulo 4 113
Assim, se quisermos indexar x [1,2], temos que usar o seguinte deslocamento:
>>> offset = 3*1+1*2
>>> x.flat[offset]
6

Numpy suporta ordem C (isto é, coluna) e ordem Fortran (isto é, linha). Por exemplo,
>>> x = np.array([[1, 2, 3], [7, 8, 9]], dtype=np.int8,order='C')
>>> x.strides
(3, 1)
>>> x = np.array([[1, 2, 3], [7, 8, 9]], dtype=np.int8,order='F')
>>> x.strides
(1, 2)

Observe a diferença entre as passadas para as duas ordens. Para a ordem C, leva 3
bytes para se mover entre as linhas e 1 byte para se mover entre as colunas, enquanto
para a ordem Fortran, leva 1 byte para se mover entre as linhas, mas 2 bytes para se
mover entre as colunas. Este padrão continua para dimensões superiores:
>>> x = np.arange(125).reshape((5,5,5)).astype(np.int8)
>>> x.strides
(25, 5, 1)
>>> x[1,2,3]
38

Para obter o elemento [1,2,3] usando offsets de byte, podemos fazer o seguinte:
>>> offset = (25*1 + 5*2 +1*3)
>>> x.flat[offset]
38

Mais uma vez, criar visualizações por meio de fatias apenas altera o passo!
>>> x = np.arange(3,dtype=np.int32)
>>> x.strides
(4,)
>>> y = x[::-1]
>>> y.strides
(-4,)

A transposição também apenas troca a passada,


>>> x = np.array([[1, 2, 3], [7, 8, 9]], dtype=np.int8,order='F')
>>> x.strides
(1, 2)
>>> y = x.T
>>> y.strides # negative!
(2, 1)

Em geral, a remodelagem não altera apenas a passada, mas às vezes pode fazer cópias
dos dados. O layout da memória (ou seja, avanços) pode afetar o desempenho por
causa do cache da CPU. A CPU puxa dados da memória principal em blocos de forma
que, se muitos itens puderem ser operados consecutivamente em um único bloco, isso
reduz o número de transferências necessárias da memória principal que acelera a
computação.
114 Capítulo 4
4.8 Operações de Array Element-Wise

As operações aritméticas de pares usuais são element-wise em Numpy:


>>> x*3
array([[ 3, 6, 9],
[21, 24, 27]], dtype=int8)
>>> y = x/x.max()
>>> y
array([[0.11111111, 0.22222222, 0.33333333],
[0.77777778, 0.88888889, 1. ]])
>>> np.sin(y) * np.exp(-y)
array([[0.09922214, 0.17648072, 0.23444524],
[0.32237812, 0.31917604, 0.30955988]])

Dica de programação: cuidado com as operações locais do Numpy


É fácil bagunçar as operações no local, como x - = xT para matrizes Numpy,
portanto, eles devem ser evitados em geral e podem levar a bugs difíceis de
encontrar mais tarde.

4.9 Funções universais

Agora que sabemos como criar e manipular matrizes Numpy, vamos considerar como
computar com outros recursos Numpy. Funções universais (ufuncs) são funções
Numpy otimizadas para calcular matrizes Numpy no nível C (ou seja, fora do
interpretador Python). Vamos calcular o seno trigonométrico:
>>> a = np.linspace(0,1,20)
>>> np.sin(a)
array([0. , 0.05260728, 0.10506887, 0.15723948, 0.20897462,
0.26013102, 0.310567 , 0.36014289, 0.40872137, 0.45616793,
0.50235115, 0.54714315, 0.59041986, 0.63206143, 0.67195255,
0.70998273, 0.74604665, 0.78004444, 0.81188195, 0.84147098])

Observe que o Python tem um módulo integrado math com a sua própria função
seno:
>>> from math import sin
>>> [sin(i) for i in a]
[0.0, 0.05260728333807213, 0.10506887376594912,
0.15723948186175024, 0.20897462406278547, 0.2601310228046501,
0.3105670033203749, 0.3601428860007191, 0.40872137322898616,
0.4561679296190457, 0.5023511546035125, 0.547143146340223,
0.5904198559291864, 0.6320614309590333, 0.6719525474315213,
0.7099827291448582, 0.7460466536513234, 0.7800444439418607,
0.8118819450498316, 0.8414709848078965]

A saída é uma lista, não uma matriz Numpy, e para processar todos os elementos de
a, tivemos que usar as compreensões de lista para calcular o seno.
Capítulo 4 115
Isso ocorre porque o Python a função math funciona apenas uma de cada vez com
cada membro da matriz. A função seno do Numpy não precisa dessa semântica extra
porque a computação é executada no código C do Numpy fora do interpretador Python.
É daí que vem a aceleração de 200 a 300 vezes do Numpy em relação ao código Python
simples. Portanto, faça o seguinte:
>>> np.array([sin(i) for i in a])
array([0. , 0.05260728, 0.10506887, 0.15723948, 0.20897462,
0.26013102, 0.310567 , 0.36014289, 0.40872137, 0.45616793,
0.50235115, 0.54714315, 0.59041986, 0.63206143, 0.67195255,
0.70998273, 0.74604665, 0.78004444, 0.81188195, 0.84147098])
totalmente o propósito de usar o Numpy. Sempre use ufuncs Numpy sempre que
possível!
>>> np.sin(a)
array([0. , 0.05260728, 0.10506887, 0.15723948, 0.20897462,
0.26013102, 0.310567 , 0.36014289, 0.40872137, 0.45616793,
0.50235115, 0.54714315, 0.59041986, 0.63206143, 0.67195255,
0.70998273, 0.74604665, 0.78004444, 0.81188195, 0.84147098])

4.10 Entrada / Saída de Dados Numpy

O Numpy facilita a entrada e saída de dados de arquivos:


>>> x = np.loadtxt('sample1.txt')
>>> x
array([[ 0., 0.],
[ 1., 1.],
[ 2., 4.],
[ 3., 9.],
[ 4., 16.],
[ 5., 25.],
[ 6., 36.],
[ 7., 49.],
[ 8., 64.],
[ 9., 81.]])
>>> # each column with different type
>>> x = np.loadtxt('sample1.txt',dtype='f4,i4')
>>> x
array([(0., 0), (1., 1), (2., 4), (3., 9), (4., 16), (5.,
e→ 25),
(6., 36), (7., 49), (8., 64), (9., 81)],
dtype=[('f0', '<f4'), ('f1', '<i4')])

Matrizes Numpy podem ser salvas com a função correspondente np.savetxt.

4.11 Linear Algebra

Numpy tem acesso direto ao comprovado código de álgebra linear LAPACK / BLAS.
A principal entrada para funções de álgebra linear no Numpy é por meio do submódulo
linalg,
116 Capítulo 4
>>> np.linalg.eig(np.eye(3)) # runs underlying LAPACK/BLAS
(array([1., 1., 1.]), array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]]))
>>> np.eye(3)*np.arange(3) # does this work as expected?
array([[0., 0., 0.],
[0., 1., 0.],
[0., 0., 2.]])

Para obter produtos linha-coluna da matriz, você pode usar o objeto matrix,
>>> np.eye(3)*np.matrix(np.arange(3)).T # row-column multiply,
matrix([[0.],
[1.],
[2.]])

De forma mais geral, você pode usar o produto da Numpy dot,


>>> a = np.eye(3)
>>> b = np.arange(3).T
>>> a.dot(b)
array([0., 1., 2.])
>>> b.dot(b)
5

A vantagem do dot é que ele funciona em dimensões arbitrárias. Isso é útil para
contrações semelhantes a tensores (consulte Numpy tensordot para obter mais
informações). Desde o Python 3.6, há também a notação @ para multiplicação da
matriz Numpy
>>> a = np.eye(3)
>>> b = np.arange(3).T
>>> a @ b
array([0., 1., 2.])

4.12 Broadcasting

Broadcasting é incrivelmente poderoso, mas leva tempo para entender. Citando do


Guide to NumPy de Travis Oliphant [1]:
1. Todas as matrizes de entrada com ndim menor do que a matriz de entrada do maior
ndim têm 1 pré-pendentes em suas formas.
2. O tamanho em cada dimensão da forma de saída é o máximo de todas as formas
de entrada nessa dimensão.
3. Uma entrada pode ser usada no cálculo se for a forma em uma dimensão específica
ou corresponder à forma de saída ou tiver o valor exatamente 1.
4. Se uma entrada tiver um tamanho de dimensão de 1 em sua forma, a primeira
entrada de dados nessa dimensão será usada para todos os cálculos ao longo dessa
dimensão. Em outras palavras, a maquinaria de passo do ufunc simplesmente não
pisará ao longo dessa dimensão quando de outra forma necessário (a passada será
0 para essa dimensão).
Capítulo 4 117
Uma maneira mais fácil de pensar sobre essas regras é a seguinte:
1. Se as formas da matriz tiverem comprimentos diferentes, preencha a forma menor
com alguns.
2. Se alguma dimensão correspondente não corresponder, faça cópias ao longo da
dimensão 1-.
3. Se alguma dimensão correspondente não tiver um, gere um erro. Alguns exemplos
ajudarão. Considere estas duas matrizes:
>>> x = np.arange (3)
>>> y = np.arange (5)

e você deseja calcular o produto elemento-sábio deles. O problema é que essa


operação não é definida para matrizes de formatos diferentes. Podemos definir o que
este produto elemento-sábio significa neste caso com o seguinte loop,
>>> out = []
>>> for i in x:
... for j in y:
... out.append(i*j)
...
>>> out
[0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 0, 2, 4, 6, 8]

Mas agora perdemos as dimensões de entrada de x e y. Podemos conservá-los


remodelando a saída da seguinte maneira:
>>> out=np.array(out).reshape(len(x),-1) # -1 means infer the
e→ remaining dimension
>>> out
array([[0, 0, 0, 0, 0],
[0, 1, 2, 3, 4],
[0, 2, 4, 6, 8]])

Outra maneira de pensar sobre o que acabamos de calcular é como o produto externo
da matriz,
>>> from numpy import matrix
>>> out=matrix(x).T * y
>>> out
matrix([[0, 0, 0, 0, 0],
[0, 1, 2, 3, 4],
[0, 2, 4, 6, 8]])

Mas como pode você generaliza isso para lidar com várias dimensões? Vamos
considerar adicionar uma dimensão singleton y como
>>> x[:,None].shape(
3, 1)

Podemos usar np.newaxis em vez de None para a leitura. Agora, se tentarmos


isso diretamente, a transmissão irá lidar com as dimensões incompatíveis fazendo
cópias ao longo da dimensão singleton:
118 Capítulo 4
>>> x[:,None]*y
array([[0, 0, 0, 0, 0],
[0, 1, 2, 3, 4],
[0, 2, 4, 6, 8]])

e isso funciona com expressões mais complicadas:


>>> from numpy import cos
>>> x[:,None]*y + cos(x[:,None]+y)
array([[ 1. , 0.54030231, -0.41614684, -0.9899925 , -0.65364362],
[ 0.54030231, 0.58385316, 1.0100075 , 2.34635638, 4.28366219],
[-0.41614684, 1.0100075 , 3.34635638, 6.28366219, 8.96017029]])

Mas e se você não gostar do formato da matriz resultante?


>>> x*y[:,None] # change the placement of the singleton dimension
array([[0, 0, 0],
[0, 1, 2],
[0, 2, 4],
[0, 3, 6],
[0, 4, 8]])

Agora, vamos considerar um exemplo maior


>>> X = np.arange(2*4).reshape(2,4)
>>> Y = np.arange(3*5).reshape(3,5)

onde você deseja multiplicar por elemento esses dois juntos. O resultado será uma matriz
multidimensional de 2 x 4 x 3 x 5:
>>> X[:,:,None,None] * Y
array([[[[ 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0]],

[[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]],

[[ 0, 2, 4, 6, 8],
[10, 12, 14, 16, 18],
[20, 22, 24, 26, 28]],

[[ 0, 3, 6, 9, 12],
[15, 18, 21, 24, 27],
[30, 33, 36, 39, 42]]],

[[[ 0, 4, 8, 12, 16],


[20, 24, 28, 32, 36],
[40, 44, 48, 52, 56]],

[[ 0, 5, 10, 15, 20],


[25, 30, 35, 40, 45],
Capítulo 4 119
[50, 55, 60, 65, 70]],

[[ 0, 6, 12, 18, 24],


[30, 36, 42, 48, 54],
[60, 66, 72, 78, 84]],

[[ 0, 7, 14, 21, 28],


[35, 42, 49, 56, 63],
[70, 77, 84, 91, 98]]]])

Vamos descompactar um de cada vez e ver o que a transmissão está fazendo com
cada multiplicação,
>>> X[0,0]*Y # 1st array element
array([[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]])
>>> X[0,1]*Y # 2nd array element
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
>>> X[0,2]*Y # 3rd array element
array([[ 0, 2, 4, 6, 8],
[10, 12, 14, 16, 18],
[20, 22, 24, 26, 28]])

Podemos somar os itens ao longo de qualquer dimensão com o argumento de palavra-


chave axis,
>>> (X[:,:,None,None]*Y).sum(axis=3) # sum along 4th dimension
array([[[ 0, 0, 0],
[ 10, 35, 60],
[ 20, 70, 120],
[ 30, 105, 180]],

[[ 40, 140, 240],


[ 50, 175, 300],
[ 60, 210, 360],
[ 70, 245, 420]]])

Abreviando Loops com Broadcasting


Usando broadcasting, calcule o número de maneiras por quarto (ou seja, 25
centavos) podem ser divididos em centavos, moedas e moedas:
>>> n=0 # start counter
>>> for n_d in range(0,3): # at most 2 dimes
... for n_n in range(0,6): # at most 5 nickels
... for n_p in range(0,26): # at most 25 pennies
... value = n_d*10+n_n*5+n_p
... if value == 25:
... print('dimes=%d, nickels=%d, pennies=%d'%(n_d,
n_n,n_p))
... n+=1

(continuação)
120 Capítulo 4

...
dimes=0, nickels=0, pennies=25
dimes=0, nickels=1, pennies=20
dimes=0, nickels=2, pennies=15
dimes=0, nickels=3, pennies=10
dimes=0, nickels=4, pennies=5
dimes=0, nickels=5, pennies=0
dimes=1, nickels=0, pennies=15
dimes=1, nickels=1, pennies=10
dimes=1, nickels=2, pennies=5
dimes=1, nickels=3, pennies=0
dimes=2, nickels=0, pennies=5
dimes=2, nickels=1, pennies=0
>>> print('n = ',n)
n = 12

Isso pode ser feito em uma linha com transmissão:


>>> n_d = np.arange(3)
>>> n_n = np.arange(6)
>>> n_p = np.arange(26)
>>> # matches n above
>>> (n_p + 5*n_n[:,None] + 10*n_d[:,None,None]==25).sum()
12

Isso significa que os loops aninhados for acima são equivalentes à transmissão
Numpy, então sempre que você ver esse padrão de loop aninhado, pode ser uma
oportunidade para transmissão.

4.13 Matrizes Mascaradas

O Numpy também permite mascarar seções de matrizes Numpy. Isso é muito popular
no processamento de imagens:
>>> x = np.array([2, 1, 3, np.nan, 5, 2, 3, np.nan])
>>> x
array([ 2., 1., 3., nan, 5., 2., 3., nan])
>>> np.mean(x)
nan
>>> m = np.ma.masked_array(x, np.isnan(x))
>>> m
masked_array(data=[2.0, 1.0, 3.0, --, 5.0, 2.0, 3.0, --],
mask=[False, False, False, True, False, False,
False, True],
fill_value=1e+20)
>>> np.mean(m)
2.6666666666666665
>>> m.shape
(8,)
>>> x.shape
Capítulo 4 121
(8,)
>>> m.fill_value=9999
>>> m.filled()
array([2.000e+00, 1.000e+00, 3.000e+00, 9.999e+03, 5.000e+00,
2.000e+00,
3.000e+00, 9.999e+03])

Criação de matrizes Numpy de objetos personalizados


Para tornar os objetos personalizados compatíveis com matrizes Numpy, temos
que definir o método de __array__ :
>>> from numpy import arange
>>> class Foo():
... def __init__(self): # note the double underscores
... self.size = 10
... def __array__(self): # produces numpy array
... return arange(self.size)
...
>>> np.array(Foo())
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

4.14 Números de ponto flutuante

Existem limitações de precisão ao representar números de ponto flutuante em um


computador com memória finita. Por exemplo, o seguinte mostra essas limitações ao
adicionar dois números simples,
>>> 0,1 + 0.2
0,30000000000000004

Por que a saída não é 0,3? O problema é a representação de ponto flutuante dos dois
números e o algoritmo que os adiciona. Para representar um inteiro em binário, basta
escrevê-lo em potências de 2. Por exemplo, 230 = (11100110)2. Python pode fazer
essa conversão usando formatação de string,
>>> '{0: b}'.format (230)
'11100110'

Para adicionar inteiros, nós apenas adicionamos os bits correspondentes e os


ajustamos no número permitido de bits. A menos que haja um estouro (os resultados
não podem ser representados com esse número de bits), não há problema. Representar
o ponto flutuante é mais complicado porque temos que representar esses números
como frações binárias. O padrão IEEE 754 requer que os números de ponto flutuante
sejam representados como ±C × 2E , onde C é o significado (mantissa) e E é o
expoente.
Para representar uma fração decimal regular como uma fração binária, precisamos
calcular a expansão da fração da seguinte forma a1/2 + a2/22 + um2/33... Em outras
palavras, precisamos encontrar os coeficientes ai.
122 Capítulo 4
Podemos fazer isso usando o mesmo processo que usaríamos para uma fração
decimal: continue dividindo pelas potências fracionárias de 1/2 e acompanhe as
partes inteiras e fracionárias. Função do Python divmod pode fazer a maior parte do
trabalho para isso. Por exemplo, para representar 0,125 como uma fração binária,
>>> a = 0.125
>>> divmod(a*2,1)
(0.0, 0.25)

O primeiro item na tupla é o quociente e o outro é o resto. Se o quociente for maior


que 1, ocorrespondente termo ai é um e, caso contrário, é zero. Para este exemplo,
nós temos a1 = 0. Para obter o próximo mandato na expansão, nós apenas manter
multiplicando por 2,que nos move para a direita ao longo da expansão para ai+1 e
assim por diante. Então,
>>> a = 0.125
>>> q,a = divmod(a*2,1)
>>> (q,a)
(0.0, 0.25)
>>> q,a = divmod(a*2,1)
>>> (q,a)
(0.0, 0.5)
>>> q,a = divmod(a*2,1)
>>> (q,a)
(1.0, 0.0)

O algoritmo para quando o termo restante é zero. Portanto, temos esse 0.125 =
(0.001)2.A especificação requer que o termo principal na expansão seja um. Portanto,
-
temos 0.125 = (1000). × 2 3.Isso significa que o significando é 1 e o expoente é -3.
Agora, vamos voltar ao nosso problema principal 0,1 + 0,2 desenvolvendo a
representação 0,1 codificando as etapas individuais acima:
>>> a = 0.1
>>> bits = []
>>> while a>0:
... q,a = divmod(a*2,1)
... bits.append(q)
...
>>> ''.join(['%d'%i for i in bits])
'0001100110011001100110011001100110011001100110011001101'

Observe que a representação tem um padrão de repetição infinita. Isto significa que
temos (1.1001)2×2−4. O padrão IEEE não tem uma maneira de representar
sequências que se repetem infinitamente. No entanto, podemos calcular isso:

 1 1 3
+ 4n =
2 4n−3 2 5
n=1

-
Portanto, 0.1 ≈ 1.6 × 2 4. De acordo com o padrão IEEE 754, para o tipo float,
temos 24 bits para o significando e 23 bits para a parte fracionária. Como não
podemos representar a sequência de repetição infinita, temos que arredondar para 23
bits, 10011001100110011001101.
Capítulo 4 123
Assim, enquanto a representação do significando costumava ser 1,6, com este
arredondamento, agora é
>>> b = '10011001100110011001101'
>>> 1+sum([int(i)/(2**n) for n,i in enumerate(b,1)])1.600000023841858
-
Portanto, agora temos 0.1 ≈ 1.600000023841858 × 2 4 = 0.10000000149011612.
Para a expansão 0,2, temos a mesma sequência de repetição com um expoente
-
diferente, de modo que temos 0.2 ≈ 1.600000023841858 × 2 3 =
0.20000000298023224. Para adicionar 0,1 + 0,2 em binário, devemos ajustar os
expoentes até que eles correspondam ao maior dos dois. Assim,
0.11001100110011001100110
+1.10011001100110011001101
--------------------------
10.01100110011001100110011

Agora, a soma deve ser escalada de volta para caber nos bits disponíveis do
significando para que o resultado é 1.00110011001100110011010 com
expoente -2. Calculando isso da maneira usual, conforme mostrado abaixo, obtém-
se o resultado:
>>> k='00110011001100110011010'
>>> ('%0.12f'%((1+sum([int(i)/(2**n)
... for n,i in enumerate(k,1)]))/2**2))
'0.300000011921'

que corresponde ao que obtemos com numpy


>>> import numpy as np
>>> '%0.12f'%(np.float32(0.1) + np.float32(0.2))
'0.300000011921'

Todo o processo prossegue da mesma forma para flutuadores de 64 bits. Python


possui módulos de fractions e decimal que permitem representações de
números mais exatas. O módulo decimal é particularmente importante para certos
cálculos financeiros.
Erro de arredondamento Vamos considerar o exemplo de adição de
100.000.000 e 10
em ponto flutuante de 32 bits.
>>> '{0:b}'.format(100000000)
'101111101011110000100000000'

Isto significa que 100, 000, 000 = (1.01111101011110000100000000)2 × 226.


Do mesmo modo, 10 = (1.010)2 × 23. Para adicioná-los, temos que fazer com que os
expoentes correspondam da seguinte forma:
1.01111101011110000100000000
+0.00000000000000000000001010
-------------------------------
1.01111101011110000100001010

Agora, temos que arredondar porque temos apenas 23 bits à direita da vírgula decimal
e obter 1.0111110101111000010000, perdendo assim ofinal 10 bits. Isso
efetivamente torna o decimal 10 = (1010)2 com o qual começamos se torna 8 = (1000)2.
Assim, usando Numpy novamente,
124 Capítulo 4
>>> format(np.float32(100000000) + np.float32(10),'10.3f')
'100000008.000'

O problema aqui é que a ordem de magnitude entre os dois números era tão grande
que resultou em perda nos bits do significando, pois o número menor foi deslocado
para a direita. Ao somar números como esses, o algoritmo de soma Kahan (consulte
math.fsum()) pode gerenciar efetivamente esses erros de arredondamento:
>>> import math
>>> math.fsum([np.float32(100000000),np.float32(10)])
100000010.0

Erro de cancelamento O erro de cancelamento (perda de significância) ocorre


quando dois números de ponto flutuante quase iguais são subtraídos. Vamos
considerar a subtração de 0,11111112 e 0,11111111. Como frações binárias,
temos o seguinte:
1.11000111000111001000101 E-4
-1.11000111000111000110111 E-4
---------------------------
0,00000000000000000011100

Como uma fração binária , isto é 1,11 com expoente -23 ou (175.)10 × 2-23 ≈
0.00000010430812836. Em Numpy, essa perda de precisão é mostrada no seguinte:
>>> format(np.float32(0.1111112)-np.float32(0.1111111),'1.17f')
'0.00000010430812836'

Para resumir, ao usar o ponto flutuante , você deve verificar a igualdade aproximada
usando algo como Numpy allclose em vez da igualdade Python usual (ou seja,
==). Isso impõe limites de erro em vez de igualdade estrita. Sempre que possível, use
uma escala fixa para empregar valores inteiros em vez de frações decimais. Os
números de ponto flutuante de 64 bits de precisão dupla são muito melhores do que
a precisão simples e, embora não eliminem esses problemas, efetivamente chutam a
lata para todos, exceto os requisitos de precisão mais estritos. O algoritmo Kahan é
eficaz para somar números de ponto flutuante em dados muito grandes sem acumular
erros de arredondamento. Para minimizar os erros de cancelamento, refaça o fator do
cálculo para evitar a subtração de dois números quase iguais.

Dica de programação: Módulo decimal


Python tem um módulo embutido decimal que usa um método diferente para
gerenciar números de ponto flutuante, especialmente para cálculos financeiros. A
desvantagem é que o decimal é muito mais lento do que o padrão IEEE para
ponto flutuante descrito aqui.
Capítulo 4 125
4.15 Avançada Numpy dtypes

Numpy dtypes pode também ajuda ler seções de arquivos de dados binários
estruturados. Por exemplo, um arquivo WAV tem um formato de cabeçalho de 44
bytes:
Item Description
------------------- ---------------------------------------
chunk_id "RIFF"
chunk_size 4-byte unsigned little-endian integer
format "WAVE"
fmt_id "fmt"
fmt_size 4-byte unsigned little-endian integer
audio_fmt 2-byte unsigned little-endian integer
num_channels 2-byte unsigned little-endian integer
sample_rate 4-byte unsigned little-endian integer
byte_rate 4-byte unsigned little-endian integer
block_align 2-byte unsigned little-endian integer
bits_per_sample 2-byte unsigned little-endian integer
data_id "data"
data_size 4-byte unsigned little-endian integer

Você pode abrir este arquivo de amostra de teste fora da web usando o seguinte código
para obter os primeiros quatro bytes de chunk_id:
>>> from urllib.request import urlopen
>>> fp=urlopen('https://www.kozco.com/tech/piano2.wav')
>>> fp.read(4) b'RIFF'

Lendo os próximos quatro bytes little-endian dá o chunk_size:


>>> fp.read(4)
b'\x04z\x12\x00'

Observe que há um problema de endianness dos bytes retornados. Podemos obter os


próximos quatro bytes da seguinte maneira:
>>> fp.read (4)
b'WAVE '

Continuando assim, poderíamos pegar o resto do cabeçalho, mas é melhor usar o


seguinte custom dtype:
>>> header_dtype = np.dtype([
... ('chunkID','S4'),
... ('chunkSize','<u4'),
... ('format','S4'),
... ('subchunk1ID','S4'),
... ('subchunk1Size','<u4'),
... ('audioFormat','<u2'),
... ('numChannels','<u2'),
... ('sampleRate','<u4'),
... ('byteRate','<u4'),
... ('blockAlign','<u2'),
... ('bitsPerSample','<u2'),
... ('subchunk2ID','S4'),
... ('subchunk2Size','u4'),
... ])
126 Capítulo 4
Então, começamos tudo de novo e fazemos o seguinte, mas com urlretrieve, que
grava o arquivo temporário de que o Numpy precisa:
>>> from urllib.request import urlretrieve
>>> path, _ = urlretrieve('https://www.kozco.com/tech/piano2.wav')
>>> h=np.fromfile(path,dtype=header_dtype,count=1)
>>> print(h)
[(b'RIFF', 1210884, b'WAVE', b'fmt ', 16, 1, 2, 48000, 192000, 4,
16, b'data', 1210848)]

Observe que a saída encapsula ordenadamente os bytes individuais com seus


problemas endian correspondentes. Agora que temos o cabeçalho que nos diz quantos
bytes estão nos dados, podemos ler o restante dos dados e processá-los corretamente
usando os outros campos do cabeçalho.

Formatando matrizes Numpy


Às vezes, as matrizes Numpy podem ficar muito densas para serem visualizadas.
A função np.set_printoptions pode ajudar a reduzir a confusão visual,
fornecendo opções de formatação personalizadas para os diferentes tipos de dados
Numpy. Lembre-se de que é um problema de formatação e nenhum dos dados
subjacentes é alterado. Por exemplo, para alterar a representação de matrizes
Numpy de ponto flutuante para %3.2f, podemos fazer o seguinte:
np.set_printoptions(formatter={'float':lambda i:'%3.2f'%i}

Referências

1. TE Oliphant, A Guide to NumPy (Trelgol Publishing, Austin, 2006)


127
Capítulo 5
Pandas

Pandas é um módulo poderoso que é otimizado em cima do Numpy e fornece um


conjunto de estruturas de dados particularmente adequado para séries temporais e
análises de dados no estilo planilha (pense em tabelas dinâmicas no Excel). Se você
está familiarizado com o pacote R estatístico, então você pode pensar nos Pandas
como fornecendo um dispositivo movido a Numpy DataFrame para Python. O
Pandas fornece um objeto DataFrame (entre outros) construído em uma plataforma
Numpy para facilitar a manipulação de dados (especialmente para séries temporais)
para processamento estatístico. O Pandas é particularmente popular em finanças
quantitativas. Os principais recursos do Pandas incluem manipulação e alinhamento
rápidos de dados, ferramentas para troca de dados entre diferentes formatos e entre
bancos de dados SQL, tratamento de dados perdidos e limpeza de dados confusos.

5.1 Usando a série

A maneira mais fácil de pensar sobre os objetos da série Pandas é como um contêiner
para duas matrizes Numpy, uma para o índice e outra para os dados. Lembre-se de
que os arrays Numpy já possuem indexação de inteiros, assim como as listas normais
do Python.
>>> import pandas as pd
>>> x = pd.Series([1,2,30,0,15,6])
>>> x
0 1
1 2
2 30
3 0
4 15
5 6
dtype: int64

Este objeto pode ser indexado no estilo Numpy simples,


>>> x [1:3] # Numpy
1 2
128 Capítulo 5
2 30
dtype: int64

Também podemos obter os arrays Numpy diretamente,


>>> x.values # values
array([ 1, 2, 30, 0, 15, 6])
>>> x.values[1:3]
array([ 2, 30])
>>> x.index # index is Numpy array-like
RangeIndex(start=0, stop=6, step=1)

Ao contrário dos arrays Numpy, você pode ter tipos mistos,


>>> s = pd.Series([1,2,'anything','more stuff'])
>>> s
0 1
1 2
2 anything
3 more stuff
dtype: object
>>> s.index # Series index
RangeIndex(start=0, stop=4, step=1)
>>> s[0] # The usual Numpy slicing rules apply
1
>>> s[:-1]
0 1
1 2
2 anything
dtype: object
>>> s.dtype # object data type
dtype('O')

Cuidado que tipos mistos em uma única coluna podem levar a ineficiências de
downstream e outros problemas. O índice na série pd.Series generaliza além da
indexação de inteiros. Por exemplo,
>>> s = pd.Series([1,2,3],index=['a','b','cat'])
>>> s['a']
1
>>> s['cat']
3

Por causa de seu legado como uma ferramenta de processamento de dados financeiros
(ou seja, preços de ações), o Pandas é realmente bom no gerenciamento de séries
temporais
>>> dates = pd.date_range('20210101',periods=12)
>>> s = pd.Series(range(12),index=dates) # explicitly assign index
>>> s # default is calendar-daily
2021-01-01 0
2021-01-02 1
2021-01-03 2
2021-01-04 3
2021-01-05 4
2021-01-06 5
2021-01-07 6
Capítulo 5 129

Fig. 5.1 Gráfico rápido do objeto Series

2021-01-08 7
2021-01-09 8
2021-01-10 9
2021-01-11 10
2021-01-12 11
Freq: D, dtype: int64

Você pode fazer algumas estatísticas descritivas básicas sobre os dados (não o
índice!)
>>> s.mean()
5.5
>>> s.std()
3.605551275463989

Você também pode plotar (veja a Fig. 5.1) a Series usando seus métodos:
>>> s.plot(kind='bar',alpha=0.3) # can add extra matplotlib
c→ keywords

Dados podem ser resumidos pelo índice. Por exemplo, para contar os dias individuais
da semana para os quais temos dados:
>>> s.groupby(by=lambda i:i.dayofweek).count()
0 2
1 2
2 1
3 1
130 Capítulo 5
4 2
5 2
6 2
dtype: int64

Observe que a convenção 0 é segunda-feira, 1 é terça-feira e assim por diante.


Portanto, havia apenas um domingo (dia 6) no conjunto de dados. O método
groupby divide os dados em grupos separados com base no predicado fornecido
por meio do argumento de palavra-chave by. Considere a seguinte Series,
>>> x = pd.Series(range(5),index=[1,2,11,9,10])
>>> x
1 0
2 1
11 2
9 3
10 4
dtype: int64

Vamos agrupá-lo da seguinte forma, de acordo com os elementos nos valores são
pares ou ímpares usando o módulo do operador (%),
>>> grp = x.groupby(lambda i:i%2) # odd or even
>>> grp.get_group(0) # even group
2 1
10 4
dtype: int64
>>> grp.get_group(1) # odd group
1 0
11 2
9 3
dtype: int64

A primeira linha agrupa os elementos daobjetos Series se o índice é par ou ímpar.


A função lambda retorna 0 ou 1 dependendo se o índice correspondente é par ou
ímpar, respectivamente. A próxima linha mostra o grupolinha seguinte mostra o 0
(ou seja, par) e, em seguida, agrupo 1 (ímpar). Agora que temos grupos separados,
podemos realizar uma ampla variedade de resumos em cada um para reduzi-los a um
único valor. Por exemplo, a seguir, obtemos o valor máximo de cada grupo:
>>> grp.max() # max in each group
0 4
1 3
dtype: int64

Observe que a operação acima retorna outro objeto Series com um correspondente
index aos elementos [0,1]. Haverá tantos grupos quantas forem as saídas
exclusivas da função by.
Capítulo 5 131
5.2 Usando DataFrame

Enquanto o objeto Series pode ser pensado como um encapsulamento de duas


matrizes Numpy (índice e valores), o Pandas DataFrame é um encapsulamento de
um grupo de objetos Series que compartilham um único índice. Podemos criar um
DataFrame com dicionários como a seguir:
>>> df = pd.DataFrame({'col1': [1,3,11,2], 'col2': [9,23,0,2]})
>>> df
col1 col2
0 1 9
1 3 23
2 11 0
3 2 2

Observe que as chaves no dicionário de entrada agora são os títulos das colunas
(rótulos) do DataFrame, com cada coluna correspondente correspondendo à lista
de valores correspondentes do dicionário. Assim como o objeto Series, o
DataFrame também possui um index, que é a coluna [0,1,2,3] na extrema
esquerda. Podemos extrair elementos de cada coluna usando o iloc, que ignora o
índice dado e retorna ao corte Numpy tradicional,
>>> df.iloc[:2,:2] # get section
col1 col2
0 1 9
1 3 23

ou por corte ou directamente usando a ponto, notação de como mostrado abaixo:


>>> df['col1'] # indexing
0 1
1 3
2 11
3 2
Name: col1, dtype: int64
>>> df.col1 # use dot notation
0 1
1 3
2 11
3 2
Name: col1, dtype: int64

Dica de programação: espaços em nomes de coluna


Contanto que os nomes das colunas no DataFrame não contém espaços ou
outros eval-able sintaxe como hífens, você pode usar o acesso de estilo de
atributo de notação de ponto para os valores da coluna. Você pode relatar os nomes
das colunas usando df.columns.

As operações subsequentes no DataFrame preservam sua estrutura em colunas


como a seguir:
132 Capítulo 5
>>> df.sum()
col1 17
col2 34
dtype: int64

onde cada coluna foi totalizada. Agrupar e agregar com o DataFrame é ainda mais
poderoso do que com Series. Vamos construir o seguinte DataFrame,
>>> df = pd.DataFrame({'col1': [1,1,0,0], 'col2': [1,2,3,4]})
>>> df
col1 col2
0 1 1
1 1 2
2 0 3
3 0 4

No DataFrame acima, observe que a coluna col1 possui apenas duas entradas
distintas. Podemos agrupar os dados usando esta coluna da seguinte forma:
>>> grp=df.groupby('col1')
>>> grp.get_group(0)
col1 col2
2 0 3
3 0 4
>>> grp.get_group(1)
col1 col2
0 1 1
1 1 2

Observe que cada grupo corresponde às entradas para as quais col1 era um de seus
dois valores. Agora que agrupamos em col1, como no objeto Series, também
podemos resumir funcionalmente cada um dos grupos da seguinte forma:
>>> grp.sum()
col2
col1
0 7
1 3

onde a sum é aplicada em cada um dos dataframes presentes em cada grupo. Observe
que o index da saída acima é cada um dos valores da original col1.
O DataFrame pode calcular novas colunas com base nas colunas existentes
usando o método eval conforme mostrado abaixo:
>>> df['sum_col']=df.eval('col1+col2')
>>> df
col1 col2 sum_col
0 1 1 2
1 1 2 3
2 0 3 3
3 0 4 4

Observe que você pode atribuir a saída a uma nova coluna para o DataFrame como
mostrado. Podemos agrupar por várias colunas como mostrado abaixo:
Capítulo 5 133
>>> grp = df.groupby(['sum_col','col1'])

Fazendo da operação sum em cada grupo resulta o seguinte:


>>> res = grp.sum()
>>> res
col2
sum_col col1

2 1 1
3 0 3
1 2
4 0 4

Esta saída é muito mais complicada do que qualquer coisa que vimos até agora, então
vamos examiná-la cuidadosamente. Abaixo dos cabeçalhos, a primeira linha 2 1 1
indica que para sum_col = 2 e para todos os valores de col1 (ou seja, apenas o valor
1), o valor de col2 é 1. Para a próxima linha, o mesmo padrão se aplica, exceto que
para sum_col = 3, há agora dois valores para col1, a saber 0 e 1, cada um com seus
dois valores correspondentes para cada operação sum em col2. Essa exibição em
camadas é uma maneira de ver o resultado. Observe que as camadas acima não são
uniformes. Como alternativa, podemos unstack este resultado para obter a seguinte
visualização tabular do resultado anterior:
>>> res.unstack()
col2
col1 0 1
sum_col
2 NaN 1.0
3 3.0 2.0
4 4.0 NaN

Os valores NaN indicam posições na tabela onde não há entrada. Por exemplo, para
o par (sum_col = 2, col2 = 0), não há nenhum valor correspondente no
DataFrame, como você pode verificar olhando para o penúltimo bloco de código.
Também não há nenhuma entrada correspondente ao par (sum_col=4,col2=1) .
Assim, isso mostra que a apresentação original no penúltimo bloco de código é a
mesma que este, apenas sem as entradas faltantes mencionadas acima indicadas por
NaN.
Vamos continuar com a indexação de dataframes.
>>> import numpy as np
>>> data=np.arange(len(dates)*4).reshape(-1,4)
>>> df = pd.DataFrame(data,index=dates,
... columns=['A','B','C','D' ])
>>> df
A B C D
2021-01-01 0 1 2 3
2021-01-02 4 5 6 7
2021-01-03 8 9 10 11
2021-01-04 12 13 14 15
2021-01-05 16 17 18 19
2021-01-06 20 21 22 23
2021-01-07 24 25 26 27
2021-01-08 28 29 30 31
2021-01-09 32 33 34 35
2021-01-10 36 37 38 39
134 Capítulo 5
2021-01-11 40 41 42 43
2021-01-12 44 45 46 47

Agora, você pode acessar cada uma das colunas por nome, da seguinte forma:
>>> df['A']
2021-01-01 0
2021-01-02 4
2021-01-03 8
2021-01-04 12
2021-01-05 16
2021-01-06 20
2021-01-07 24
2021-01-08 28
2021-01-09 32
2021-01-10 36
2021-01-11 40
2021-01-12 44
Freq: D, Name: A, dtype: int64

Ou, usando uma notação de estilo de atributo mais rápida


>>> df.A
2021-01-01 0
2021-01-02 4
2021-01-03 8
2021-01-04 12
2021-01-05 16
2021-01-06 20
2021-01-07 24
2021-01-08 28
2021-01-09 32
2021-01-10 36
2021-01-11 40
2021-01-12 44
Freq: D, Name: A, dtype: int64

Agora, podemos fazer alguns cálculos básicos e indexação.


>>> df.loc[:dates[3]] # unlike the Python convention, this
c→ includes endpoints!
A B C D
2021-01-01 0 1 2 3
2021-01-02 4 5 6 7
2021-01-03 8 9 10 11
2021-01-04 12 13 14 15
>>> df.loc[:,'A':'C'] # all rows by slice of column labels
A B C
2021-01-01 0 1 2
2021-01-02 4 5 6
2021-01-03 8 9 10
2021-01-04 12 13 14
2021-01-05 16 17 18
2021-01-06 20 21 22
2021-01-07 24 25 26
2021-01-08 28 29 30
2021-01-09 32 33 34
Capítulo 5 135
2021-01-10 36 37 38
2021-01-11 40 41 42
2021-01-12 44 45 46

5.3 Reindexando

Um DataFrame ou Series tem um índice que pode alinhar dados usando


reindexação,
>>> x = pd.Series(range(3),index=['a','b','c'])
>>> x
a 0
b 1
c 2
dtype: int64
>>> x.reindex(['c','b','a','z'])
c 2.0
b 1.0
a 0.0
z NaN
dtype: float64

Observe como o objeto Series recém-criado tem um novo índice e preenche os itens
ausentes com NaN. Você pode preencher por outros valores usando o argumento de
palavra-chave fill_value na reindexação. Também é possível fazer back-fill e
forward-fill (ffill) de valores ao trabalhar com dados ordenados como a seguir:
>>> x = pd.Series(['a','b','c'],index=[0,5,10])
>>> x
0 a
5 b
10 c
dtype: object
>>> x.reindex(range(11),method='ffill')
0 a
1 a
2 a
3 a
4 a
5 b
6 b
7 b
8 b
9 b
10 c
dtype: object

Métodos de interpolação mais complicados são possíveis, mas não usando reindexar
diretamente. A reindexação também se aplica a dataframes, mas em uma ou em
ambas as dimensões.
>>> df = pd.DataFrame(index=['a','b','c'],
... columns=['A','B','C','D'],
... data = np.arange(3*4).reshape(3,4))
136 Capítulo 5
>>> df
A B C D
a 0 1 2 3
b 4 5 6 7
c 8 9 10 11
Agora, podemos reindexar isso pelo índice como no seguinte:
>>> df.reindex(['c','b','a','z'])
A B C D
c 8.0 9.0 10.0 11.0
b 4.0 5.0 6.0 7.0
a 0.0 1.0 2.0 3.0
z NaN NaN NaN NaN

Observe como o elemento ausente z foi preenchido como com o objeto da Série
anterior. O mesmo comportamento se aplica à reindexação das colunas como no
seguinte:
>>> df.reindex(columns=['D','A','C','Z','B'])
D A C Z B
a 3 0 2 NaN 1
b 7 4 6 NaN 5
c 11 8 10 NaN 9

Novamente, temos o mesmo comportamento de preenchimento, agora apenas


aplicado às colunas. O mesmo back-fill e forward-fill funcionam com colunas/índices
ordenados como antes.

5.4 Excluindo itens

Retornando ao nosso objeto Series anterior,


>>> x = pd.Series (range(3), index=['a','b','c'])

Para se livrar dos dados no 'a' índice, podemos usar o método drop que retornará
um novo objeto Series com os dados especificados removidos,
>>> x.drop('a')
b 1
c 2
dtype: int64

Tenha em mente que este é um novo objeto Series, a menos que usemos o argumento
de palavra-chave inplace ou explicitamente usando del,
>>> del x ['a']

O mesmo padrão se aplica a DataFrames .


>>> df = pd.DataFrame(index=['a','b','c'],
... columns=['A','B','C','D'],
... data = np.arange(3*4).reshape(3,4))
>>> df.drop('a')
A B C D
b 4 5 6 7
c 8 9 10 11
Capítulo 5 137
Ou, ao longo da dimensão da coluna,
>>> df.drop('A',axis=1)
B C D
a 1 2 3
b 5 6 7
c 9 10 11

Novamente, os mesmos comentários sobre o uso de del e inplace também se


aplicam a DataFrames.

5.5 Indexação Avançada

Pandas fornece um fatiamento muito poderoso e rápido,


>>> x = pd.Series(range(4),index=['a','b','c','d'])
>>> x['a':'c']
a 0
b 1
c 2
dtype: int64

Nota que, ao contrário da indexação regular do Python, ambos os pontos de


extremidade são incluídos aqui ao fatiar com rótulos. Isso também pode ser usado
para atribuir valores como a seguir:
>>> x['a':'c']=999
>>> x
a 999
b 999
c 999
d 3
dtype: int64

O comportamento análogo se aplica a DataFrames.


>>> df = pd.DataFrame(index=['a','b','c'],
... columns=['A','B','C','D'],
... data = np.arange(3*4).reshape(3,4))
>>> df['a':'b']
A B C D
a 0 1 2 3
b 4 5 6 7

Você pode escolher colunas individuais sem os dois pontos (:)


>>> df[['A','C']]
A C
a 0 2
b 4 6
c 8 10

A mistura de fatias baseadas em rótulos com fatias de cólon do tipo Numpy é possível
usando loc,
138 Capítulo 5
>>> df.loc['a':'b',['A','C']]
A C
a 02
b 46

A ideia é que o primeiro argumento para os índices loc as linhas e o segundo indexe
as colunas. As heurísticas permitem a indexação do tipo Numpy sem se preocupar
com os rótulos. Você pode voltar para a indexação simples do Numpy com iloc.
>>> df.iloc[0,-2:]
C 2
D 3
Name: a, dtype: int64

5.6 Broadcasting e Alinhamento de Dados

A principal coisa a se ter em mente ao operar em uma ou mais Series ou objetos


DataFrame é que o index sempre alinha o cálculo.
>>> x = pd.Series(range(4),index=['a','b','c','d'])
>>> y = pd.Series(range(3),index=['a','b','c'])

Note-se que Y está ausente, um dos índicesem, xpor isso, quando adicioná-los,
>>> x+y
a 0.0
b 2.0
c 4.0
d NaN
dtype: float64

Observe que, como y faltava um dos índices em, ele foi preenchido com um NaN.
Este comportamento também se aplica a dataframes,
>>> df = pd.DataFrame(index=['a','b','c'],
... columns=['A','B','C','D'],
... data = np.arange(3*4).reshape(3,4))
>>> ef = pd.DataFrame(index=list('abcd'),
... columns=list('ABCDE'),
... data = np.arange(4*5).reshape(4,5))
>>> ef
A B C D E
a 0 1 2 3
4
b 5 6 7 8 9
c 10 11 12 13 14
d 15 16 17 18 19
>>> df
A B C D
a 0 1 2 3
b 4 5 6 7
c 8 9 10 11
>>> df+ef
A B C D E
Capítulo 5 139
a 0.0 2.0 4.0 6.0 NaN
b 9.0 11.0 13.0 15.0 NaN
c 18.0 20.0 22.0 24.0 NaN
d NaN NaN NaN NaN NaN

Observe que os elementos não sobrepostos são preenchidos com NaN. Para operações
simples, você pode especificar o valor de preenchimento usando a operação nomeada.
Por exemplo, no último caso,
>>> df.add(ef,fill_value=0)
A B C D E
a 0.0 2.0 4.0 6.0 4.0
b 9.0 11.0 13.0 15.0 9.0
c 18.0 20.0 22.0 24.0 14.0
d 15.0 16.0 17.0 18.0 19.0

>>> s = df.loc['a'] # take first row


>>> s
A 0
B 1
C 2
D 3
Name: a, dtype: int64

Quando adicionamos este objeto Series com ocompleto DataFrame, obtemos o


seguinte:
>>> s + df
A B C D
a 0 2 4
6
b 4 6 8 10
c 8 10 12 14

Compare isso com ooriginal DataFrame,


>>> df
A B C D
a 0 1 2 3
b 4 5 6 7
c 8 9 10 11

Isso mostra que o objeto Series foi transmitido para baixo as linhas, alinhando com
as colunas no DataFrame. Aqui está um exemplo de um objeto Series diferente que
está faltando algumas das colunas no DataFrame,
>>> s = pd.Series([1,2,3],index=['A','D','E'])
>>> s+df
A B C D E
a 1.0 NaN NaN 5.0 NaN
b 5.0 NaN NaN 9.0 NaN
c 9.0 NaN NaN 13.0 NaN

Observe que a transmissão ainda ocorre nas linhas, alinhando com as colunas, mas
preenche as entradas ausentes com NaN.
Aqui está um teste rápido de Python que usa expressões regulares para testar
números primos relativamente pequenos,
140 Capítulo 5
>>> import re
>>> pattern = r'^1?$|^(11+?)\1+$'
>>> def isprime(n):
... return (re.match(pattern, '1'*n) is None) #*
...

Agora, podemos descobrir qual rótulo de coluna tem mais números primos
>>> df.applymap(isprime)
A B C D
a False False True True
b False True False True
c False False False True

Os booleanos são convertidos automaticamente na sum abaixo:


>>> df.applymap(isprime).sum()
A 0
B 1
C 1
D 3
dtype: int64

Isso apenas arranha a superfície dos tipos de análise de dados de fluidos que são quase
automáticos usando Pandas.

Dica de programação: Pandas Performance


Pandas groupby, apply e applymap são flexíveis e poderosos, mas o Pandas
precisa avaliá-los no interpretador Python e não no código Pandas otimizado, o
que resulta em uma desaceleração significativa. Portanto, é sempre melhor usar as
funções que são construídas no próprio Pandas, em vez de definir funções Python
puras para alimentar esses métodos.

5.7 O Categorical and Merging

Pandas oferece suporte a algumas operações algébricas relacionais, como junções de


tabelas.
>>> df = pd.DataFrame(index=['a','b','c'],
... columns=['A','B','C'],
... data = np.arange(3*3).reshape(3,3))
>>> df
A B C
a 0 1 2
b 3 4 5
c 6 7 8
>>> ef = pd.DataFrame(index=['a','b','c'],
... columns=['A','Y','Z'],
... data = np.arange(3*3).reshape(3,3))
>>> ef
Capítulo 5 141
A Y Z
a 0 1 2
b 3 4 5
c 6 7 8

A junção da tabela é implementada na função de merge.


>>> pd.merge(df,ef,on='A')
A B C Y Z
0 0 1 2 1 2
1 3 4 5 4 5
2 6 7 8 7 8

O argumento de palavra-chave on diz para mesclar os dois DataFrames onde eles


têm correspondência entradas na coluna A. Observe que o índice não foi preservado
na mesclagem. Para tornar as coisas mais interessantes, vamos fazer o DataFrame ef
diferente, eliminando uma das linhas,
>>> ef.drop('b',inplace=True)
>>> ef
A Y Z
a 0 1 2
c 6 7 8

Agora, vamos tentar a fusão novamente.


>>> pd.merge(df,ef,on='A')
A B C Y Z
0 0 1 2 1 2
1 6 7 8 7 8

Observe que apenas os elementos de A que correspondem a ambos os DataFrames


(ou seja, estão na interseção de ambos) são preservados. Podemos alterar isso usando
o argumento de palavra-chave how.
>>> pd.merge(df,ef,on='A',how='left')
A B C Y Z
0 0 1 2 1.0 2.0
1 3 4 5 NaN NaN
2 6 7 8 7.0 8.0

O argumento de palavra-chave how=left informa a junção para manter todas as


chaves no DataFrame esquerdo (df neste caso) e preencher com NaN sempre que
faltar no DataFrame direito (ef). Se ef tiver elementos ao longo de A ausentes em
df, eles desapareceriam,
>>> ef = pd.DataFrame(index=['a','d','c'],
... columns=['A','Y','Z'],
... data = 10*np.arange(3*3).reshape(3,3))
>>> ef
A Y Z
a 0 10 20
d 30 40 50
c 60 70 80
142 Capítulo 5
>>> pd.merge(df,ef,on='A',how='left')
A B C Y Z
0 0 1 2 10.0 20.0
1 3 4 5 NaN NaN
2 6 7 8 NaN NaN

Da mesma forma, podemos fazer how=right para use as chaves corretas do


DataFrame,
>>> pd.merge(df,ef,on='A',how='right')
A B C Y Z
0 0 1.0 2.0 10 20
1 30 NaN NaN 40 50
2 60 NaN NaN 70 80

Podemos usar how = outer para obter o união das chaves,


>>> pd.merge(df,ef,on='A',how='outer')
A B C Y Z
0 0 1.0 2.0 10.0 20.0
1 3 4.0 5.0 NaN NaN
2 6 7.0 8.0 NaN NaN
3 30 NaN NaN 40.0 50.0
4 60 NaN NaN 70.0 80.0

Outra tarefa comum é dividir os dados contínuos em compartimentos discretos.


>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> bins = [0,5,10]
>>> cats = pd.cut(a,bins)
>>> cats
[NaN, (0.0, 5.0], (0.0, 5.0], (0.0, 5.0], (0.0, 5.0], (0.0, 5.0],
(5.0, 10.0], (5.0, 10.0], (5.0, 10.0], (5.0, 10.0]]
Categories (2, interval[int64]): [(0, 5] < (5, 10]]

A função pd.cut leva os dados no array e os coloca na variável categórica cats.


>>> cats.categories
IntervalIndex([(0, 5], (5, 10]],
closed='right',
dtype='interval[int64]')

O meio- intervalos abertos indicam os limites de cada categoria. Você pode alterar a
paridade dos intervalos passando o argumento de palavra-chave right = False.
>>> cats.codes
array([-1, 0, 0, 0, 0, 0, 1, 1, 1, 1], dtype=int8)

O -1 acima significa que o 0 não está incluído em nenhuma das duas categorias
porque o intervalo está aberto à esquerda. Você pode contar o número de elementos
em cada categoria, conforme mostrado a seguir ,
>>> pd.value_counts(cats)
(0, 5] 5
(5, 10] 4
dtype: int64
Capítulo 5 143
Nomes descritivos para cada categoria podem ser passados usando o argumento de
palavra-chave labels.
>>> cats = pd.cut(a,bins,labels=['one','two'])
>>> cats
[NaN, 'one', 'one', 'one', 'one', 'one', 'two', 'two', 'two',
e→ 'two']
Categories (2, object): ['one' < 'two']

Observe que se você passar um argumento inteiro para bins, ele será
automaticamente dividido em categorias de tamanhos iguais. A função qcut é muito
semelhante, exceto que se divide em quartis.
>>> a = np.random.rand(100) # uniform random variables
>>> cats = pd.qcut(a,4,labels=['q1','q2','q3','q4'])
>>> pd.value_counts(cats)
q4 25
q3 25
q2 25
q1 25
dtype: int64

5.8 Uso de memória e dtypes

Você se verá processando muitos dados com o Pandas. Aqui estão algumas dicas para
fazer isso de forma eficiente. Primeiro, precisamos do conjunto de dados Penguins
da Seaborn,
>>> import seaborn as sns
>>> df = sns.load_dataset('penguins')
>>> df.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 Male
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 Female
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 Female
3 Adelie Torgersen NaN NaN NaN NaN NaN
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 Female

Este não é um particularmente grande conjunto de dados, mas será suficiente. Vamos
examinar os dtypes do DataFrame,
>>> df.dtypes
species object
island object
bill_length_mm float64
bill_depth_mm float64
flipper_length_mm float64
body_mass_g float64
sex object
dtype: object
144 Capítulo 5
Observe que alguns deles estão marcados object. Isso geralmente significa
ineficiência porque esse tipo de d generalizado pode consumir uma quantidade
excessiva de memória. O Pandas vem com uma maneira simples de avaliar o
consumo de memória do seu DataFrame,
>>> df.memory_usage(deep=True)
Index 128
species 21876
island 21704
bill_length_mm 2752
bill_depth_mm 2752
flipper_length_mm 2752
body_mass_g 2752
sex 20995
dtype: int64

Agora, temos uma ideia do nosso consumo de memória para este DataFrame, e
podemos melhorá-lo alterando os dtypes. O tipo categórico que discutimos
anteriormente pode ser especificado como um novo tipo d para a coluna sex,
>>> ef = df.astype({'sex':'category'})
>>> ef.memory_usage(deep=True)
Index 128
species 21876
island 21704
bill_length_mm 2752
bill_depth_mm 2752
flipper_length_mm 2752
body_mass_g 2752
sex 548
dtype: int64

Isso resulta em quase uma redução de 40 vezes na memória para isso, o que pode ser
significativo se o DataFrame tiver milhares de linhas, por exemplo. Isso funciona
porque há muito mais linhas do que valores distintos na coluna sex. Vamos continuar
usando category como o dtype para as colunas species and island.
>>> ef = df.astype({'sex':'category',
... 'species':'category',
... 'island':'category'})
>>> ef.memory_usage(deep=True)
Index 128
species 616
island 615
bill_length_mm 2752
bill_depth_mm 2752
flipper_length_mm 2752
body_mass_g 2752
sex 548
dtype: int64
Capítulo 5 145
Para comparar, podemos colocá-los lado a lado em um novo DataFrame,
>>> (pd.DataFrame({'df':df.memory_usage(deep=True),
... 'ef':ef.memory_usage(deep=True)})
... .assign(ratio= lambda i:i.ef/i.df))
df ef ratio
Index 128 128 1.000000
species 21876 616 0.028159
island 21704 615 0.028336
bill_length_mm 2752 2752 1.000000
bill_depth_mm 2752 2752 1.000000
flipper_length_mm 2752 2752 1.000000
body_mass_g 2752 2752 1.000000
sex 20995 548 0.026101

Isto mostra um espaço de memória muito menor para as colunas que se alterar a tipo
categórico. Também podemos alterar os tipos numéricos dopadrão float64, se não
precisarmos desse nível de precisão. Por exemplo, a coluna flipper_length_mm é
medida em milímetros e não há parte fracionária em nenhuma das medições. Assim,
podemos alterar essa coluna como o seguinte tipo d e salvar quatro vezes a memória,
>>> ef = df.astype({'sex':'category',
... 'species':'category',
... 'island':'category',
... 'flipper_length_mm': np.float16})
>>> ef.memory_usage(deep=True)
Index 128
species 616
island 615
bill_length_mm 2752
bill_depth_mm 2752
flipper_length_mm 688
body_mass_g 2752
sex 548
dtype: int64

Aqui está o resumo novamente,


>>> (pd.DataFrame({'df':df.memory_usage(deep=True),
... 'ef':ef.memory_usage(deep=True)})
... .assign(ratio= lambda i:i.ef/i.df))
df ef ratio
Index 128 128 1.000000
species 21876 616 0.028159
island 21704 615 0.028336
bill_length_mm 2752 2752 1.000000
bill_depth_mm 2752 2752 1.000000
flipper_length_mm 2752 688 0.250000
body_mass_g 2752 2752 1.000000
sex 20995 548 0.026101

Deste modo, alterando o dtypes padrão object para outros dtypes mais pequenas
pode resultar numa economia significativa e potencialmente acelerar o
processamento a jusante para dataframes .
146 Capítulo 5
Isso é particularmente verdadeiro ao extrair dados da web diretamente em dataframes
usando pd.read_html, que, embora os dados numéricos na página da web,
normalmente resultará em um desnecessariamente pesado dtype object.

5.9 Operações Comuns

Um problema comum é como dividir uma coluna de string em componentes. Por


exemplo,
>>> df = pd.DataFrame(dict(name=['Jon Doe','Jane Smith']))
>>> df.name.str.split(' ',expand=True)
0 1
0 Jon Doe
1 Jane Smith

A etapa principal é o argumento de palavra-chave expand que converte o resultado


em um DataFrame. O resultado pode ser atribuído ao mesmo DataFrame usando o
seguinte,
>>> df[['first','last']]=df.name.str.split(' ',expand=True)
>>> df
name first last
0 Jon Doe Jon Doe
1 Jane Smith Jane Smith

Observe que a falha em usar o argumento de palavra-chave expand resulta em uma


lista de saída em vez de um DataFrame
>>> df.name.str.split(' ')
0 [Jon, Doe]
1 [Jane, Smith]
Name: name, dtype: object

Isso pode ser corrigido usando apply na saída para converter de uma list em um
objeto Series como mostrado,1
>>> df.name.str.split(' ').apply(pd.Series)
0 1
0 Jon Doe
1 Jane Smith

O método apply é um dos métodos mais poderosos e gerais DataFrame. Ele


opera nas colunas individuais dos objetos (ou seja, pd.Series) do DataFrame.
Ao contrário do método applymap que deixa a forma do DataFrame inalterada, o
método apply pode retornar objetos de uma forma diferente. Por exemplo, fazer
algo como df.apply (lambda i: i [i> 3]) em uma DataFrame com linhas
numéricas retornará um pequeno truncado NaN-filled DataFrame.

1Existem muitos outros métodos de string Python no submódulo .str , como rstrip, upper e
title.
Capítulo 5 147
Além disso, o argumento de palavra-chave df.apply (raw = True) acelera o
método operando diretamente na matriz Numpy subjacente nas colunas DataFrame.
Isso significa que o 'apply' método processa as matrizes Numpy diretamente em
vez dos objetos usuais ’pd.Series’.
O método transform está intimamente relacionada à apply , mas deve
produzir uma saída DataFrame com as mesmas dimensões. Por exemplo,
>>> df = pd.DataFrame({'A': [1,1,2,2], 'B': range(4)})
>>> df
A B
0 1 0
1 1 1
2 2 2
3 2 3

Podemos agrupar por ' A ' e reduza usando a agregação usual,


>>> df.groupby('A').sum()
B
A
1 1
2 5

Mas usando .transform() podemos transmitir os resultados para seus respectivos


locais no original DataFrame.
>>> df.groupby('A').transform('sum')
B
0 1
1 1
2 5
3 5

O método DataFrame describe é útil para resumir um determinado DataFrame,


como mostrado abaixo,
>>> df = pd.DataFrame(index=['a','b','c'], columns=['A','B','C'],
e→ data = np.arange(3*3).reshape(3,3))
>>> df.describe()
A B C
count 3.0 3.0 3.0
mean 3.0 4.0 5.0
std 3.0 3.0 3.0
min 0.0 1.0 2.0
25% 1.5 2.5 3.5
50% 3.0 4.0 5.0
75% 4.5 5.5 6.5
max 6.0 7.0 8.0

É frequentemente útil para se livrar de acidentes entradas duplicadas.


>>> df = pd.DataFrame({'A': [1,1,2,2,2,3], 'B': range(6)})
>>> df.drop_duplicates('A')
A B
0 1 0
2 2 2
5 3 5

O argumento de palavra-chave keep decide qual das entradas duplicadas reter.


148 Capítulo 5
5.10 Exibindo DataFrames

Pandas tem o método set_option para alterar a exibição visual de DataFrames sem
alterar os elementos de dados correspondentes.
>>> pd.set_option('display.float_format','{:.2f}'.format)

Observe que o argumento pode ser callable que produz a string formatada. Essas
configurações personalizadas podem ser desfeitas com reset_option, como em
pd.reset_option('display.float_format')

A opção chop é útil para cortar a precisão excessiva da tela,


>>> pd.set_option('display.chop',1e-5)

Dentro do Jupyter Notebook, a formatação pode utilizar elementos HTML com o


método DataFrame style.format. Por exemplo, conforme mostrado na Fig. 5.2,
>>> from pandas_datareader import data
>>> df=data.DataReader("F", 'yahoo', '20200101',
e→ '20200110').reset_index()

>>> (df.style.format(dict(Date='{:%m/%d/%Y}'))
... .hide_index()
... .highlight_min('Close',color='red')
... .highlight_max('Close',color='lightgreen')
... )
<pandas.io.formats.style.Styler object at 0x7f9376a22460>

formata a tabela resultante com o preço de fechamento mínimo em destaque na


vermelho e o preço máximo de fechamento destacado em verde. Fornecer esses tipos
de dicas visuais rápidas é extremamente importante para escolher os principais
elementos de dados. Note-se que o parêntesis acima é para utilizar as novas linhas
para separar os métodos de dot, que é um estilo herdado do DataFrame R. A etapa
principal é expor a formatação de estilo com format() e usar seus métodos para
estilizar o HTML resultante

. Fig. 5.2 Itens destacados em HTML no DataFrame


Capítulo 5 149

Fig. 5.3 Gradientes de cores personalizados para renderização de HTML DataFrame

Fig. 5.4 Gráficos de barras de fundo personalizados para DataFrame

tabela no Notebook Jupyter. O seguinte muda o gradiente visual de acordo com a cor
da coluna Volume como na Fig. 5.3.
>>> (df.style.format(dict(Date='{:%m/%d/%Y}'))
... .hide_index()
... .background_gradient(subset='Volume',cmap='Blues')
... )
<pandas.io.formats.style.Styler object at 0x7f9376a8a0d0>

Gráficos de barras de fundo também podem ser incorporados na representação da


tabela no Jupyter Notebook, como a seguir (ver Fig. 5.4),
>>> (df.style.format(dict(Date='{:%m/%d/%Y}'))
... .hide_index()
... .bar('Volume',color='lightblue',align='zero')
... )
<pandas.io.formats.style.Styler object at 0x7f9374711f70>
150 Capítulo 5
5.11 Multi-índice

Encontramos o Pandas MultiIndex ao usar groupby com várias colunas, mas


podem ser criados separadamente:
>>> idx = pd.MultiIndex.from_product([['a','b'],[1,2,3]])
>>> data = 10*np.arange(6).reshape(6,1)
>>> df = pd.DataFrame(data=data,index=idx,columns=['A'])
>>> df
A
a 1 0
2 10
3 20
b 1 30
2 40
3 50

que é mais compacto do que o seguinte,


>>> df.reset_index()
level_0 level_1 A
0 a 1 0
1 a 2 10
2 a 3 20
3 b 1 30
4 b 2 40
5 b 3 50

Mas lembre-se que não demos ao índice um nome quando o criamos, o que explica o
uniformativo cabeçalhos, nível_0 e nível_1. Podemos trocar os dois níveis do
índice no DataFrame,
>>> df.swaplevel()
A
1 a 0
2 a 10
3 a 20
1 b 30
2 b 40
3 b 50

O pd.IndexSlice torna muito mais fácil indexar o DataFrame usando o


acessador loc,
>>> ixs = pd.IndexSlice
>>> df.loc[ixs['a',:],:]
A
a 1 0
2 10
3 20
>>> df.loc[ixs[:,2],:]
A
a 2 10
b 2 40
Capítulo 5 151
Observe que pode haver muitos mais níveis para um índice múltiplo. Eles também
podem ir no índice da coluna,
>>> rx = pd.MultiIndex.from_product([['a','b'],[1,2,3]])
>>> cx = pd.MultiIndex.from_product([['A','B','C'],[2,3]])
>>> data=[[2, 3, 9, 3, 4, 1],
... [9, 5, 9, 7, 2, 1],
... [9, 4, 4, 3, 2, 1],
... [1, 0, 4, 5, 5, 5],
... [5, 8, 1, 6, 1, 7],
... [0, 8, 9, 2, 1, 9]]
>>> df = pd.DataFrame(index=rx,columns=cx,data=data)
>>> df
A B C
2 3 2 3 2 3
a 1 2 3 9 3 4 1
2 9 5 9 7 2 1
3 9 4 4 3 2 1
b 1 1 0 4 5 5 5
2 5 8 1 6 1 7
3 0 8 9 2 1 9

Você pode usar pd.IndexSlice para colunas e linhas,


>>> df.loc[ixs['a',:],ixs['A',:]]=1
>>> df
A B C
2 3 2 3 2 3
a 1 1 1 9 3 4 1
2 1 1 9 7 2 1
3 1 1 4 3 2 1
b 1 1 0 4 5 5 5
2 5 8 1 6 1 7
3 0 8 9 2 1 9

É útil adicionar nomes aos níveis do índice,


>>> df.index = df.index.set_names(['X','Y'])
>>> df
A B C
2 3 2 3 2 3
X Y
a 1 1 1 9 3 4 1
2 1 1 9 7 2 1
3 1 1 4 3 2 1
b 1 1 0 4 5 5 5
2 5 8 1 6 1 7
3 0 8 9 2 1 9

Mesmo com esses complexos multi-índices nas linhas / colunas, o método groupby
ainda funciona, mas com uma especificação completa da coluna particular como
('B', 2),
>>> df.groupby(('B',2)).sum()
A B C
2 3 3 2 3
152 Capítulo 5
(B, 2)
1 5 8 6 1 7
4 2 1 8 7 6
9 2 10 12 7 11

Para entender como isso funciona, pegue a fatia da coluna e examine seus elementos
exclusivos. Isso explica os valores no índice de linha resultante da saída.
>>> df.loc[:,('B',2)].unique()
array([9, 4, 1])

Agora, temos que examinar as partições que são criadas no DataFrame por cada
um desses valores, como:
>>> df.groupby(('B',2)).get_group(4)
A B C
2 3 2 3 2 3
X Y
a 3 1 1 4 3 2 1
b 1 1 0 4 5 5 5

e, em seguida, a soma desses grupos produz a saída final. Você também pode usar a
função apply no grupo para calcular a saída não escalar. Por exemplo, para subtrair
o mínimo de cada elemento no grupo, podemos fazer o seguinte,
>>> df.groupby(('B',2)).apply(lambda i:i-i.min())
A B C
2 3 2 3 2 3
X Y
a 1 1 0 0 1 3 0
2 1 0 0 5 1 0
3 0 1 0 0 0 0
b 1 0 0 0 2 3 4
2 0 0 0 0 0 0
3 0 7 0 0 0 8

5.12 Pipes

Pandas implementa encadeamento de métodos com a função pipe. Mesmo que isso
não seja Pythônico, é mais fácil do que compor funções juntas que manipulam
dataframes de ponta a ponta.
>>> df = pd.DataFrame(index=['a','b','c'],
... columns=['A','B','C'],
... data = np.arange(3*3).reshape(3,3))
>>> df.pipe(lambda i:i*10).pipe(lambda i:3*i)
A B C
a 0 30 60
b 90 120 150
c 180 210 240
Capítulo 5 153
Suponha que precisamos encontrar os casos para os quais a soma das colunas é um
número ímpar. Podemos criar uma variável descartável intermediária t usando
assign e, em seguida, extrair a seção correspondente do DataFrame como a
seguir,
>>> df.assign(t=df.A+df.B+df.C).query('t%2==1').drop('t',axis=1)
A B C
a 0 1 2
c 6 7 8

O método assign aceita uma função cujo argumento é o DataFrame próprio ou o


nomeado DataFrame. O método query então filtra o resultado intermediário de
acordo com a estranheza de t e a etapa final é remover a variável t que não
precisamos mais na saída.

5.13 Arquivos de dados e bancos de dados

O Pandas possui utilitários de E/S poderosos para manipular planilhas do Excel e


CSV.
>>> df.to_excel('this_excel.file.xls')

Você verá que a planilha fornecida tem as datas formatadas de acordo com a
representação de data interna do Excel.
Se você tiver o PyTables instalado, poderá gravar na HDFStore. Você também
pode manipular HDF5 diretamente de PyTables.
>>> df.to_hdf('filename.h5','keyname')

Você pode ler isto mais tarde usando


>>> dg=pd.read_hdf('filename.h5','keyname')

para obter seus dados de volta. Você pode criar um banco de dados SQLite
imediatamente porque o SQLite está incluído no próprio Python.
>>> import sqlite3
>>> cnx = sqlite3.connect(':memory:')
>>> df = pd.DataFrame(index=['a','b','c'],
... columns=['A','B','C'],
... data = np.arange(3*3).reshape(3,3))
>>> df.to_sql('TableName',cnx)

Agora, podemos recarregar do banco de dados usando a álgebra relacional usual


>>> from pandas.io import sql
>>> p2 = sql.read_sql_query('select * from TableName', cnx)
>>> p2
index A B C
0 a 0 1 2
1 b 3 4 5
2 c 6 7 8
154 Capítulo 5
5.14 Personalizando Pandas

Desde Pandas 0.23, temos extensions.register_dataframe_accessor, que


permite fácil extensão de Pandas Dataframes/Series sem subclasses.
>>> df = pd.DataFrame(index=['a','b','c'],
... columns=['A','B','C','D'],
... data = np.arange(3*4).reshape(3,4))
>>> df
A B C D
a 0 1 2 3
b 4 5 6 7
c 8 9 10 11

O código a seguir define um acessador personalizado que se comporta como se fosse


um nativo método DataFrame.
>>> @pd.api.extensions.register_dataframe_accessor('custom')
... class CustomAccess:
... def init (self,df): # receives DataFrame
... assert 'A' in df.columns # some input validation
... assert 'B' in df.columns
... self._df = df
... @property # custom attribute
... def odds(self):
... 'drop all columns that have all even elements'
... df = self._df
... return df[df % 2==0].dropna(axis=1,how='all')
... def avg_odds(self): # custom method
... 'average only odd terms in each column'
... df = self._df
... return df[df % 2==1].mean(axis=0)
...

Agora, com isso estabelecido, podemos usar nosso novo método prefixado com o
namespace custom, como a seguir,
>>> df.custom.odds # as attribute
A C
a 0 2
b 4 6
c 8 10
>>> df.custom.avg_odds() # as method
A nan
B 5.00
C nan
D 7.00
dtype: float64

Importante, você pode usar qualquer palavra que desejar além de custom. Basta
especificá-lo no decorador.
Capítulo 5 155
O análogo register_series_accessor faz a mesma coisa para objetos Series e
o register_index_accessor para Index objetos.2

5.15 Operações de Rolling e Preenchimento

Devido ao legado do Pandas em finanças quantitativas, muitos cálculos de séries


temporais contínuos são fáceis de computar. Vamos carregar alguns dados de preços
de ações.
>>> from pandas_datareader import data
>>> df=data.DataReader("F", 'yahoo', '20200101',
e→ '20200130').reset_index()
>>> df.head()
Date High Low Open Close Volume Adj Close
0 2020-01-02 9.42 9.19 9.29 9.42 43425700.00 9.26
1 2020-01-03 9.37 9.15 9.31 9.21 45040800.00 9.06
2 2020-01-06 9.17 9.06 9.10 9.16 43372300.00 9.01
3 2020-01-07 9.25 9.12 9.20 9.25 44984100.00 9.10
4 2020-01-08 9.30 9.17 9.23 9.25 45994900.00 9.10

Podemos calcular a média sobre os três elementos à direita,


>>> df.rolling(3).mean().head(5)
High Low Open Close Volume Adj Close
0 nan nan nan nan nan nan
1 nan nan nan nan nan nan
2 9.32 9.13 9.23 9.26 43946266.67
9.11
3 9.26 9.11 9.20 9.21 44465733.33 9.05
4 9.24 9.12 9.18 9.22 44783766.67 9.07

Observe que só obtemos saídas válidas para uma janela final totalmente preenchida.
Se os pontos finais da janela são ou não usados no cálculo, é determinado pelo
argumento de palavra-chave closed. Além da janela retangular padrão, outras
janelas como Blackman e Hamming estão disponíveis. A função df.rolling()
produz um objeto Rolling com métodos como apply, aggregate e outros.
Semelhante ao roll, cálculos de janela exponencialmente ponderados podem ser
computados com o método ewm(),
>>> df.ewm(3).mean()
High Low Open Close Volume Adj Close
0 9.42 9.19 9.29 9.42 43425700.00 9.26
1 9.39 9.17 9.30 9.30 44348614.29 9.14
2 9.30 9.12 9.21 9.24 43926424.32 9.08
3 9.28 9.12 9.21 9.24 44313231.43 9.09
4 9.29 9.14 9.22 9.25 44864456.98 9.09
5 9.29 9.15 9.24 9.25 46979043.75 9.10
6 9.31 9.18 9.25 9.25 44906738.44 9.10
7 9.30 9.16 9.25 9.25 45919910.43 9.09
8 9.31 9.17 9.24 9.26 45113266.20 9.10

2O
módulo de limpeza de dados de terceiros pyjanitor utiliza essa abordagem extensivamente.
156 Capítulo 5
9 9.30 9.18 9.25 9.24 47977202.99 9.09
10 9.30 9.17 9.24 9.22 47020077.94 9.07
11 9.28 9.16 9.23 9.21 45632324.49 9.05
12 9.27 9.14 9.21 9.21 46637216.86 9.05
13 9.26 9.15 9.21 9.20 44926124.49 9.04
14 9.24 9.09 9.19 9.18 52761475.78 9.03
15 9.21 9.06 9.17 9.14 56635156.17 8.98
16 9.14 8.99 9.10 9.07 57676520.00 8.92
17 9.11 8.96 9.06 9.05 64587200.41 8.90
18 9.07 8.93 9.01 9.00 63198880.10 8.89
19 9.01 8.88 8.96 8.96 58089908.44 8.88

Mal arranhamos a superfície do que o Pandas é capaz e ignoramos completamente


seus poderosos recursos de gerenciamento de datas e horários. Há muito mais para
aprender e a documentação online e os tutoriais no site principal do Pandas são ótimos
para aprender mais.
157
Capítulo 6
Visualizando dados

As seções a seguir discutem a visualização de dados usando os principais módulos


do Python, mas antes de chegarmos a isso, é importante compreender os princípios
específicos de visualização de dados porque esses módulos podem lidar com o como
da plotagem, mas não com o quê.
Antes de construir uma visualização de dados, o principal é se colocar na posição
do visualizador. É fácil errar nessa parte porque você (como autor) normalmente tem
um vocabulário visual maior do que o do visualizador e, combinado com sua
familiaridade com os dados, o torna propenso a ignorar muitos pontos de confusão
da perspectiva do visualizador.
Para piorar, a percepção humana é principalmente uma invenção da imaginação,
dirigida para compensar as limitações de nosso hardware visual físico. Isso significa
que não é apenas a beleza que está nos olhos de quem vê, mas também a sua
visualização de dados! A boa ciência de dados se esforça para comunicar fatos
desapaixonados, mas nossa resposta emocional aos recursos visuais é pré-cognitiva,
o que significa que já respondemos à apresentação muito antes de estarmos
conscientes de fazê-lo. Os espectadores mais tarde justificam suas impressões
emocionais iniciais da apresentação após o fato, em vez de chegar a uma conclusão
fundamentada. Assim, o autor da visualização deve estar constantemente alerta para
tais problemas.
Nada do que estamos discutindo aqui é novo e a apresentação visual de
informações quantitativas é um campo bem estabelecido, digno de estudo por si só.
No mínimo, você deve estar familiarizado com as propriedades perceptivas dos
gráficos de dados como uma hierarquia em termos de precisão de comunicação. No
topo (ou seja, mais preciso) está usando a posição para representar dados (ou seja,
gráficos de dispersão) porque nossa cognição visual humana é muito boa em
identificar grupos de pontos à distância. Portanto, sempre que possível, esforce-se
para usar a posição para comunicar o aspecto mais importante do seu visual. O
próximo na ordem é o comprimento (isto é, gráficos de barras) porque podemos
distinguir facilmente as diferenças de comprimento, desde que tudo o mais no gráfico
esteja alinhado. Depois do comprimento vem o ângulo, o que explica por que as
cabines dos aviões têm medidores que apontam todos na mesma direção, permitindo
ao piloto detectar instantaneamente quando um medidor está errado por causa da
deflexão angular da agulha. Em seguida, vem a área, o volume
158 Capítulo 6
e a densidade. É difícil para nós detectar se dois círculos têm a mesma área, a menos
que um seja significativamente maior que o outro. O volume é o pior, pois é
fortemente influenciado pela forma. Se você já tentou colocar um jarro de água em
uma panela quadrada, sabe como isso é difícil de julgar. A cor também é difícil
porque a cor vem com uma forte bagagem emocional que pode ser uma distração,
apesar dos problemas de daltonismo.
Assim, o protocolo consiste em descobrir qual é a mensagem principal da
visualização dos dados e, em seguida, utilizar a hierarquia gráfica para codificar essa
mensagem por posição, sempre que possível. As mensagens colaterais são então
relegadas a itens mais abaixo na hierarquia, como ângulo ou cor. Em seguida, remova
tudo o mais da visualização que não contribua para suas mensagens, incluindo cores,
linhas, eixos, texto padrão ou qualquer outra coisa que atrapalhe. Lembre-se de que
os módulos de visualização Python vêm com seus próprios padrões, o que pode não
ser condizente com a particular mensagem desejada em.

6.1 Matplotlib

Matplotlib é a principal ferramenta de visualização de gráficos científicos em Python.


Como todos os grandes projetos de código aberto, ele se originou para satisfazer uma
necessidade pessoal. No momento de seu início, John Hunter usava principalmente
Matlab para visualização científica, mas quando começou a integrar dados de fontes
diferentes usando Python, ele percebeu que precisava de uma solução Python para
visualização, então escreveu a versão inicial do Matplotlib. Desde os primeiros anos,
Matplotlib deslocou os outros métodos concorrentes para visualização científica
bidimensional e hoje é um projeto muito ativamente mantido, mesmo sem John
Hunter, que infelizmente faleceu em 2012. Além disso, outros projetos como
alavancagem seaborn primitivas matplotlib para especializada plotagem. Desta
forma, o Matplotlib foi absorvido pela infraestrutura de visualização para a
comunidade científica Python. A força chave e duradoura do Matplotlib é sua
completude - praticamente qualquer tipo de enredo científico bidimensional que você
possa imaginar pode ser gerado, com qualidade de publicação completa, usando
Matplotlib, como pode ser visto no Concurso John Hunter Excellence in Plotting.
Existem duas peças conceituais principais para Matplotlib: a tela e os artistas. A
tela pode ser considerada o destino da visualização e os artistas desenham na tela.
Para criar uma tela no Matplotlib, você pode usar a função plt.figure mostrada
abaixo. Em seguida, a plt.plot funçãocoloca Line2D artistasna tela que desenha
a Fig. 6.1 e são retornados como saída em uma lista Python. Posteriormente, iremos
manipular esses artistas retornados.
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> plt.figure() # setup figure
<Figure size 640x480 with 0 Axes>
>>> x = np.arange(10) # Create some data
>>> y = x**2
>>> plt.plot(x,y)
Capítulo 6 159

Fig. 6.1 Gráfico básico Matplotlib

[<matplotlib.lines.Line2D object at 0x7f9373184850>]


>>> plt.xlabel('This is the xlabel') # apply labels
Text(0.5, 0, 'This is the xlabel')
>>> plt.ylabel('This is the ylabel')
Text(0, 0.5, 'This is the ylabel')
>>> plt.show() # show figure

Observe que se você executar o procedimento acima em um interpretador Python


simples, o processo irá congelar (ou bloquear) na última linha. Isso ocorre porque o
Matplotlib está preocupado em renderizar a janela GUI, que é o alvo final do gráfico
resultante. A função plt.show aciona os artistas para renderizar na tela. A razão
pela qual isso vem por último é para que os artistas possam ser convocados para
renderizar tudo de uma vez.
Embora o uso dessas funções seja a forma tradicional de usar o Matplotlib, a
interface orientada a objetos é mais organizada e compacta. O seguinte refaz o acima
usando o estilo orientado a objetos:
>>> from matplotlib.pylab import subplots
>>> fig,ax = subplots()
>>> ax.plot(x,y)
>>> ax.set_xlabel('this is xlabel')
>>> ax.set_ylabel('this is ylabel')

Observe que a etapa principal é usar a função subplots para gerar objetos para a
janela da figura e os eixos. Em seguida, os comandos de plotagem são anexados ao
respectivo objeto ax. Isso torna mais fácil acompanhar as múltiplas visualizações
sobrepostas no mesmo ax.
160 Capítulo 6
6.1.1 Configurando Padrões

Você pode definir os padrões para plotagem usando o dicionário rcParams


>>> import matplotlib
>>> matplotlib.rcParams['lines.linewidth']=2.0 # all plots with
c→ have this linewidth

Alternativamente, você poderia ter definido a linewidth em uma base por linha
usando argumentos de palavra-chave,
plot(arange(10),linewidth=2.0) # using linewidth keyword

6.1.2 Legends

Legends identifica as linhas no gráfico (ver Fig. 6.2) e loc indica a posição da
legenda.
>>> fig,ax=subplots()
>>> ax.plot(x,y,x,2*y,'or--')
>>> ax.legend(('one','two'),loc='best')

Fig. 6.2 Várias linhas nos mesmos eixos com uma lenda
Capítulo 6 161

Fig. 6.3 múltiplos subtramas na mesma figura

6.1.1 Subplots

A mesma função subplots permite múltiplos subplots (ver Fig. 6.3) com cada eixo
indexado como uma matriz Numpy,
>>> fig,axs = subplots(2,1) # 2-rows, 1-column
>>> axs[0].plot(x,y,'r-o')
>>> axs[1].plot(x,y*3,'g--s')

6.1.1 Spines

O retângulo que contém os gráficos tem quatro assim chamados espinhos. Estes são
gerenciados com o objeto spines. No exemplo abaixo, a nota que axs ccontém um
array capaz de fatiar de objetos de eixos individuais (ver Fig. 6.4). A subtrama no
canto superior esquerdo corresponde a axs[0,0]. Para esta subtrama, a lombada à
direita se torna invisível ao definir sua cor para ’none’. Observe que para
Matplotlib, a string 'none' é tratada de maneira diferente do normal Python None.
A lombada na parte inferior é movida para o center usando set_position e as
posições dos ticks são atribuídas com set_ticks_position.
>>> fig,axs = subplots(2,2)
>>> x = np.linspace(-np.pi,np.pi,100)
>>> y = 2*np.sin(x)
>>> ax = axs[0,0]
>>> ax.set_title('centered spines')
162 Capítulo 6
>>> ax.plot(x,y)
>>> ax.spines['left'].set_position('center')
>>> ax.spines['right'].set_color('none')
>>> ax.spines['bottom'].set_position('center')
>>> ax.spines['top'].set_color('none')
>>> ax.xaxis.set_ticks_position('bottom')
>>> ax.yaxis.set_ticks_position('left')

O próximo subplot no canto inferior esquerdo, axes[1,0], tem a lombada inferior


movida para a posição ’zero’ dos dados.
>>> ax = axs[1,0]
>>> ax.set_title('zeroed spines')
>>> ax.plot(x,y)
>>> ax.spines['left'].set_position('zero')
>>> ax.spines['right'].set_color('none')
>>> ax.spines['bottom'].set_position('zero')
>>> ax.spines['top'].set_color('none')
>>> ax.xaxis.set_ticks_position('bottom')
>>> ax.yaxis.set_ticks_position('left')

O próximo subplot no canto superior direito, axes[0,1] tem a lombada inferior


movida para a posição 0,1 do sistema de coordenadas dos eixos com a lombada
esquerda na posição 0,6 do sistema de coordenadas dos eixos (mais sobre sistemas
de coordenadas posteriormente).
>>> ax = axs[0,1]
>>> ax.set_title('spines at axes (0.6, 0.1)')
>>> ax.plot(x,y)
>>> ax.spines['left'].set_position(('axes',0.6))
>>> ax.spines['right'].set_color('none')
>>> ax.spines['bottom'].set_position(('axes',0.1))
>>> ax.spines['top'].set_color('none')
>>> ax.xaxis.set_ticks_position('bottom')
>>> ax.yaxis.set_ticks_position('left')

6.1.5 Dividir eixos

A função subplots também permite a partilha de eixos individuais entre lotes


usando sharex e sharey (ver Fig. 6.5),que é particularmente útil para alinhar
parcelas de séries temporais.
>>> fig, axs = subplots(3,1,sharex=True,sharey=True)
>>> t = np.arange(0.0, 2.0, 0.01)
>>> s1 = np.sin(2*np.pi*t)
>>> s2 = np.exp(-t)
>>> s3 = s1*s2
>>> axs[0].plot(t,s1)
>>> axs[1].plot(t,s2)
>>> axs[2].plot(t,s3)
>>> ax.set_xlabel('x-coordinate')
Capítulo 6 163

Fig. 6.4 Espinhas referem-se às bordas do quadro e podem ser movidas dentro da figura

Fig. 6.5 Múltiplos subplots podem compartilhar um único xeixo


164 Capítulo 6
6.1.6 Superfícies 3D

Matplotlib é principalmente um pacote de plotagem bidimensional, mas tem alguns


recursos tridimensionais limitados que se baseiam principalmente em mecanismos de
projeção. O mecanismo principal para isso está no submódulo mplot3d. O código
a seguir desenha a Fig. 6.6. O objeto Axes3D possui o método plot_surface para
desenhar o gráfico tridimensional enquanto o módulo cm possui os mapas de cores.
>>> from mpl_toolkits.mplot3d import Axes3D
>>> from matplotlib import cm
>>> fig = plt.figure()
>>> ax = Axes3D(fig)
>>> X = np.arange(-5, 5, 0.25)
>>> Y = np.arange(-5, 5, 0.25)
>>> X, Y = np.meshgrid(X, Y)
>>> R = np.sqrt(X**2 + Y**2)
>>> Z = np.sin(R)
>>> ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.jet)
<mpl_toolkits.mplot3d.art3d.Poly3DCollection object at
0x7f9372d6b5b0>

6.1.7 Usando Primitivos de Patch

Você pode plotar círculos, polígonos, etc. usando primitivos disponíveis em o módulo
matplotlib.patches (ver Fig. 6.7).

Fig. 6.6 Gráfico de


superfície tridimensional
Capítulo 6 165
Fig. 6.7 Matplotlib Patches
desenham primitivos
gráficos na tela

>>> from matplotlib.patches import Circle, Rectangle, Ellipse


>>> fig,ax = subplots()
>>> ax.add_patch(Circle((0,0), 1,color='g'))
>>> ax.add_patch(Rectangle((-1,-1),
... width = 2, height = 2,
... color='r',
... alpha=0.5)) # transparency
>>> ax.add_patch(Ellipse((0,0),
... width = 2, height = 4, angle = 45,
... color='b',
... alpha=0.5)) # transparency
>>> ax.axis('equal')
(-1.795861705, 1.795861705, -1.795861705, 1.795861705)
>>> ax.grid(True)

Você também pode usar hachuras cruzadas em vez de cores (consulte a Fig. 6.8).
>>> fig,ax = subplots()
>>> ax.add_patch(Circle((0,0),
... radius=1,
... facecolor='w',
... hatch='x'))
>>> ax.grid(True)
>>> ax.set_title('Using cross-hatches',fontsize=18)

6.1.8 Patches em 3D

Você também pode adicionar patches aos eixos 3D. O eixo tridimensional é criado
passando o argumento de palavra-chave subplot_kw ao dicionário {'projection':
'3d'}. Os patches Circle são criados da maneira usual e, em seguida, adicionados
a este eixo. O método pathpatch_2d_to_3d do módulo art3d muda o visualizador
166 Capítulo 6
Fig. 6.8 Hachuras
cruzadas em vez de
cores

Fig. 6.9 As primitivas de


patch Matplotlib podem
ser desenhadas em três
dimensões também em

perspectiva para cada patch ao longo da direção indicada. Isso é o que cria o efeito
tridimensional (ver Fig. 6.9).
>>> import mpl_toolkits.mplot3d.art3d as art3d
>>> fig, ax = subplots(subplot_kw={'projection':'3d'})
>>> c = Circle((0,0),radius=3,color='r')
>>> d = Circle((0,0),radius=3,color='b')
>>> ax.add_patch(c)
>>> ax.add_patch(d)
>>> art3d.pathpatch_2d_to_3d(c,z=0,zdir='y')
>>> art3d.pathpatch_2d_to_3d(d,z=0,zdir='z')

Os gráficos bidimensionais também podem ser empilhados em três dimensões (ver


Fig. 6.10). Os caminhos dos polígonos fechados individuais das parcelas são
extraídos usando get_paths
Capítulo 6 167
Fig. 6.10 patch
personalizados podem ser
criados a partir de outras
representações Matplotlib

Fig. 6.11 Elementos


gráficos em
diferentes sistemas
de coordenadas
aparecem na mesma
figura

e então usados para criar os PathPatch objetos são adicionados ao eixo. Como
antes, a etapa final é definir a visão em perspectiva de cada patch usando
pathpatch_2d_to_3d.

>>> from numpy import pi, arange, linspace, sin, cos


>>> from matplotlib.patches import PathPatch
>>> x = linspace(0,2*pi,100)
>>> # create polygons from graphs
>>> fig, ax = subplots()
>>> p1=ax.fill_between(x,sin(x),-1)
>>> p2=ax.fill_between(x,sin(x-pi/3),-1)
>>> path1=p1.get_paths()[0] # get closed polygon for p1
>>> path2=p2.get_paths()[0] # get closed polygon for p2
>>> ax.set_title('setting up patches from 2D graph')
>>> fig, ax = subplots(subplot_kw={'projection':'3d'})
168 Capítulo 6
>>> pp1 = PathPatch(path1,alpha=0.5) # need to assign this for
c→ later
>>> pp2 = PathPatch(path2,color='r',alpha=0.5) # need to assign
c→ this for later
>>> # add patches
>>> ax.add_patch(pp1)
>>> ax.add_patch(pp2)
>>> # transform patches
>>> art3d.pathpatch_2d_to_3d(pp1,z=0,zdir='y')
>>> art3d.pathpatch_2d_to_3d(pp2,z=1,zdir='y')

6.1.9 Usando Transformações

Um dos pontos fortes subestimados do Matplotlib é como ele gerencia múltiplos


sistemas de coordenadas enquanto cria visualizações de dados. Esses sistemas de
coordenadas fornecem os mapeamentos de escala entre os dados e o espaço da
visualização renderizada. O data coordinate system é o sistema de
coordenadas dos pontos de dados, enquanto o display coordinate system é o
sistema da figura exibida. O método ax.transData converte as coordenadas de
dados (5, 5) em coordenadas no sistema de coordenadas de exibição (ver Fig. 6.11).
>>> fig,ax = subplots()
>>> # line in data coordinates
>>> ax.plot(np.arange(10), np.arange(10))
>>> # marks the middle in data coordinates
>>> ax.plot(5,5,'o',markersize=10,color='r')
>>> # show the same point but in display coordinates
>>> print(ax.transData.transform((5,5)))
[2560. 1900.8]

Se criarmos eixos com tamanhos diferentes usando o argumento de palavra-chave


figsize, então o método transData retorna coordenadas diferentes para cada
eixo, mesmo que se refiram ao mesmo ponto nas coordenadas de dados.
>>> fig,ax = subplots(figsize=(10,4))
>>> ax.transData.transform((5,5))
array([4000., 1584.])
>>> fig,ax = subplots(figsize=(5,4))
>>> ax.transData.transform((5,5))
array([2000., 1584.])

Além disso, se você plotar isso em uma janela GUI (não no bloco de notas Jupyter)
e redimensionar a figura usando o mouse e fazer isso novamente, você também obterá
coordenadas diferentes dependendo de como a janela foi redimensionada. Por uma
questão prática, você raramente trabalha em display coordinates. Você pode
voltar às coordenadas de dados usando
ax.transData.inverted().transform.
O axes coordinate system é a caixa da unidade que contém os eixos. O
método transAxes mapeia neste sistema de coordenadas. Por exemplo, veja a Fig.
6.12 onde o método transAxes é usado para o argumento de palavra-chave
transform,
Capítulo 6 169
Fig. 6.12 Os elementos
podem ser fixados em
posições na figura,
independentemente das
coordenadas de dados
usando o sistema de
coordenadas de eixos

>>> from matplotlib.pylab import gca


>>> fig,ax = subplots()
>>> ax.text(0.5,0.5,
... 'Middle of plot',
... transform = ax.transAxes,
... fontsize=18)
>>> ax.text(0.1,0.1,
... 'lower left',
... transform = ax.transAxes,
... fontsize=18)
>>> ax.text(0.8,0.8,
... 'upper right',
... transform = ax.transAxes,
... fontsize=18)
>>> ax.text(0.1,0.8,
... 'upper left',
... transform = ax.transAxes,
... fontsize=18)
>>> ax.text(0.8,0.1,
... 'lower right',
... transform = ax.transAxes,
... fontsize=18)

Isso pode ser útil para anotar gráficos de forma consistente, independentemente dos
dados. Você pode combinar isso com os patches para criar uma mistura de
coordenadas de dados e itens de coordenadas de eixos em seus gráficos como na Fig.
6.13, a partir do seguinte código:
>>> fig,ax = subplots()
>>> x = linspace(0,2*pi,100)
>>> ax.plot(x,sin(x) )
>>> ax.add_patch(Rectangle((0.1,0.5),
... width = 0.5,
... height = 0.2,
... color='r',
... alpha=0.3,
... transform = ax.transAxes))
>>> ax.axis('equal')
170 Capítulo 6
(-0.3141592653589793, 6.5973445725385655,
-1.0998615404412626, 1.0998615404412626)

6.1.10 Anotando plotagens com texto

Matplotlib implementa uma classe unificada para texto, o que significa que você pode
manipular o texto da mesma forma em qualquer lugar em que ele apareça em uma
visualização. Adicionar texto é simples com ax.text (consulte a Fig. 6.14):
>>> fig,ax = subplots()
>>> x = linspace(0,2*pi,100)
>>> ax.plot(x,sin(x) )
>>> ax.text(pi/2,1,'max',fontsize=18)
>>> ax.text(3*pi/2,-1.1,'min',fontsize=18)
>>> ax.text(pi,0,'zero',fontsize=18)
>>> ax.axis((0,2*pi,-1.25,1.25))
(0.0, 6.283185307179586, -1.25, 1.25)

Também podemos usar bounding boxes para circundar o texto como na Fig.a
seguir 6.15 , usando o argumento de palavra-chave bbox,
>>> fig,ax = subplots()
>>> x = linspace(0,2*pi,100)
>>> ax.plot(x,sin(x))
>>> ax.text(pi/2,1-0.5,'max',
... fontsize=18,
... bbox = {'boxstyle':'square','facecolor':'yellow'})

Fig. 6.13 Primitivos de patch Matplotlib pode ser usada em diferentes sistemas de coordenadas
Capítulo 6 171
Fig. 6.14 texto pode ser
adicionado na figura

Fig. 6.15 caixas


delimitadoras pode ser
adicionado ao texto

6.1.11 Anotando gráficos com setas

Os gráficos podem ser anotados com setas usando ax.annotate como no seguinte
(Fig. 6.16):
>>> fig,ax = subplots()
>>> x = linspace(0,2*pi,100)
>>> ax.plot(x,sin(x))
>>> ax.annotate('max',
... xy=(pi/2,1), # where to put arrow endpoint
... xytext=(pi/2,0.3), # text position in data coordinates
... arrowprops={'facecolor':'black','shrink':0.05},
... fontsize=18,
... )
>>> ax.annotate('min',
... xy=(3/2.*pi,-1), # where to put arrow endpoint
... xytext=(3*pi/2.,-0.3), # text position in data coordinates
... arrowprops={'facecolor':'black','shrink':0.05},
... fontsize=18,
... )
172 Capítulo 6
Fig. 6.16 Setas chamam
atenção para pontos no
gráfico.

Também podemos especificar a coordenada do texto usando o sistema


textcoords, como discutimos anteriormente (ver Fig. 6.17), onde o sistema de
coordenadas é especificado com a string ’axes fraction’ em vez de usar
ax.transAxes.
>>> fig,ax = subplots()
>>> x = linspace(0,2*pi,100)
>>> ax.plot(x,sin(x) )
>>> ax.annotate('max',
... xy=(pi/2,1), # where to put arrow endpoint
... xytext=(0.3,0.8), # text position in axes coordinates
... textcoords='axes fraction',
... arrowprops={'facecolor':'black',
... 'shrink':0.05},
... fontsize=18,
... )
>>> ax.annotate('min',
... xy=(3/2.*pi,-1), # where to put arrow endpoint
... xytext=(0.8,0.2), # text position in data coordinates
... textcoords='axes fraction',
... arrowprops={'facecolor':'black',
... 'shrink':0.05,
... 'width':10,
... 'headwidth':20,
... 'headlength':6},
... fontsize=18,
... )

Às vezes, você quer apenas a seta e não o texto como na Fig. 6.18, onde as
coordenadas xytext são para a cauda da seta e connectionstyle especifica a
curva do arco.
>>> fig,ax = subplots()
>>> x = linspace(0,2*pi,100)
>>> ax.set_title('Arrow without text',fontsize=18)
>>> ax.annotate("", # leave the text-string argument empty
... xy=(0.2, 0.2), xycoords='data',
... xytext=(0.8, 0.8), textcoords='data',
Capítulo 6 173
Fig. 6.17 A representação
das setas pode ser
cuidadosamente detalhada

Fig. 6.18 Seta sem o texto

... arrowprops=dict(arrowstyle="->",
... connectionstyle="arc3,rad=0.3",
... linewidth=2.0),
... )

Como as setas são importantes para apontar para elementos gráficos ou para
representar campos vetoriais complicados (por exemplo, velocidades e direções do
vento), o Matplotlib oferece muitas opções de personalização.

6.1.12 Incorporando subtramas escaláveis e não escaláveis

Os subplots incorporados podem ser dimensionados ou não com o restante dos dados
plotados. Os seguintes círculos embutidos na Fig. 6.19 não mudarão, mesmo se outros
dados forem plotados dentro da mesma figura via AnchoredDrawingArea.
Observe que nós, artistas,
174 Capítulo 6
Fig. 6.19 Subtramas
incorporadas podem ser
independentes de dados de
escala na mesma figura

são adicionados à área de desenho ancorada, suas dimensões estão em coordenadas


de pixel, mesmo se o transform argumento de palavra-chave é definido para o
artista.
>>> from mpl_toolkits.axes_grid1.anchored_artists import
AnchoredDrawingArea
>>> fig,ax = subplots()
>>> fig.set_size_inches(3,3)
>>> ada = AnchoredDrawingArea(40, 20, 0, 0,
... loc=1, pad=0.,
... frameon=False)
>>> p1 = Circle((10, 10), 10)
>>> ada.drawing_area.add_artist(p1)
>>> p2 = Circle((30, 10), 5, fc="r")
>>> ada.drawing_area.add_artist(p2)
>>> ax.add_artist(ada)
<mpl_toolkits.axes_grid1.anchored_artists.AnchoredDrawingArea object
at 0x7f939833e760>

Isto é diferente de AnchoredAuxTransformBox que vai facilitar


dimensionamento com coordenadas de dados na Fig. 6.20 onde as escalas elipse
incorporados com machado,
>>> from matplotlib.patches import Ellipse
>>> from mpl_toolkits.axes_grid1.anchored_artists import
AnchoredAuxTransformBox

>>> fig,ax = subplots()


>>> box = AnchoredAuxTransformBox(ax.transData, loc=2)
>>> # in data coordinates
>>> el = Ellipse((0,0),
... width=0.1,
... height=0.4,
... angle=30)
>>> box.drawing_area.add_artist(el)
>>> ax.add_artist(box)
<mpl_toolkits.axes_grid1.anchored_artists.AnchoredAuxTransformBox
object at 0x7f9372f5dbe0>

Adicionando planos para isso vai deixar os círculos inalterado.


Capítulo 6 175
Fig. 6.20 Em oposição à Fig.
6,19,esta subtrama mudará
com o dimensionamento dos
outros gráficos de dados nesta
figura

6.1.13 Animações

Matplotlib fornece animações flexíveis que utilizam todos os outros primitivos


gráficos. Uma maneira de criar animações é gerar sequências de artistas em um
iterável e, em seguida, animá-los (ver Fig. 6.21).
>>> import matplotlib.animation as animation
>>> fig,ax = subplots()
>>> x = np.arange(10)
>>> frames = [ax.plot(x,x,x[i],x[i],'ro',ms=5+i*10)
... for i in range(10)]
>>> # You must assign in the next line!
>>> g=animation.ArtistAnimation(fig,frames,interval=50)

Isso funciona bem para relativamente poucos frames, mas você também pode criar
frames dinamicamente (consulte a Fig. 6.22):
>>> import matplotlib.animation as animation
>>> x = np.arange(10)
>>> linewidths =[10,20,30]
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> line, = ax.plot(x,x,'-ro',ms=20,linewidth=5.0)
>>> def update(data):
... line.set_linewidth(data)
... return (line,)
...
>>> ani = animation.FuncAnimation(fig, update, x, interval=500)
>>> plt.show()

Para fazer com que eles sejam animados em um notebook Jupyter, você deve
convertê-los em animações JavaScript correspondentes usando to_jshtml. Por
exemplo, em uma célula, faça o seguinte (ver Fig. 6.23):
>>> fig,ax = subplots()
>>> frames = [ax.plot(x,x,x[i],x[i],'ro',ms=5+i*10)
176 Capítulo 6

Fig. 6.21 Animações Matplotlib utilizam os outros primitivos gráficos

Fig. 6.22 Animações podem


ser geradas dinamicamente
com funções

.. . para i no intervalo(10)]
>>> g=animação.ArtistAnimation (fig, frames, interval= 50)

e na próxima célula, faça o seguinte:


from IPython.display import
HTML HTML(g.to_jshtml())
Capítulo 6 177

Fig. 6.23 Animações no bloco de notas Jupyter podem ser reproduzidas no navegador usando
to_jshtml

6.1.14 Usando caminhos diretamente

Mais acesso de baixo nível para plotagem está disponível no Matplotlib usando
caminhos, que você pode considerar como a programação de uma caneta que está
desenhando na tela. Por exemplo, patches são feitos de paths. Os caminhos têm
vértices e comandos de desenho correspondentes. Por exemplo, para a Fig. 6.24, isso
desenha uma linha entre dois pontos
>>> from matplotlib.path import Path
>>> vertices=[ (0,0),(1,1) ]
>>> codes = [ Path.MOVETO, # move stylus to (0,0)
... Path.LINETO] # draw line to (1,1)
>>> path = Path(vertices, codes) #create path
>>> fig, ax = subplots()
>>> # convert path to patch
>>> patch = PathPatch(path,linewidth=10)
>>> ax.add_patch(patch)
>>> ax.set_xlim(-.5,1.5)
(-0.5, 1.5)
>>> ax.set_ylim(-.5,1.5)
(-0.5, 1.5)
>>> plt.show()

Os caminhos também podem usar curvas de Bézier quadráticas e cúbicas para


conectar pontos, como na Fig. 6.25,
>>> vertices=[ (-1,0),(0,1),(1,0),(0,-1),(-1,0) ]
>>> codes = [Path.MOVETO, # move stylus to (0,0)
... Path.CURVE3, # draw curve
... Path.CURVE3, # draw curve
... Path.CURVE3, # draw curve
178 Capítulo 6

Fig. 6.24 Os caminhos desenham na tela usando instruções específicas do tipo caneta

Fig. 6.25 Os caminhos podem ter curvas de Bézier

... Path.CURVE3,]
>>> path = Path(vertices, codes) #create path
>>> fig, ax = subplots()
>>> # convert path to patch
>>> patch = PathPatch(path,linewidth=2)
>>> ax.add_patch(patch)
>>> ax.set_xlim(-2,2)
(-2.0, 2.0)
>>> ax.set_ylim(-2,2)
Capítulo 6 179
Fig. 6.26 Setas tangentes à
curva

(-2.0, 2.0)
>>> ax.set_title('Quadratic Bezier Curve Path')
>>> for i in vertices:
... _=ax.plot(i[0],i[1],'or' )# control points
... _=ax.text(i[0],i[1],'control\n
c→ point',horizontalalignment='center')
...

Setas e gráficos podem ser combinados em uma única figura para mostrar a derivada
direcional em pontos da curva (ver Fig. 6.26),
>>> x = np.linspace(0,2*np.pi,100)
>>> y = np.sin(x)
>>> fig, ax = subplots()
>>> ax.plot(x,y)
>>> u = []
>>> # subsample x
>>> x = x[::10]
>>> for i in zip(np.ones(x.shape),np.cos(x)):
... v=np.array(i)
... u.append(v/np.sqrt(np.dot(v,v)) )
...
>>> U=np.array(u)
>>> ax.quiver(x,np.sin(x),U[:,0],U[:,1])
>>> ax.grid()
>>> ax.axis('equal')
(-0.3141592653589793, 6.5973445725385655, -1.0998615404412626,
1.0998615404412626)

6.1.15 Interagindo com gráficos usando controles deslizantes

O widget GUI Matplotlib (Qt ou outro backend) pode ter callbacks para responder
aos movimentos das teclas ou do mouse. Isso torna mais fácil adicionar gráficos
básicos de interatividade na janela da GUI.
180 Capítulo 6

Fig. 6.27 Widgets interativos podem ser anexados ao backend da GUI do Matplotlib.

Eles também estão disponíveis em matplotlib.widgets de dentro do bloco de


notas Jupyter. Os widgets são anexados à função de retorno de chamada update
usando on_changed ou on_clicked e as alterações nos widgets específicos acionam
o retorno de chamada (consulte a Fig. 6.27) onde os widgets Slider,
RadioButtons e Button são anexados aos callbacks.
>>> from matplotlib.widgets import Slider, Button, RadioButtons
>>> from matplotlib.pylab import subplots_adjust, axes
>>> fig, ax = subplots()
>>> subplots_adjust(left=0.25, bottom=0.25)
>>> # setup data
>>> t = arange(0.0, 1.0, 0.001)
>>> a0, f0 = 5, 3
>>> s = a0*sin(2*pi*f0*t)
>>> # draw main plot
>>> l, = ax.plot(t,s, lw=2, color='red')
>>> ax.axis([0, 1, -10, 10])
(0.0, 1.0, -10.0, 10.0)
>>> axcolor = 'lightgoldenrodyellow'
>>> # create axes for widgets
>>> axfreq = axes([0.25, 0.1, 0.65, 0.03], facecolor=axcolor)
>>> axamp = axes([0.25, 0.15, 0.65, 0.03], facecolor=axcolor)
>>> sfreq = Slider(axfreq, 'Freq', 0.1, 30.0, valinit=f0)
>>> samp = Slider(axamp, 'Amp', 0.1, 10.0, valinit=a0)
>>> def update(val):
... amp = samp.val
... freq = sfreq.val
... l.set_ydata(amp*sin(2*pi*freq*t))
... draw()
...
>>> # attach callbacks to widgets
>>> sfreq.on_changed(update)
Capítulo 6 181
0
>>> samp.on_changed(update)
0
>>> resetax = axes([0.8, 0.025, 0.1, 0.04])
>>> button = Button(resetax, 'Reset', color=axcolor,
c→ hovercolor='0.975')
>>> def reset(event):
... sfreq.reset()
... samp.reset()
...
>>> # attach callback to button
>>> button.on_clicked(reset)
0
>>> rax = axes([0.025, 0.5, 0.15, 0.15], facecolor=axcolor)
>>> radio = RadioButtons(rax, ('red', 'blue', 'green'), active=0)
>>> def colorfunc(label):
... l.set_color(label)
... draw()
...
>>> # attach callback to radio buttons
>>> radio.on_clicked(colorfunc)
0

6.1.16 Colormaps

Matplotlib fornece muitos mapas de cores úteis. A função imshow pega um array de
entrada e plota as células nesse array com a cor correspondente ao valor de cada
entrada (ver Fig. 6.28).
>>> fig, ax = subplots()
>>> x = np.linspace(-1,1,100)
>>> y = np.linspace(-3,1,100)
>>> ax.imshow(abs(x + y[:,None]*1j)) # use broadcasting

As cores do Matplotlib são organizadas no submódulo matplotlib.colors e o


módulo matplotlib.pylab.cm tem os mapas de cores em uma única interface.
Para cada mapa de cores nomeado em cm, há um mapa de cores invertido. Por
exemplo, cm.Blues e cm.Blues_r. Podemos usar esse mapa de cores com
imshow, como na Fig. 6.28, para criar a Fig. 6.29 com o argumento de palavra-
chave cmap.
>>> fig, ax = subplots()
>>> ax.imshow(abs(x + y[:,None]*1j),cmap=cm.Blues)

Como as cores têm um forte impacto perceptivo para a compreensão visual dos dados,
Matplotlib são organizados em termos de cíclico, sequencial, divergente ou
qualitativo, cada um adaptado para tipos específicos de dados numéricos, categóricos
ou nominais.
182 Capítulo 6
Fig. 6.28 Matplotlib
imshow exibe os elementos
da matriz como cores

Fig. 6.29 O mesmo da


Fig. 6.28, mas com o
colormap cm.Blues
Capítulo 6 183
Fig. 6.30 Os atributos dos
elementos Matplotlib podem
ser descobertos e alterados
usando getp e setp

6.1.17 Controle de baixo nível usando setp e getp

Porque existem tantos elementos Matplotlib e tantos atributos correspondentes, as


funções getp e setp tornam mais fácil descobrir e alterar os atributos de qualquer
elemento em particular (ver Fig. 6.30).
>>> from matplotlib.pylab import setp, getp
>>> fig, ax = subplots()
>>> c = ax.add_patch(Circle((0,0),1,facecolor='w',hatch='-|'))
>>> # set axes background rectangle to blue
>>> setp(ax.get_children()[-1],fc='lightblue')
[None]
>>> ax.set_title('upper right corner is axes background')
>>> ax.set_aspect(1)
>>> fig.show()

6.1.18 Interagindo com figuras

Matplotlib Matplotlib fornece vários mecanismos de retorno de chamada subjacentes


para expandir o uso interativo da janela de figura usando o Qt ou outro backend GUI.
Eles funcionam com o notebook Jupyter usando o módulo ipympl e a magia celular
do %matplotlib widget .
184 Capítulo 6
6.1.19 Eventos de teclado

A janela da figura Matplotlib GUI é capaz de ouvir e responder aos eventos do teclado
(ou seja, pressionamentos de tecla digitados) quando a janela da figura está em foco.
As funções mpl_connect e mpl_disconnect anexam ouvintes aos eventos na janela
da GUI e acionam o retorno de chamada correspondente quando as alterações são
detectadas. Observe o uso de sys.stdout.flush() para limpar a saída padrão.
import sys
fig, ax = plt.subplots()
# disconnect default handlers
fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)
ax.set_title('Keystroke events',fontsize=18)
def on_key_press(event):
print (event.key)
sys.stdout.flush()

# connect function to canvas and listen


fig.canvas.mpl_connect('key_press_event', on_key_press) plt.show()

Agora, você deve ser capaz de digitar as teclas na janela da figura e ver alguma saída
no terminal. Podemos fazer o callback interagir com os artistas na janela da figura
referenciando-os, como no exemplo a seguir:
fig, ax = plt.subplots()
x = np.arange(10)
line, = ax.plot(x, x*x,'-o') # get handle of Line2D object
ax.set_title('More Keystroke events',fontsize=18)
def on_key_press(event):
# If event.key is one of shorthand color notations, set line
c→ color
if event.key in 'rgb':
line.set_color(event.key)
fig.canvas.draw() # force redraw

# disconnect default figure handlers


fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)
# connect function to canvas and listen
fig.canvas.mpl_connect('key_press_event', on_key_press) plt.show()

Você também pode aumentar o tamanho do marcador usando esta mesma técnica
adicionando outro retorno de chamada como no exemplo a seguir:
def on_key_press2(event):
# If the key is one of the shorthand color notations,
set the line color
if event.key in '123':
val = int(event.key)*10
line.set_markersize(val)
fig.canvas.draw() # force redraw

fig.canvas.mpl_connect('key_press_event', on_key_press2)
Capítulo 6 185
Você também pode usar teclas modificadoras como alt, ctrl, shift.
import re
def on_key_press3(event):
'alt+1,alt+2, changes '
if re.match('alt\+?',event.key):
key,=re.match('alt\+(.?)',event.key).groups(0)
val = int(key)/5.
line.set_mew(val)
fig.canvas.draw() # force redraw

fig.canvas.mpl_connect('key_press_event', on_key_press3)
plt.show()

Agora, você pode usar os números, letras e modificadores fornecidos em suas teclas
para alterar a linha incorporada. Observe que também há um evento
on_key_press_release se você quiser conectar várias letras ou ter um teclado
barulhento.

6.1.20 Eventos do mouse

Você também pode tocar no movimento do mouse e nos cliques na janela da figura
usando um mecanismo semelhante. Esses eventos podem responder com coordenadas
de figura / dados e um ID inteiro do botão (ou seja, esquerda / meio / direita)
pressionado.
fig, ax = plt.subplots()
# disconnect default handlers
fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)

def on_button_press(event):
button_dict = {1:'left',2:'middle',3:'right'}
print ("clicked %s button" % button_dict[ event.button ])
print ("figure coordinates:", event.x, event.y)
print ("data coordinates:", event.xdata, event.ydata)
sys.stdout.flush()

fig.canvas.mpl_connect('button_press_event', on_button_press)
plt.show()

Aqui está outra versão que coloca pontos em cada ponto de clique.
fig, ax = plt.subplots()
ax.axis([0,1,0,1])
# disconnect default handlers
fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)

o=[]
def on_button_press(event):
button_dict = {1:'left',2:'middle',3:'right'}
ax.plot(event.xdata,event.ydata,'o')
o.append((event.xdata,event.ydata))
sys.stdout.flush()
fig.canvas.draw()
186 Capítulo 6
fig.canvas.mpl_connect('button_press_event', on_button_press)
plt.show()

Além dos cliques, você também pode posicionar o mouse sobre um artista na tela e
clicar nele para acessar esse artista. Isso é conhecido com o evento pick.
fig, ax = plt.subplots()
ax.axis([-1,1,-1,1])
ax.set_aspect(1)
for i in range(5):
x,y= np.random.rand(2).T
circle = Circle((x, y),radius=0.1 , picker=True)
ax.add_patch(circle)

def on_pick(event):
artist = event.artist
artist.set_fc(np.random.random(3))
fig.canvas.draw()

fig.canvas.mpl_connect('pick_event', on_pick)
plt.show()

Ao clicar nos círculos da figura, você pode alterar aleatoriamente suas cores
correspondentes. Observe que você deve definir o argumento de palavra-chave
picker = True quando o artista for instanciado. Você também pode fazer
help(plt.connect) para usar outros eventos.

6.2 Seaborn

Seaborn facilita plotagens especializadas que tornam rápido e fácil avaliar


visualmente os aspectos estatísticos dos dados. Como o Seaborn é construído em
cima do Matplotlib, ele pode direcionar qualquer saída que o Matplotlib suporte.
Seaborn é extraordinariamente bem escrito, com documentação fantástica e código-
fonte fácil de ler. Podemos importar o Seaborn da maneira usual, lembrando que
também queremos carregar a interface tradicional plt para Matplotlib.
>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> import seaborn as sns

Seaborn vem com muitos conjuntos de dados interessantes,


>>> tips = sns.load_dataset('tips')
>>> tips.head()
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4
Capítulo 6 187
Fig. 6.31 Gráfico de
dispersão paraconjuntos
de dados tips

Podemos facilmente investigar as relações entre pares de variáveis usando


sns.relplot() na Fig. 6.31,
>>> sns.relplot(x='total_bill',y='tip',data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f9370b261c0>

Embora pudéssemos facilmente recriar este gráfico no Matplotlib simples usando


scatter, a prática do Seaborn é transmitir as trama de dados de pandas tips e
referência as colunas a serem plotados com argumentos de palavra-chave x e y. Em
seguida, outras colunas de dataframe podem ser atribuídas a outras funções gráficas
na renderização que permitem camadas de conceitos gráficos em uma única figura.
Além disso, o plano de fundo e a moldura são estilisticamente diferentes dos padrões
do Matplotlib. É importante ressaltar que o objeto resultante retornado é uma
instância seaborn.axisgrid.FacetGrid, que portanto, se quisermos
recuperar os elementos nativos Matplotlib, temos que extraí-los do FacetGrid
como os atributos, fig e ax, por exemplo.
Atribuindo a função hue a coluna smoker do dataframe, o gráfico resultante é o
mesmo de antes, exceto os círculos individuais são coloridos pela coluna categórica
smoker do dataframe, como na Fig. 6.32,
>>> sns.relplot(x='total_bill',hue='smoker',y='tip',data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f9370c3f400>

Em vez de alterar a cor para cada categoria de fumante, podemos atribuir à coluna
smoker a função style, que muda a forma do marcador na Fig. 6.33,
>>> sns.relplot(x='total_bill',y='tip',
... style='smoker', # different marker shapes
... data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f9372fe1700>
188 Capítulo 6
Fig. 6.32 Pontos de dados
individuais colorido usando
ocoluna categórica smoker

Fig. 6.33 Em vez de cor,


marcadores diferentes podem
ser usados com coluna
categórica smoker

Você também pode especificar qualquer tipo de marcador Matplotlib válido para cada
um dos dois valores categóricos da coluna smoker na Fig. 6.34, especificando o
argumento de palavra-chave markers,
>>> sns.relplot(x='total_bill',y='tip',
... style='smoker',
... markers=['s','^'],data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f9376c6c310>

A coluna size do dataframe de entrada pode ser usado para dimensionar cada um
dos marcadores na Fig. 6.35,
Capítulo 6 189
Fig. 6.34 Marcadores
personalizados podem ser
especificados para pontos de
dados individuais usando
ocoluna categórica smoker

Fig. 6.35 A coluna size


categórica no conjunto de
dados de tips pode escalar os
pontos individuais

>>> sns.relplot(x='total_bill',y='tip',
... size='size', # scale markers
... data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f93713bd310>

Isso é discreto por causa do exclusivo valores dessa coluna no dataframe,


>>> tips['size'].unique()
array([2, 3, 4, 1, 6, 5])

Esses tamanhos podem ser escalados entre os limites especificados pelo argumento
de palavra-chave sizes, como sizes=(5,10). Observe que fornecer um número
contínuo
190 Capítulo 6
Fig. 6.36 Opções como
transparência (ou seja,
valor alfa) que não são
usadas pelo Seaborn
podem ser passadas para o
renderizador Matplotlib
comoargumentos de
palavra-chave

coluna como total_bill para oargumento de palavra-chave size irá armazenar


essa coluna automaticamente, em vez de permitir uma gama contínua de tamanhos
de marcadores. O valor de transparência de cada ponto é especificado pelo argumento
de palavra-chave alpha que é passado para o Matplotlib renderizar. Isso significa
que a Seaborn não o usa para filtrar as opções que fluem para a Matplotlib scatter
como na Fig. 6.36.
>>> sns.relplot(x='total_bill',y='tip',alpha=0.3,data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f937127b640>

Além disso, como Seaborn processa oargumento de palavra-chave size, não é


possível empurrar a palavra-chave size argumento scatter até a renderização final
Matplotlib. Portanto, a desvantagem é que a Seaborn faz alterações editoriais
específicas na aparência do gráfico resultante que podem ser difíceis de ajustar fora
da semântica da Seaborn. O gráfico de scatter é o padrão kind para
relplot(), mas um gráfico de linha pode ser usado em vez do argumento de
palavra-chave kind=’line.

6.2.1 Agregação automática

Se um conjunto de dados tiver vários valores x com diferentes y-values, então a


Seaborn irá agregar esses valores para produzir um envelope de contenção para a
linha resultante.
>>> fmri = sns.load_dataset('fmri')
>>> fmri.head()
subject timepoint event region signal
0 s13 18 stim parietal -0.02
Capítulo 6 191
1 s5 14 stim parietal -0.08
2 s12 18 stim parietal -0.08
3 s11 18 stim parietal -0.05
4 s10 18 stim parietal -0.04

Existem cinquenta e seis valores de signal para cada um dos valores de


timepoint,
>>> fmri['timepoint'].value_counts()
18 56
8 56
1 56
2 56
3 56
4 56
5 56
6 56
7 56
9 56
17 56
10 56
11 56
12 56
13 56
14 56
15 56
16 56
0 56
Name: timepoint, dtype: int64

Aqui estão algumas estatísticas sobre odo grupo timepoint=0,


>>> fmri.query('timepoint==0')['signal'].describe()
count 56.00
mean -0.02
std 0.03
min -0.06
25% -0.04
50% -0.02
75% 0.00
max 0.07
Name: signal, dtype: float64

Devido a esta multiplicidade, Seaborn irá traçar a média de cada um dos pontos,
conforme mostrado com o marker=’o’ com intervalos de confiança acima e abaixo
especificando o intervalo de confiança de 95% da estimativa média como na Fig.
6.37,
>>> sns.relplot(x='timepoint', y='signal', kind='line',data=fmri,
c→ marker='o')
<seaborn.axisgrid.FacetGrid object at 0x7f93709c5df0>

A desvantagem é que a estimativa é calculada usando bootstrapping, que pode ser


lento para grandes conjuntos de dados e pode ser desativado usando o argumento de
palavra-chave ci=None ou substituído por ci='sd', que em vez disso usará o
desvio padrão mais fácil de calcular. O argumento da palavra-chave
estimator=None fará com que isso pare completamente. Tal como acontece com
kind=’scatter’, colunas de dataframe adicionais podem ser
192 Capítulo 6
Fig. 6.37 Dados contendo
múltiplos valores de x para
diferentes valores de y
podem ser
automaticamente
agregados com estimativa
inicializada de intervalos
de confiança

recrutados para distinguir o gráfico resultante. O seguinte usa hue = 'event' para
desenhar gráficos de linha distintos para cada uma das categorias de fmri.event,
>>> fmri.event.unique()
array(['stim', 'cue'], dtype=object)

Agora, temos gráficos de linha distintos para cada categórico fmri.event como na
Fig. 6.38,
>>> sns.relplot(x='timepoint', y='signal',
... kind='line', data=fmri,
... hue='event', marker='o')
<seaborn.axisgrid.FacetGrid object at 0x7f93709c5df0>

Se a coluna hue for numérico em vez de categórico, as cores serão dimensionadas


em um intervalo contínuo. Considere o seguinte conjunto de dados e a
Fig.correspondente 6.39:
>>> dots = sns.load_dataset('dots').query('align == "dots"')
>>> dots.head()
align choice time coherence firing_rate
0 dots T1 -80 0.00 33.19
1 dots T1 -80 3.20 31.69
2 dots T1 -80 6.40 34.28
3 dots T1 -80 12.80 32.63
4 dots T1 -80 25.60 35.06
>>> sns.relplot(x='time', y='firing_rate',
... hue='coherence', style='choice',
... kind='line', data=dots)
<seaborn.axisgrid.FacetGrid object at 0x7f9370883cd0>

Existem dois estilos de linha diferentes porque existem dois valores únicos diferentes
na coluna choice. As linhas são coloridas de acordo com a coluna numérica.
coherence
Capítulo 6 193
Fig. 6.38 Acategórico event
coluna depode colorir cada
conjunto de dados de maneira
diferente

Fig. 6.39 Quando as colunas


são numéricas em vez de
categóricas, as cores podem
ser escaladas ao longo de um
intervalo contínuo

Seaborn é particularmente forte com paletas de cores. O seguinte gera um mapa de


cores sobre n_colors:
>>> palette = sns.color_palette('viridis', n_colors=6)

Então, redesenhar o mesmo gráfico na Fig. 6.40 torna as linhas coloridas


individualmente um pouco mais distintas, especialmente com uma largura de linha
ligeiramente mais espessa,
>>> sns.relplot(x='time', y='firing_rate',palette=palette,
... hue='coherence', style='choice',
... kind='line', data=dots, linewidth=2)
<seaborn.axisgrid.FacetGrid object at 0x7f937085a130>
194 Capítulo 6
Fig. 6.40 O mesmo da Fig.
6.39, exceto com cores
diferentes

6.2.2 Multiple Plots

Como o relplot retorna um objeto FacetGrid, é simples criar vários subplots


especificando os argumentos de palavra-chave row e col. A Fig.seguir 6.41 a
empilha os dois subplots lado a lado em duas colunas porque há dois valores
exclusivos para a coluna time no dataframe.
>>> tips.time.unique()
['Dinner', 'Lunch']
Categories (2, object): ['Dinner', 'Lunch']

>>> sns.relplot(x='total_bill', y='tip', hue='smoker',


... col='time', # columns for time
... data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f9370704ee0>

Vários gráficos em grade podem ser gerados ao longo de várias linhas e colunas. O
argumento de palavra-chave col_wrap impede que a Fig.a seguir 6.42 preencha um
gráfico muito amplo e, em vez disso, coloca cada um dos subgráficos em linhas
separadas,
>>> sns.relplot(x='timepoint', y='signal', hue='event',
... style='event',
... col='subject', col_wrap=5,
... height=3, aspect=.75, linewidth=2.5,
... kind='line',
... data=fmri.query('region == "frontal"'))
<seaborn.axisgrid.FacetGrid object at 0x7f9370895be0>
Capítulo 6 195

Fig. 6.41 Matplotlib os subplots são chamados de facetas no Seaborn

Fig. 6.42 Facetas suportam tilings complexos


196 Capítulo 6
Fig. 6.43 Seaborn suporta
histogramas

6.2.3 Gráficos de distribuição

Visualizar distribuições de dados é fundamental para qualquer análise estatística, bem


como para diagnosticar problemas com modelos de aprendizado de máquina. Seaborn
é particularmente forte em fornecer opções bem pensadas para visualizar
distribuições de dados. A técnica de visualização de distribuição de dados univariada
mais simples é o histograma,
>>> penguins = sns.load_dataset("penguins")
>>> penguins.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.10 18.70 181.00 3750.00 Male
1 Adelie Torgersen 39.50 17.40 186.00 3800.00 Female
2 Adelie Torgersen 40.30 18.00 195.00 3250.00 Female
3 Adelie Torgersen nan nan nan nan NaN
4 Adelie Torgersen 36.70 19.30 193.00 3450.00 Female

O Seaborn displot desenha um histograma rápido na Fig. 6.43,


>>> sns.displot (penguins, x="flipper_length_mm")
<seaborn.axisgrid.FacetGrid object at 0x7f9370951b20>

A largura dos bins no histograma pode ser selecionada com o argumento de palavra-
chave binwidth e o número de bins pode ser selecionado com o argumento de
palavra-chave bins. Para dados categóricos com alguns valores distintos, os bins
podem ser selecionados como uma sequência de valores distintos, como
bins=[1,3,5,8]. Como alternativa, o Seaborn pode lidar com isso
automaticamente com o argumento de palavra-chave discrete=True.
Outras colunas podem ser usadas para criar histogramas semitransparentes
sobrepostos como na Fig.a seguir 6.44:
>>> sns.displot(penguins, x="flipper_length_mm", hue="species")
<seaborn.axisgrid.FacetGrid object at 0x7f9363c04f70>
Capítulo 6 197
Fig. 6.44 Múltiplos histogramas
podem ser sobrepostos usando
cores e transparência.

Fig. 6.45 Igual à Fig. 6.44,


exceto sem linhas verticais
perturbadoras.

Seaborn seleciona inteligentemente os bins para combinar para cada respectivo


histograma, o que é difícil de fazer com a função padrão do Matplotlib hist().
Às vezes, as linhas verticais do histograma podem causar distração. Eles podem
ser removidos com o argumento de palavra-chave element = 'step' na Fig. 6.45,
>>> sns.displot(penguins, x='flipper_length_mm',
... hue='species',
... element='step')
<seaborn.axisgrid.FacetGrid object at 0x7f9363b01f70>

Em vez de camadas usando transparência, os histogramas podem ser empilhados no


topo uns dos outros (ver Fig. 6.46), mas isso pode ser difícil de conciliar com muitos
histogramas e alguns histogramas dominantes que podem sobrecarregar ou
obscurecer os outros.
>>> sns.displot(penguins, x="flipper_length_mm",
... hue="species",
198 Capítulo 6
Fig. 6.46 Múltiplos
histogramas podem ser
empilhados

... multiple="stack")
<seaborn.axisgrid.FacetGrid object at 0x7f9372f6fbb0>

Alternativamente, se houver são poucos histogramas, as barras podem ser estreitadas


o suficiente para caber lado a lado usando o argumento de palavra-chave
multiple=’dodge’. Para que os histogramas sejam usados como estimativas de
funções de densidade de probabilidade legítimas, eles devem ser escalados
apropriadamente e isso é feito por meio do argumento de palavra-chave
stat=’probability’.
Os histogramas aproximam a função de densidade de probabilidade univariada
com funções retangulares, mas as funções gaussianas podem ser usadas para criar
Estimativas de densidade de kernel (KDE) mais suaves da mesma função de
densidade de probabilidade na Fig. 6.47 usando o argumento de palavra-chave kind.
>>> sns.displot(penguins, x="flipper_length_mm", kind="kde")
<seaborn.axisgrid.FacetGrid object at 0x7f936398f3d0>

A suavidade do gráfico resultante é determinada pelo parâmetro de largura de banda


do KDE que está disponível como o argumento de palavra-chave bw_adjust.
Lembre-se de que, embora a suavidade possa ser visualmente agradável, pode
representar descontinuidades incorretas ou suprimir recursos nos dados que podem
ser importantes.
Além das funções de densidade de probabilidade univariada, Seaborn fornece
ferramentas de visualização poderosas para funções de densidade de probabilidade
bivariada (ver Fig. 6.48).
>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm")
<seaborn.axisgrid.FacetGrid object at 0x7f9363635730>

Figura 6.48 mostra a grade bidimensional que registra o número de pontos de dados
em cada um com a escala de cores correspondente para essas contagens . Usando o
argumento de palavra-chave kind='kde' também funciona com distribuições
bivariadas. Se não houver muita sobreposição entre as distribuições bivariadas, elas
podem ser plotadas em cores diferentes usando o argumento de palavra-chave hue
(consulte a Fig. 6.49). Diferentes larguras de bin para a
Capítulo 6 199
Fig. 6.47 Seaborn
também suporta Kernel
Density Estimates
(KDEs)

Fig. 6.48 Histograma


bivariado
200 Capítulo 6
Fig. 6.49 Múltiplos
histogramas bivariados
podem ser desenhados
com cores diferentes,

cada dimensão de coordenada pode ser selecionada usando o argumento de palavra-


chave binwidth mas com uma tupla definindo a largura do compartimento para
cada dimensão de coordenada. O argumento de palavra-chave cbar desenha a escala
de cores correspondente, mas quando combinada com o argumento de palavra-chave
hue, ele desenhará várias escalas de cores (uma para cada categoria codificada com
hue), o que torna a Fig.resultante 6.49 muito aglomerada.
>>> sns.displot(penguins, x='bill_length_mm',
... y='bill_depth_mm', hue='species')
<seaborn.axisgrid.FacetGrid object at 0x7f93632f2040>

Traçar as distribuições marginais de uma distribuição bivariada é muito útil e a


Seaborn torna isso fácil com o jointplot como na Fig. 6.50.
>>> sns.jointplot(data=penguins,
... x="bill_length_mm",
... y="bill_depth_mm")
<seaborn.axisgrid.JointGrid object at 0x7f9362f25670>

O jointplot retorna um objeto JointGrid que permite desenhar os marginais


separadamente usando o método plot_marginals() para a Fig. 6.51,
>>> g = sns.JointGrid(data=penguins,
... x="bill_length_mm",
... y="bill_depth_mm")
>>> g.plot_joint(sns.histplot)
<seaborn.axisgrid.JointGrid object at 0x7f93632e8f10>
>>> g.plot_marginals(sns.kdeplot,fill=True)
<seaborn.axisgrid.JointGrid object at 0x7f93632e8f10>

Observe que os marginais agora são plotagens KDE em vez de plotagens de caixa.
Argumentos de palavra-chave não usados em plot_marginals() são passados para
o argumento de função (fill = True para sns.kdeplot neste caso). Além do
Jointplot, o Seaborn fornece o Pairplot que irá produzir um Jointplot para
todos os pares de colunas no dataframe de entrada.
Capítulo 6 201
Fig. 6.50 Distribuições
marginais
correspondentes às
distribuições bivariadas
podem ser desenhadas
ao longo dos eixos
horizontal / vertical.

Fig. 6.51 Distribuições


marginais
personalizadas também
podem ser desenhadas
202 Capítulo 6
Fig. 6.52 Distribuições
discretas ao longo de
variáveis categóricas O

Seaborn oferece muitas opções para visualizar gráficos categóricos. Considerando


que temos usado principalmente categorias para fornecer cores distintas ou gráficos
de sobreposição, Seaborn fornece visualizações especializadas especificamente para
dados categóricos. Por exemplo, o seguinte gráfico jittered na Fig. 6.52 mostra a
dispersão do total_bill com base no dia da semana.
>>> sns.catplot(x="day", y="total_bill", data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f93623c2610>

Importante, os pontos individuais são espalhados aleatoriamente ao longo da direção


horizontal para evitar sobrepor e obscurecer os pontos. Isso pode ser desativado
usando o argumento de palavra-chave jitter = False.
Um método interessante em vez de usar jitter para evitar o obscurecimento de
dados é usar um swarmplot usando o argumento de palavra-chave
kind='swarm' na Fig. 6.53,
>>> sns.catplot(data=tips,
... x="day",
... y="total_bill",
... hue="sex",
... kind="swarm")
<seaborn.axisgrid.FacetGrid object at 0x7f93623cb220>

A extensão dos braços estendidos em forma de árvore de Natal na Fig. 6.53 substitui
o posicionamento aleatório devido ao tremor. Um aspecto fascinante desse enredo é
o efeito de agrupamento que ele produz, que automaticamente chama sua atenção
para certas características que seriam difíceis de determinar de antemão. Por
exemplo, a Fig. 6.53 parece mostrar que a conta total para homens é maior do que
para mulheres no sábado e, em particular, está em torno de total_bill = 20. Usando
o Seaborn displot, podemos verificar isso rapidamente com o seguinte código para
a Fig. 6.54:
Capítulo 6 203
Fig. 6.53 O mesmo da
Fig. 6.52, mas usando
um enxame gráfico de

Fig. 6.54 O histograma


mostra detalhes do
gráfico de enxame

>>> sns.displot(tips.query('day=="Sat" and sex=="Male"'),


... x='total_bill')
<seaborn.axisgrid.FacetGrid object at 0x7f9362021a90>

Cores, linhas e apresentação geral de Os gráficos Seaborn podem ser controlados


com sns.set_theme() em um nível global ou em um nível específico de figura
usando um gerenciador de contexto como sns.axes_style("white"), por exemplo.
O controle de granulação mais fina de fontes e outros detalhes estão disponíveis com
a função set(). Sequências de cores para dados discretos ou cores claras
distinguíveis para dados contínuos são
204 Capítulo 6
fortemente suportadas no Seaborn, como vimos anteriormente com color_palette.
O sistema perceptivo humano responde fortemente às cores e muitas das armadilhas
comuns foram reconciliadas na abordagem da Seaborn para gerenciar cores para
dados.

6.3 Bokeh

A combinação de tecnologias de visualização baseadas na web e Python abre uma


ampla gama de possibilidades para visualização interativa baseada na web usando
estruturas Javascript modernas. Isso torna possível implantar visualizações de dados
Python com usuários interagindo por meio de navegadores da web modernos, como
Google Chrome ou Mozilla Firefox (não o Internet Explorer!). Bokeh é um módulo
Python de código aberto que torna muito mais fácil desenvolver e implantar essas
visualizações interativas baseadas na web porque fornece primitivas que evitam a
escrita de Javascript de baixo nível.

6.3.1 Usando Bokeh Primitives

Aqui está um exemplo rápido que gera a Fig. 6.55:


from bokeh.plotting import figure, output_file, show
# prepare some data
x = range(10)
y = [i**2 for i in x]
# create a new plot with a title and axis labels
p = figure(title="plot the square", # title of figure
x_axis_label='x',
y_axis_label='y',
width= 400, # figure width
height = 300) # figure height
# add a line renderer with legend and line thickness
p.line(x, y,
legend=r"x^2", # text that appears in legend
line_width=2) # width of line

# show the results


show(p)

Bokeh segue a mesma filosofia que Matplotlib em termos de camadas do gráfico


elementos na tela. Como o Bokeh pode renderizar para vários endpoints (incluindo
notebooks Jupyter), escolhemos o endpoint como um arquivo html de saída com a
função output_file. Feito isso, a etapa principal é criar uma tela de figura com a
função de figure e fornecer parâmetros para a figura como x_axis_label e
width. Então, adicionamos a linha com o método line no objeto figura incluem
parâmetros de linha como line_width. Finalmente, a função show realmente grava
o HTML no arquivo especificado em output_file. Assim, a chave
Capítulo 6 205

Fig. 6.55 Gráfico Bokeh básico

do benefício do Bokeh é que ele permite preparar a visualização interativa em Python


enquanto renderiza o Javascript necessário no arquivo de saída HTML.
Na margem direita da figura, temos as ferramentas padrão para a figura. A
ferramenta panorâmica no topo arrasta a linha com o quadro da figura. A ferramenta
de zoom está logo abaixo disso. Essas ferramentas são incorporadas ao HTML como
funções Javascript que são executadas pelo navegador. Isso significa que o Bokeh
criou o HTML estático e o Javascript incorporado. Os dados para a plotagem também
são incorporados no documento HTML renderizado. Como Matplotlib, Bokeh
fornece controle de baixo nível de cada elemento que é renderizado na tela.
Simultaneamente, isso significa que você pode criar gráficos poderosos e atraentes
usando as primitivas do kit de ferramentas ao custo de programar cada elemento do
gráfico. Como Matplotlib, Bokeh também suporta uma galeria 1de gráficos de
exemplo detalhados que você pode usar como pontos de partida para suas próprias
visualizações.
Você pode especificar as propriedades individuais de um elemento gráfico (ou
seja, glifo) quando ele é adicionado à tela. A próxima linha adiciona círculos
vermelhos de um raio especificado para marcar cada um dos pontos de dados (Fig.
6.56):
p.circle(x,y,radius=0.2,fill_color='red')

Você também pode adicionar propriedades após o fato salvando o objeto individual
da seguinte forma:
c = p.circle(x,y)
c.glyph.radius= 0.2
c.glyph.fill_color = 'red'

1
https://docs.bokeh.org/en/latest/docs/gallery.html.
206 Capítulo 6

Fig. 6.56 Plotar com marcadores personalizados para cada ponto de dados

Para adicionar vários glifos à sua figura, use os métodos do objeto de figura (por
exemplo, p.circle(), p.line(), p.square()). A documentação principal
[1] possui uma lista abrangente de primitivas disponíveis. É apenas uma questão de
aprender o vocabulário dos glifos disponíveis e atribuir suas propriedades

6.3.2 Bokeh Layouts

Análogo a função subplots em Matplotlib, Bokeh possui as funções row e


column que organizam figuras individuais na mesma tela. O próximo exemplo usa
a função row para colocar duas figuras lado a lado (Fig. 6.57).
from bokeh.plotting import figure, show
from bokeh.layouts import row, column
from bokeh.io import output_file
output_file('bokeh_row_plot.html')
x = range(10)
y = [i**2 for i in x]
f1 = figure(width=200,height=200)
f1.line(x,y,line_width=3)
f2 = figure(width=200,height=200)
f2.line(x,x,line_width=4,line_color='red') show(row(f1,f2))

Neste exemplo, criamos os objetos de figuras individuais e então usamos o método


show() na função row() para criar o layout lado a lado das duas figuras f1 e f2.
Isso pode ser combinado com a função column para criar layouts de grade como a
seguir (Fig. 6.58):
Capítulo 6 207

Fig. 6.57 Gráfico de linha

from bokeh.plotting import figure, show


from bokeh.layouts import row, column
from bokeh.io import output_file

output_file('bokeh_row_column_plot.html')

x = range(10)
y = [i**2 for i in x]
f1 = figure(width=200,height=200)
f1.line(x,y,line_width=3)
f2 = figure(width=200,height=200)
f2.line(x,x,line_width=4,line_color='red')
f3 = figure(width=200,height=200)
f3.line(x,x,line_width=4,line_color='black')
f4 = figure(width=200,height=200)
f4.line(x,x,line_width=4,line_color='green')
show(column(row(f1,f2),row(f3,f4)))

Observe que, como as figuras individuais são tratadas separadamente, elas têm seu
próprio conjunto de ferramentas. Isso pode ser aliviado usando
bokeh.layouts.gridplot ou a função mais geral
bokeh.layouts.layout.

6.3.3 Widgets Bokeh

Os widgets interativos permitem que o usuário interaja e explore a visualização


Bokeh. O site principal possui o inventário de widgets disponíveis. Eles são baseados
principalmente na biblioteca Bootstrap JavaScript. Todos os widgets têm o mesmo
padrão de implementação de duas etapas. A primeira etapa é criar o widget conforme
será disposto na tela. A segunda etapa é criar o retorno de chamada que será acionado
quando o usuário interagir com o widget.
208 Capítulo 6

Fig. 6.58 Gráfico linha-coluna Bokeh

Aqui é onde as coisas ficam complicadas. Dado que o retorno de chamada


especifica algum tipo de ação com base no widget, onde essa ação ocorrerá? Se a
saída for um arquivo HTML renderizado pelo navegador, essa ação deve ser tratada
via Javascript e executada no navegador. As complicações começam quando essa
interação é orientada ou dependente de objetos no espaço de trabalho do Python.
Lembre-se de que, após a criação do arquivo HTML, não há mais Python. Se você
quiser callbacks que utilizem um processo Python, terá que usar o bokeh para
hospedar esse aplicativo (Fig. 6.59).
Vamos começar com os callbacks que são manipulados por Javascript na saída
HTML estática, como a seguir:
from bokeh.io import output_file, show
from bokeh import events
from bokeh.models.widgets import Button
from bokeh.models.callbacks import CustomJS
output_file("button.html")
cb = CustomJS(args=dict(),code='''
alert("ouch!");
''')
button = Button(label='Hit me!') # create button object
Capítulo 6 209
Fig. 6.59 Retorno de chamada
de JavaScript para widget de Hit me!
botão

Fig. 6.60 Retorno de


chamada de JavaScript
para widget suspenso

botão de.js_on_event (events.ButtonClick, cb) show (button)

A etapa chave aqui é a função CustomJS que pega a string incorporada de Javascript
válido e a empacota para o arquivo HTML estático. O widget de botão não tem
argumentos, portanto, a variável args é apenas um dicionário vazio. A próxima parte
importante é a função js_on_event que especifica o evento (ou seja, ButtonClick)
e o retorno de chamada atribuído para tratar esse evento. Agora, quando o arquivo
HTML de saída for criado e você renderizar a página no navegador, clique no botão
chamado Hit me! você receberá um pop-up do navegador com o texto ouch nele.
Seguindo essa estrutura, podemos tentar algo mais envolvente, como no seguinte
(Fig. 6.60):
from bokeh.io import output_file, show
from bokeh.models.callbacks import CustomJS
from bokeh.layouts import widgetbox
from bokeh.models.widgets import Dropdown
output_file("bokeh_dropdown.html")
cb = CustomJS(args=dict(),code='alert(cb_obj.value)')
menu = [("Banana", "item_1"),
("Apple", "item_2"),
("Mango", "item_3")]
dropdown = Dropdown(label="Dropdown button", menu=menu,callback=cb)
show(widgetbox(dropdown))

Neste exemplo, estamos usando o widget DropDown e preenchendo-o com os itens


do menu. O primeiro elemento da tupla é a string que aparecerá na lista suspensa e o
segundo elemento é a variável Javascript associada a ela. O retorno de chamada é o
mesmo alert, exceto agora com a variável cb_obj. Este é o objeto de retorno de
chamada que é automaticamente passado para a função Javascript quando é ativado.
Neste caso, especificamos o callback no argumento de palavra-chave callback em
vez de depender do tipos event como antes. Agora, após as páginas renderizadas no
navegador e você clicar e descer em um elemento do menu, você deverá ver a janela
pop-up.
O exemplo a seguir combina o menu suspenso e o gráfico de linha que criamos
anteriormente. O único elemento novo aqui é passar o objeto de linha para a função
CustomJS por meio do argumento de palavra-chave args. Uma vez dentro do código
Javascript, podemos alterar o código embutido line_color do glifo de linha com base
no valor selecionado no menu suspenso por meio da variável cb_obj.value. Isso nos
permite ter um widget para controlar a propriedade de cor da linha. Usando a mesma
abordagem, podemos alterar outras
210 Capítulo 6
Fig. 6.61 Cor da linha de
atualizações de retorno de
chamada Javascript

propriedades de outros objetos, desde que as passemos por meio do argumento de


palavra-chave para args a função CustomJS (Fig. 6.61).
from bokeh.plotting import figure, output_file, show
from bokeh.layouts import column
from bokeh.models.widgets import Dropdown
from bokeh.models.callbacks import CustomJS
output_file("bokeh_line_dropdown.html")
# make some data
x = range(10)
y = [i**2 for i in x]
# create the figure as usual
p = figure(width = 300, height=200, # figure width and height
tools="save,pan", # only these tools
x_axis_label='x', # label x and y axes
y_axis_label='y')
# add the line,
line = p.line(x,y, # x,y data
line_color='red',# in red
line_width=3) # line thickness
# elements for pulldown
menu = [("Red", "red"),("Green", "green"),("Blue", "blue")]
# pass the line object and change line_color based on pulldown
c→ choice
cb = CustomJS(args=dict(line=line),
code='''
var line = line.glyph;
var f = cb_obj.value;
line.line_color = f;
''')
# assign callback to Dropdown
dropdown = Dropdown(label="Select Line Color",
c→ menu=menu,callback=cb)
# use column to put Dropdown above figure
show(column(dropdown,p))
Capítulo 6 211
Fig. 6.62 O retorno de chamada
do Javascript atualiza a
frequência da onda senoidal do
gráfico

O último exemplo mostra como manipular as propriedades da linha usando o


retorno de chamada executado pelo navegador. O Bokeh pode ir mais fundo alterando
os próprios dados e deixando a figura incorporada reagir a essas mudanças. O
próximo exemplo é muito semelhante ao último, exceto que aqui usamos o objeto
ColumnDataSource para transportar dados do Python para o navegador. Em
seguida, no código Javascript embutido, descompactamos os dados e, em seguida,
alteramos a matriz de dados com base na ação no menu suspenso. A parte crucial é
acionar a atualização de dados e desenhar usando a função
source.change.emit() (Fig. 6.62).
from bokeh.plotting import figure, output_file, show
from bokeh.layouts import row, column
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import Dropdown
from bokeh.models.callbacks import CustomJS
import numpy as np
output_file("bokeh_ColumnDataSource.html")
# make some data
t = np.linspace(0,1,150)
y = np.cos(2*np.pi*t)
# create the ColumnDataSource and pack the data in it
source = ColumnDataSource(data=dict(t=t, y=y))
# create the figure as usual
p = figure(width = 300, height=200, tools="save,pan",
x_axis_label='time (s)',y_axis_label='Amplitude')
# add the line, but now using the ColumnDataSource
line = p.line('t','y',source=source,line_color='red')
menu = [("1 Hz", "1"), ("5 Hz", "5"), ("10 Hz", "10")]
cb = CustomJS(args=dict(source=source),
code='''
var data = source.data;
var f = cb_obj.value;
var pi = Math.PI;
t = data['t'];
y = data['y'];
212 Capítulo 6
for (i = 0; i < t.length; i++) {
y[i] = Math.cos(2*pi*t[i]*f)
}
source.change.emit();
''')
dropdown = Dropdown(label="Select Wave Frequency",
c→ menu=menu,callback=cb)
show(column(dropdown,p))
A vantagem dessa abordagem é que ela cria um arquivo HTML autocontido que
não precisa mais do Python. Por outro lado, pode ser que o retorno de chamada
requeira computação Python ativa que não é possível para JavaScript no navegador.
Neste caso, podemos usar o bokeh server como em
Terminal> bokeh servir bokeh_ColumnDataSource_server.py
que irá executar um pequeno servidor e abrir uma página da web apontando para a
página local. A página da web contém basicamente o mesmo conteúdo do último
exemplo, embora você possa notar que a página é menos responsiva do que o último
exemplo. Isso ocorre porque a interação deve retornar ao processo do servidor e, em
seguida, ao navegador, em vez de ser atualizada no próprio navegador. O conteúdo
de bokeh_ColumnDataSource_server é mostrado abaixo
from bokeh.plotting import figure, show, curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Select
from bokeh.models.widgets import Dropdown
import numpy as np
# make some data
t = np.linspace(0,1,150)
y = np.cos(2*np.pi*t)
# create the ColumnDataSource and pack the data in it
source = ColumnDataSource(data=dict(t=t,y=y))

# create the figure as usual


p = figure(width = 300, height=200, tools="save,pan",
x_axis_label='time (s)',y_axis_label='Amplitude')
# add the line, but now using the ColumnDataSource
line = p.line('t','y',source=source,line_color='red')
menu = [("1 Hz", "1"),
("5 Hz", "5"),
("10 Hz", "10")]
def cb(attr,old,new):
f=float(freq_select.value)
d = dict(x=source.data['t'],y=np.cos(2*np.pi*t*f))
source.data.update(d)

freq_select = Select(value='1 Hz', title='Frequency (Hz)',


options=['1','5','10'])
freq_select.on_change('value',cb)
curdoc().add_root(column(freq_select,p))
Observe que o elemento Select ocupa o lugar do menu DropDown. O
freq_select.on_change('value',cb) é como a seleção suspensa é comunicada ao
processo do servidor Python. É importante ressaltar que o argumento da palavra-
chave options levam uma sequência de strings ['1', '5', '10'] , embora
tenhamos que converter as strings de volta para float no retorno de chamada.
Capítulo 6 213
freq_select = Select(value='1 Hz',
title='Frequency (Hz)',
options=['1','5','10'])

A etapa chave final é incluir curdoc().add_root(column(freq_select,p)) que


cria um orientado column que está anexado ao documento e que responderá ao
retorno de chamada. Observe que há muitas opções de configuração para implantar
um bokeh servidor, incluindo o uso de Tornado como backend e a execução de vários
threads (consulte a documentação principal para obter detalhes). O Bokeh também tem
muitas opções de integração com o notebook Jupyter.
O Bokeh está em desenvolvimento ativo e o design geral e a arquitetura do Bokeh
são bem pensados. A combinação de visualizações JavaScript e Python vendidas por
meio de navegadores da web é um tópico de crescimento incrível na comunidade de
código aberto. Bokeh representa apenas uma (e provavelmente a melhor) das muitas
implementações dessa estratégia conjunta. Fique atento para novos desenvolvimentos
e recursos da equipe do Bokeh, mas tenha em mente que, ao contrário do Matplotlib,
o Bokeh é menos maduro.

6.4 Altair

Outro projeto no espaço de visualizações científicas baseadas na web é Altair que


é um declarativo módulo de visualização. Declarativo significa que os elementos de
dados são atribuídos a uma função visual que o Altair implementa; em contraste com
Matplotlib em que todos os detalhes da construção devem ser especificados (ou seja,
visualização obrigatória). Altair implementa a gramática gráfica JSON Vega-Lite para
visualização. Isso significa que os gráficos renderizados de baixo nível são realmente
implementados em JavaScript via Vega. A melhor maneira de trabalhar com o Altair
é por meio dos cadernos Jupyter, pois a criação de visualizações autônomas exige uma
série de etapas adicionais, cujos detalhes estão no site de documentação principal. Ao
usar o Jupyter Notebook, pode ser necessário habilitar o renderizador Altair,
import altair as alt

O objeto Altair principal é o Chart objeto.


>>> import altair as alt
>>> from altair import Chart
>>> import vega_datasets
>>> cars = vega_datasets.data('cars')
>>> chart = Chart(cars)

Importante, entrada de data no Chart deve ser um dataframe Pandas, objeto


altair.Data ou uma URL referenciando um arquivo CSV ou JSON no formato
denominado tidy, o que basicamente significa que as colunas são variáveis e as linhas
do dataframe são observações dessas variáveis. Para criar a visualização do Altair,
você deve decidir sobre uma marca (ou seja, glifo) para os valores de dados. Fazendo
chart.mark_point() vai apenas desenhar um círculo no caderno Jupyter. Para
desenhar um gráfico, você deve especificar
214 Capítulo 6
Fig. 6.63 Gráfico
unidimensional do Altair 0 50 100 150 200 250 300 350 400 450 500
Displacement

25
Fig. 6.64 gráfico bidimensional
do Altair
20

Acceleration
15

10

0
0 50 100 150 200 250 300 350 400 450 500
Displacement

Canais do para os outros elementos do dataframe como no seguinte correspondente


à Fig. 6.63:
>>> chart.mark_point().encode(x='Displacement')
alt.Chart(...)

que cria um gráfico de dispersão unidimensional usando a coluna Deslocamento


do dataframe de entrada. Conceitualmente, significa encode crie um mapeamento
entre os dados e sua representação visual. Usando este padrão, para criar um gráfico
bidimensional X-Y, precisamos especificar um canal adicional para os dados y como
a seguir (ver Fig. 6.64):
>>> chart.mark_point().encode(x = 'Displacement',
... y = 'Acceleration')
alt.Chart(...)

Isso cria um gráfico bidimensional de aceleração versus deslocamento. Observe que


o objeto chart tem acesso aos nomes das colunas no dataframe de entrada para que
possamos acessá-los por seus nomes de string como argumentos de palavra-chave
para o método encode. Como cada linha tem um nome categórico correspondente,
podemos usá-lo como a dimensão da cor no gráfico XY criado, usando o argumento
de palavra-chave color como na Fig.a seguir 6.65:
>>> chart.mark_point().encode(x='Displacement',
... y='Acceleration',
... color='Origin')
alt.Chart(...)

A chave para usar Altair é perceber que é uma camada fina que aproveita a biblioteca
de visualização JavaScript Vega-Lite.
Capítulo 6 215
Origin
25
Europe
Japan
USA
20
Acceleration

15

10

0
0 50 100 150 200 250 300 350 400 450 500
Displacement

Fig. 6.65 A cor de cada fabricante é determinada pela coluna Origem no quadro de dados de entrada

Assim, você pode construir qualquer tipo de visualização Vega-lite no Altair e


conectá-la aos dataframes do Pandas ou outras construções Python sem ter que
escrever o JavaScript você mesmo. Se você quiser alterar as cores individuais ou as
larguras das linhas dos glifos, terá que usar o editor Vega e, em seguida, puxar a
especificação Vega-lite de volta para o Altair, o que não é difícil, mas envolve várias
etapas. A questão é que esse tipo de personalização não é o objetivo do Altair. Como
um módulo de visualização declarativa, a ideia é deixar esses detalhes para o Altair
e se concentrar no mapeamento dos dados para os elementos visuais.

6.4.1 Detalhando o Altair

O controle dos elementos individuais da apresentação visual no Altair pode ser


realizado usando a família de funções de nível superior configure_ como na Fig.
6.66.
>>> (chart.configure_axis(titleFontSize=20,
... titleFont='Consolas')
... .mark_point()
... .encode(x='Displacement',
... y='Acceleration',
... color='Origin')
... )
alt.Chart(...)

Observe que titleFontSize para os títulos em cada um dos eixos aumentou e a


família de fontes correspondente foi atualizada. Em vez de usar os nomes de coluna
padrão como rótulos de título, podemos usar os objetos alt.X() e alt.Y() para
personalizá-los como na Fig.a seguir 6.67:
>>> (chart.configure_axis(titleFontSize=20,
... titleFont='Consolas')
216 Capítulo 6
Origin
25
Europe
Japan
USA
20
Acceleration

15

10

0
0 50 100 150 200 250 300 350 400 450 500
Displacement

Fig. 6.66 A família de fontes título e tamanho pode ser definido com configure_axis

Origin
25
Europe
Japan
USA
20
Acceleration (m / s)

15

10

0
0 50 100 150 200 250 300 350 400 450 500
Displacement (m)

Fig. 6.67 Com alt.X e alt.Y,os rótulos de cada eixo pode ser alterada

... .mark_point()
... .encode(x=alt.X('Displacement',
... title='Displacement (m)'),
... y=alt.Y('Acceleration',
... title='Acceleration (m/s)'),
... color='Origin')
... )
alt.Chart(...)

Observe que cada um dos rótulos possui unidades. Se quiséssemos controlar o eixo
vertical, poderíamos ter especificado configure_axisLeft e apenas esse eixo seria
afetado por essas mudanças.
Capítulo 6 217
Europe

Origin
Japan
USA

0 20 40 60 80 100 120 140 160 180 200 220 240


Horse power

Fig. 6.68 Barcharts são gerados por mark_bar nas colunas nomeadas x e y colunas no tdataframe

Existem também configure_axisTop e configure_axisRight, configure_axisX,


configure_axisY também. A rotulagem das marcas de escala no eixo é controlada
por labelFontSize e outros parâmetros relacionados.
Os gráficos de barras são gerados seguindo o mesmo padrão, exceto com
mark_bar, como na Fig.a seguir 6.68:
>>> chart.mark_bar().encode(y='Origin',x='Horsepower')
alt.Chart(...)

Da mesma forma, os gráficos de área são gerados usando mark_area e assim por
diante. Você pode manter interactive() no final da chamada para tornar o
enredo ampliável com a roda do mouse. Os gráficos podem ser salvos
automaticamente nos formatos PNG e SVG, mas requerem ferramentas adicionais de
automação do navegador ou altair_saver. Os gráficos também podem ser salvos
em arquivos HTML com os embeddings JavaScript necessários.

6.4.2 Agregações e transformações

O Altair pode realizar certas agregações em elementos de dados de um dataframe.


Isso evita que você tenha que adicionar agregações em outro dataframe separado que
teria que ser passado como entrada (consulte a Fig. 6.69).
>>> cars = vega_datasets.data('cars')
>>> (alt.Chart(cars).mark_point(size=200,
... filled=True)
... .encode(x='Year:T',
... y='mean(Horsepower)')
... )
alt.Chart(...)

O texto mean na string significa que a média da Horsepower será usada para o valor
y (ver Fig. 6.69). O sufixo : T significa que a coluna Year no dataframe deve ser
tratado como um carimbo de data / hora. Os argumentos para a função mark_point
controlar as propriedades de tamanho e preenchimento dos marcadores de ponto.
Na Fig. 6.69 anterior, calculamos a média do Horsepower, mas incluímos todas
as origens dos veículos. Se quisermos incluir apenas veículos fabricados nos EUA,
podemos usar o método transform_filter, como na seguinte Fig. 6.70:
>>> (alt.Chart(cars).mark_point(size=200,filled=True)
... .encode(x='Year:T',
218 Capítulo 6
160

140

120
Mean of Horsepower

100

80

60

40

20

0
1970 1972 1974 1976 1978 1980 1982
Year

Fig 6,69 agregações como tomando a mean estão disponíveis através Altair

180

160

140
Mean of Horsepower

120

100

80

60

40

20

0
1970 1972 1974 1976 1978 1980 1982
Year

Fig 6,70 Os filtros podem ser usados nas agregações com transform_filter

... y='mean(Horsepower)',
... )
... .transform_filter('datum.Origin=="USA"')
... )
alt.Chart(...)

A transformação do filtro garante que apenas os veículos dos EUA sejam concluídos
no cálculo da média. A palavra datum é como o Vega-lite se refere aos seus
elementos de dados. Na
Capítulo 6 219
100

90

80

70
Mean of Horsepower

60

50

40

30

20

10

0
1970 1972 1974 1976 1978 1980 1982
Year

Fig. 6.71 O alt.FieldRangePredicate filtra uma gama de valores

além de expressões, o Altair fornece objetos predicados que podem realizar operações
de filtragem avançada. Por exemplo, usando o FieldRangePredicate,
podemos selecionar um intervalo de valores de uma variável contínua como na
seguinte Fig. 6.71:
>>> (alt.Chart(cars).mark_point(size=200,filled=True)
... .encode(x='Year:T',
... y='mean(Horsepower)',
... )
... .transform_filter(alt.FieldRangePredicate('Horsepower',
... [75,100]))
... )
alt.Chart(...)

Isso significa que apenas os valores de Horsepower entre 75 e 100 serão


considerados no cálculo e no gráfico Altair resultante (Fig. 6.72).
>>> (alt.Chart(cars).mark_point(size=200,
... filled=True)
... .encode(x='Year:T',
... y=alt.Y('mean(Horsepower)',
... scale=alt.Scale(domain=[60,110])),
... )
... .transform_filter(alt.FieldRangePredicate('Horsepower',
... [75,100]))
... .properties(width=300,height=200)
... )
alt.Chart(...)

Observe que os limites de escala da figura resultante foram ajustados usando


alt.Scale com a palavra-chave domain. Os predicados podem ser combinados
usando operações lógicas como LogicalNotPredicate e outros.
220 Capítulo 6
110

100
Mean of Horsepower

90

80

70

60
1970 1972 1974 1976 1978 1980 1982
Year
Fig. 6.72 Os limites do eixo podem ser ajustados usando alt.Scale

O método transform_calculate permite que expressões básicas sejam aplicadas


aos elementos de dados. Por exemplo, para calcular o quadrado da potência, podemos
fazer o seguinte Fig 6.73:.
>>> from altair import datum
>>> h1=(alt.Chart(cars).mark_point(size=200,
... filled=True)
... .encode(x='Year:T',
... y='sqh:Q')
... .transform_calculate(sqh =
e→ datum.Horsepower**2)
... )

Nota que o tipo de cálculo resultante deve ser especificado usando :Q (para
quantitativo) em referência à resultante.
Também podemos usar o método transform_aggregate para usar a variável
resultante sqh do método anterior transform_calculate para calcular a média
sobre os quadrados, como mostrado abaixo, enquanto o agrupamento ao longo do
Year, como na Fig. 6,74,
>>> h2=(alt.Chart(cars).mark_point(size=200,
... filled=True,
... color='red')
... .encode(x='Year:T',
... y='msq:Q')
... .transform_calculate(sqh =
e→ datum.Horsepower**2)
... .transform_aggregate(msq='mean(sqh)',
... groupby=['Year'])
... )

Estes dois gráficos podem ser sobrepostas utilizando o operador + (Fig. 6,75)
>>> h1+h2
alt.LayerChart(...)
Capítulo 6 221
55,000

50,000

45,000

40,000

35,000

30,000
sqh

25,000

20,000

15,000

10,000

5,000

0
1970 1972 1974 1976 1978 1980 1982
Year

Fig. 6.73 Cálculos incorporados são implementados com transform_calculate

110

100
Mean of Horsepower

90

80

70

60
1970 1972 1974 1976 1978 1980 1982
Year

Fig. 6.74 Transform e agregações computam itens intermediários em visualizações Altair

Existem outras funções na família transform_* incluindo transform_lookup,


transform_window e transform_bin. O site de documentação principal contém
detalhes.
222 Capítulo 6
55,000

50,000

45,000

40,000

35,000
sqh , msq

30,000

25,000

20,000

15,000

10,000

5,000

0
1970 1972 1974 1976 1978 1980 1982
Year

Fig. 6.75 Os gráficos podem ser sobrepostos com o operador plus

6.4.3 Interactive Altair

Altair possui recursos interativos herdados do Vega-lite. Elas são expressas como
ferramentas que podem ser facilmente atribuídas às visualizações do Altair. O
principal componente das interações é a seleção de objetos. Por exemplo, o seguinte
cria um objeto selection_interval() (veja a Fig. 6.76),
>>> brush = alt.selection_interval()

>>> chart.mark_point().encode(y='Displacement',
... x='Horsepower')\
... .properties(selection=brush)
alt.Chart(...)

De dentro do navegador da web, você pode arrastar o mouse no gráfico e você


verá uma caixa de seleção retangular aparecer, mas nada mudará porque não
anexamos o seletor brush a uma ação no gráfico. Para alterar a cor dos objetos
selecionados no gráfico, podemos usar o método alt.condition no método
encode (Fig. 6.77),
>>> (chart.mark_point().encode(y='Displacement',
... x='Horsepower',
... color=alt.condition(brush,
... 'Origin:N',
... alt.value('lightgray')))
... .properties(selection=brush)
... )
alt.Chart(...)
Capítulo 6 223
500

450

400

350
Displacement

300

250

200

150

100

50

0
0 20 40 60 80 100 120 140 160 180 200 220 240

Horsepower

Fig. 6.76 A seleção interativa pode ser habilitada com alt.selection_interval

500
Origin
Europe
450
Japan
USA
400

350
Displacement

300

250

200

150

100

50

0
0 20 40 60 80 100 120 140 160 180 200 220 240

Horsepower

Fig. 6.77 A seleção de elementos no gráfico aciona alt.Condition, que muda como os
elementos são renderizados
224 Capítulo 6
A condição significa que se os itens na seleção de pincel forem True, os pontos serão
coloridos de acordo com seus valores de Origin e, caso contrário, definidos como
lightgray usando alt.value, que define o valor a ser usado para a codificação.
Seletores podem ser compartilhados entre tabelas, como no seguinte (Fig
>>> chart = (alt.Chart(cars)
... .mark_point()
... .encode(x='Horsepower',
... color=alt.condition(brush,
... 'Origin:O',
... alt.value('lightgray')))
... .properties(selection=brush)
... )
>>> chart.encode(y='Displacement') & chart.encode(y='Acceleration')
alt.VConcatChart(...)

Uma série de coisas sutis aconteceram aqui. Primeiro, observe que a variável chart só
tem a coordenada x definido e que os dados cars estão embutidos. O símbolo E comercial
vertical & empilha os dois verticalmente. É apenas na última linha que a coordenada y para
cada gráfico é selecionada. O seletor é usado para ambos os gráficos, de forma que a
seleção com o mouse em qualquer um deles fará com que a seleção (por meio da
alt.condition) seja destacada em ambos os gráficos. Esta é uma interação
complicada que requer poucas linhas de código!
Os outros objetos de seleção como alt.selection_multi e alt.selection_single
permitir a seleção de itens individuais usando o clique do mouse ou ações de passar o
mouse (por exemplo, alt.selection_single(on='mouseover')). As seleções para
alt.selection_multi requerem um clique do mouse enquanto mantém pressionada a
tecla shift.
Altair é uma abordagem nova e inteligente do cenário de visualização em constante
mudança. Ao pegar carona no Vega-lite, o módulo garantiu que ele pode acompanhar esse
importante corpo de trabalho. Altair traz novas visualizações que não fazem parte do
vocabulário padrão Matplotlib ou Bokeh para o Python. Ainda assim, Altair é muito menos
maduro do que Matplotlib ou Bokeh. O próprio Vega-lite é um pacote complicado com
suas próprias dependências de JavaScript. Quando algo quebra, é difícil consertar porque
o problema pode ser muito profundo e espalhado na pilha de JavaScript para um
programador Python alcançar (ou mesmo identificar!).

6.5 Holoviews

Holoviews fornecem um meio de anotar dados para facilitar a visualização downstream


usando Bokeh, Plotly ou Matplotlib. O conceito-chave é fornecer semântica aos elementos
de dados que permitem a construção de visualizações de dados. Esta é uma abordagem
declarativa semelhante ao Altair, mas é agnóstica em relação à construção de visualização
downstream, enquanto altair tem um alvo downstream fixo de Vega-Lite.
>>> import numpy as np
>>> import pandas as pd
Capítulo 6 225
500
Origin
Europe
450
Japan
USA
400

350
Displacement

300

250

200

150

100

50

0
0 20 40 60 80 100 120 140 160 180 200 220 240
Horsepower

25

20
Acceleration

15

10

0
0 20 40 60 80 100 120 140 160 180 200 220 240
Horsepower

Fig. 6.78 Seletores interativos podem ser compartilhados em gráficos Altair

>>> import holoviews as hv


>>> from holoviews import opts
>>> hv.extension('bokeh')

A parte hv.extension declara que Bokeh deve ser usado como o construtor de
visualização downstream. Vamos criar alguns dados,
>>> xs = np.linspace(0,1,10)
>>> ys = 1+xs**2
>>> df = pd.DataFrame(dict(x=xs, y=ys))

Para visualizar esse dataframe com Holoviews, temos que decidir o que queremos
renderizar e como. Uma maneira de fazer isso é usando Holoviews Curve (veja a
Fig. 6.79).
226 Capítulo 6
Fig. 6.79 Dados de
plotagem usando
Holoviews Curve

>>> c=hv.Curve(df,'x','y')
>>> c
:Curve [x] (y)

O objeto Holoviews Curve declara que os dados são de uma função contínua que
mapeia x em colunas y no dataframe. Para renderizar este gráfico, usamos o display
Jupyter embutido,
>>> c
:Curve [x] (y)

Observe que os widgets Bokeh já estão incluídos no gráfico. O objeto Curve ainda
retém o dataframe embutido que pode ser manipulado através do atributo data.
Observe que poderíamos ter fornecido matrizes Numpy, listas Python ou um
dicionário Python em vez do dataframe Pandas.
>>> c.data.head()
x y
0 0.00 1.00
1 0.11 1.01
2 0.22 1.05
3 0.33 1.11
4 0.44 1.20

Podemos escolher Holoviews Scatter,em vez de Curve, se não quisermos


preencher os segmentos de linha entre os pontos de dados (ver Fig. 6.80),
>>> s = hv.Scatter(df,'x','y')
>>> s
:Scatter [x] (y)

Podemos combinar esses dois gráficos lado a lado na Fig. 6.81 usando o operador
sobrecarregado +,
Capítulo 6 227
Fig. 6.80 Plotting use
Holoviews Scatter

Fig. 6.81 Layouts lado a lado por meio do operador de adição

>>> s+c # notice how the Bokeh tools are shared between plots and
e→ affect both
:Layout
.Scatter.I :Scatter [x] (y)
.Curve.I :Curve [x] (y)

Eles podem ser sobrepostos usando o operador sobrecarregado * (Fig. 6.82),


>>> s*c # overlay with multiplication operator
:Overlay
.Scatter.I :Scatter [x] (y)
.Curve.I :Curve [x] (y)

Opções como width e height na figura podem ser enviadas diretamente como
mostrado na Fig. 6.83
>>> s.opts(color='r',size=10)
:Scatter [x] (y)
228 Capítulo 6
Fig. 6.82 Plotagens
sobrepostas com operador de
multiplicação

Fig. 6.83 As dimensões de visualização podem ser definidas usando width e height no método
options

>>> s.options(width=400,height=200)
:Scatter [x] (y)

Você também pode usar a magia %opts no bloco de notas Jupyter para gerenciar essas
opções. Embora seja uma abordagem muito bem organizada, ela só funciona no bloco de
notas Jupyter. Para redefinir o título conforme mostrado na Fig. 6.84, usamos o método
relabel.
>>> k = s.redim.range(y=(0,3)).relabel('Ploxxxt Title') # put a title
>>> k.options(color='g',size=10,width=400,
... height=200,yrotation=88)
:Scatter [x] (y)

Existem muitas outras opções para plotagem, conforme mostrado na galeria de referência.
A Figura 6.85 é um exemplo de uso de Barras. Observe o uso do método para criar o
objeto Bars do objeto Scatter. As altera xrotation a orientação dos marcadores
no eixo x.
Capítulo 6 229

Fig. 6.84 Visualização títulos são definidos usando relabel no objecto Holoviews

Fig. 6.85 Barplots estão disponíveis em Holoviews

>>> options = (opts.Scatter(width=400,


... height=300,
... xrotation=70,
... color='g',
... size=10),
... opts.Bars(width=400,
... height=200,
... color='blue',
... alpha=0.3)
... )
>>> (s.to(hv.Bars)*s).redim.range(y=(0,3)).options(*options)
:Overlay
.Bars.I :Bars [x] (y)
.Scatter.I :Scatter [x] (y)

Os dados podem ser cortados do objeto Holoviews onde a indexação é baseada na


coordenada x e significa None até o final dos dados x. Lembre-se de que o operador
mais combina os gráficos na Fig. 6.86,
>>> s[0:0.5] + s.select(x=(0.5,None)).options(color='b')
:Layout
.Scatter.I :Scatter [x] (y)
.Scatter.II :Scatter [x] (y)
230 Capítulo 6

Fig. 6.86 Os dados subjacentes nos gráficos Holoviews podem ser fatiados e renderizados
automaticamente

Holoviews referencia a chave das dimensões e kdims e o valor das dimensões como
vdims. Eles categorizam quais elementos devem ser considerados variáveis
independentes ou dependentes (respectivamente) no gráfico.
>>> s.kdims,s.vdims
([Dimension('x')], [Dimension('y')])

6.5.1 Dataset

Um Holoviews Dataset é não um metadado integrado que permite a visualização.


Em vez disso, um Dataset é uma maneira de criar um conjunto de dimensões que
posteriormente serão herdadas pelas visualizações downstream criadas a partir do
Dataset. A seguir, apenas as dimensões-chave precisam ser especificadas. As
outras colunas são inferidas como dimensões de valor (vdims), que podem ser
agrupadas posteriormente usando groupby.
>>> economic_data = pd.read_csv('macro_economic_data.csv')
>>> edata = hv.Dataset(data=economic_data,
... kdims=['country','year'])

>>> edata.groupby('year')
:HoloMap [year]
:Dataset [country] (growth,unem,capmob,trade)
>>> edata.groupby('country')
:HoloMap [country]
:Dataset [year] (growth,unem,capmob,trade)

Observe que o produto groupby mostra a variável independente (ou seja, dimensão
principal) year ou country que corresponde a qualquer um das outras dimensões
de valor (variáveis dependentes).
Capítulo 6 231

Fig. 6.87 Holoviews pode agrupar e plotar dados em uma etapa

. Fig. 6.88 Holoviews escolhe automaticamente o widget de controle deslizante com base no tipo de
dados na dimensão restante.

Na Fig. 6.87, o método to criar um widget suspenso correspondente para o


country.
>>> edata.to(hv.Curve,
... 'year',
... 'unem',
... groupby='country').options(height=200)
:HoloMap [country]
:Curve [year] (unem)

Também podemos agrupar por e outra dimensão-chave com um controle deslizante


correspondente para o year como mostrado na Fig. 6.88. Observe que o tipo de
widget foi inferido a partir do tipo da dimensão principal (ou seja, controle deslizante
para contínuo year e menu suspenso para discreto country).
>>> (edata.sort('country')
... .to(hv.Bars,'country','unem',groupby='year')
... .options(xrotation=45,height=300))
:HoloMap [year]
:Bars [country] (unem)

Na Fig. 6.89, não usamos nenhuma das dimensões-chave declaradas para que ambas
entrem nos widgets correspondentes.
232 Capítulo 6

Fig. 6.89 Holoviews escolhe widgets com base nas dimensões-chave

>>> edata.sort('country').to(hv.Bars,'growth','trade')
:HoloMap [country,year]
:Bars [growth] (trade)

6.5.1 Dados de imagem

Os elementos Holoviews podem lidar com dados bidimensionais tabulares ou


baseados em grade, como imagens (consulte Fig. 6.90).
>>> x = np.linspace(0, 10, 500)
>>> y = np.linspace(0, 10, 500)
>>> z = np.sin(x[:,None]*2)*y
>>> image = hv.Image(z)
>>> image
:Image [x,y] (z)

Podemos obter um histograma útil na Fig. 6.91 das cores da imagem usando o método
hist
>>> image.hist()
:AdjointLayout
:Image [x,y] (z)
:Histogram [z] (z_count)

6.5.2 dados tabulares

>>> economic_data= pd.read_csv('macro_economic_data.csv')


>>> economic_data.head()
Capítulo 6 233
Fig. 6.90 Holoviews cria
imagens de mapa de calor

Fig. 6.91 Histogramas adicionados às imagen

country year growth unem capmob trade


0 United States 1966 5.11 3.80 0 9.62
1 United States 1967 2.28 3.80 0 9.98
2 United States 1968 4.70 3.60 0 10.09
3 United States 1969 2.80 3.50 0 10.44
4 United States 1970 -0.20 4.90 0 10.50

O agrupamento dos elementos é possível especificando o vdims como uma lista


de nomes de colunas e fornecendo ao objeto Holoviews a coluna color_index nome
e uma color das paletas de cores disponíveis
(hv.Palette.colormaps.keys()). Observe que especificar a ferramenta
hover significa que os dados para cada elemento são mostrados ao passar o mouse
sobre ele (ver Fig. 6.92). Tenha cuidado para que todos os itens da legenda não
apareçam, a menos que a figura seja alta o suficiente para acomodá-los.
234 Capítulo 6

Fig. 6.92 holoviews014

>>> options = opts.Scatter(tools=['hover'],


... legend_position='left',
... color_index='country',
... width=800,height=500,
... alpha=0.5,
... color=hv.Palette('Category20'),
... size=10)
>>> c =
e→ hv.Scatter(economic_data,'year',['trade','country','unem'])
>>> c.redim.range(trade=(0,180)).options(options)
:Scatter [year] (trade,country,unem)

Usando Dataset também funciona para visualizações bidimensionais como mapas


de calor. Passar o mouse sobre a Fig. 6.93 mostra os dados correspondentes para cada
célula.
>>> options = opts.HeatMap(colorbar=True,
... width=600,
... height=300,
... xrotation=60,
... tools=['hover'])

>>>
e→ edata.to(hv.HeatMap,['year','country'],'growth').options(options)
:HeatMap [year,country] (growth)
Capítulo 6 235

Fig. 6.93 Holoviews Dataset suporta visualizações bidimensionais

6.5.3 Personalizando a Interatividade

DynamicMap adiciona interatividade às visualizações Holoviews. A dimensão


chave angle é fornecida pelo widget de controle deslizante. A função nomeada é
avaliada lentamente (Fig. 6.94).
>>> def dynamic_rotation(angle):
... radians = (angle / 180) * np.pi
... return (hv.Box(0,0,4,orientation=-radians).
options(color='r',line_width=3)
... * hv.Ellipse(0,0,(2,4), orientation=radians)
... * hv.Text(0,0,'{0}º'.format(float(angle))))
...
>>> hv.DynamicMap(dynamic_rotation,
... kdims=['angle']).redim.range(angle=(0, 360),
... y=(-3,3),
... x=(-3,3))
:DynamicMap [angle]

Figura 6.95 é outro exemplo usando widgets de controle deslizante correspondentes.


Observe que, ao declarar os intervalos de variáveis do widget do controle deslizante
como flutuantes, obtemos mais resolução no movimento do controle deslizante para
a função.
>>> def sine_curve(f=1,phase=0,ampl=1):
... xi = np.linspace(0,1,100)
... y = np.sin(2*np.pi*f*xi+phase/180*np.pi)*ampl
... return hv.Curve(dict(x=xi,y=y)).redim.range(y=(-5,5))
...
>>> hv.DynamicMap(sine_curve,
... kdims=['f','phase','ampl'])\
... .redim.range(f=(1,3.),
... phase=(0,360),
... ampl=(1,3.))
:DynamicMap [f,phase,ampl]
236 Capítulo 6

Fig. 6.94 Holoviews constrói automaticamente widgets para dimensões-chave especificadas

Fig. 6.95 Widgets Holoviews criados automaticamente derivam dos tipos de dimensões plotadas

6.5.5 Streams

Holoviews usam streams para alimentar dados em contêineres ou elementos em


vez de (por exemplo) usar os controles deslizantes para entradas. Depois que o fluxo
é definido, hv.DynamicMap pode desenhá-los na Figura 6.96,
>>> from holoviews.streams import Stream
>>> F = Stream.define('Freq',f=3)
>>> Phase = Stream.define('phase',phase=90)
>>> Amplitude = Stream.define('amplitude ',ampl=1)
>>> dm=hv.DynamicMap(sine_curve,streams = [F(f=1),
... Phase(phase=0),
... Amplitude(ampl=1)])
>>> dm
:DynamicMap []

O streaming é acionado enviando o evento,


Capítulo 6 237
Fig. 6.96 Holoviews
DynamicMap processa fluxos

>>> # running causes the plot to update


>>> dm.event(f=2,ampl=2,phase=30)

We can also use the nonblocking .periodic() method,


>>> dm.periodic(0.1,count=10,timeout=8,param_fn=lambda
c→ i:{'f':i,'phase':i*180})

6.5.6 Integração do Pandas com hvplot

Holoviews podem ser integrados com dataframes do Pandas para acelerar a plotagem
comum cenários, colocando opções de plotagem adicionais no objeto Pandas
DataFrame.
>>> import hvplot.pandas

Recupere o seguinte dataframe,


>>> df.head()
x y
0 0.00 1.00
1 0.11 1.01
2 0.22 1.05
3 0.33 1.11
4 0.44 1.20

Os gráficos podem ser gerados diretamente a partir do método dataframe hvplot()


(ver Fig. 6.97),
>>> df.hvplot() # now uses Bokeh backend instead of matplotlib
:NdOverlay [Variable]
:Curve [index] (value)
238 Capítulo 6

Fig. 6.97 Gráficos Holoviews gerados diretamente a partir de dataframes Pandas usando hvplot

Fig. 6.98 Gráficos de barras Holoviews usando groupby

Aqui está um agrupamento criado usando Pandas e desenhado com Holoviews.


Observe que não há rótulo y no gráfico. Isso ocorre porque o objeto intermediário
criado pelo agrupamento é um objeto Series sem uma coluna rotulada. O gráfico
de barras horizontal (barh()) é mostrado na Fig. 6.98.
>>> economic_data.groupby('country')['trade'].sum().hvplot.barh()
:Bars [country] (trade)

Observe o tipo de produto intermediário,


>>> type(economic_data.groupby('country')['trade'].sum())
<class 'pandas.core.series.Series'>

Uma maneira de corrigir o rótulo y ausente é lançar o objeto intermediário Series


como um dataframe usando to_frame() com uma coluna rotulada para o somatório
(ver Fig. 6.99).
>>> (economic_data.groupby('country')['trade'].sum()
... .to_frame('trade
c→ (units)')
Capítulo 6 239

Fig. 6.99 Os nomes das colunas do Pandas trama de dados set rótulos novisualização

.Fig 6.100 Barcharts podem ser encomendados usando o métodopandas sort_values()

... .hvplot.barh()
... )
:Bars [country] (trade (units))

Podemos também usar pandas para ordenar as barras por valor usando sort_values
() como mostrado na Fig. 6.100.
>>> (economic_data.groupby('country')['trade'].sum()
... .sort_values()
... .to_frame('trade(units)')
... .hvplot.barh()
... )
:Bars [country] (trade(units))
Desempilhar o agrupamento resulta na Fig. 6.101,
>>> # here is an unstacked grouping
>>> (economic_data.groupby(['year','country'])['trade']
... .sum()
... .unstack()
240 Capítulo 6
Fig. 6.101
holoviews025

... .head()
... )
country Austria Belgium Canada Denmark ... Sweden United Kingdom United States West Germany
year ...
1966 50.83 73.62 38.45 62.29 ... 44.79 37.93 9.62 37.89
1967 51.54 74.54 40.16 58.78 ... 43.73 37.83 9.98 38.81
1968 50.88 73.59 41.07 56.87 ... 42.46 37.76 10.09 39.51
1969 51.63 78.45 42.77 56.77 ... 43.52 41.93 10.44 41.40
1970 55.52 84.38 44.17 57.01 ... 46.28 42.80 10.50 43.07

[5 rows x 14 columns]

O gráfico de barras está faltando cores para cada país. Observe que temos que
fornecer o hv.Dimension para obter o dimensionamento do rótulo y corrigido, mas
o label não aparece. Também usamos o argumento de palavra-chave rot para
alterar a orientação do rótulo do tique. A legenda também é muito alta (ver Fig.
6.102).
>>> (economic_data.groupby(['year','country'])['trade']
... .sum()
... .unstack()
... .hvplot.bar(stacked=True,rot=45)
... .redim(value=hv.Dimension('value',label='trade',
range=(0,1000)))... )
:Bars [year,Variable] (value)

Eles podem ser corrigidos na Fig. 6.103 usando a magia da célula com o Variable
como color_index porque o dataframe não fornece um nome correspondente para
os valores nas células do dataframe. O rótulo y ainda não está acessível, mas usando
relabel, pelo menos podemos obter um título próximo ao yeixo.
Capítulo 6 241

Fig. 6.102 Mais controle para eixos e rótulos vem de hv.Dimension

Fig. 6.103gráficos de Holoviews pode desenharbarras empilhados com poucas linhas de código

>>> options = opts.Bars(tools=['hover'],


... legend_position='left',
... color_index='Variable',
... width=900,
... height=400)

>>> (economic_data.groupby(['year','country'])['trade']
... .sum()
... .unstack()
... .hvplot.bar(stacked=True,rot=45)
... .redim(value=hv.Dimension('value',label='trade',range=(0,1000)))
... .relabel('trade(units)').options(options)
... )
:Bars [year,Variable] (value)
242 Capítulo 6

Fig. 6.104 Visualizações Holoviews podem ser fatiadas como matrizes Numpy

O intervalo x das barras pode ser selecionado por meio do fatiamento do objeto
holoviews resultante, conforme mostrado abaixo, mostrando apenas anos após 1980
(Fig. 6.104).
>>> options = opts.Bars(tools=['hover'],
... legend_position='left',
... color_index='Variable',
... width=900,
... height=400)

>>> (economic_data.groupby(['year','country'])['trade']
... .sum()
... .unstack()
... .hvplot.bar(stacked=True,rot=45)
... .redim(value=hv.Dimension('value',
... label='trade',
... range=(0,1000)))
... .relabel('trade(units)').options(options)[1980:]
... )
:Bars [year,Variable] (value)

O principal problema com a rotulagem com hvplot é que a operação de redução no


groupby não tem um nome que holoviews possa pegar, o que torna difícil aplicar
rótulos. O próximo bloco reformula os dados para o formato que holoviews prefere
(ver Fig. 6.105).
>>> options = opts.Bars(tools=['hover'],
... legend_position='left',
... color_index='country',
... width=900,
... stacked=True,
... fontsize=dict(title=18,
... ylabel=16,
... xlabel=16),
... height=400)

>>> k=(economic_data.groupby(['year','country'])['trade']
Capítulo 6 243

Fig. 6.105 Holoviews usa os nomes de colunas pandas e índices para etiquetas

... .sum().
... to_frame().T
... .melt(value_name='trade'))

>>> (hv.Bars(k,kdims=['year','country'])
... .options(options)
... .relabel('Trade').redim.range(trade=(0,1200)))
:Bars [year,country] (trade)

6.5.7 Gráficos de rede

Holoviews fornece ferramentas para fazer gráficos de rede e se integra perfeitamente


ao networkx. Aqui estão alguns padrões úteis,
>>> import networkx as nx

>>> defaults = dict(width=400, height=400, padding=0.1)


>>> hv.opts.defaults(opts.EdgePaths(**defaults),
... opts.Graph(**defaults),
... opts.Nodes(**defaults))

Podemos usar networkx para criar a Fig. 6.106. Observe que, ao passar o mouse
sobre os círculos, você pode obter informações sobre os nós que estão incorporados
no gráfico. Observe que o foco também mostra o índice do nó.
>>> G = nx.karate_club_graph()
>>> hv.Graph.from_networkx(G,
...
c→ nx.layout.circular_layout).opts(tools=['hover'])
:Graph [start,end]

Vamos criar o gráfico Holoviews separadamente para que possamos inspecioná-lo.


Observe que nodes e edgepaths são como Holoviews representa internamente o
gráfico.
244 Capítulo 6
Fig. 6.106 Holoviews
suporta o desenho de
gráficos de rede

Os nodes mostram as variáveis independentes no gráfico entre colchetes e as


variáveis dependentes entre parênteses. Nesse caso, significa que o clube é um
atributo de nó do gráfico. Os edgepaths são as posições dos nós individuais que
são determinados pela chamada nx.layout.circular_layout.
>>> H=hv.Graph.from_networkx(G, nx.layout.circular_layout)
>>> H.nodes
:Nodes [x,y,index] (club)
>>> H.edgepaths
:EdgePaths [x,y]

Vamos adicionar alguns pesos de borda para agráfico NetworkX e, em seguida,


reconstruir o gráfico Holoviews correspondente na Fig 6,107,
>>> for i,j,d in G.edges(data=True):
... d['weight'] = np.random.randn()**2+1
...
>>> H=hv.Graph.from_networkx(G, nx.layout.circular_layout)
>>> H
:Graph [start,end] (weight)

Observe que pairar sobre as bordas agora exibe os pesos das bordas, mas você não
pode mais inspecionar os próprios nós, como no anterior Renderização. Observe
também que a borda é destacada ao passar o mouse.
>>> H.opts(inspection_policy='edges')
:Graph [start,end] (weight)

Você pode pelo menos colorir as arestas com base em seus valores de peso e escolher
o mapa de cores.
Capítulo 6 245
Fig. 6.107 pesos
vantagem adicional aos
gráficos de rede
Holoviews

>>> H.opts(inspection_policy='nodes',
... edge_color_index='weight',
... edge_cmap='hot')
:Graph [start,end] (weight)

É deselegante, mas você pode sobrepor dois gráficos usando o operador de


multiplicação com políticas de inspeção diferentes para obter informações de foco
sobre os nós e bordas (Figs. 6.108 e 6.109).
>>> H.opts(inspection_policy='edges', clone=True) * H
:Overlay
.Graph.I :Graph [start,end] (weight)
.Graph.II :Graph [start,end] (weight)

Você pode alterar ainda mais a espessura das bordas usando hv.dim.
>>> H.opts(edge_line_width=hv.dim('weight'))
:Graph [start,end] (weight)

Você pode usar o Set1 mapa de corespara colorir os nós com base nos valores club
no gráfico.
>>> H.opts(node_color=hv.dim('club'),cmap='Set1')
:Graph [start,end] (weight)

Podemos inspecionar uma árvore geradora mínima para este gráfico usando o que
aprendemos até agora sobre os gráficos Holoviews em Fig. 6.110.
246 Capítulo 6
Fig. 6.108 Políticas de
inspeção Holoviews
controlam o que o
mouse mostra

Fig. 6.109 Os gráficos


de rede Holoviews
podem ter diferentes
espessuras de borda
Capítulo 6 247
Fig. 6.110 Os gráficos
de rede Holoviews
podem ter nós coloridos

>>> t = nx.minimum_spanning_tree(G)
>>> T=hv.Graph.from_networkx(t, nx.layout.kamada_kawai_layout)
>>> T.opts(node_color=hv.dim('club'),cmap='Set1',
... edge_line_width=hv.dim('weight')*3,
... inspection_policy='edges',
... edge_color_index='weight',edge_cmap='cool')
:Graph [start,end] (weight)

Salvando Objetos Holoviz Holoviews fornece um método hv.save() para salvar


os gráficos renderizados em um arquivo HTML. Enquanto não houver callbacks
dependentes do servidor, o HTML gerado é um arquivo estático fácil de distribuir
(Fig. 6.111).

6.5.8 Painel Holoviz para painéis

O painel cria painéis com elementos atualizados dinamicamente.


>>> import panel as pn
>>> pn.extension()

Semelhante ao ipywidgets, o Panel tem uma interact para anexar um retorno


de chamada Python a um widget. O exemplo a seguir mostra como relatar um valor
com base em um controle deslizante (ver Fig. 6.112):
>>> def show_value(x):
... return x
...
248 Capítulo 6
Fig. 6.111 O gráfico de
rede Holoviews mostra
uma árvore de
abrangência mínima

Fig. 6.112 A painel


interact conecta
callbacks aos widgets

>>> app=pn.interact(show_value, x=(0, 10))


>>> app
Column
[0] Column
[0] IntSlider(end=10, name='x', value=5, value_throttled=5)
[1] Row
[0] Str(int, name='interactive07377')

O objeto retornado por pn.interact pode ser indexado e pode ser reorientado.
Observe que o texto aparece à esquerda em vez de na parte inferior (como mostrado
na Fig. 6.113).
>>> print(app)
Column
[0] Column
[0] IntSlider(end=10, name='x', value=5, value_throttled=5)
[1] Row
[0] Str(int, name='interactive07377')
>>> pn.Row(app[1], app[0]) # text and widget oriented row-wise
c→ instead of default column-wise
Row

[0] Row
[0] Str(int, name='interactive07377')
[1] Column
[0] IntSlider(end=10, name='x', value=5, value_throttled=5)

Tipos de componentes do painel Existem três tipos principais de componentes no


painel:
• Painel: Um painel envolve uma visão de um objeto externo (texto, imagem,
plotagem, etc.).
• Painel: Um painel apresenta vários componentes em uma linha, coluna ou grade.
Capítulo 6 249

Fig. 6.113 O Holoviews panel suporta texto markdown

Fig. 6.114 O painel depends decorador conecta callbacks a widgets

• Widget: Um widget fornece controles de entrada para adicionar recursos


interativos ao seu painel.
O seguinte mostra um objeto markdown pn.panel de renderização do(excluindo
Math-JaX):
>>> pn.panel('### This is markdown **text**')
Markdown(str)

Você também pode ter HTML bruto como um elemento Panel,


>>> pn.pane.HTML('<marquee width=500><b>Breaking News</b>: some
c→ news.</marquee>')
HTML(str)

O mecanismo de layout principal para painéis é pn.Row e pn.Column. Também



pn.Tabs e pn.GridSpec para layouts mais complicados.
>>> pn.Column(pn.panel('### This is markdown **text**'),
... pn.pane.HTML('<marquee width=500><b>Breaking News</b>:
c→ some news.</marquee>'))
Column
[0] Markdown(str)
[1] HTML(str)

Podemos usar um widget e conectar aos painéis. Uma maneira de fazer isso é com o
decorador @ pn.depends, que conecta a string de entrada da caixa de entrada à
função de retorno title_text (Fig. 6.114). Observe que você deve pressionar
ENTER para atualizar o texto.
>>> text_input = pn.widgets.TextInput(value='cap words')

>>> @pn.depends(text_input.param.value)
... def title_text(value):
... return '## ' + value.upper()
...
>>> app2 = pn.Row(text_input, title_text)
>>> app2

Aqui está um widget de preenchimento automático. Observe que você deve digitar
pelo menos dois caracteres (Figs. 6.115 e 6.116).
>>> autocomplete = pn.widgets.AutocompleteInput(
... name='Autocomplete Input',
250 Capítulo 6
Fig. 6.115 Widget de
preenchimento
automático do painel

Fig. 6.116 O painel suporta dashboards com visualizações Holoviews embutidas

... options=economic_data.country.unique().tolist(),
... placeholder='Write something here and <TAB> to complete')

>>> pn.Column(autocomplete,
... pn.Spacer(height=50)) # the spacer adds some
c→ vertical space
Column
[0] AutocompleteInput(name='Autocomplete
c→ Input',options=['United States', ...], placeholder='Write
c→ something h...)
[1] Spacer(height=50)

Quando estiver satisfeito com seu aplicativo, você pode anotá-lo com servable().
No bloco de notas Jupyter, esta anotação não tem efeito, mas a execução do comando
panel serve com o bloco de notas Jupyter (ou arquivo Python simples) criará um
servidor Web local com o painel anotado.
Podemos criar um painel rápido de nossos economic_data usando esses
elementos. Observe os parâmetros do decorador na função barchart. Você pode
definir os extremos do IntRangeSlider e, em seguida, mover o intervalo
arrastando o meio do widget indicado.
>>> pulldown = (pn.widgets.Select(name='Country',
... options=economic_data.country
... .unique()
... .tolist())
Capítulo 6 251
... )

>>> range_slider = pn.widgets.IntRangeSlider(start=1966,


... end=1990,
... step=1,
... value=(1966,1990))

>>> @pn.depends(range_slider.param.value,pulldown.param.value)
... def barchart(interval, country):
... start,end = interval
... df=
c→ economic_data.query('country=="%s"'%(country))[['year','trade']]
... return (df.set_index('year')
... .loc[start:end]
... .hvplot.bar(x='year',y='trade')
... .relabel(f'Country: {country}'))
...
>>> app=pn.Column(pulldown,range_slider,barchart)
>>> app
Column
[0] Select(name='Country', options=['United States', ...],
value='United States')
[1] IntRangeSlider(end=1990, start=1966, value=(1966, 1990),
value_throttled=(1966, 1990))
[2] ParamFunction(function)

Observe que o retorno de chamada é Python, portanto, requer um servidor Python


backend para atualizar o gráfico.

6.6 Plotly

Plotly é uma biblioteca de visualização baseada na web que é mais fácil de usar com
plotly_express. A principal vantagem plotly_express mais simples plotly é que
é muito menos tedioso para criar tramas comuns. Vamos considerar o seguinte
dataframe:
>>> import pandas as pd
>>> import plotly_express as px
>>> gapminder = px.data.gapminder()
>>> gapminder2007 = gapminder.query('year == 2007')
>>> gapminder2007.head()
country continent year lifeExp pop gdpPercap iso_alpha iso_num
11 Afghanistan Asia 2007 43.83 31889923 974.58 AFG 4
23 Albania Europe 2007 76.42 3600523 5937.03 ALB 8
35 Algeria Africa 2007 72.30 33333216 6223.37 DZA 12
47 Angola Africa 2007 42.73 12420476 4797.23 AGO 24
59 Argentina Americas 2007 75.32 40301927 12779.38 ARG 32

The scatter function draws the plot in Fig. 6.117.


>>> fig=px.scatter(gapminder2007,
... x='gdpPercap',
... y='lifeExp',
... width=400,height=400)


252 Capítulo 6
Fig. 6.117 Gráfico de
dispersão

básico As colunas do dataframe que devem ser desenhadas são selecionadas usando
os argumentos de palavra-chave x e y. As palavras-chave width e height
especificam o tamanho do gráfico. O objeto fig é um pacote de instruções do Plotly
que são passadas ao navegador para renderizar usando as funções Javascript do Plotly
que incluem uma barra de ferramentas interativa de funções gráficas comuns, como
zoom, etc.
Como no Altair, você pode atribuir colunas de dataframe a atributos gráficos. A
Fig.seguir 6.118 a atribui a coluna categórica continent à cor na figura.
>>> fig=px.scatter(gapminder2007,
... x='gdpPercap',
... y='lifeExp',
... color='continent',
... width=900,height=400)

Figura 6.119 atribui a coluna do dataframe 'pop ' ao tamanho do marcador na figura
e também especifica o tamanho da figura com os argumentos de palavra-chave
width e height. O size e size_max garantem que os tamanhos dos marcadores
se encaixem perfeitamente na janela de plotagem.
>>> fig=px.scatter(gapminder2007,
... x='gdpPercap',
... y='lifeExp',
... color='continent',
... size='pop',
... size_max=60,
... width=900,height=400)

No navegador, passar o mouse sobre o marcador de dados irá disparar uma nota
popup com o nome do país na Fig. 6.120.
Capítulo 6 253

Fig. 6.118 Mesmo gráfico de dispersão da Fig. 6.117, mas agora os respectivos países são coloridos
separadamente

Fig. 6.119 O mesmo da Fig. 6.118 com o valor da população escalando os tamanhos dos marcadores

>>> fig=px.scatter(gapminder2007,
... x='gdpPercap',
... y='lifeExp',
... color='continent',
... size='pop',
... size_max=60,
... hover_name='country')

Subplots no Matplotlib são conhecidos como facetas no Plotly. Ao especificar


facet_col, o gráfico de dispersão pode ser dividido nessas facetas, conforme
mostrado na Fig. 6.121, onde log_x muda a escala horizontal para logarítmica.
>>> fig=px.scatter(gapminder2007,
... x='gdpPercap',
... y='lifeExp',
254 Capítulo 6

Fig. 6.120 Igual à Fig. 6.119, mas com dicas de ferramentas de foco nos marcadores e novo tamanho
de plotagem

... color='continent',
... size='pop',
... size_max=60,
... hover_name='country',
... facet_col='continent',
... log_x=True)

Plotly também pode construir animações. O argumento animation_frame indica


que a animação deve incrementar a coluna year no data frame. O argumento
animation_group aciona os grupos fornecidos para serem renderizados novamente
em cada quadro de animação. Ao construir animações, é importante manter os eixos
fixos para que o movimento da animação não seja confuso. Os argumentos range_x
e range_y assegure os eixos (Fig. 6.122).
>>> fig=px.scatter(gapminder,
... x='gdpPercap',
... y='lifeExp',
... size='pop',
... size_max=60,
... color='continent',
... hover_name='country',
... log_x=True,
... range_x=[100,100_000],
... range_y=[25,90],
... animation_frame='year',
... animation_group='country',
... labels=dict(pop='Population',
... gdpPercap='GDP per Capita',
... lifeExp='Life Expectancy'))
Capítulo 6 255

Fig. 6.121 As facetas do gráfico são aproximadamente equivalentes aos subtramas do Matplotlib

Os gráficos estatísticos comuns são fáceis com plotly_express. Considere os


seguintes dados:
>>> tips = px.data.tips()
>>> tips.head()
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4

O histograma da soma das pontas particionadas pelo status smoker é mostrado na


Fig. 6.123
>>> fig=px.histogram(tips,
... x='total_bill',
... y='tip',
... histfunc='sum',
... color='smoker',
... width=400, height=300)

Tudo o que o Plotly precisa para renderizar o gráfico no navegador está contido no
objeto fig; então, para alterar qualquer coisa no gráfico, você deve alterar o item
correspondente neste objeto. Por exemplo, para alterar a cor de um dos histogramas
na
256 Capítulo 6

Fig. 6.122 Plotly pode animar plotagens complexas com a palavra-chave argumento
animation_frame

Fig. 6.123 Histograma


de plotly categorizado
por status smoker
Capítulo 6 257
Fig. 6.124 Igual à
Fig. 6.123, mas com
mudança de cor

Fig. 6.123, acessamos e atualizamos o atributo do objeto


fig.data[0].marker.color. O resultado é mostrado na Fig. 6.124
>>> fig=px.histogram(tips,
... x='total_bill',
... y='tip',
... histfunc='sum',
... color='smoker',
... width=400, height=300)
>>> # access other properties from variable
>>> fig.data[0].marker.color='purple'

Plotly pode desenhar outros gráficos estatísticos como o boxplot entalhado


mostrado na Fig. 6.125. O argumento da palavra-chave orientation apresenta os
boxplots horizontalmente.
>>> fig=px.box(tips,
... x='total_bill',
... y='day',
... orientation='h',
... color='smoker',
... notched=True,
... width=800, height=400,
... category_orders={'day': ['Thur', 'Fri', 'Sat',
c→ 'Sun']})
Tramas de violino são uma maneira alternativa de exibir unidimensional distribuições
(ver Fig. 6.126).
>>> fig=px.violin(tips,
... y='tip',
... x='smoker',
... color='sex',
... box=True, points='all',
... width=600,height=400)

Os gráficos marginais podem ser incluídos usando os argumentos de palavra-chave


marginal_x e marginal_y conforme mostrado na Fig. 6.127.
258 Capítulo 6

Fig. 6.125 Boxplots pode ser horizontal ou verticalmente orientada

Fig. 6.126 suporta Plotly parcelas violino para unidimensional probabilidade visualizações função
densidade

O argumento da palavra-chave trendline significa que o ajuste de mínimos


quadrados ordinários deve ser usado para desenhar a linha de tendência no subplot
do meio.
>>> fig=px.scatter(tips,
... x='total_bill',
... y='tip',
... color='smoker',
... trendline='ols',
... marginal_x='violin',
... marginal_y='box',
... width=600,height=700)
Capítulo 6 259

Fig. 6.127 Plotagens nas margens de um gráfico central são possíveis usando os argumentos de
palavra-chave marginal_x e marginal_y

Esta seção curta apenas arranha a superfície do que Plotly é capaz de. O site de
documentação principal é o recurso principal para tipos gráficos emergentes. O Plotly
express torna muito mais fácil gerar as especificações complexas do Plotly que são
renderizadas pela biblioteca Javascript do Plotly, mas todo o poder do Plotly está
disponível usando o namespace bare plotly.

Referências

1. Bokeh Development Team. Bokeh: biblioteca Python para visualização interativa (2020)

Você também pode gostar