如何在Python中按相似的索引/属性对元组/对象列表进行分组?

57

给定一个列表

old_list = [obj_1, obj_2, obj_3, ...]

我想创建一个列表:

new_list = [[obj_1, obj_2], [obj_3], ...]

如何判断两个Python对象的特定属性是否相等?

我可以编写一些 for 循环和 if 条件语句,但这样很丑陋。有没有更符合Python风格的方式来处理呢?顺便说一下,这些对象的属性都是字符串类型。

或者,如果是包含元组(长度相同)的列表而不是对象,则也需要一种解决方案。


“一个包含元组(长度相同)而不是对象的列表”这句话的意思是“一个包含所有长度相同的元组的列表”?如果是,那么这些元组是按照哪个“属性”进行分组的呢?顺便说一下,元组不也是对象吗? - eyquem
@eyquem:1. 是的;2. 元组在特定索引处分组。索引处的项是一个字符串;3. 我相信是这样,但我不确定 :-) - Aufwind
4个回答

96

defaultdict就是这样实现的。

尽管for循环通常是必不可少的,但if语句则不是。

from collections import defaultdict


groups = defaultdict(list)

for obj in old_list:
    groups[obj.some_attr].append(obj)

new_list = groups.values()

4
当然,这并没有保留(或以任何方式尊重)群组的原始顺序。因此,这可能是或可能不是@Druss想要的。 - tjollans
2
@jollybox.de: “不保留(或以任何方式尊重)群组的原始顺序”,正确。那什么时候变成了要求? - S.Lott
1
我不知道这是否是一个要求,原始问题并没有明确说明。我最初就是那样理解这个问题的。不过,回答得很好。 - tjollans
1
刚刚意识到,如果将dict的使用与itertools.groupby的答案结合起来,甚至不需要使用defaultdict - JAB
3
难道不应该调用list(groups.values())来实际返回OP想要的内容吗?我的意思是,否则,如果调用new_list[0],她会得到TypeError:'dict_values' object does not support indexing(至少在我的机器上)。 - sup
显示剩余2条评论

40

以下是两种情况,它们都需要以下导入:

import itertools
import operator
你将使用 itertools.groupbyoperator.attrgetteroperator.itemgetter。对于按 obj_1.some_attr == obj_2.some_attr 进行分组的情况:
get_attr = operator.attrgetter('some_attr')
new_list = [list(g) for k, g in itertools.groupby(sorted(old_list, key=get_attr), get_attr)]

对于a[some_index] == b[some_index]

get_item = operator.itemgetter(some_index)
new_list = [list(g) for k, g in itertools.groupby(sorted(old_list, key=get_item), get_item)]

注意需要进行排序,因为itertools.groupby会在键值改变时创建一个新的分组。


注意你可以使用这种方法来创建类似于S.Lott答案中的dict,但不必使用collections.defaultdict

使用字典推导式(仅适用于Python 3+,可能也适用于Python 2.7,但我不确定):

groupdict = {k: g for k, g in itertools.groupby(sorted_list, keyfunction)}

对于早期版本的Python,或者作为更加简洁的替代方法:

groupdict = dict(itertools.groupby(sorted_list, keyfunction))

groupdict = {k: g for k, g in itertools.groupby(sorted_list, keyfunction)} 应该改为 groupdict = {k: list(g) for k, g in itertools.groupby(sorted_list, keyfunction)},因为 g 只是一个迭代器。 - undefined

16

你也可以尝试使用itertools.groupby。请注意,下面的代码仅供参考,应根据您的需求进行修改:

data = [[1,2,3],[3,2,3],[1,1,1],[7,8,9],[7,7,9]]

from itertools import groupby

# for example if you need to get data grouped by each third element you can use the following code
res = [list(v) for l,v in groupby(sorted(data, key=lambda x:x[2]), lambda x: x[2])]# use third element for grouping

1
基本上我的回答是正确的,但你忘记了一个重要的方面:在使用 groupby 之前进行排序。 - JAB
2
@JAB - 你说得对。感谢你注意到我。 - Artsiom Rudzenka
@JAB - 为什么在使用groupby之前需要排序? - Sahil Chhabra
2
@SahilChhabra 请阅读我的回答,我解释了原因。 - JAB

2

最近,我也遇到了类似的问题。感谢上面提供的解决方案。我对上述方法的计算时间进行了小比较。在我的实现中,我保留字典,因为看到键很好。使用defaultdict的方法获胜。

from collections import defaultdict
import time
import itertools
import pandas as pd
import random


class Person:
    def __init__(self,name,age):
        self.name=name
        self.age=age

    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"


def method_with_dict(people):
    groups={}
    for person in people:
        if person.age in groups:
            groups[person.age].append(person)
        else:
            groups[person.age]=[person]
    return groups


def method_with_defaultdict(people):
    groups=defaultdict(list)
    for person in people:
        groups[person.age].append(person)
    return groups


def group_by_age_with_itertools(people):
    people.sort(key=lambda x: x.age)
    groups={}
    for age,group in itertools.groupby(people,key=lambda x: x.age):
        groups[age]=list(group)
    return groups


def group_by_age_with_pandas(people):
    df=pd.DataFrame([(p.name,p.age) for p in people],columns=["Name","Age"])
    groups=df.groupby("Age")["Name"].apply(list).to_dict()
    return {k: [Person(name,k) for name in v] for k,v in groups.items()}


if __name__ == "__main__":
    num_people=1000
    min_age,max_age=18,80
    people=[Person(name=f"Person {i}",age=random.randint(min_age,max_age)) for i in
            range(num_people)]

    N=10000
    start_time=time.time()
    for i in range(N):
        result_defaultdict=method_with_defaultdict(people)
    end_time=time.time()
    print(f"method_with_defaultdict: {end_time - start_time:.6f} seconds")


    start_time=time.time()
    for i in range(N):
        result_dict=method_with_dict(people)
    end_time=time.time()
    print(f"method_with_dict: {end_time - start_time:.6f} seconds")

    start_time=time.time()
    for i in range(N):
        result_itertools=group_by_age_with_itertools(people)
    end_time=time.time()
    print(f"method_with_itertools: {end_time - start_time:.6f} seconds")

    start_time=time.time()
    for i in range(N):
        result_pandas=group_by_age_with_pandas(people)
    end_time=time.time()
    print(f"method_with_pandas: {end_time - start_time:.6f} seconds")


method_with_defaultdict: 0.954309 seconds
method_with_dict: 1.301710 seconds
method_with_itertools: 1.868009 seconds
method_with_pandas: 34.422366 seconds

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接