Capítulo 33, Avançado
Arquivos maiores que a memória: memmap
Quando o conjunto de dados não cabe na memória, você não precisa carregá-lo. O `memmap` trata um arquivo em disco como um array, e o sistema operacional traz para a memória só o pedaço que você toca.
Código deste capítulo: avancado/cap33_memmap.py
Um arquivo que se comporta como array
O np.memmap mapeia um arquivo binário na memória. Você escreve e lê como em um array comum, e o sistema operacional cuida de transferir as páginas do disco sob demanda. Aqui, um arquivo com um milhão de float32:
from pathlib import Path
import numpy as np
caminho = Path("grande.bin")
mm = np.memmap(caminho, dtype=np.float32, mode="w+", shape=(1_000_000,))
mm[:] = np.arange(1_000_000, dtype=np.float32)
mm.flush()
print(caminho.stat().st_size, mm.shape)
del mm
4000000 (1000000,)
O arquivo tem exatamente 1.000.000 × 4 bytes: o memmap é só os dados, sem cabeçalho, e por isso você precisa informar o tipo e a forma ao abri-lo de novo. O flush garante que as alterações cheguem ao disco, e o del solta o arquivo.
Abrir só para ler
Os modos mais usados são "r" (somente leitura), "r+" (leitura e escrita em um arquivo existente) e "w+" (cria ou apaga o arquivo e abre para escrita). No modo de leitura, o array é protegido contra alterações acidentais:
lido = np.memmap(caminho, dtype=np.float32, mode="r", shape=(1_000_000,))
print(lido[999_999], lido[:3], isinstance(lido, np.ndarray))
try:
lido[0] = 1.0
except ValueError as erro:
print(erro)
999999.0 [0. 1. 2.] True
assignment destination is read-only
Um memmap é um ndarray, e por isso aceita tudo o que você já aprendeu: fatias, máscaras, agregações.
Processar em blocos
Mesmo mapeado, um cálculo sobre o array inteiro pode precisar de memória para os intermediários. A solução é percorrer o arquivo em blocos, acumulando o resultado. Aqui, a média de um milhão de valores com no máximo 250 mil em memória por vez:
def media_em_blocos(mm, bloco=250_000):
total = 0.0
for inicio in range(0, len(mm), bloco):
total += float(mm[inicio : inicio + bloco].sum(dtype=np.float64))
return total / len(mm)
print(media_em_blocos(lido), np.arange(1_000_000, dtype=np.float64).mean())
499999.5 499999.5
O sum(dtype=np.float64) acumula em precisão dupla, mesmo que os dados sejam float32, para o erro de arredondamento não crescer com o tamanho.
`np.load` com `mmap_mode`
Um arquivo .npy (capítulo 24) já carrega o tipo e a forma no cabeçalho. Com mmap_mode="r", o np.load o abre como memmap sem você repetir esses dados:
np.save("grande.npy", np.arange(1_000_000, dtype=np.float32))
vista = np.load("grande.npy", mmap_mode="r")
print(type(vista).__name__, vista[10])
memmap 10.0
Esse costuma ser o jeito mais prático: guarde em .npy e abra com mmap_mode.
Limpeza
Um memmap mantém o arquivo aberto. No Windows, não dá para apagar um arquivo enquanto algum array ainda o mapeia, então solte as referências antes:
del lido, vista
for nome in ("grande.bin", "grande.npy"):
Path(nome).unlink()
print(Path("grande.bin").exists())
False
Quando usar memmap | Quando não |
|---|---|
| Arquivo maior que a memória | O dado cabe com folga: carregue de uma vez, é mais rápido |
| Acesso a pedaços (uma janela, algumas linhas) | Você vai ler tudo muitas vezes: o disco é bem mais lento que a memória |
| Vários processos lendo o mesmo dado | Dados em formato de tabela: use Parquet com pandas |
Exercício 1
Máximo de um arquivo, em blocos
Escreva maximo_em_blocos(caminho, dtype, tamanho, bloco) que abra o arquivo como memmap somente leitura e devolva o maior valor, lendo bloco elementos por vez. Teste gravando um arquivo temporário.