İç içe sözlük değerini elde etmek için Python güvenli yöntemi


145

Yuvalanmış bir sözlüğüm var. Değerleri güvenli bir şekilde çıkarmanın tek bir yolu var mı?

try:
    example_dict['key1']['key2']
except KeyError:
    pass

Ya da belki python get()iç içe sözlük için bir yöntem var ?



1
Sorunuzdaki kod, bence, iç içe değerleri sözlükten çıkarmanın en iyi yoludur. Yan tümcede her zaman varsayılan bir değer belirleyebilirsiniz except keyerror:.
Peter Schorn

Yanıtlar:


281

getİki kez kullanabilirsiniz :

example_dict.get('key1', {}).get('key2')

NoneVarsa key1veya key2yoksa bu geri döner .

Bunun hala bir AttributeErrorif değerini yükseltebileceğini example_dict['key1'], ancak bir diksiyon (veya bir getyönteme sahip dikt benzeri bir nesne ) olmadığını unutmayın. try..exceptYayınladığınız kod getireceğini TypeErroreğer yerine example_dict['key1']unsubscriptable olduğunu.

Diğer bir fark, try...exceptilk eksik anahtardan hemen sonra kısa devrelerin olmasıdır. Çağrı zinciri getbunu yapmaz.


Sözdizimini korumak istiyorsanız, example_dict['key1']['key2']ancak KeyError'ları yükseltmesini istemiyorsanız, Hasher tarifini kullanabilirsiniz :

class Hasher(dict):
    # https://stackoverflow.com/a/3405143/190597
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

example_dict = Hasher()
print(example_dict['key1'])
# {}
print(example_dict['key1']['key2'])
# {}
print(type(example_dict['key1']['key2']))
# <class '__main__.Hasher'>

Bir anahtar eksik olduğunda bunun boş bir Hasher döndürdüğünü unutmayın.

Çünkü Hasherbir alt sınıfınız dictbir Hasher'i aynı şekilde kullanabilirsiniz dict. Tüm aynı yöntemler ve sözdizimi mevcuttur, Hashers sadece eksik anahtarlara farklı davranır.

Normal bir dönüştürebilirsiniz dictbir içine Hasherböyle:

hasher = Hasher(example_dict)

ve kolayca Hasherbir normaline dönüştürün dict:

regular_dict = dict(hasher)

Başka bir alternatif, çirkinliği bir yardımcı işlevde gizlemektir:

def safeget(dct, *keys):
    for key in keys:
        try:
            dct = dct[key]
        except KeyError:
            return None
    return dct

Böylece kodunuzun geri kalanı nispeten okunabilir kalabilir:

safeget(example_dict, 'key1', 'key2')

38
python'un bu dava için güzel bir çözümü yok mu ?:(
Arti

Benzer bir uygulamayla ilgili bir sorunla karşılaştım. D = {key1: None} varsa, ilk get None döndürür ve sonra bir istisnanız olur): Bunun için bir çözüm bulmaya çalışıyorum
Huercio

1
safegetO güvenle gibi şeyler yapamaz, yani orijinal sözlüğü üzerine yazar beri yöntem çok güvenli olmayan yollarla bir çok olduğunu safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox

4
@KurtBourbaki: dct = dct[key] yeniden atar yeni bir değer yerel değişken dct . Bu orijinal dikdörtgeni değiştirmez (bu nedenle orijinal dikte bundan etkilenmez safeget.) Öte yandan, dct[key] = ...kullanılmışsa, orijinal dikti değiştirilmiş olurdu. Başka bir deyişle, Python'da isimler değerlere bağlıdır . Bir isme yeni bir değer
atamak

1
safegetAyrıca halinde iç içe geçmiş bir dict anahtar başarısız olur yöntem mevcuttur, ancak değer sıfırdır. Bir TypeError: 'NoneType' object is not subscriptablesonraki yinelemede atılacak
Stanley F.

60

Ayrıca python azaltma kullanabilirsiniz :

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)

5
Sadece functools'un artık Python3'te bir yerleşik olmadığını ve functools'tan içe aktarılması gerektiğini belirtmek istedim, bu da bu yaklaşımı biraz daha az zarif hale getiriyor.
yoniLavi

3
Bu yoruma hafif düzeltme: azaltma artık Py3'te yerleşik değildir. Ama bunun neden daha az zarif olduğunu anlamıyorum. Bu vermez tek astar için daha az uygun hale getirilmesi ve bir tek astar olma otomatik nitelemek ya da "zarif" olarak diskalifiye şey değildir.
PaulMcG

30

Tüm bu yanıtı ve yaptığım küçük değişiklikleri birleştirerek bu işlevin yararlı olacağını düşünüyorum. güvenli, hızlı, kolay bakım.

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

Misal :

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>

1
Jinja2 şablonları için mükemmel
Thomas

Bu iyi bir çözüm olsa da bir dezavantaj vardır: ilk anahtar mevcut olmasa veya işleve sözlük argümanı olarak iletilen değer bir sözlük olmasa bile, işlev ilk öğeden sonuncuya gider. Temel olarak, bunu her durumda yapar.
Arseny

1
deep_get({'a': 1}, "a.b")verir Noneama böyle bir istisna KeyErrorya da başka bir şey beklenir .
stackunderflow

@edityouprofile. o zaman sadece gelen değişim dönüş değeri küçük değişiklik yaparım gerekir NoneiçinRaise KeyError
Yuda Prawira

15

Yoav'ın cevabına dayanarak, daha güvenli bir yaklaşım:

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else None, keys, dictionary)

12

Özyinelemeli bir çözüm. En verimli değil ama diğer örneklerden biraz daha okunabilir buluyorum ve functools'a dayanmıyor.

def deep_get(d, keys):
    if not keys or d is None:
        return d
    return deep_get(d.get(keys[0]), keys[1:])

Misal

d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code'])     # => 200
deep_get(d, ['garbage', 'status_code'])  # => None

Daha parlak bir versiyon

def deep_get(d, keys, default=None):
    """
    Example:
        d = {'meta': {'status': 'OK', 'status_code': 200}}
        deep_get(d, ['meta', 'status_code'])          # => 200
        deep_get(d, ['garbage', 'status_code'])       # => None
        deep_get(d, ['meta', 'garbage'], default='-') # => '-'
    """
    assert type(keys) is list
    if d is None:
        return default
    if not keys:
        return d
    return deep_get(d.get(keys[0]), keys[1:], default)

8

Azaltma yaklaşımı düzgün ve kısa olsa da, basit bir döngünün grok yapmak daha kolay olduğunu düşünüyorum. Ayrıca varsayılan bir parametre ekledim.

def deep_get(_dict, keys, default=None):
    for key in keys:
        if isinstance(_dict, dict):
            _dict = _dict.get(key, default)
        else:
            return default
    return _dict

Tek astarı azaltmanın nasıl çalıştığını anlamak için bir egzersiz olarak, aşağıdakileri yaptım. Ama sonuçta döngü yaklaşımı benim için daha sezgisel görünüyor.

def deep_get(_dict, keys, default=None):

    def _reducer(d, key):
        if isinstance(d, dict):
            return d.get(key, default)
        return default

    return reduce(_reducer, keys, _dict)

kullanım

nested = {'a': {'b': {'c': 42}}}

print deep_get(nested, ['a', 'b'])
print deep_get(nested, ['a', 'b', 'z', 'z'], default='missing')

5

Denemenizi öneririm python-benedict.

dictAnahtar yolu desteği ve çok daha fazlasını sağlayan bir alt sınıftır.

Kurulum: pip install python-benedict

from benedict import benedict

example_dict = benedict(example_dict, keypath_separator='.')

artık tuş yolunu kullanarak iç içe değerlere erişebilirsiniz :

val = example_dict['key1.key2']

# using 'get' method to avoid a possible KeyError:
val = example_dict.get('key1.key2')

veya anahtarlar listesini kullanarak iç içe değerlere erişin :

val = example_dict['key1', 'key2']

# using get to avoid a possible KeyError:
val = example_dict.get(['key1', 'key2'])

GitHub'da iyi test edilmiş ve açık kaynaklı :

https://github.com/fabiocaccamo/python-benedict


@ perfecto25 teşekkür ederim! Yakında yeni özellikler yayınlayacağım, bizi izlemeye devam edin 😉
Fabio Caccamo

@ perfecto25 Liste dizinlerine destek ekledim, örn. d.get('a.b[0].c[-1]')
Fabio Caccamo

From_toml işlevi uygulanmış görünmüyor. Ve BeneDict'i içe aktarmak zor olabilir.
DLyons

@DLyons yanlış, her durumda GitHub'da bir sorun açmaktan çekinmeyin.
Fabio Caccamo

1
Evet, orada. Yazık ki özledim - beni biraz kurtarırdı. Benedict çok kullanışlı bir işleve sahip gibi görünüyor.
DLyons

4

Bir dikdörtgeni saran ve bir anahtara dayalı olarak alabilen basit bir sınıf:

class FindKey(dict):
    def get(self, path, default=None):
        keys = path.split(".")
        val = None

        for key in keys:
            if val:
                if isinstance(val, list):
                    val = [v.get(key, default) if v else None for v in val]
                else:
                    val = val.get(key, default)
            else:
                val = dict.get(self, key, default)

            if not val:
                break

        return val

Örneğin:

person = {'person':{'name':{'first':'John'}}}
FindDict(person).get('person.name.first') # == 'John'

Anahtar yoksa, Nonevarsayılan olarak geri döner . Sarıcıdaki bir default=anahtarı kullanarak bunu geçersiz kılabilirsiniz FindDict- örneğin`:

FindDict(person, default='').get('person.name.last') # == doesn't exist, so ''

3

ikinci düzey bir anahtar alımı için şunları yapabilirsiniz:

key2_value = (example_dict.get('key1') or {}).get('key2')

2

Özellikleri derinlemesine almak için bunu gördükten sonra , dictnokta gösterimini kullanarak iç içe değerleri güvenle almak için aşağıdakileri yaptım . Bu benim için işe dictsyaradı çünkü benim serileştirilmiş MongoDB nesneler, bu yüzden anahtar isimlerinin .s içermediğini biliyorum . Ayrıca, benim bağlamda, ben Nonebenim verilerde olmayan bir falsy düşme değeri ( ) belirtebilirsiniz , bu yüzden işlevi çağırırken try / hariç desen önleyebilirsiniz.

from functools import reduce # Python 3
def deepgetitem(obj, item, fallback=None):
    """Steps through an item chain to get the ultimate value.

    If ultimate value or path to value does not exist, does not raise
    an exception and instead returns `fallback`.

    >>> d = {'snl_final': {'about': {'_icsd': {'icsd_id': 1}}}}
    >>> deepgetitem(d, 'snl_final.about._icsd.icsd_id')
    1
    >>> deepgetitem(d, 'snl_final.about._sandbox.sbx_id')
    >>>
    """
    def getitem(obj, name):
        try:
            return obj[name]
        except (KeyError, TypeError):
            return fallback
    return reduce(getitem, item.split('.'), obj)

7
fallbackaslında işlevde kullanılmaz.
153957

Bunun.
JW

Obj [name] adını verdiğimizde neden obj.get (name, fallback) ve try-catch'dan kaçındığımızda (try-catch'i istiyorsanız, o zaman None yerine geri dönüş
yapmayın

Teşekkürler @ 153957. Onardım. Ve evet @JW, bu benim kullanım durumum için çalışıyor. sep=','Belirli (sep, yedek) koşullar için genelleştirmek üzere bir anahtar kelime arg ekleyebilirsiniz . Ve @denvar, azaltma dizisinden sonra objtür söylenirse, intobj [name] yakaladığım TypeError değerini yükseltir. Bunun yerine obj.get (name) veya obj.get (name, fallback) kullansaydım, AttributeError değerini yükseltirdi, bu yüzden her iki şekilde de yakalamam gerekirdi.
Donny Winston

1

Yine aynı şey için başka bir işlev, anahtarın bulunup bulunmadığını göstermek için bir boole döndürür ve bazı beklenmedik hataları işler.

'''
json : json to extract value from if exists
path : details.detail.first_name
            empty path represents root

returns a tuple (boolean, object)
        boolean : True if path exists, otherwise False
        object : the object if path exists otherwise None

'''
def get_json_value_at_path(json, path=None, default=None):

    if not bool(path):
        return True, json
    if type(json) is not dict :
        raise ValueError(f'json={json}, path={path} not supported, json must be a dict')
    if type(path) is not str and type(path) is not list:
        raise ValueError(f'path format {path} not supported, path can be a list of strings like [x,y,z] or a string like x.y.z')

    if type(path) is str:
        path = path.strip('.').split('.')
    key = path[0]
    if key in json.keys():
        return get_json_value_at_path(json[key], path[1:], default)
    else:
        return False, default

örnek kullanım:

my_json = {'details' : {'first_name' : 'holla', 'last_name' : 'holla'}}
print(get_json_value_at_path(my_json, 'details.first_name', ''))
print(get_json_value_at_path(my_json, 'details.phone', ''))

(Doğru, 'holla')

(Yanlış, '')



0

Unutmayın ki kendi kodumda faydalı bulduğum cevap uyarlaması:

example_dict.setdefaut('key1', {}).get('key2')

KeyError'dan kaçınmanız için o tuşa sahip değilse, key1 için bir sözlük girdisi oluşturur. Benim yaptığım gibi o anahtar eşleşmesini içeren bir iç içe sözlük bitirmek istiyorsanız, bu en kolay çözüm gibi görünüyor.


0

Anahtarlardan biri eksikse bir anahtar hatayı yükseltmek makul bir şey olduğundan, bunu kontrol edemeyiz ve bu kadar tek yapamayız:

def get_dict(d, kl):
  cur = d[kl[0]]
  return get_dict(cur, kl[1:]) if len(kl) > 1 else cur

0

reduceListe ile çalışmasını sağlamak için çok az gelişme . Ayrıca veri yolunu dizi yerine noktalara bölünerek dize olarak kullanma.

def deep_get(dictionary, path):
    keys = path.split('.')
    return reduce(lambda d, key: d[int(key)] if isinstance(d, list) else d.get(key) if d else None, keys, dictionary)

0

Kullandığım bir çözüm double get'e benzer, ancak başka bir mantık kullanarak bir TypeError'dan kaçınma ek yeteneği ile:

    value = example_dict['key1']['key2'] if example_dict.get('key1') and example_dict['key1'].get('key2') else default_value

Bununla birlikte, sözlük ne kadar iç içe olursa, bu o kadar hantal hale gelir.


0

İç içe sözlük / JSON aramaları için diktatör kullanabilirsiniz

pip install diktatör

dikte nesnesi

{
    "characters": {
        "Lonestar": {
            "id": 55923,
            "role": "renegade",
            "items": [
                "space winnebago",
                "leather jacket"
            ]
        },
        "Barfolomew": {
            "id": 55924,
            "role": "mawg",
            "items": [
                "peanut butter jar",
                "waggy tail"
            ]
        },
        "Dark Helmet": {
            "id": 99999,
            "role": "Good is dumb",
            "items": [
                "Shwartz",
                "helmet"
            ]
        },
        "Skroob": {
            "id": 12345,
            "role": "Spaceballs CEO",
            "items": [
                "luggage"
            ]
        }
    }
}

Lonestar'ın öğelerini almak için nokta ile ayrılmış bir yol sağlamanız yeterlidir;

import json
from dictor import dictor

with open('test.json') as data: 
    data = json.load(data)

print dictor(data, 'characters.Lonestar.items')

>> [u'space winnebago', u'leather jacket']

anahtarın yolda olmaması durumunda yedek değer sağlayabilirsiniz

Harf muhafazasını görmezden gelmek ve 'dışında' diğer karakterleri kullanmak gibi daha fazla seçenek yapabilirsiniz. yol ayırıcı olarak,

https://github.com/perfecto25/dictor


0

Bu cevabı çok az değiştirdim . Numaralı bir liste kullanıp kullanmadığımızı kontrol ettim. Şimdi onu her şekilde kullanabiliriz. deep_get(allTemp, [0], {})ya deep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)vs

def deep_get(_dict, keys, default=None):
    def _reducer(d, key):
        if isinstance(d, dict):
            return d.get(key, default)
        if isinstance(d, list):
            return d[key] if len(d) > 0 else default
        return default
    return reduce(_reducer, keys, _dict)

0

Zaten çok sayıda iyi cevap var ama ben de indeks ile listelere ulaşmayı destekleyen JavaScript karada lodash get get adlı bir işlev ile geldim :

def get(value, keys, default_value = None):
'''
    Useful for reaching into nested JSON like data
    Inspired by JavaScript lodash get and Clojure get-in etc.
'''
  if value is None or keys is None:
      return None
  path = keys.split('.') if isinstance(keys, str) else keys
  result = value
  def valid_index(key):
      return re.match('^([1-9][0-9]*|[0-9])$', key) and int(key) >= 0
  def is_dict_like(v):
      return hasattr(v, '__getitem__') and hasattr(v, '__contains__')
  for key in path:
      if isinstance(result, list) and valid_index(key) and int(key) < len(result):
          result = result[int(key)] if int(key) < len(result) else None
      elif is_dict_like(result) and key in result:
          result = result[key]
      else:
          result = default_value
          break
  return result

def test_get():
  assert get(None, ['foo']) == None
  assert get({'foo': 1}, None) == None
  assert get(None, None) == None
  assert get({'foo': 1}, []) == {'foo': 1}
  assert get({'foo': 1}, ['foo']) == 1
  assert get({'foo': 1}, ['bar']) == None
  assert get({'foo': 1}, ['bar'], 'the default') == 'the default'
  assert get({'foo': {'bar': 'hello'}}, ['foo', 'bar']) == 'hello'
  assert get({'foo': {'bar': 'hello'}}, 'foo.bar') == 'hello'
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.0.bar') == 'hello'
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.1') == None
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.1.bar') == None
  assert get(['foo', 'bar'], '1') == 'bar'
  assert get(['foo', 'bar'], '2') == None
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.