Python:从集合中获取项目

7 投票
3 回答
1436 浏览
提问于 2025-04-16 17:30

一般来说,Python 的集合(set)似乎并不是为了通过键来获取项目而设计的。显然,字典(dictionary)就是为了这个目的而存在的。但是,是否有办法在给定一个键的情况下,从集合中找到一个与这个键相等的实例呢?

我知道这正是字典的用途,但我认为有合理的理由想要在集合中做到这一点。假设你定义了一个类似这样的类:

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

现在,假设我将要创建大量的 Person 对象,每次创建一个 Person 对象时,我需要确保它不是之前创建的 Person 对象的重复。只要两个 Person 对象的 firstname 相同,就认为它们是重复的,不管其他的实例变量是什么。因此,显而易见的做法是把所有的 Person 对象放进一个集合中,并定义 __hash____eq__ 方法,这样 Person 对象就可以通过它们的 firstname 来比较。

另一种选择是创建一个 Person 对象的字典,并使用单独创建的 firstname 字符串作为键。这样做的缺点是我会重复存储 firstname 字符串。在大多数情况下这并不是个问题,但如果我有 10,000,000 个 Person 对象呢?冗余的字符串存储可能会在内存使用上造成很大的负担。

但是,如果两个 Person 对象比较相等,我需要能够检索到原始对象,以便将其他实例变量(除了 firstname)合并,这样才能满足业务逻辑的要求。这又让我回到了我的问题:我需要某种方法从集合中检索实例。

有没有办法做到这一点?还是说使用字典是唯一的选择呢?

3 个回答

2

我觉得你可以在这里找到答案:

在Python中超越工厂模式

3

是的,你可以这样做:set 是可以被遍历的。不过要注意,这个操作的时间复杂度是 O(n),而字典的操作时间复杂度是 O(1)

所以,你需要在 速度内存 之间做个权衡。这是个经典的问题。就我个人而言,我会选择优化速度(也就是使用字典),因为在只有 10,000,000 个对象的情况下,内存不会很快就不够用,而且使用字典真的很简单。

至于 firstname 字符串的额外内存消耗:因为在 Python 中字符串是不可变的,所以把 firstname 属性作为键时,不会创建一个新的字符串,而只是复制了它的引用。

8

在这里,我建议使用字典。把 firstname 这个实例变量当作字典的键使用时,并不会复制它——字典只是会用同一个对象。我觉得字典占用的内存不会比集合多多少。

如果想真正节省内存,可以给你的类添加一个 __slots__ 属性。这样可以防止你创建的 10,000,000 个实例每个都有一个 __dict__ 属性,这样能节省比字典相比集合可能带来的更多内存。

编辑: 这里有一些数据来支持我的说法。我定义了一个简单的示例类,用来存储一对对随机字符串:

def rand_str():
    return str.join("", (chr(random.randrange(97, 123))
                         for i in range(random.randrange(3, 16))))

class A(object):
    def __init__(self):
        self.x = rand_str()
        self.y = rand_str()
    def __hash__(self):
        return hash(self.x)
    def __eq__(self, other):
        return self.x == other.x

在我的机器上,1,000,000 个这个类的实例所占用的内存

random.seed(42)
s = set(A() for i in xrange(1000000))

是 240 MB。如果我给这个类添加了

    __slots__ = ("x", "y")

,那么内存使用量就降到了 112 MB。如果我把相同的数据存储在字典中

def key_value():
    a = A()
    return a.x, a

random.seed(42)
d = dict(key_value() for i in xrange(1000000))

,那么没有 __slots__ 时会使用 249 MB,有了 __slots__ 后则使用 121 MB。

撰写回答