Dia desses eu precisava fazer uma função para converter strings
vazias em algum valor arbitrário, isso em 3 arquivos diferentes, com estruturas de dados completamente diferentes e aninhadas, em Python. Então pensei comigo mesmo:
Qual a forma mais genérica possível de fazer isso?
Vamos imaginar uma estrutura de dados bem simples, mas com os 3 tipos de dados que eu tinha:
- string
- dict
- list
{
"nome": "",
"idade": 25,
"endereco": {
"rua": "",
"cidade": "São Paulo",
"estado": ""
},
"contatos": ["", "email@example.com"]
}
A primeira solução que me veio na cabeça foi via Typescript, algo mais ou menos assim:
type NestedObject = { [key: string]: any };
const convertArray = (data: any[]): any[] =>
data.map(substituirStringsVazias); // Aplica recursivamente em cada elemento
const convertObject = (data: NestedObject): NestedObject =>
Object.entries(data).reduce(
(acc, [key, value]) => ({
...acc,
[key]: substituirStringsVazias(value), // Aplica recursivamente em cada valor do objeto
}),
{}
);
const substituirStringsVazias = (data: NestedObject | any[]): NestedObject | any[] => {
if (Array.isArray(data)) return convertArray(data);
if (typeof data === "object" && data !== null) return convertObject(data);
if (typeof data === "string" && data === "") return "NAO_ENCONTRADO";
return data;
};
Ou seja, primeiramente você precisa cobrir todos os tipos que deseja converter e usar de recursão para converter os dados de forma aninhada.
Você pode estar se pensando por que que eu não reduzi o Object.entries
para "NAO_ENCONTRADO"
de uma vez.
A resposta é porque eu não posso perder a estrutura de dados inicial, só desejo converter valores vazios existentes em um valor padrão.
A lógica é que eu preciso iniciar testando se é de tal tipo, então pensei em começar a testar pelo tipo de Lista, caso não seja eu testo se é do tipo Objeto. Se for do tipo Objeto então eu itero pelas suas entradas (combinação de [chave, valor]) e para converter um Array em JS a gente precisa usar o reduce
. Porém caso não seja Objeto ele pode cair no teste se é uma String, com isso eu tenho mapeado os 3 tipos que quero converter.
Então se tenho todo o cenário mapeado, eu preciso chamar essa função a cada elemento novo que eu encontro, com essa lógica já em mente fica fácil de unir essa lógica em apenas uma função, eu pessoalmente prefiro as funções separadas, mas teve uma época na minha vida que eu tentava converter o máximo possível de funções em one-line
. Então nesse caso nosso código ficaria assim:
type NestedObject = { [key: string]: any };
const substituirStringsVazias = (data: NestedObject | any[]): NestedObject | any[] =>
Array.isArray(data)
? data.map(substituirStringsVazias) // Mapeia cada elemento do array aplicando a função
: typeof data === "object" && data !== null
? Object.entries(data).reduce(
(acc, [key, value]) => ({
...acc,
[key]: substituirStringsVazias(value), // Aplica recursivamente em cada valor do objeto
}),
{}
)
: typeof data === "string" && data === ""
? "NAO_ENCONTRADO" // Substitui string vazia por "NAO_ENCONTRADO"
: data; // Retorna o valor original se nenhuma condição for atendida
Dei toda essa volta apenas para retirarmos o conceito principal dessa solução: recursividade.
Então como podemos pegar esse conceito e implementar no Python?
Vamos pensar eem converter primeiramente o tipo dict
:
def substituir_strings_vazias(dados):
if isinstance(dados, dict): # Se for um dicionário
novo_dicionario = {}
for k, v in dados.items(): # Itera pelas chaves e valores
novo_dicionario[k] = substituir_strings_vazias(v) # Aplica recursivamente
return novo_dicionario
elif isinstance(dados, list): # Se for uma lista
novo_array = []
for item in dados: # Itera pelos itens da lista
novo_array.append(substituir_strings_vazias(item)) # Aplica recursivamente
return novo_array
Agora com o teste se é do tipo list a gente reconstrói a list
usando a função append
para adicionar o item convertido na list
. Agora só falta para o tipo string
.
def substituir_strings_vazias(dados):
if isinstance(dados, dict): # Se for um dicionário
novo_dicionario = {}
for k, v in dados.items(): # Itera pelas chaves e valores
novo_dicionario[k] = substituir_strings_vazias(v) # Aplica recursivamente
return novo_dicionario
elif isinstance(dados, list): # Se for uma lista
novo_array = []
for item in dados: # Itera pelos itens da lista
novo_array.append(substituir_strings_vazias(item)) # Aplica recursivamente
return novo_array
elif isinstance(dados, str) and dados == "": # Se for string vazia
return "NAO_ENCONTRADO"
return dados # Retorna o valor original para outros casos
E com isso reconstruímos a mesma lógica de antes.
Porém depois que eu fiz isso me peguei pensando que haveria uma forma pythonica
de se fazer isso de um jeito melhor, então me lembrei do dictionary comprehension
, logo refatorei para:
def substituir_strings_vazias(dados):
if isinstance(dados, dict): # Se for um dicionário
return {k: substituir_strings_vazias(v) for k, v in dados.items()}
elif isinstance(dados, list): # Se for uma lista
novo_array = []
for item in dados: # Itera pelos itens da lista
novo_array.append(substituir_strings_vazias(item)) # Aplica recursivamente
return novo_array
elif isinstance(dados, str) and dados == "": # Se for string vazia
return "NAO_ENCONTRADO"
return dados # Retorna o valor original para outros casos
Isso me logicamente me lembrou de list comprehension
, então refatorei para:
def substituir_strings_vazias(dados):
if isinstance(dados, dict): # Se for um dicionário
return {k: substituir_strings_vazias(v) for k, v in dados.items()}
elif isinstance(dados, list): # Se for uma lista
return [substituir_strings_vazias(item) for item in dados]
elif isinstance(dados, str) and dados == "": # Se for string vazia
return "NAO_ENCONTRADO"
return dados # Retorna o valor original para outros casos
Então se entramos com esses valores:
dados = {
"nome": "",
"idade": 25,
"endereco": {
"rua": "",
"cidade": "São Paulo",
"estado": ""
},
"contatos": ["", "email@example.com"]
}
Nossa saída será:
{
"nome": "NAO_ENCONTRADO",
"idade": 25,
"endereco": {
"rua": "NAO_ENCONTRADO",
"cidade": "São Paulo",
"estado": "NAO_ENCONTRADO"
},
"contatos": ["NAO_ENCONTRADO", "email@example.com"]
}
Como isso funciona passo a passo?
A função vê que dados é um dicionário.
- Ela começa um loop pelas chaves e valores:
- Chave: "nome", Valor: ""
- É uma string vazia → retorna "NAO_ENCONTRADO".
- Chave: "idade", Valor: 25
- Não é uma string vazia → retorna 25.
- Chave: "endereco", Valor: outro dicionário.
- Chamada recursiva para processar o dicionário "endereco".
- "rua": "" → retorna "NAO_ENCONTRADO".
- "cidade": "São Paulo" → retorna "São Paulo".
- "estado": "" → retorna "NAO_ENCONTRADO".
- Resultado: {"rua": "NAO_ENCONTRADO", "cidade": "São Paulo", "estado": "NAO_ENCONTRADO"}.
- Chamada recursiva para processar o dicionário "endereco".
- Chave: "contatos", Valor: uma lista.
- Chamada recursiva para processar a lista:
- Primeiro item: "" → retorna "NAO_ENCONTRADO".
- Segundo item: "email@example.com" → retorna "email@example.com".
- Resultado: ["NAO_ENCONTRADO", "email@example.com"].
- Chamada recursiva para processar a lista:
- Chave: "nome", Valor: ""
E isso me deu um insight de reutilizar essa função, pois ela é deveras genérica, para normalizar datasets que possuamvalores nulos ou vazios.
Eu comecei pensando em Typescript pois é mais natural para mim, já tenho algum tempinho com Python mas quanto mais programo com ela mais eu gosto.
ps: a diferença entre os códigos finais é abissal, né?
Top comments (0)