#python #python-3.x #set
Вопрос:
Я хочу создать набор, содержащий как целые числа, так и числа с плавающей точкой. Что-то вроде этого:
s = {4, 6.7, 2.12, 9}
Однако я столкнулся с чем-то неожиданным (по крайней мере, для меня): я не могу добавить и 9
(целое число), и 9.0
(с плавающей точкой). Вот пример:
>>> s = {4, 6.7, 2.12, 9}
>>> s
{9, 2.12, 4, 6.7}
>>> s.add(9.0)
>>> s
{9, 2.12, 4, 6.7}
- Почему это происходит?
- Как я могу добавить оба числа в свой набор?
Я не хочу 9.0 in s
оставаться верным, если я не добавил число с плавающей точкой в свой набор. Я действительно не могу понять, как это сделать.
В качестве примечания я заметил, что это верно и для ключей словаря. Поэтому я не могу сопоставить 3
и 3.0
разные значения.
Комментарии:
1. Ты не можешь. Python рассматривает их как эквивалент:
print(9 == 9.0)
2. Почему бы не сохранить их в виде строк? это должно технически позволить вам хранить и
9
то, и другое, и9.0
в этом случае3. @RiccardoBucco … нам нужно будет понять, зачем вам нужна эта функциональность … но создание нового типа класса «набор», возможно, является одной из возможностей … скорее всего, есть лучший способ справиться с этим
4. Вы могли бы составить набор кортежей
(x, type(x).__name__)
5. Вместо того, чтобы хранить их в виде строк, храните их в виде кортежей: (значение, класс). Например,
s.add((9.0, type(9.0)))
Ответ №1:
В Python 9.0 == 9
равенство и является основой уникальности множеств.
Если вы хотите, чтобы числа float
и int
числа не сталкивались в вашем set
, у вас на самом деле есть два свойства для сравнения: значение и тип.
Самое простое решение-просто сохранить их оба в наборе, используя a tuple
. Например
s = set()
s.add( (float, 1.0) )
s.add( (float, 5.0) )
s.add( (int, 5) )
s.add( (float, 5.0) )
Просто, но немного неудобно, и зависит от вас, чтобы установить правильный тип: ничто не мешает вам добавлять float
значение, используя int
значение. В качестве альтернативы вы можете реализовать set
подкласс, который обрабатывает магию за вас
class TypedSet(set):
def add(self, v):
vtype = type(v)
super().add( (vtype, v) )
def remove(self, v):
vtype = type(v)
super().remove( (vtype, v) )
s = TypedSet()
s.add(1.0)
s.add(5.0)
s.add(5)
s.add(5.0)
Примечание: для реальной реализации этого потребуется гораздо больше работы.
Вышеизложенное приведет к следующему набору (обратите внимание, что дубликат 5.0 ведет себя так, как ожидалось).
TypedSet({(<class 'float'>, 1.0), (<class 'int'>, 5), (<class 'float'>, 5.0)})
Так что это возможно, но на вашем месте я бы остановился и проверил, действительно ли вы вообще хотите это делать. Если у вас есть две вещи, которые вы хотите отслеживать, может быть, вам нужны два набора?
Вы всегда можете объединить наборы в кортеж, когда вам нужно повторить/поработать с ними, например
my_floats = set()
my_ints = set()
my_floats.add(1.0)
my_floats.add(5.0)
my_ints.add(5)
my_floats.add(5.0)
combined = (*my_floats, *my_ints) # combine to a tuple
Даст вам следующее (без всякой магии)…
(1.0, 5.0, 5)
Комментарии:
1.
TypedSet
для реализации потребуется гораздо больше материала, чтобы работать без неожиданностей (например, метод обновления и переопределение всех магических методов, необходимых для операторов набора).2. Да, я думаю, что это сомнительная идея, поэтому я не хотел идти дальше с ней-добавил примечание.
Ответ №2:
set
по сути, это хэш-таблица. Чтобы справиться с конфликтами, он проверяет, равны ли два объекта с одинаковыми хэшами, и если да, то он просто предполагает, что тот, который у него уже есть, правильный. Или что — то в этом роде.
Проблема в том, что floats
целые числа имеют тот же хэш-код, что и целые числа. Кстати, хэш любого достаточно малого целого числа в python равен этому целому числу.
Вместо повторной реализации или создания подклассов set
проще повторно реализовать float
класс , чтобы просто сделать его хэш отличным от целого числа:
class hfloat(float):
def __hash__(self):
if self == int(self):
return hash((float, float.__hash__(self)))
else:
return float.__hash__(self)
s = {4, 6.7, 2.12, 9}
s.add(hfloat(9.0))
print(s)
# {2.12, 4, 6.7, 9, 9.0}
отказ от ответственности: a hfloat
и a float
с одинаковым целочисленным значением теперь не будут иметь одинаковый хэш, так что это также может произойти:
s = {9.0, hfloat(9.0)}
print(s)
# {9.0, 9.0}
Поступайте так, как предлагают комментарии, и хранение (value, type)
2-кортежей в хэш-таблице может быть лучше для вашего варианта использования.
Комментарии:
1. Насколько это безопасно?
2. Спасибо, это определенно работает!! 😀
3. @RiccardoBucco, пока вы не наткнетесь на номер, где его нет 😛
4. @don’t talkjustcode что ты имеешь в виду? О каких цифрах вы говорите?
5. Я проголосовал против. Любая арифметика между экземпляром hfloat и другим поплавком просто вернет поплавок. Такой подход потребовал бы, чтобы код, заполняющий набор, был очень осторожен, чтобы создавать только эти волшебные экземпляры hfloat. Это уродливый и хрупкий способ обойти это, скорее всего, вызовет больше проблем, чем решит. Вместо этого поддерживать один набор целых чисел и один набор поплавков было бы проще, проще и быстрее (поиск, возможно, можно было бы объединить с помощью экземпляра ChainMap).