11. Um breve passeio pela biblioteca padrão — parte II¶
Este segundo passeio apresenta alguns módulos avançados que atendem necessidades de programação profissional. Estes módulos raramente aparecem em scripts pequenos.
11.1. Formatando a saída¶
O módulo reprlib
fornece uma versão de repr()
personalizado para exibições abreviadas de contêineres grandes ou profundamente aninhados:
>>> import reprlib
>>> reprlib.repr(set('supercalifragilisticexpialidocious'))
"{'a', 'c', 'd', 'e', 'f', 'g', ...}"
O módulo pprint
oferece um controle mais sofisticado na exibição tanto
de objetos embutidos quanto aqueles criados pelo usuário de maneira que fique
legível para o interpretador. Quando o resultado é maior que uma linha, o
“pretty printer” acrescenta quebras de linha e indentação para revelar as
estruturas de maneira mais clara:
>>> import pprint
>>> t = [[[['black', 'cyan'], 'white', ['green', 'red']], [['magenta',
... 'yellow'], 'blue']]]
...
>>> pprint.pprint(t, width=30)
[[[['black', 'cyan'],
'white',
['green', 'red']],
[['magenta', 'yellow'],
'blue']]]
O módulo textwrap
formata parágrafos de texto para que caibam em uma
dada largura de tela:
>>> import textwrap
>>> doc = """The wrap() method is just like fill() except that it returns
... a list of strings instead of one big string with newlines to separate
... the wrapped lines."""
...
>>> print(textwrap.fill(doc, width=40))
The wrap() method is just like fill()
except that it returns a list of strings
instead of one big string with newlines
to separate the wrapped lines.
O módulo locale
acessa uma base de dados de formatos específicos a
determinada cultura. O argumento grouping
da função format()
oferece
uma forma direta de formatar números com separadores de grupo:
>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'English_United States.1252')
'English_United States.1252'
>>> conv = locale.localeconv() # get a mapping of conventions
>>> x = 1234567.8
>>> locale.format("%d", x, grouping=True)
'1,234,567'
>>> locale.format_string("%s%.*f", (conv['currency_symbol'],
... conv['frac_digits'], x), grouping=True)
'$1,234,567.80'
11.2. Usando templates¶
módulo string
inclui a versátil classe Template
com uma
sintaxe simplificada, adequada para ser editada por usuários finais. Isso
permite que usuários personalizem suas aplicações sem a necessidade de alterar
a aplicação.
Em um template são colocadas marcações indicando o local onde o texto variável
deve ser inserido. Uma marcação é formada por $
seguido de um
identificador Python válido (caracteres alfanuméricos e underscores).
Envolvendo-se o identificador da marcação entre chaves, permite que ele seja
seguido por mais caracteres alfanuméricos sem a necessidade de espaços.
Escrevendo-se $$
cria-se um único $
:
>>> from string import Template
>>> t = Template('${village}folk send $$10 to $cause.')
>>> t.substitute(village='Nottingham', cause='the ditch fund')
'Nottinghamfolk send $10 to the ditch fund.'
O método substitute()
levanta uma exceção KeyError
quando o
identificador de uma marcação não é fornecido em um dicionário ou em um
argumento nomeado (keyword argument). Para aplicações que podem receber
dados incompletos fornecidos pelo usuário, o método safe_substitute()
pode ser mais apropriado — deixará os marcadores intactos se os dados
estiverem faltando:
>>> t = Template('Return the $item to $owner.')
>>> d = dict(item='unladen swallow')
>>> t.substitute(d)
Traceback (most recent call last):
...
KeyError: 'owner'
>>> t.safe_substitute(d)
'Return the unladen swallow to $owner.'
Subclasses de Template podem especificar um delimitador personalizado. Por exemplo, um utilitário para renomeação em lote de fotos pode usar o sinal de porcentagem para marcações como a data atual, número sequencial da imagem ou formato do aquivo:
>>> import time, os.path
>>> photofiles = ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
>>> class BatchRename(Template):
... delimiter = '%'
>>> fmt = input('Enter rename style (%d-date %n-seqnum %f-format): ')
Enter rename style (%d-date %n-seqnum %f-format): Ashley_%n%f
>>> t = BatchRename(fmt)
>>> date = time.strftime('%d%b%y')
>>> for i, filename in enumerate(photofiles):
... base, ext = os.path.splitext(filename)
... newname = t.substitute(d=date, n=i, f=ext)
... print('{0} --> {1}'.format(filename, newname))
img_1074.jpg --> Ashley_0.jpg
img_1076.jpg --> Ashley_1.jpg
img_1077.jpg --> Ashley_2.jpg
Outra aplicação para templates é separar a lógica da aplicação dos detalhes de múltiplos formatos de saída. Assim é possível usar templates personalizados para gerar arquivos XML, relatórios em texto puro e relatórios web em HTML.
11.3. Trabalhando com formatos binários de dados¶
O módulo struct
oferece as funções pack()
e unpack()
para
trabalhar com registros binários de tamanho variável. O exemplo a seguir
mostra como iterar através do cabeçalho de informação num aquivo ZIP sem usar
o módulo zipfile
. Os códigos de empacotamento "H"
e "I"
representam números sem sinal de dois e quatro bytes respectivamente. O
"<"
indica que os números têm tamanho padrão e são little-endian (bytes
menos significativos primeiro):
import struct
with open('myfile.zip', 'rb') as f:
data = f.read()
start = 0
for i in range(3): # show the first 3 file headers
start += 14
fields = struct.unpack('<IIIHH', data[start:start+16])
crc32, comp_size, uncomp_size, filenamesize, extra_size = fields
start += 16
filename = data[start:start+filenamesize]
start += filenamesize
extra = data[start:start+extra_size]
print(filename, hex(crc32), comp_size, uncomp_size)
start += extra_size + comp_size # skip to the next header
11.4. Multi-threading¶
O uso de threads é uma técnica para desacoplar tarefas que não são sequencialmente dependentes. Threads podem ser usadas para melhorar o tempo de resposta de aplicações que aceitam entradas do usuário enquanto outras tarefas são executadas em segundo plano. Um caso relacionado é executar ações de entrada e saída (I/O) em uma thread paralelamente a cálculos em outra thread.
O código a seguir mostra como o módulo de alto nível threading
pode
executar tarefas em segundo plano enquanto o programa principal continua
a sua execução:
import threading, zipfile
class AsyncZip(threading.Thread):
def __init__(self, infile, outfile):
threading.Thread.__init__(self)
self.infile = infile
self.outfile = outfile
def run(self):
f = zipfile.ZipFile(self.outfile, 'w', zipfile.ZIP_DEFLATED)
f.write(self.infile)
f.close()
print('Finished background zip of:', self.infile)
background = AsyncZip('mydata.txt', 'myarchive.zip')
background.start()
print('The main program continues to run in foreground.')
background.join() # Wait for the background task to finish
print('Main program waited until background was done.')
O principal desafio para as aplicações que usam múltiplas threads é coordenar as threads que compartilham dados ou outros recursos. Para esta finalidade, o módulo threading oferece alguns mecanismos primitivos de sincronização, como travas (locks), eventos, variáveis de condição e semáforos.
Ainda que todas essas ferramentas sejam poderosas, pequenos erros de design podem resultar em problemas difíceis de serem diagnosticados. Por isso, a abordagem preferida para a coordenação da tarefa é concentrar todo o acesso a um recurso em um único tópico e, em seguida, usar o módulo queue
para alimentar esse segmento com solicitações de outros tópicos. Aplicações que utilizam objetos Queue
para comunicação e coordenação inter-thread são mais fáceis de serem projetados, mais legíveis e mais confiáveis.
11.5. Gerando logs¶
O módulo logging
oferece um completo e flexível sistema de log. Da
maneira mais simples, mensagens de log são enviadas para um arquivo ou para
sys.stderr
:
import logging
logging.debug('Debugging information')
logging.info('Informational message')
logging.warning('Warning:config file %s not found', 'server.conf')
logging.error('Error occurred')
logging.critical('Critical error -- shutting down')
Isso produz a seguinte saída:
WARNING:root:Warning:config file server.conf not found
ERROR:root:Error occurred
CRITICAL:root:Critical error -- shutting down
Por padrão, mensagens informativas e de depuração são suprimidas e a saída é
enviada para a saída de erros padrão (stderr). Outras opções de saída incluem
envio de mensagens através de correio eletrônico, datagramas, sockets ou para
um servidor HTTP. Novos filtros podem selecionar diferentes formas de envio de
mensagens, baseadas na prioridade da mensagem: DEBUG
, INFO
,
WARNING
, ERROR
e CRITICAL
.
O sistema de log pode ser configurado diretamente do Python ou pode ser carregado a partir de um arquivo de configuração editável pelo usuário para logs personalizados sem a necessidade de alterar a aplicação.
11.6. Referências fracas¶
Python faz gerenciamento automático de memória (contagem de referências para a maioria dos objetos e garbage collection [coleta de lixo] para eliminar ciclos). A memória ocupada por um objeto é liberada logo depois da última referência a ele ser eliminada.
Essa abordagem funciona bem para a maioria das aplicações, mas ocasionalmente
surge a necessidade de rastrear objetos apenas enquanto estão sendo usados por
algum outro. Infelizmente rastreá-los cria uma referência, e isso os fazem
permanentes. O módulo weakref
oferece ferramentas para rastrear objetos
sem criar uma referência. Quando o objeto não é mais necessário, ele é
automaticamente removido de uma tabela de referências fracas e uma chamada
(callback) é disparada. Aplicações típicas incluem cacheamento de objetos
que são muito custosos para criar:
>>> import weakref, gc
>>> class A:
... def __init__(self, value):
... self.value = value
... def __repr__(self):
... return str(self.value)
...
>>> a = A(10) # create a reference
>>> d = weakref.WeakValueDictionary()
>>> d['primary'] = a # does not create a reference
>>> d['primary'] # fetch the object if it is still alive
10
>>> del a # remove the one reference
>>> gc.collect() # run garbage collection right away
0
>>> d['primary'] # entry was automatically removed
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
d['primary'] # entry was automatically removed
File "C:/python37/lib/weakref.py", line 46, in __getitem__
o = self.data[key]()
KeyError: 'primary'
11.7. Ferramentas para trabalhar com listas¶
Muitas necessidades envolvendo estruturas de dados podem ser satisfeitas com o tipo embutido lista. Entretanto, algumas vezes há uma necessidade por implementações alternativas que sacrificam algumas facilidades em nome de melhor desempenho.
O módulo array
oferece uma classe array
, semelhante a uma
lista, mas que armazena apenas dados homogêneos e de maneira mais compacta. O
exemplo a seguir mostra um vetor de números armazenados como números binários
de dois bytes sem sinal (código de tipo "H"
) ao invés dos 16 bytes
usuais para cada item em uma lista de int
:
>>> from array import array
>>> a = array('H', [4000, 10, 700, 22222])
>>> sum(a)
26932
>>> a[1:3]
array('H', [10, 700])
O módulo collections
oferece um objeto deque()
que comporta-se
como uma lista mas com appends e pops pela esquerda mais rápidos, porém
mais lento ao percorrer o meio da sequência. Esses objetos são adequados para
implementar filas e buscas de amplitude em árvores de dados (breadth first
tree searches):
>>> from collections import deque
>>> d = deque(["task1", "task2", "task3"])
>>> d.append("task4")
>>> print("Handling", d.popleft())
Handling task1
unsearched = deque([starting_node])
def breadth_first_search(unsearched):
node = unsearched.popleft()
for m in gen_moves(node):
if is_goal(m):
return m
unsearched.append(m)
Além de implementações alternativas de listas, a biblioteca também oferece
outras ferramentas como o módulo bisect
com funções para manipulação
de listas ordenadas:
>>> import bisect
>>> scores = [(100, 'perl'), (200, 'tcl'), (400, 'lua'), (500, 'python')]
>>> bisect.insort(scores, (300, 'ruby'))
>>> scores
[(100, 'perl'), (200, 'tcl'), (300, 'ruby'), (400, 'lua'), (500, 'python')]
O módulo heapq
oferece funções para implementação de heaps baseadas
em listas normais. O valor mais baixo é sempre mantido na posição zero. Isso é
útil para aplicações que acessam repetidamente o menor elemento, mas não querem
reordenar a lista toda a cada acesso:
>>> from heapq import heapify, heappop, heappush
>>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
>>> heapify(data) # rearrange the list into heap order
>>> heappush(data, -5) # add a new entry
>>> [heappop(data) for i in range(3)] # fetch the three smallest entries
[-5, 0, 1]
11.8. Aritmética decimal com ponto flutuante¶
O módulo decimal
oferece o tipo Decimal
para aritmética
decimal com ponto flutuante. Comparado a implementação embutida float
que usa aritmética binária de ponto flutuante, a classe é especialmente útil
para:
aplicações financeiras que requerem representação decimal exata,
controle sobre a precisão,
controle sobre arredondamento para satisfazer requisitos legais,
rastreamento de casas decimais significativas, ou
applications where the user expects the results to match calculations done by hand.
Por exemplo, calcular um imposto de 5% sobre uma chamada telefônica de 70 centavos devolve diferentes resultados com aritmética de ponto flutuante decimal ou binária. A diferença torna-se significativa se os resultados são arredondados para o centavo mais próximo.
>>> from decimal import *
>>> round(Decimal('0.70') * Decimal('1.05'), 2)
Decimal('0.74')
>>> round(.70 * 1.05, 2)
0.73
O resultado de Decimal
considera zeros à direita, automaticamente
inferindo quatro casas decimais a partir de multiplicandos com duas casas
decimais. O módulo decimal
reproduz a aritmética como fazemos à mão e
evita problemas que podem ocorrer quando a representação binária do ponto
flutuante não consegue representar quantidades decimais com exatidão.
A representação exata permite à classe Decimal
executar cálculos de
módulo e testes de igualdade que não funcionam bem em ponto flutuante
binário:
>>> Decimal('1.00') % Decimal('.10')
Decimal('0.00')
>>> 1.00 % 0.10
0.09999999999999995
>>> sum([Decimal('0.1')]*10) == Decimal('1.0')
True
>>> sum([0.1]*10) == 1.0
False
O módulo decimal
implementa a aritmética com tanta precisão quanto
necessária:
>>> getcontext().prec = 36
>>> Decimal(1) / Decimal(7)
Decimal('0.142857142857142857142857142857142857')