簡體   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