Что такое хороший питонический способ поиска дубликатов объектов?

Я часто использую sorted и groupby чтобы найти дубликаты элементов в iterable. Теперь я вижу, что это ненадежно:

from itertools import groupby
data = 3 * ('x ',  (1,), u'x')
duplicates = [k for k, g in groupby(sorted(data)) if len(list(g)) > 1]
print duplicates
# [] printed - no duplicates found - like 9 unique values

причина, по которой код выше терпит неудачу в Python 2.х пояснил,здесь.

что такое надежный питонический способ поиска дубликатов?

Я искал аналогичные вопросы / ответы на SO. Лучший из них -"в Python, как я могу взять список и уменьшить его до списка дубликатов?", но принятое решение-это не подходящие для Python (это процессуальные мультилинии для ... если. .. добавлять... еще... добавлять... результат возврата) и другие решения ненадежны (зависит от нереализованной транзитивности оператора"

[EDIT] закрытые. Принятый ответ помог мне обобщить выводы в моем ответе ниже более общего характера.

мне нравится использовать встроенные типы для представления, например, структуры дерева. Вот почему я теперь боюсь микса.

5 ответов


Примечание: предполагает записи hashable

>>> from collections import Counter
>>> data = 3 * ('x ',  (1,), u'x')
>>> [k for k, c in Counter(data).iteritems() if c > 1]
[u'x', 'x ', (1,)]

вывод:

  • если все элементы хэшируются и по крайней мере Python 2.7, лучшее решение выше с коллекций.Счетчик.
  • если некоторые элементы не хэшируются или Python 2.7+ отсутствует, то решение groupby(sorted(..)) очень хорошо в условиях
    • не комбинировать str и unicode или
    • Не использовать любой тип с именем, расположенным афабетически между "str"и " unicode". (обычно "кортеж" или "type")
  • если данные unhashable и смешанных типов, то ничего выше можно использовать и после этого самое лучшее использовать:
    • Counter(map(pickled.dumps, data)) вместо Counter(data) и, наконец, unpickle или
    • groupby(sorted(data, key=pickled.dumps)) Если распаковка нежелательна или нет python 2.7
  • A"наивное решение" обсуждаемый ниже может быть лучше, чем маринование для очень небольшого количества предметов примерно меньше, чем 300.

все остальные решения в других вопросах хуже.

Примечания:

  • похоже, что счетчик классов можно скопировать и вставить в более низкие версии Python.
  • A"наивное решение", который ищет каждый элемент в целом данных могут быть использованы для очень небольшого количества элементов. Он имеет то преимущество, что не требует хэшируемых данных и не зависит от транзитивности по умолчанию оператор"

Я думал о предварении элементов по типу или расширении их хэшем для хэшируемых элементов, это помогает в настоящее время, но это не безопасное решение, потому что та же проблема будет с "


эта тема мне интересна, поэтому я приурочил вышеупомянутое решение к решению, принятому в другом потоке.

метод счетчика этот поток очень элегантный; однако, принятый ответ в этом потоке в Python, как я могу взять список и уменьшить его до списка дубликатов? кажется, примерно в 2 раза быстрее.

import random as rn
import timeit
from collections import Counter

a = [rn.randint(0,100000) for i in xrange(10000)]

def counter_way(x):
    return [k for k,v in Counter(x).iteritems() if v > 1]

def accepted_way(x): #accepted answer in the linked thread
    duplicates = set()
    found = set()
    for item in x:
        if item in found:
            duplicates.add(item)
        else:         
            found.add(item)
    return duplicates


t1 = timeit.timeit('counter_way(a)', 'from __main__ import counter_way, a', number = 100)
print "counter_way: ", t1
t2 = timeit.timeit('accepted_way(a)','from __main__ import accepted_way, a', number = 100)
print "accepted_way: ", t2

результаты:

counter_way:  1.15775845813
accepted_way:  0.531060022992

Я пробовал это по разным спецификациям, и результат всегда один и тот же.


однако в обоих решениях есть ловушка. Причина в том, что он объединяет значения с тем же хэшем. Таким образом, это зависит от того, могут ли используемые значения иметь одинаковый хэш. Это не тот сумасшедший комментарий, как вы можете подумать (я также был удивлен ранее), потому что Python хэширует некоторые значения особым образом. Попробуйте:

from collections import Counter


def counter_way(x):
    return [k for k,v in Counter(x).iteritems() if v > 1]

def accepted_way(x): #accepted answer in the linked thread
    duplicates = set()
    found = set()
    for item in x:
        if item in found:
            duplicates.add(item)
        else:         
            found.add(item)
    return duplicates


a = ('x ',  (1,), u'x') * 2

print 'The values:', a
print 'Counter way duplicates:', counter_way(a)
print 'Accepted way duplicates:', accepted_way(a)
print '-' * 50

# Now the problematic values.
a = 2 * (0, 1, True, False, 0.0, 1.0)

print 'The values:', a
print 'Counter way duplicates:', counter_way(a)
print 'Accepted way duplicates:', accepted_way(a)

1, 1.0 и True имеют одинаковый хэш по определению, а также 0, 0.0 и False. Он печатает следующее На моей консоли (подумайте о последнем две строки -- все значения должны быть фактически дубликатами):

c:\tmp\___python\hynekcer\so10247815>python d.py
The values: ('x ', (1,), u'x', 'x ', (1,), u'x')
Counter way duplicates: [u'x', 'x ', (1,)]
Accepted way duplicates: set([u'x', 'x ', (1,)])
--------------------------------------------------
The values: (0, 1, True, False, 0.0, 1.0, 0, 1, True, False, 0.0, 1.0)
Counter way duplicates: [0, 1]
Accepted way duplicates: set([False, True])

просто потому, что мне было любопытно, вот решение, которое делает разницу между 0, False, 0.0 и т. д. Он основан на Сортировке последовательности с my_cmp это учитывает также тип элемента. Конечно, это очень медленно по сравнению с вышеупомянутыми решениями. Это из-за сортировки. Но сравните результаты!

import sys
import timeit
from collections import Counter

def empty(x):
    return

def counter_way(x):
    return [k for k,v in Counter(x).iteritems() if v > 1]

def accepted_way(x): #accepted answer in the linked thread
    duplicates = set()
    found = set()
    for item in x:
        if item in found:
            duplicates.add(item)
        else:         
            found.add(item)
    return duplicates


def my_cmp(a, b):
    result = cmp(a, b)
    if result == 0:
        return cmp(id(type(a)), id(type(b)))
    return result    


def duplicates_via_sort_with_types(x, my_cmp=my_cmp):

    last = '*** the value that cannot be in the sequence by definition ***'
    duplicates = []
    added = False
    for e in sorted(x, cmp=my_cmp):
        if my_cmp(e, last) == 0:
            ##print 'equal:', repr(e), repr(last), added
            if not added:
                duplicates.append(e)
                ##print 'appended:', e
                added = True
        else:
            ##print 'different:', repr(e), repr(last), added
            last = e
            added = False
    return duplicates


a = [0, 1, True, 'a string', u'a string', False, 0.0, 1.0, 2, 2.0, 1000000, 1000000.0] * 1000

print 'Counter way duplicates:', counter_way(a)
print 'Accepted way duplicates:', accepted_way(a)
print 'Via enhanced sort:', duplicates_via_sort_with_types(a) 
print '-' * 50

# ... and the timing
t3 = timeit.timeit('empty(a)','from __main__ import empty, a', number = 100)
print "empty: ", t3
t1 = timeit.timeit('counter_way(a)', 'from __main__ import counter_way, a', number = 100)
print "counter_way: ", t1
t2 = timeit.timeit('accepted_way(a)','from __main__ import accepted_way, a', number = 100)
print "accepted_way: ", t2
t4 = timeit.timeit('duplicates_via_sort_with_types(a)','from __main__ import duplicates_via_sort_with_types, a', number = 100)
print "duplicates_via_sort_with_types: ", t4

он печатает на моей консоли:

c:\tmp\___python\hynekcer\so10247815>python e.py
Counter way duplicates: [0, 1, 2, 'a string', 1000000]
Accepted way duplicates: set([False, True, 2.0, u'a string', 1000000.0])
Via enhanced sort: [False, 0.0, 0, True, 1.0, 1, 2.0, 2, 1000000.0, 1000000, 'a string', u'a string']
--------------------------------------------------
empty:  2.11195471969e-05
counter_way:  0.76977053612
accepted_way:  0.496547434023
duplicates_via_sort_with_types:  11.2378848197