Как я могу создать набор, содержащий как числа int, так и числа с плавающей запятой?

#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}
 
  1. Почему это происходит?
  2. Как я могу добавить оба числа в свой набор?

Я не хочу 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).