Словник без регістру


78

Я хотів би, щоб мій словник не враховував регістр.

У мене є такий приклад коду:

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()]але я не впевнений, як найкраще інтегрувати це в мій існуючий код. (Якщо це все одно буде розумним, простим підходом).


4
Див. PEP-455 : це заплановано для стандартного включення бібліотеки в Python 3.5 (як collections.TransformDict, за умови, що перетворення є str.casefoldподібним)
Nick T

6
@NickT Цей PEP відхилено. python.org/dev/peps/pep-0455/#rejection
user1556435

Відповіді:


43

Якщо я вас правильно розумію, і ви хочете знайти ключові словники без урахування регістру, одним із способів буде підклас 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())

1
Чи не існує спеціального вбудованого модуля, який також вимагається "ввести"?
Всезнай

26
Ось повний перелік методів, які можуть потребувати перевантаження: setitem , getitem , містить , get, has_key, поп, setdefault та оновлення. init та fromkeys також слід перевантажити, щоб переконатися, що словник правильно ініціалізований. Можливо, я помиляюся, і десь Python обіцяє, що get, hash_key, pop, setdefault, update і init буде реалізовано з точки зору getitem , setitem і містить, якщо вони були перевантажені, але я не думаю.
Всезначний

4
додано __contains__, get, and has_keyдо відповіді, оскільки в підсумку я їх кодував :)
Майкл Мерчант

7
Це рішення дуже обмежене, оскільки воно не працює для багатьох поширених застосувань dict. Не використовуйте його у своєму коді - це зламає всі, крім найпростіших застосувань. Очевидно @MichaelMerchant намагався додати відсутні речі, але модерація спростувала зміни (те ж саме сталося і зі мною). Я додав новий відповідь , який повинен бути використовувати як крапля в dictзаміні тут .
m000

2
Краще від підкласифікації, UserDictніж dict docs.python.org/3.5/library/collections.html#userdict-objects
rite2hhh

68

Затверджено в даний час відповідь не працює для багатьох випадків, тому він не може бути використаний як крапля в 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)

3
Це чудово, але є одна незначна проблема. Супервизначення updateє update(self, E=None, **F), значення не Eє обов’язковим. Ви перевизначили його для створенняE необхідним. Додайте, =Noneі це буде ідеально.
Нік Вільямс,

18
Вони говорили, що Python - це просто. Вони говорили, що Python - це весело.
rr-

2
@ rr-. Щоб бути цілком справедливим, уявіть, що ви робите це, скажімо C.
Божевільний фізик

1
Nitpick, але це не має належної підтримки для нормалізації Unicode.
Божевільний фізик

6
У python 3 basestringвилучено абстрактний тип . strможе використовуватися як заміна.
Ян Шац

56

Просто для запису. Я знайшов приголомшливе втілення запитів :

https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37


10
from requests.structures import CaseInsensitiveDict
JimB

6
Це може спрацювати, але якщо все, що вам потрібно, це просто нечутливий до розгляду справи, глупо додавати запити як залежність саме для цього.
Сантьягобасульто

3
@santiagobasulto - це "безглуздо", поки співвідношення (потрібно до роботи / час до закінчення терміну) не переходить до нескінченності
qneill

15

У моєму конкретному випадку мені знадобився пошук без урахування регістру, однак я не хотів змінювати початковий регістр ключа. Наприклад:

>>> 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 як ключ під час використання словника.


Приємно, дякую! :) (Зверніть увагу, що цей клас не реалізує конструктор "dict (iterable)", що не враховує регістр, тому, якщо він вам потрібен, його потрібно додати)
Йоріл

2
Ви повинні використовувати .casefold()замість .lower()для порівняння,, self.key.casefold() == other.key.casefold()щоб дозволити "ß"і "ss"прирівняти як істинні, серед інших.
AJNeufeld

10

Чи не могли б ви використати string.lower()введені дані та використовувати повністю малий словник? Це трохи хакерське рішення, але воно працює


Це трохи хакі, але я думаю, що це відповідає тому, що Кім намагався.
John Y

Це не хакі. Насправді це менш схильний до помилок спосіб, ніж перевизначення класу словника.
Saher Ahwal

4
Це чудово, якщо ви не хочете зберегти початковий випадок, коли вперше встановлюєте ключ.
Даніель Ретлісбергер

4

Я змінив просте, але гарне рішення від 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()])

1
Ви повинні використовувати .casefold()замість цього .lower()для порівняння self.casefold() == other.key.casefold()та hash(self.casefold()), щоб дозволити "ß" та "ss" ототожнювати як істинні, серед інших.
AJNeufeld

3

Хоча словник, що не враховує регістр, є рішенням, і є відповіді на те, як цього досягти, у цьому випадку існує, можливо, простіший спосіб. Достатньо пошуку без урахування регістру:

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

3
Набагато краще використовувати вбудовані рядкові методи, ніж модуль регулярних виразів, коли вбудовані модулі можуть легко з цим справлятися, що вони можуть у цьому випадку.
John Y

дякую спокій. Зараз у мене недостатньо часу, тому ваше швидке та просте рішення мене влаштовує. спасибі
Кім

@John Y: Яким було б вирішення цього питання без регулярних виразів? Я цього не бачу.
Якоб Борг

Кім про це вже згадував: використовуйте метод string.lower (). Інші відповіді також згадували про це. Коментарі не підходять для розміщення коду, тому, мабуть, я опублікую власну відповідь.
John Y

+1 Це рішення найкраще для мене працювало, оскільки в моєму випадку важливий випадок ключового словаря, і простого зниження клавіші на наборі недостатньо.
pleasemorebacon

1

Ви можете виконати пошук, нечутливий до регістру, з використанням одного вкладиша:

>>> 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)


Зверніть увагу, що повертається лише перший збіг.


0

Якщо вам потрібно зробити це лише один раз у своєму коді (отже, немає вказівки на функцію), найпростіший спосіб вирішити проблему - це:

нижній регістр = {key.lower (): значення для (ключ, значення) у original_dict}

Тут я припускаю, що диктований, про який йде мова, не настільки великий - дублювати його може бути неелегантно, але якщо він не великий, це нічого не зашкодить.

Перевага цього у відповіді @ Fred (хоча це також працює) полягає в тому, що він дає той самий результат, що і дикт, коли ключа немає: KeyError.


-1

Я просто налаштував функцію для обробки цього:

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

Можливо, не ідеально, якщо ви хочете зробити це глобально, але добре працює, якщо це лише підмножина, для якої ви хочете це використовувати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.