Python: lista de dict, se houver, incremente um valor de dict, caso não acrescente um novo dict

107

Eu gostaria de fazer algo assim.

list_of_urls = ['http://www.google.fr/', 'http://www.google.fr/', 
                'http://www.google.cn/', 'http://www.google.com/', 
                'http://www.google.fr/', 'http://www.google.fr/', 
                'http://www.google.fr/', 'http://www.google.com/', 
                'http://www.google.fr/', 'http://www.google.com/', 
                'http://www.google.cn/']

urls = [{'url': 'http://www.google.fr/', 'nbr': 1}]

for url in list_of_urls:
    if url in [f['url'] for f in urls]:
         urls[??]['nbr'] += 1
    else:
         urls.append({'url': url, 'nbr': 1})

Como eu posso fazer ? Não sei se devo usar a tupla para editá-la ou descobrir os índices da tupla.

Qualquer ajuda ?

Natim
fonte

Respostas:

207

Essa é uma maneira muito estranha de organizar as coisas. Se você armazenou em um dicionário, é fácil:

# This example should work in any version of Python.
# urls_d will contain URL keys, with counts as values, like: {'http://www.google.fr/' : 1 }
urls_d = {}
for url in list_of_urls:
    if not url in urls_d:
        urls_d[url] = 1
    else:
        urls_d[url] += 1

Este código para atualizar um dicionário de contagens é um "padrão" comum em Python. É tão comum que haja uma estrutura de dados especial defaultdict, criada apenas para tornar isso ainda mais fácil:

from collections import defaultdict  # available in Python 2.5 and newer

urls_d = defaultdict(int)
for url in list_of_urls:
    urls_d[url] += 1

Se você acessar o defaultdictusando uma chave e a chave ainda não estiver no defaultdict, a chave será adicionada automaticamente com um valor padrão. O defaultdictpega o chamável que você passou e o chama para obter o valor padrão. Nesse caso, passamos na aula int; quando o Python o chama, int()ele retorna um valor zero. Portanto, na primeira vez que você faz referência a um URL, sua contagem é inicializada em zero e, em seguida, você adiciona um à contagem.

Mas um dicionário cheio de contagens também é um padrão comum, então Python fornece uma classe pronta para uso: containers.Counter você apenas cria uma Counterinstância chamando a classe, passando qualquer iterável; ele constrói um dicionário onde as chaves são valores do iterável e os valores são contagens de quantas vezes a chave apareceu no iterável. O exemplo acima então se torna:

from collections import Counter  # available in Python 2.7 and newer

urls_d = Counter(list_of_urls)

Se você realmente precisa fazer da maneira que mostrou, a maneira mais fácil e rápida seria usar qualquer um desses três exemplos e construir o que você precisa.

from collections import defaultdict  # available in Python 2.5 and newer

urls_d = defaultdict(int)
for url in list_of_urls:
    urls_d[url] += 1

urls = [{"url": key, "nbr": value} for key, value in urls_d.items()]

Se você estiver usando o Python 2.7 ou mais recente, poderá fazê-lo em uma linha:

from collections import Counter

urls = [{"url": key, "nbr": value} for key, value in Counter(list_of_urls).items()]
Steveha
fonte
Eu gosto de enviar para um modelo django para que eu possa fazer: `{% for u in urls%} {{u.url}}: {{u.nbr}} {% endfor%}
Natim
3
Você ainda pode fazer {% para url, nbr em urls.items%} {{url}}: {{nbr}} {% endfor%}
stefanw
160

Usar o padrão funciona, mas também:

urls[url] = urls.get(url, 0) + 1

usando .get, você pode obter um retorno padrão se ele não existir. Por padrão, é Nenhum, mas no caso que enviei para você, seria 0.

mikelikespie
fonte
12
Na verdade eu acho que essa é a melhor resposta, já que é agnóstica no dicionário fornecido, o que é um grande bônus para mim.
Bouncner
Esta é uma boa solução limpa.
Dylan Hogg
1
Esta deve ser a resposta. Eficiente, limpo e direto ao ponto !! Espero que o stackoverflow permita que a comunidade decida a resposta junto com o pôster da pergunta.
mowienay
Realmente gosto de esta resposta só não funciona se a chave for Nenhum ^^ Ou bem ... Precisa de mais alguns passos ...
Cedric
25

Use defaultdict :

from collections import defaultdict

urls = defaultdict(int)

for url in list_of_urls:
    urls[url] += 1
Greg Hewgill
fonte
17

Isso sempre funciona bem para mim:

for url in list_of_urls:
    urls.setdefault(url, 0)
    urls[url] += 1
musgo
fonte
3

Para fazer exatamente do seu jeito? Você poderia usar a estrutura for ... else

for url in list_of_urls:
    for url_dict in urls:
        if url_dict['url'] == url:
            url_dict['nbr'] += 1
            break
    else:
        urls.append(dict(url=url, nbr=1))

Mas é bastante deselegante. Você realmente tem que armazenar os urls visitados como uma LISTA? Se você classificá-lo como um dicionário, indexado pela string url, por exemplo, seria muito mais limpo:

urls = {'http://www.google.fr/': dict(url='http://www.google.fr/', nbr=1)}

for url in list_of_urls:
    if url in urls:
        urls[url]['nbr'] += 1
    else:
        urls[url] = dict(url=url, nbr=1)

Algumas coisas a serem observadas nesse segundo exemplo:

  • veja como usar um dict para urlsremove a necessidade de passar por toda a urlslista ao testar um único url. Essa abordagem será mais rápida.
  • Usar em dict( )vez de colchetes torna seu código mais curto
  • usando list_of_urls, urlse urlcomo nomes de variáveis tornar o código muito difícil de analisar. É melhor encontrar algo mais claro, como urls_to_visit, urls_already_visitede current_url. Eu sei, é mais longo. Mas está mais claro.

E, claro, estou assumindo que dict(url='http://www.google.fr', nbr=1)é uma simplificação da sua própria estrutura de dados, porque, do contrário, urlspoderia ser simplesmente:

urls = {'http://www.google.fr':1}

for url in list_of_urls:
    if url in urls:
        urls[url] += 1
    else:
        urls[url] = 1

O que pode ficar muito elegante com a postura defaultdict :

urls = collections.defaultdict(int)
for url in list_of_urls:
    urls[url] += 1
Nicolas Dumazet
fonte
A segunda versão é boa porque posso converter o dicionário em uma lista depois.
Natim
3

Exceto pela primeira vez, cada vez que uma palavra é vista, o teste da instrução if falha. Se você estiver contando um grande número de palavras, muitas provavelmente ocorrerão várias vezes. Em uma situação em que a inicialização de um valor só ocorrerá uma vez e o aumento desse valor ocorrerá muitas vezes, é mais barato usar uma instrução try:

urls_d = {}
for url in list_of_urls:
    try:
        urls_d[url] += 1
    except KeyError:
        urls_d[url] = 1

você pode ler mais sobre isso: https://wiki.python.org/moin/PythonSpeed/PerformanceTips

pilatipus
fonte