Python Programming For Data Analysis Traduzido
Python Programming For Data Analysis Traduzido
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
© 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.
Referências
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.
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
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
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'>
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
1.1.5 Strings
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'
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.
>>> 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]
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'
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}
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.
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!
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
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
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
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]
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
Eles também podem ser especificados usando os nomes em vez das posições como
segue:,
>>> foo (y= 2, x= 1)
x=1y=2
foo(x=20, y=30)
compute_this(position=20, velocity=30)
position in m
velocity in m/s
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
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}
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]
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
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
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
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
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,
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')
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>>
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).
(continuação)
Capítulo 1 35
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.
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']
4
Consulte o contextlib módulo integrado.
Capítulo 1 37
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 )
dill.dumps(foo)
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))
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
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'>
(continuação)
Capítulo 1 41
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
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}
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)
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
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
1.1.11 Geradores
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
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 ê
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]
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
...
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
...
Agora que sabemos como isso funciona, o wrapper pode ser abreviado como o
seguinte
>>> def wrapper(coroutine):
... yield from coroutine
...
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
...
1.1.12 Decoradores
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]
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
...
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.
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 o decorador unique para garantir que não haja valores duplicados,
>>> from enum import unique
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
...
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')
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
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
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))
...
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')
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')
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.
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
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
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)
Isto irá despejar muito material, então você provavelmente vai querer usar os outros
sinalizadores para filtrar.
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)
Isso é útil ao embutir Python em uma GUI. Sua milhagem pode variar de outra forma,
mas é um bom truque na pitada!
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
filehandler = logging.FileHandler('mylog.log')
formatter = logging.Formatter("%(asctime)s - %(name)s -
c→ %(funcName)s - %(levelname)s - %(message)s") # set format
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
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
...
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
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
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.
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
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]
(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.
>>> 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
...
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
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 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
À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.
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).
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
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
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.
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
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
...
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.
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)
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)
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
. ..
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)
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
...
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))))
...
Agora, vamos tentar a saída novamente e notar que a nova versão int da função foi
executada .
>>> foo (1)
2
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.
(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]}
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
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
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
Referências
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.
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.
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
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
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.
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
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
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
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.
2Para
resolver conflitos, o conda implementa um solucionador de satisfazibilidade (SAT), que é
um problema combinatório clássico.
Capítulo 3 102
Referências
4.1 Dtypes
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'
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')
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]])
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]])
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])
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]])
/* Block of memory */
char *data;
/* Indexing scheme */
int nd;
npy_intp *dimensions;
npy_intp *strides;
/* Other stuff */
PyObject *base;
int flags;
PyObject *weakreflist;
} PyArrayObject;0
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
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,)
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
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])
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.]])
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
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)
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]]],
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]])
(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 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.
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])
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'
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'
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
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.
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'
Referências
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
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
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
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
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
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
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'])
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
5.3 Reindexando
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
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']
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
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
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
Isso apenas arranha a superfície dos tipos de análise de dados de fluidos que são quase
automáticos usando Pandas.
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
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
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.
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
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
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')
>>> (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>
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>
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
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
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')
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, 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
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
6.1 Matplotlib
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
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
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')
Fig. 6.4 Espinhas referem-se às bordas do quadro e podem ser movidas dentro da figura
Você pode plotar círculos, polígonos, etc. usando primitivos disponíveis em o módulo
matplotlib.patches (ver Fig. 6.7).
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
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')
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.
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
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)
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
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.
À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
... 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.
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
6.1.13 Animações
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
.. . para i no intervalo(10)]
>>> g=animação.ArtistAnimation (fig, frames, interval= 50)
Fig. 6.23 Animações no bloco de notas Jupyter podem ser reproduzidas no navegador usando
to_jshtml
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()
Fig. 6.24 Os caminhos desenham na tela usando instruções específicas do tipo caneta
... 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)
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.
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
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
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()
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
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.
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
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
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
>>> sns.relplot(x='total_bill',y='tip',
... size='size', # scale markers
... data=tips)
<seaborn.axisgrid.FacetGrid object at 0x7f93713bd310>
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
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>
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>
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
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
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.
... multiple="stack")
<seaborn.axisgrid.FacetGrid object at 0x7f9372f6fbb0>
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)
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.
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
6.3 Bokeh
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
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.
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))
6.4 Altair
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
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
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
Fig. 6.68 Barcharts são gerados por mark_bar nas colunas nomeadas x e y colunas no tdataframe
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.
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
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(...)
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
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
110
100
Mean of Horsepower
90
80
70
60
1970 1972 1974 1976 1978 1980 1982
Year
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
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(...)
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
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
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
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 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
>>> 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)
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.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
>>> 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.88 Holoviews escolhe automaticamente o widget de controle deslizante com base no tipo de
dados na dimensão restante.
Na Fig. 6.89, não usamos nenhuma das dimensões-chave declaradas para que ambas
entrem nos widgets correspondentes.
232 Capítulo 6
>>> edata.sort('country').to(hv.Bars,'growth','trade')
:HoloMap [country,year]
:Bars [growth] (trade)
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)
>>>
e→ edata.to(hv.HeatMap,['year','country'],'growth').options(options)
:HeatMap [year,country] (growth)
Capítulo 6 235
Fig. 6.95 Widgets Holoviews criados automaticamente derivam dos tipos de dimensões plotadas
6.5.5 Streams
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
Fig. 6.97 Gráficos Holoviews gerados diretamente a partir de dataframes Pandas usando hvplot
Fig. 6.99 Os nomes das colunas do Pandas trama de dados set rótulos novisualização
... .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.103gráficos de Holoviews pode desenharbarras empilhados com poucas linhas de código
>>> (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)
>>> 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)
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]
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)
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
>>> 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)
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)
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
... 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
... )
>>> @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)
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
’
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')
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)
Fig. 6.121 As facetas do gráfico são aproximadamente equivalentes aos subtramas do Matplotlib
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.126 suporta Plotly parcelas violino para unidimensional probabilidade visualizações função
densidade
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)