8

I was looking for a set()-like method to deduplicate a list, except that the items figuring in the original list are not hashable (they are dicts).

I spent a while looking for something adequate, and I ended up writing this little function:

def deduplicate_list(lst, key):
    output = []
    keys = []
    for i in lst:
        if not i[key] in keys:
            output.append(i)
            keys.append(i[key])

    return output

Provided that a key is correctly given and is a string, this function does its job pretty well. Needless to say, if I learn about a built-in or a standard library module which allows the same functionality, I'll happily drop my little routine in favor of a more standard and robust choice.

Are you aware of such implementation?

-- Note

The following one-liner found from this answer,

[dict(t) for t in set([tuple(d.items()) for d in l])]

while clever, won't work because I have to work with items as nested dicts.

-- Example

For clarity purposes, here is an example of using such a routine:

with_duplicates = [
    {
        "type": "users",
        "attributes": {
            "first-name": "John",
            "email": "john.smith@gmail.com",
            "last-name": "Smith",
            "handle": "jsmith"
        },
        "id": "1234"
    },
    {
        "type": "users",
        "attributes": {
            "first-name": "John",
            "email": "john.smith@gmail.com",
            "last-name": "Smith",
            "handle": "jsmith"
        },
        "id": "1234"
    }
]

without_duplicates = deduplicate_list(with_duplicates, key='id')
Community
  • 1
  • 1
Jivan
  • 21,522
  • 15
  • 80
  • 131

4 Answers4

3

You are picking only the first dict in your list for every distinct value for key. itertools.groupby is the built-in tool that can do that for you - sort and group by key and take only the first from each group:

from itertools import groupby

def deduplicate(lst, key):
    fnc = lambda d: d.get(key)  # more robust than d[key]
    return [next(g) for k, g in groupby(sorted(lst, key=fnc), key=fnc)]
user2390182
  • 72,016
  • 6
  • 67
  • 89
0

This answer will help to solve a more generic problem - find unique elements not by a single attribute (id in your case), but if any nested attribute differs

The following code will return a list of indices of the unique elements

import copy

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that contains
  only other hashable types (including any lists, tuples, sets, and
  dictionaries).
  """

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

l = [
    {
        "type": "users",
        "attributes": {
            "first-name": "John",
            "email": "john.smith@gmail.com",
            "last-name": "Smith",
            "handle": "jsmith"
        },
        "id": "1234"
    },
    {
        "type": "users",
        "attributes": {
            "first-name": "AAA",
            "email": "aaa.aaah@gmail.com",
            "last-name": "XXX",
            "handle": "jsmith"
        },
        "id": "1234"
    },
    {
        "type": "users",
        "attributes": {
            "first-name": "John",
            "email": "john.smith@gmail.com",
            "last-name": "Smith",
            "handle": "jsmith"
        },
        "id": "1234"
    },
]

# get indicies of unique elements
In [254]: list({make_hash(x):i for i,x in enumerate(l)}.values())
Out[254]: [1, 2]
Community
  • 1
  • 1
MaxU - stand with Ukraine
  • 205,989
  • 36
  • 386
  • 419
  • 1
    Do not use hashes to determine if two objects are the same. You always have the risk of hash collisions. – Rob Jun 03 '16 at 12:57
  • @Rob, doesn't the `set` implementation use hash (C or Cython implementation of this function) for generating "virtual" keys? And i guess it's a common practice to use `set(lst)` - for deduplicating lists... – MaxU - stand with Ukraine Jun 03 '16 at 13:14
  • `set` uses hashes indeed, but only to make the implementation faster. If two objects have the same hash but different values, they will still be separate in a `set`, but not in this implementation. – Rob Jun 03 '16 at 13:58
0

You could try a short version which is based on the link to the answer, which you provided in the question:

key = "id"
deduplicated = [val for ind, val in enumerate(l)
                if val[key] not in [tmp[key] for tmp in l[ind + 1:]]]
print(deduplicated)

Note, this will take the last element of duplicates

DomTomCat
  • 8,189
  • 1
  • 49
  • 64
0

In your example, the value returned by the key is hashable. If this is always the case, then use this:

def deduplicate(lst, key):
    return list({item[key]: item for item in lst}.values())

If there are duplicates, only the last matching duplicate is retained.

dln385
  • 11,630
  • 12
  • 48
  • 58