繁体   English   中英

'setdefault' dict 方法的用例

[英]Use cases for the 'setdefault' dict method

Python 2.5 中添加的collections.defaultdict大大减少了对dictsetdefault方法的需要。 本题为我们集体教育:

  1. 今天在 Python 2.6/2.7 中, setdefault仍然有用的是什么?
  2. setdefault的哪些流行用例被collections.defaultdict取代了?

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

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

# 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 
from collections import defaultdict
new = defaultdict(list)
for (key, value) in data:
    new[key].append( value ) # all keys have a default already

有时你想确保特定的键在创建字典后存在。 defaultdict在这种情况下不起作用,因为它只在显式访问时创建密钥。 假设您使用带有许多标头的类似 HTTP 的东西——有些是可选的,但您需要它们的默认值:

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用于关键字参数字典,例如在这个函数中:

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很棒,就像一个新列表,但如果它是动态的,就不是那么好了。

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

相反,我使用了一个普通的字典:

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 并返回它的值:

class intGen:
    def __init__(self):
        self.i = 0

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

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

由于大多数答案状态setdefaultdefaultdict会让您在键不存在时设置默认值。 但是,我想就setdefault的用例指出一个小警告。 当 Python 解释器执行setdefault时,它总是会评估函数的第二个参数,即使键存在于字典中也是如此。 例如:

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 总是递归地调用函数。

由于提到了记忆,如果您考虑使用记忆增强功能,更好的选择是使用 functools.lru_cache 装饰器。 lru_cache 更好地处理递归函数的缓存要求。

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

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

考虑一个尝试。 添加单词时,如果需要但不存在子节点,则必须创建它以扩展 trie。 当查询一个词是否存在时,缺少子节点表示该词不存在,不应创建。

defaultdict 不能这样做。 相反,必须使用带有 get 和 setdefault 方法的常规字典。

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

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

>>> mydata = local()
>>> mydata.__dict__
{'number': 42}
>>> mydata.__dict__.setdefault('widgets', [])
[]
>>> mydata.widgets
[]

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

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

对象将它们的属性存储在__dict__属性中。 碰巧的是, __dict__属性在对象创建后的任何时候都是可写的。 它也是一本字典而不是defaultdict 在一般情况下,对象将__dict__作为defaultdict是不明智的,因为这会使每个对象都具有所有合法标识符作为属性。 所以我无法预见对 Python 对象的任何更改都会摆脱__dict__.setdefault ,除了如果它被认为没有用的话将其完全删除。

defaultdict相对于dict ( dict.setdefault ) 的一个缺点是defaultdict对象会在每次给出不存在的键时创建一个新项(例如==print )。 此外, defaultdict类通常不如dict类常见,它更难序列化 IME。

PS IMO functions|methods 并不意味着改变一个对象,不应该改变一个对象。

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

#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 []

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

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

下面是一些 setdefault 的例子来展示它的用处:

"""
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'])

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

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

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

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

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

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

但这比 defaultdicts 存在之前没有必要。

我认为上面没有提到的另一个用例。 有时,您通过对象的 id 保留对象的缓存字典,其中主实例在缓存中,并且您希望在丢失时设置缓存。

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

当您总是希望为每个不同的 id 保留一个实例时,无论您每次如何获取 obj,这都非常有用。 例如,当对象属性在内存中更新并延迟保存到存储中时。

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

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

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在您不想修改已设置的值的情况下可能很有用。 例如,当您有重复的数字并且希望将它们视为一组时。 在这种情况下,如果您遇到重复的已设置的duplicate键,您将不会更新该键的值。 您将保留第一个遇到的值。 就好像您只迭代/更新重复的键一次。

下面是记录排序列表的键/元素索引的代码示例:

nums = [2,2,2,2,2]
d = {}
for idx, num in enumerate(sorted(nums)):
    # This will be updated with the value/index of the of the last repeated key
    # d[num] = idx # Result (sorted_indices): [4, 4, 4, 4, 4]
    # In the case of setdefault, all encountered repeated keys won't update the key.
    # However, only the first encountered key's index will be set 
    d.setdefault(num,idx) # Result (sorted_indices): [0, 0, 0, 0, 0]

sorted_indices = [d[i] for i in nums]

[编辑]大错特错! setdefault 总是会触发 long_computation,Python 很急切。

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

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

它消耗 3 行和 2 或 3 次查找, 我会很高兴地写

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

我喜欢这里给出的答案:

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

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

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

defaultdict示例:

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

setdefault不会覆盖:

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

CPython 中setdefault的另一个用例是它在所有情况下都是原子的,而如果您使用从 lambda 创建的默认值,则defaultdict将不是原子的。

cache = {}

def get_user_roles(user_id):
    if user_id in cache:
        return cache[user_id]['roles']

    cache.setdefault(user_id, {'lock': threading.Lock()})

    with cache[user_id]['lock']:
        roles = query_roles_from_database(user_id)
        cache[user_id]['roles'] = roles

如果两个线程同时执行cache.setdefault ,则只有其中一个能够创建默认值。

相反,如果您使用了 defaultdict:

cache = defaultdict(lambda: {'lock': threading.Lock()}

这将导致竞争条件。 在我上面的示例中,第一个线程可以创建一个默认锁,第二个线程可以创建另一个默认锁,然后每个线程可以锁定自己的默认锁,而不是每个线程尝试锁定单个锁的预期结果。


从概念上讲, setdefault基本上是这样的(如果您使用空列表、空字典、int 或其他不是用户 python 代码(如 lambda)的默认值,defaultdict 也会像这样):

gil = threading.Lock()

def setdefault(dict, key, value_func):
    with gil:
        if key not in dict:
            return
       
        value = value_func()

        dict[key] = value

从概念上讲, defaultdict基本上是这样的(仅当使用像 lambda 这样的 python 代码时——如果您使用空列表,则不是这样):

gil = threading.Lock()

def __setitem__(dict, key, value_func):
    with gil:
        if key not in dict:
            return

    value = value_func()

    with gil:
        dict[key] = value

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM