关于python:’setdefault’dict方法的用例

Use cases for the 'setdefault' dict method

在Python 2.5中添加collections.defaultdict大大减少了对dictsetdefault方法的需求。 这个问题适合我们的集体教育:

  • 今天在Python 2.6 / 2.7中什么是setdefault仍然有用?
  • 什么常见的setdefault用例被collections.defaultdict取代?

  • 您可以说defaultdict在填充dict之前对设置默认值有用,而setdefault对于在填充dict时或之后设置默认值很有用。

    可能是最常见的用例:对项目进行分组(在未排序的数据中,否则使用itertools.groupby)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # really verbose
    new = {}
    for (key, value) in data:
        if key in new:
            new[key].append( value )
        else:
            new[key] = [value]


    # easy with setdefault
    new = {}
    for (key, value) in data:
        group = new.setdefault(key, []) # key might exist already
        group.append( value )


    # even simpler with defaultdict
    new = defaultdict(list)
    for (key, value) in data:
        new[key].append( value ) # all keys have a default already

    有时您希望在创建dict后确保存在特定键。 defaultdict在这种情况下不起作用,因为它只在显式访问时创建密钥。认为你使用HTTP-ish和许多标题 - 有些是可选的,但你想要它们的默认值:

    1
    2
    3
    4
    headers = parse_headers( msg ) # parse the message, get a dict
    # now add all the optional headers
    for headername, defaultvalue in optional_headers:
        headers.setdefault( headername, defaultvalue )


    我通常使用setdefault作为关键字参数dicts,例如在此函数中:

    1
    2
    3
    4
    5
    6
    7
    8
    def notify(self, level, *pargs, **kwargs):
        kwargs.setdefault("persist", level >= DANGER)
        self.__defcon.set(level, **kwargs)
        try:
            kwargs.setdefault("name", self.client.player_entity().name)
        except pytibia.PlayerEntityNotFound:
            pass
        return _notify(level, *pargs, **kwargs)

    在包含关键字参数的函数的包装器中调整参数是很好的。


    当默认值是静态时(如新列表),defaultdict非常好,但如果它是动态的则不是很多。

    例如,我需要一个字典来将字符串映射到唯一的int。 defaultdict(int)将始终使用0作为默认值。同样,defaultdict(intGen())总是产生1。

    相反,我使用了常规字典:

    1
    2
    3
    4
    5
    nextID = intGen()
    myDict = {}
    for lots of complicated stuff:
        #stuff that generates unpredictable, possibly already seen str
        strID = myDict.setdefault(myStr, nextID())

    请注意dict.get(key, nextID())是不够的,因为我需要稍后能够引用这些值。

    intGen是我构建的一个小类,它自动递增一个int并返回它的值:

    1
    2
    3
    4
    5
    6
    7
    class intGen:
        def __init__(self):
            self.i = 0

        def __call__(self):
            self.i += 1
        return self.i

    如果某人有办法用defaultdict做到这一点,我很乐意看到它。


    当我想在OrderedDict中使用默认值时,我使用setdefault()。没有标准的Python集合可以同时执行这两种操作,但是有一些方法可以实现这样的集合。


    正如穆罕默德所说,在某些情况下,您有时只希望设置默认值。一个很好的例子是首先填充,然后查询的数据结构。

    考虑一个特里。添加单词时,如果需要子节点但不存在,则必须创建子节点以扩展trie。当查询单词的存在时,丢失的子节点表示该单词不存在且不应创建。

    defaultdict无法执行此操作。相反,必须使用带有get和setdefault方法的常规字典。


    从理论上讲,如果你有时想要设置默认值,有时候不是,那么setdefault仍然会很方便。在现实生活中,我没有遇到过这样的用例。

    但是,一个有趣的用例来自标准库(Python 2.6,_threadinglocal.py):

    1
    2
    3
    4
    5
    6
    7
    >>> mydata = local()
    >>> mydata.__dict__
    {'number': 42}
    >>> mydata.__dict__.setdefault('widgets', [])
    []
    >>> mydata.widgets
    []

    我会说使用__dict__.setdefault是一个非常有用的案例。

    编辑:碰巧,这是标准库中唯一的例子,它在评论中。因此,可能仅仅证明setdefault的存在是合理的。不过,这里有一个解释:

    对象将其属性存储在__dict__属性中。实际上,__dict__属性在对象创建后的任何时候都是可写的。它也是一个不是defaultdict的字典。在一般情况下,对象具有__dict__作为defaultdict是不明智的,因为这将使每个对象具有所有合法标识符作为属性。所以我无法预见Python对象除了__dict__.setdefault之外的任何变化,除非它被认为是无用的完全删除它。


    defaultdict over dict(dict.setdefault)的一个缺点是defaultdict对象创建了一个新项目EVERYTIME非现有密钥被给出(例如,使用==print)。此外,defaultdict类通常不如dict类更常见,因此更难以将其序列化为IME。

    附:不意味着改变对象的IMO函数方法不应该改变对象。


    由于大多数答案状态setdefaultdefaultdict将允许您在键不存在时设置默认值。但是,我想指出关于setdefault的用例的一个小警告。当Python解释器执行setdefault时,即使字符中存在键,它也将始终计算函数的第二个参数。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    In: d = {1:5, 2:6}

    In: d
    Out: {1: 5, 2: 6}

    In: d.setdefault(2, 0)
    Out: 6

    In: d.setdefault(2, print('test'))
    test
    Out: 6

    正如您所看到的,即使字典中已经存在2,也会执行print。如果您计划使用setdefault进行memoization等优化,这一点就变得尤为重要。如果你将一个递归函数调用作为setdefault的第二个参数添加,你将无法获得任何性能,因为Python总是以递归方式调用该函数。


    以下是setdefault的一些示例,以显示其用途:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    """
    d = {}
    # To add a key->value pair, do the following:
    d.setdefault(key, []).append(value)

    # To retrieve a list of the values for a key
    list_of_values = d[key]

    # To remove a key->value pair is still easy, if
    # you don't mind leaving empty lists behind when
    # the last value for a given key is removed:
    d[key].remove(value)

    # Despite the empty lists, it's still possible to
    # test for the existance of values easily:
    if d.has_key(key) and d[key]:
        pass # d has some values for key

    # Note: Each value can exist multiple times!
    """

    e = {}
    print e
    e.setdefault('Cars', []).append('Toyota')
    print e
    e.setdefault('Motorcycles', []).append('Yamaha')
    print e
    e.setdefault('Airplanes', []).append('Boeing')
    print e
    e.setdefault('Cars', []).append('Honda')
    print e
    e.setdefault('Cars', []).append('BMW')
    print e
    e.setdefault('Cars', []).append('Toyota')
    print e

    # NOTE: now e['Cars'] == ['Toyota', 'Honda', 'BMW', 'Toyota']
    e['Cars'].remove('Toyota')
    print e
    # NOTE: it's still true that ('Toyota' in e['Cars'])

    我没有想到的另一个用例如上所述。
    有时你通过id来保存对象的缓存字典,其中主要实例在缓存中,并且你想在丢失时设置缓存。

    1
    return self.objects_by_id.setdefault(obj.id, obj)

    无论你每次如何获得一个obj,当你总是想要为每个不同的id保留一个实例时,这很有用。例如,当对象属性在内存中更新并且延迟保存到存储时。


    我偶然发现了一个非常重要的用例:当你只需要一个规范对象(而不是多个碰巧相同的对象)时,dict.setdefault()对于多线程代码非常有用。

    例如,Python 3.6.0中的(Int)Flag Enum有一个错误:如果多个线程竞争复合(Int)Flag成员,最终可能会有多个:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    from enum import IntFlag, auto
    import threading

    class TestFlag(IntFlag):
        one = auto()
        two = auto()
        three = auto()
        four = auto()
        five = auto()
        six = auto()
        seven = auto()
        eight = auto()

        def __eq__(self, other):
            return self is other

        def __hash__(self):
            return hash(self.value)

    seen = set()

    class cycle_enum(threading.Thread):
        def run(self):
            for i in range(256):
                seen.add(TestFlag(i))

    threads = []
    for i in range(8):
        threads.append(cycle_enum())

    for t in threads:
        t.start()

    for t in threads:
        t.join()

    len(seen)
    # 272  (should be 256)

    解决方案是使用setdefault()作为保存计算复合成员的最后一步 - 如果另一个已经保存,则使用它而不是新的,保证唯一的Enum成员。


    我经常使用setdefault,得到这个,在字典中设置默认值(!!!); os.environ字典有点普遍:

    1
    2
    # Set the venv dir if it isn't already overridden:
    os.environ.setdefault('VENV_DIR', '/my/default/path')

    不太简洁,这看起来像这样:

    1
    2
    3
    # Set the venv dir if it isn't already overridden:
    if 'VENV_DIR' not in os.environ:
        os.environ['VENV_DIR'] = '/my/default/path')

    值得注意的是,您还可以使用结果变量:

    1
    venv_dir = os.environ.setdefault('VENV_DIR', '/my/default/path')

    但这比默认存在之前的那些要少。


    [编辑]非常错! setdefault总是会触发long_computation,Python很渴望。

    扩展塔特尔的答案。对我来说,最好的用例是缓存机制。代替:

    1
    2
    3
    if x not in memo:
       memo[x]=long_computation(x)
    return memo[x]

    消耗3行和2或3次查找,我很乐意写

    1
    return memo.setdefault(x, long_computation(x))


    我喜欢这里给出的答案:

    http://stupidpythonideas.blogspot.com/2013/08/defaultdict-vs-setdefault.html

    简而言之,应该根据您希望如何处理下游空键的查找(即KeyError与默认值)来做出决策(在非性能关键型应用程序中)。


    setdefault()的不同用例是当您不想覆盖已设置密钥的值时。 defaultdict覆盖,而setdefault()则不覆盖。对于嵌套字典,更常见的情况是,只有在尚未设置密钥时才要设置默认值,因为您不想删除当前的子字典。这是当你使用setdefault()时。

    defaultdict的示例:

    1
    2
    3
    4
    5
    6
    >>> from collection import defaultdict()
    >>> foo = defaultdict()
    >>> foo['a'] = 4
    >>> foo['a'] = 2
    >>> print(foo)
    defaultdict(None, {'a': 2})

    setdefault不会覆盖:

    1
    2
    3
    4
    5
    >>> bar = dict()
    >>> bar.setdefault('a', 4)
    >>> bar.setdefault('a', 2)
    >>> print(bar)
    {'a': 4}

    我重写了接受的答案,并为新手提供了便利。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #break it down and understand it intuitively.
    new = {}
    for (key, value) in data:
        if key not in new:
            new[key] = [] # this is core of setdefault equals to new.setdefault(key, [])
            new[key].append(value)
        else:
            new[key].append(value)


    # easy with setdefault
    new = {}
    for (key, value) in data:
        group = new.setdefault(key, []) # it is new[key] = []
        group.append(value)



    # even simpler with defaultdict
    new = defaultdict(list)
    for (key, value) in data:
        new[key].append(value) # all keys have a default value of empty list []

    另外,我将这些方法分类为参考:

    1
    2
    3
    4
    5
    6
    dict_methods_11 = {
                'views':['keys', 'values', 'items'],
                'add':['update','setdefault'],
                'remove':['pop', 'popitem','clear'],
                'retrieve':['get',],
                'copy':['copy','fromkeys'],}