Replace all arguments (of all strings) that exist in a Dict with . format

Asked

Viewed 99 times

2

I wonder if there is any pythonica way to replace all the arguments (of all strings) that exist in dictionary with . format . For example:

dicionario = {"nome": "{nome_usuario}", "idade": 26, "infos": [ {"nome": "{nome_usuario}" }]}
informacoes = {"nome_usuario": "Joao"}

Without having to do:

dicionario["nome"] = dicionario["nome"].format(**informacoes)
dicionario["infos"][0]["nome"] = dicionario["infos"][0]["nome"].format(**informacoes)

I wonder if there’s any more efficient way to do that.

NOTE: The dictionary would be a JSON file

3 answers

4


If there is a possibility to change the format, you can use the structure Template library native string python. For example, instead of making the values between keys, {nome_usuario}, you could put the prefix $, $nome_usuario, and replace values through the template.

{
  "nome": "$nome_usuario", 
  "idade": 26, 
  "infos": [
    {
      "nome": "$nome_usuario"
    }
  ]
}

So, having the values in a dictionary:

informacoes = {"nome_usuario": "Joao"}

Just read the file and apply the template:

import string


informacoes = {"nome_usuario": "Joao"}

with open('data.json') as stream:
  content = stream.read()

template = string.Template(content)

print(template.substitute(**informacoes))

With that you’ll have the way out:

{
  "nome": "João", 
  "idade": 26, 
  "infos": [
    {
      "nome": "João"
    }
  ]
}
  • Thank you very much! This solution suits me perfectly. I didn’t know this structure Template, I will read more about it.

2

You can create a subclass of Dict, which has a substitution parameter, and do the .format directly on __getitem__ - Then the Python engine itself generates the formatting on each string you’re going to use, and you don’t even need to have a "dot in the program where you format the strings" - they’ll just appear formatted in the substitution. The point of this approach is that if you exchange the contents of the dictionary "information" - as is the same object referenced within the modified dictionary, all new readings will be done with the new information.

A simple version can inherit from Dict, make a recursive substitution in creation, treating lists as a special case:

class AutoFormatDict(dict):
    def __init__(self, original_dict, format_data):
        for key, value in original_dict.items():
            if isinstance(value, dict):
                original_dict[key] = AutoFormatDict(value, format_data)
            elif isinstance(value, list):
                for position, list_value in enumerate(value):
                    if isinstance(list_value, dict):
                        value[position] = AutoFormatDict(list_value, format_data)
        super().__init__(original_dict)
        self.format_data = format_data

    def __getitem__(self, key):
        value = super().__getitem__(key)
        return value.format(**self.format_data) if isinstance(value, str) else value     

And in the terminal, given the two dictionaries that you present in the question, it works like this:



In [24]: data = AutoFormatDict(dicionario, informacoes)                                    

In [25]: data["infos"][0]["nome"]                                                          
Out[25]: 'Joao'

In [26]: informacoes                                                                       
Out[26]: {'nome_usuario': 'Joao'}

In [27]: informacoes["nome_usuario"] = "Pedro"                                             

In [28]: data["infos"][0]["nome"]                                                          
Out[28]: 'Pedro'

Sophisticated version and "Production ready"

A somewhat more sophisticated implementation can make the recursive substitution read-only - within the "getitem" and can inherit from "Collections.abc.Sequence" instead of "Dict" - this ensures that the substitution will work for all the methods of reading a dictionary (iteration with "for", "get" methods, "setdefault", "pop" and etc...). This approach needs a specialized companion class the list also - since the "getitem" method of the list is separated.

This version can be considered "advanced" because it uses the classes of "Collections.abc" - but it’s good enough to be placed in a production code in any application, or published in a Pypi library.

(And of course, since we’re sophisticated, we can make another auxiliary class -- so that we don’t have an error in the strings that were missing in the substitution)

So inheriting from collections.abc.MutableMapping and collections.abc.MutableSequence. Like most of the methods will do the same thing - simply pass the parameters received for the internal "date" structure, we can refactor this to a base class - and we just need to implement the insert and the __iter__ separately.


from collections import defaultdict
from collections.abc import MutableMapping, MutableSequence, Sequence, Mapping



class _AutoFormatMixin:
    def __init__(self, template_data, subst_data):
        self.data = template_data
        self.substitution = defaultdict(lambda: '', subst_data)

    def __len__(self):
        return len(self.data)

    def __setitem__(self, key, value):
        self.data[key] = value

    def __delitem__(self, key):
        del self.data[key]

    def __getitem__(self, key):
        value = self.data[key]
        if isinstance(value, Mapping):
            return AutoFormatDict(value, self.substitution)
        elif isinstance(value, str):
            return value.format_map(self.substitution)
        elif isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray)):
            return AutoFormatList(value, self.substitution)
        return value

    def __repr__(self):
        return f"{self.__class__.__name__}({self.data!r}, {self.substitution!r})"


class AutoFormatDict(_AutoFormatMixin, MutableMapping):
    def __iter__(self):
        return iter(self.data)


class AutoFormatList(_AutoFormatMixin, MutableSequence):
    def insert(self, index, value):
        self.data.insert(index, value)

And working:

In [83]: data = AutoFormatDict(dicionario, informacoes)                                    

In [84]: data.substitution["nome_usuario"] = "Beatriz"                                     

In [85]: data["infos"][0]["nome"]                                                          
Out[85]: 'Beatriz'

In [86]: data.substitution["nome_usuario"] = "Bruno"                                       

In [87]: data["infos"][0]["nome"]                                                          
Out[87]: 'Bruno'
  • The two solutions proposed by you are very sophisticated! Congratulations... they helped me too.

0

You can implement a recursive function capable of traversing all the keys and values of the dictionary in search of values that are of the type string only then apply the formatting, see only:

def formatar_strings( d, info ):
    for k, v in d.items():
        if isinstance(v, dict):
            d[k] = formatar_strings(v, info)
        elif isinstance(v, list):
            d[k] = [formatar_strings(e, info) for e in v]
        elif isinstance(v, str):
            d[k] = d[k].format(**info)
    return d


dicionario = {"nome": "{nome_usuario}", "idade": 26, "infos": [ {"nome": "{nome_usuario}" }]}
informacoes = {"nome_usuario": "Joao"}
saida = formatar_strings( dicionario, informacoes );
print(saida)

Exit:

{'nome': 'Joao', 'idade': 26, 'infos': [{'nome': 'Joao'}]}

See working on Repl.it

Browser other questions tagged

You are not signed in. Login or sign up in order to post.