Словарь без учета регистра
Я бы хотел, чтобы в моем словаре не учитывался регистр.
У меня есть этот пример кода:
text = "practice changing the color"
words = {'color': 'colour',
'practice': 'practise'}
def replace(words,text):
keys = words.keys()
for i in keys:
text= text.replace(i ,words[i])
return text
text = replace(words,text)
print text
Вывод = практика изменения цвета
Я хотел бы еще одну строку, "practice changing the Color"
, (где Color
начинается с заглавной буквы), чтобы также дать тот же результат.
Я считаю, что есть общий способ преобразования в нижний регистр с помощьюmydictionary[key.lower()]
но я не уверен, как лучше интегрировать это в мой существующий код. (Если бы это был разумный, простой подход в любом случае).
13 ответов
Если я вас правильно понимаю, и вы хотите, чтобы ключевые словари использовались без учета регистра, одним из способов было бы создать подкласс dict и перегрузить установщик / получатель:
class CaseInsensitiveDict(dict):
def __setitem__(self, key, value):
super(CaseInsensitiveDict, self).__setitem__(key.lower(), value)
def __getitem__(self, key):
return super(CaseInsensitiveDict, self).__getitem__(key.lower())
Утвержденный в настоящее время ответ не работает во многих случаях, поэтому его нельзя использовать в качестве вставки dict
замена. Некоторые хитрые моменты в получении правильного dict
замена:
- перегрузка всех методов, включающих ключи
- правильная обработка нестроковых ключей
- правильно обрабатывать конструктор класса
Следующее должно работать намного лучше:
class CaseInsensitiveDict(dict):
@classmethod
def _k(cls, key):
return key.lower() if isinstance(key, basestring) else key
def __init__(self, *args, **kwargs):
super(CaseInsensitiveDict, self).__init__(*args, **kwargs)
self._convert_keys()
def __getitem__(self, key):
return super(CaseInsensitiveDict, self).__getitem__(self.__class__._k(key))
def __setitem__(self, key, value):
super(CaseInsensitiveDict, self).__setitem__(self.__class__._k(key), value)
def __delitem__(self, key):
return super(CaseInsensitiveDict, self).__delitem__(self.__class__._k(key))
def __contains__(self, key):
return super(CaseInsensitiveDict, self).__contains__(self.__class__._k(key))
def has_key(self, key):
return super(CaseInsensitiveDict, self).has_key(self.__class__._k(key))
def pop(self, key, *args, **kwargs):
return super(CaseInsensitiveDict, self).pop(self.__class__._k(key), *args, **kwargs)
def get(self, key, *args, **kwargs):
return super(CaseInsensitiveDict, self).get(self.__class__._k(key), *args, **kwargs)
def setdefault(self, key, *args, **kwargs):
return super(CaseInsensitiveDict, self).setdefault(self.__class__._k(key), *args, **kwargs)
def update(self, E={}, **F):
super(CaseInsensitiveDict, self).update(self.__class__(E))
super(CaseInsensitiveDict, self).update(self.__class__(**F))
def _convert_keys(self):
for k in list(self.keys()):
v = super(CaseInsensitiveDict, self).pop(k)
self.__setitem__(k, v)
Только для записи. Я нашел потрясающую имплементацию на запросы:
https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py
В моем конкретном случае мне нужен был поиск без учета регистра, однако я не хотел изменять исходный регистр ключа. Например:
>>> d = {}
>>> d['MyConfig'] = 'value'
>>> d['myconfig'] = 'new_value'
>>> d
{'MyConfig': 'new_value'}
Вы можете видеть, что словарь все еще имеет оригинальный ключ, однако он доступен без учета регистра. Вот простое решение:
class CaseInsensitiveKey(object):
def __init__(self, key):
self.key = key
def __hash__(self):
return hash(self.key.lower())
def __eq__(self, other):
return self.key.lower() == other.key.lower()
def __str__(self):
return self.key
Переопределения __hash__ и __eq__ необходимы как для получения, так и для установки записей в словаре. Это создает ключи, которые хэшируют одну и ту же позицию в словаре, если они равны без учета регистра.
Теперь либо создайте пользовательский словарь, который инициализирует CaseInsensitiveKey, используя предоставленный ключ:
class CaseInsensitiveDict(dict):
def __setitem__(self, key, value):
key = CaseInsensitiveKey(key)
super(CaseInsensitiveDict, self).__setitem__(key, value)
def __getitem__(self, key):
key = CaseInsensitiveKey(key)
return super(CaseInsensitiveDict, self).__getitem__(key)
или просто убедитесь, что всегда используете экземпляр CaseInsensitiveKey в качестве ключа при использовании словаря.
Рассматриваете ли вы использовать string.lower()
на ваших входах и используя полностью строчный словарь? Это немного хакерское решение, но оно работает
Я изменил простое, но хорошее решение, с помощью Pleasemorebacon (спасибо!), Сделав его немного более компактным, автономным и с небольшими обновлениями, чтобы позволить конструкцию из {'a':1, 'B':2}
и поддержка __contains__
протокол. Наконец, так как CaseInsensitiveDict.Key
как ожидается, будет строкой (что еще может быть чувствительным к регистру или нет), это хорошая идея для получения Key
класс из str
, то можно, например, сбросить CaseInsensitiveDict
с json.dumps
из коробки.
# caseinsensitivedict.py
class CaseInsensitiveDict(dict):
class Key(str):
def __init__(self, key):
str.__init__(key)
def __hash__(self):
return hash(self.lower())
def __eq__(self, other):
return self.lower() == other.lower()
def __init__(self, data=None):
super(CaseInsensitiveDict, self).__init__()
if data is None:
data = {}
for key, val in data.items():
self[key] = val
def __contains__(self, key):
key = self.Key(key)
return super(CaseInsensitiveDict, self).__contains__(key)
def __setitem__(self, key, value):
key = self.Key(key)
super(CaseInsensitiveDict, self).__setitem__(key, value)
def __getitem__(self, key):
key = self.Key(key)
return super(CaseInsensitiveDict, self).__getitem__(key)
Вот основной тестовый скрипт для тех, кто любит проверять вещи в действии:
# test_CaseInsensitiveDict.py
import json
import unittest
from caseinsensitivedict import *
class Key(unittest.TestCase):
def setUp(self):
self.Key = CaseInsensitiveDict.Key
self.lower = self.Key('a')
self.upper = self.Key('A')
def test_eq(self):
self.assertEqual(self.lower, self.upper)
def test_hash(self):
self.assertEqual(hash(self.lower), hash(self.upper))
def test_str(self):
self.assertEqual(str(self.lower), 'a')
self.assertEqual(str(self.upper), 'A')
class Dict(unittest.TestCase):
def setUp(self):
self.Dict = CaseInsensitiveDict
self.d1 = self.Dict()
self.d2 = self.Dict()
self.d1['a'] = 1
self.d1['B'] = 2
self.d2['A'] = 1
self.d2['b'] = 2
def test_contains(self):
self.assertIn('B', self.d1)
d = self.Dict({'a':1, 'B':2})
self.assertIn('b', d)
def test_init(self):
d = self.Dict()
self.assertFalse(d)
d = self.Dict({'a':1, 'B':2})
self.assertTrue(d)
def test_items(self):
self.assertDictEqual(self.d1, self.d2)
self.assertEqual(
[v for v in self.d1.items()],
[v for v in self.d2.items()])
def test_json_dumps(self):
s = json.dumps(self.d1)
self.assertIn('a', s)
self.assertIn('B', s)
def test_keys(self):
self.assertEqual(self.d1.keys(), self.d2.keys())
def test_values(self):
self.assertEqual(
[v for v in self.d1.values()],
[v for v in self.d2.values()])
Вы можете выполнить поиск без учета регистра в одном ключе:
>>> input_dict = {'aBc':1, 'xyZ':2}
>>> search_string = 'ABC'
>>> next((value for key, value in input_dict.items() if key.lower()==search_string.lower()), None)
1
>>> search_string = 'EFG'
>>> next((value for key, value in input_dict.items() if key.lower()==search_string.lower()), None)
>>>
Вы можете поместить это в функцию:
def get_case_insensitive_key_value(input_dict, key):
return next((value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()), None)
Обратите внимание, что возвращается только первое совпадение.
Хотя нечувствительный к регистру словарь является решением, и есть ответы на то, как этого добиться, в этом случае, возможно, есть более простой способ. Достаточно нечувствительного к регистру поиска:
import re
text = "Practice changing the Color"
words = {'color': 'colour', 'practice': 'practise'}
def replace(words,text):
keys = words.keys()
for i in keys:
exp = re.compile(i, re.I)
text = re.sub(exp, words[i], text)
return text
text = replace(words,text)
print text
Если вам нужно сделать это в коде только один раз (следовательно, нет никакого смысла в функции), наиболее простой способ решения проблемы следующий:
lowercase_dict = {key.lower(): значение для (ключа, значения) в исходном_dict}
Я предполагаю, что рассматриваемый dict не такой уж и большой - дублировать его может быть неэлегантно, но если он не большой, это не повредит никому.
Преимущество этого ответа по сравнению с ответом @Fred (хотя он также работает) заключается в том, что он дает тот же результат, что и dict, когда ключ отсутствует: KeyError.
Кредит: на основе ответа @m000. Следующий вариант предоставляет метод get_orig_key, отслеживая чувствительный к регистру ключ последней операции «set».
class RobbieCaseInsensitiveDict(dict):
@classmethod
def _k(cls, key):
return key.lower() if isinstance(key, str) else key
def __init__(self, *args, **kwargs):
super(RobbieCaseInsensitiveDict, self).__init__(*args, **kwargs)
self.key_dict = {}
for key in self.keys():
if isinstance(key, str):
self.key_dict[key.lower()] = key
self._convert_keys()
def get_orig_key(self, case_ins_key):
if case_ins_key in self.key_dict:
return self.key_dict[case_ins_key]
else:
return case_ins_key
def __getitem__(self, key):
return super(RobbieCaseInsensitiveDict, self).__getitem__(self.__class__._k(key))
def __setitem__(self, key, value):
if isinstance(key, str):
self.key_dict[key.lower()] = key
super(RobbieCaseInsensitiveDict, self).__setitem__(self.__class__._k(key), value)
def __delitem__(self, key):
return super(RobbieCaseInsensitiveDict, self).__delitem__(self.__class__._k(key))
def __contains__(self, key):
return super(RobbieCaseInsensitiveDict, self).__contains__(self.__class__._k(key))
def has_key(self, key):
return super(RobbieCaseInsensitiveDict, self).has_key(self.__class__._k(key))
def pop(self, key, *args, **kwargs):
return super(RobbieCaseInsensitiveDict, self).pop(self.__class__._k(key), *args, **kwargs)
def get(self, key, *args, **kwargs):
return super(RobbieCaseInsensitiveDict, self).get(self.__class__._k(key), *args, **kwargs)
def setdefault(self, key, *args, **kwargs):
if isintance(key, str):
self.key_dict[key.lower()] = key
return super(RobbieCaseInsensitiveDict, self).setdefault(self.__class__._k(key), *args, **kwargs)
def update(self, E={}, **F):
super(RobbieCaseInsensitiveDict, self).update(self.__class__(E))
super(RobbieCaseInsensitiveDict, self).update(self.__class__(**F))
def _convert_keys(self):
for k in list(self.keys()):
v = super(RobbieCaseInsensitiveDict, self).pop(k)
self.__setitem__(k, v)
Или... если вы предпочитаете использовать готовый продукт, а не взламывать его самостоятельно... попробуйте... https://pypi.org/project/case-insensitivity-dictionary/
Существует несколько подходов к этой проблеме, каждый из которых имеет свои плюсы и минусы. Просто чтобы добавить к списку (похоже, эта опция не упоминалась), можно расширить
str
класс и использовать его в качестве ключа:
class CaseInsensitiveStr(str):
def __hash__(self) -> 'int':
return hash(self.lower())
def __eq__(self, other:'str') -> 'bool':
return self.lower() == other.lower()
Это может хорошо работать, если рассматриваемый словарь является частным и для доступа к нему используется какой-то интерфейс.
class MyThing:
def __init__(self):
self._d: 'dict[CaseInsensitiveStr, int]' = dict()
def set(self, key:'str', value:'int'):
self._d[CaseInsensitiveStr(key)] = value
def get(self, key:'str') -> 'int':
return self._d[CaseInsensitiveStr(key)]
Я просто настроил функцию для обработки этого:
def setLCdict(d, k, v):
k = k.lower()
d[k] = v
return d
myDict = {}
Так что вместо
myDict['A'] = 1
myDict['B'] = 2
Вы можете:
myDict = setLCdict(myDict, 'A', 1)
myDict = setLCdict(myDict, 'B', 2)
Затем вы можете либо ввести значение в нижнем регистре, прежде чем искать его, либо написать функцию для этого.
def lookupLCdict(d, k):
k = k.lower()
return d[k]
myVal = lookupLCdict(myDict, 'a')
Вероятно, не идеально, если вы хотите сделать это глобально, но хорошо работает, если это просто подмножество, для которого вы хотите его использовать.