Какие объекты гарантированно имеют другую идентичность?

ИСХОДНЫЙ ВОПРОС:

(Мой вопрос относится к Python 3.2+, но я сомневаюсь, что это изменилось с Python 2.7.)

Предположим, я использую выражение, которое мы обычно ожидаем для создания объекта. Примеры: [1,2,3]; 42; 'abc'; range(10); True; open('readme.txt'); MyClass(); lambda x : 2 * x; и т.п.

Предположим, что два таких выражения выполняются в разное время и "оцениваются до одного и того же значения" (т. е. имеют один и тот же тип и сравниваются как равные). При каких условиях Python предоставляет то, что я называю гарантией отдельного объекта, что два выражения на самом деле создают два разных объекта (т. находятся в области одновременно)?

Я понимаю, что для объектов любого изменяемого типа действует «гарантия отдельного объекта»:

x = [1,2]
y = [1,2]
assert x is not y # guaranteed to pass 

Я также знаю, что для некоторых неизменяемых типов (str, int) гарантия не действует; а для некоторых других неизменяемых типов (bool, NoneType) выполняется противоположная гарантия:

x = True
y = not not x
assert x is not y # guaranteed to fail
x = 2
y = 3 - 1
assert x is not y # implementation-dependent; likely to fail in CPython
x = 1234567890
y = x + 1 - 1
assert x is not y # implementation-dependent; likely to pass in CPython

Но как насчет всех остальных неизменяемых типов?

В частности, могут ли два кортежа, созданные в разное время, иметь одинаковую идентичность?

Причина, по которой меня это интересует, заключается в том, что я представляю узлы в своем графе как кортежи int, а модель предметной области такова, что любые два узла различны (даже если они представлены кортежами с одинаковыми значениями). Мне нужно создать наборы узлов. Если Python гарантирует, что кортежи, созданные в разное время, являются разными объектами, я мог бы просто создать подкласс tuple, чтобы переопределить равенство для обозначения идентичности:

class DistinctTuple(tuple):
  __hash__ = tuple.__hash__
  def __eq__(self, other):
    return self is other

x = (1,2)
y = (1,2)
s = set(x,y)
assert len(s) == 1 # pass; but not what I want
x = DistinctTuple(x)
y = DistinctTuple(y)
s = set(x,y)
assert len(s) == 2 # pass; as desired

Но если кортежи, созданные в разное время, не гарантированно отличаются друг от друга, то приведенный выше метод — ужасная техника, которая скрывает дремлющую ошибку, которая может появиться случайным образом и которую может быть очень трудно воспроизвести и найти. В этом случае создание подклассов не поможет; На самом деле мне нужно будет добавить к каждому кортежу в качестве дополнительного элемента уникальный идентификатор. Кроме того, я могу преобразовать свои кортежи в списки. В любом случае, я бы использовал больше памяти. Очевидно, я бы предпочел не использовать эти альтернативы, если только мое исходное решение для создания подклассов небезопасно.

Я предполагаю, что Python не предлагает «гарантию отдельного объекта» для неизменяемых типов, встроенных или определяемых пользователем. Но я не нашел четкого утверждения об этом в документации.

ОБНОВЛЕНИЕ 1:

@LuperRouch @larsmans Спасибо за обсуждение и ответ. Вот последний вопрос, который мне все еще неясен:

Есть ли шанс, что создание объекта пользовательского типа приведет к повторному использованию существующего объекта?

Если это возможно, я хотел бы знать, как я могу проверить для любого класса, с которым я работаю, может ли он демонстрировать такое поведение.

Вот мое понимание. Каждый раз, когда создается объект определяемого пользователем класса, сначала вызывается метод класса __new__(). Если этот метод переопределить, ничто в языке не помешает программисту вернуть ссылку на существующий объект, тем самым нарушив мою "гарантию отдельного объекта". Очевидно, я могу наблюдать это, изучая определение класса.

Я не уверен, что произойдет, если пользовательский класс не переопределит __new__() (или явно полагается на __new__() из базового класса). если я напишу

class MyInt(int):
  pass

созданием объекта занимается int.__new__(). Я ожидаю, что это означает, что иногда я могу увидеть, что следующее утверждение не работает:

x = MyInt(1)
y = MyInt(1)
assert x is not y # may fail, since int.__new__() might return the same object twice?

Но в своих экспериментах с CPython я не смог добиться такого поведения. Означает ли это, что язык предоставляет «гарантию отдельного объекта» для определяемых пользователем классов, которые не переопределяют __new__, или это просто произвольное поведение реализации?

ОБНОВЛЕНИЕ 2:

Хотя мой DistinctTuple оказался совершенно безопасной реализацией, теперь я понимаю, что моя дизайнерская идея использовать DistinctTuple для моделирования узлов очень плоха.

Оператор идентификации уже доступен в языке; заставлять == вести себя так же, как is, логически излишне.

Хуже того, если == можно было сделать что-то полезное, я сделал его недоступным. Например, вполне вероятно, что где-то в моей программе я захочу посмотреть, представлены ли два узла одной и той же парой целых чисел; == идеально подошёл бы для этого - и на самом деле это то, что он делает по умолчанию...

Что еще хуже, большинство людей на самом деле ожидают, что == будет сравнивать некоторое "значение", а не идентичность - даже для определяемого пользователем класса. Они были бы застигнуты врасплох моим переопределением, которое смотрит только на личность.

Наконец... единственная причина, по которой мне пришлось переопределить ==, заключалась в том, чтобы позволить нескольким узлам с одним и тем же представлением кортежа быть частью набора. Это неправильный подход! Необходимо изменить не == поведение, а тип контейнера! Мне просто нужно было использовать мультимножества вместо наборов.

Короче говоря, хотя мой вопрос может иметь некоторую ценность для других ситуаций, я абсолютно убежден, что создание class DistinctTuple — ужасная идея для моего варианта использования (и я сильно подозреваю, что у него вообще нет подходящего варианта использования).


person max    schedule 17.04.2012    source источник
comment
Вы не можете создавать пользовательские неизменяемые типы AFAIK. В приведенном выше примере два объекта DistinctTuple всегда будут иметь разные идентификаторы.   -  person Luper Rouch    schedule 17.04.2012
comment
@LuperRouch: вы можете и даже можете получить такие гарантии, которые дают None и bool, перегрузив __new__, но неизменность объектов не обязательно обеспечивается реализацией.   -  person Fred Foo    schedule 17.04.2012
comment
@larsmans: конечно, вы можете вернуть кортеж в свой __new__, но тогда вы не создаете пользовательские объекты.   -  person Luper Rouch    schedule 17.04.2012
comment
@LuperRouch: вы можете создать пул объектов MyTuple и вернуть объекты из него в MyTuple__new__.   -  person Fred Foo    schedule 17.04.2012
comment
@larsmans: я не думаю, что это заставляет Python считать эти объекты неизменяемыми, как, например, str и tuple (ничто не гарантирует, что пул объектов не поддается изменению).   -  person Luper Rouch    schedule 17.04.2012
comment
И в любом случае max хочет разные объекты, создание подкласса кортежа без замысловатых трюков в __new__ гарантирует наличие нового идентификатора для каждого экземпляра (поскольку пользовательские типы изменяемы).   -  person Luper Rouch    schedule 17.04.2012
comment
@LuperRouch: я думаю, у нас разные представления о неизменности. В моем понимании неизменности это контракт между разработчиком и пользователем, а не что-то, навязанное реализацией языка; это также определение, которое, по-видимому, использует collections.abc. Тем не менее, может быть способ реализовать действительно неизменяемые классы, написав модули расширения на C или Cython.   -  person Fred Foo    schedule 17.04.2012
comment
@larsman: Что касается Python, неизменяемость, когда она существует, является принудительной благодаря языку — таким образом, str, tuple и int являются неизменяемыми, в то время как определяемые пользователем типы (реализованные в Python) нет. Иными словами, нет никакого способа видоизменить объект str -- по крайней мере, без использования кода Python.   -  person Ethan Furman    schedule 18.04.2012


Ответы (3)


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

Это произойдет тогда и только тогда, когда определяемый пользователем тип явно предназначен для этого. С __new__() или каким-то метаклассом.

Я хотел бы знать, как я могу проверить для любого класса, с которым я работаю, может ли он демонстрировать такое поведение.

Используй источник, Люк.

Когда дело доходит до int, небольшие целые числа предварительно выделяются, и эти предварительно распределенные целые числа используются везде, где вы создаете вычисления с целыми числами. Вы не можете заставить это работать, когда делаете MyInt(1) is MyInt(1), потому что то, что у вас есть, не является целым числом. Однако:

>>> MyInt(1) + MyInt(1) is 2
True

Это потому, что, конечно, MyInt(1) + MyInt(1) не возвращает MyInt. Он возвращает int, потому что это то, что возвращает __add__ целого числа (и именно здесь также происходит проверка предварительно выделенных целых чисел). Это во всяком случае показывает, что подклассы int в целом не особенно полезны. :-)

Означает ли это, что язык предоставляет "гарантию отдельного объекта" для определяемых пользователем классов, которые не переопределяют new, или это просто произвольное поведение реализации?

Это не гарантирует этого, потому что в этом нет необходимости. По умолчанию создается новый объект. Вы должны переопределить это, если вы не хотите, чтобы это произошло. Гарантия смысла нет.

person Lennart Regebro    schedule 18.04.2012
comment
Спасибо. Вы говорите, что гарантия не нужна; но так ли очевидно, что MyInt(1) is not MyInt(1) по умолчанию? В конце концов, MyInt использует int.__new__() для выделения памяти. Насколько мне известно, int.__new__() может всегда возвращать ссылку на память, содержащую 1, независимо от того, создаю ли я int или MyInt. - person max; 18.04.2012
comment
Нет, это не очевидно. Вам просто не нужно заботиться об этом. Как уже отмечалось, у вас нет причин создавать подкласс int, это не имеет никакого смысла. И он не возвращает ссылки на память, содержащую 1, он возвращает ссылки на объекты. Если он ведет себя так, как вы подразумеваете, тогда MyInt (1) не будет создавать MyInt, он вернет int. Чего явно нет. - person Lennart Regebro; 18.04.2012
comment
Пользовательские типы @max изменяемы (попробуйте сами: a = MyInt(); a.foo = bar; вы не можете сделать это с обычным int). Это само по себе (изменчивость) доказывает, что вы получаете новый объект каждый раз, когда вы вызываете MyInt(), иначе язык был бы полным беспорядком. - person Luper Rouch; 18.04.2012
comment
@LennartRegebro: Спасибо. Таким образом, единственный способ, которым MyInt(1) is MyInt(1) удержался бы, это если бы int.__new__ сошла с ума и кэшировала не только int объекты, но, при некоторых условиях, и объекты своих подклассов. Этого явно не происходит, но, строго говоря, язык и не обещает, что этого не будет, верно? - person max; 18.04.2012
comment
Да, если он кэшировал объекты своих подклассов или если он просто не возвращал объекты MyInt() в приведенном выше случае (что, конечно же, делает). - person Lennart Regebro; 18.04.2012

Справочник по Python, раздел 3, Модель данных:

для неизменяемых типов операции, которые вычисляют новые значения, могут фактически возвращать ссылку на любой существующий объект с тем же типом и значением, в то время как для изменяемых объектов это недопустимо.

(Выделение добавлено.)

На практике кажется, что CPython кэширует только пустой кортеж:

>>> 1 is 1
True
>>> (1,) is (1,)
False
>>> () is ()
True
person Fred Foo    schedule 17.04.2012
comment
Будет ли определяемый пользователем класс, производный от tuple, считаться неизменным для целей этой гарантии? Есть ли дешевая модификация, которую я могу сделать, чтобы сделать его изменчивым, то есть получить гарантию отдельного объекта? - person max; 17.04.2012
comment
@max: я думаю, что изменяемый подтип tuple нарушит LSP. Вместо этого вы действительно должны подклассировать list. - person Fred Foo; 17.04.2012
comment
Ну, я не планирую когда-либо изменять его. Я просто хочу получить гарантию, что каждый раз, когда он создается, это будет другой объект. На самом деле я не уверен, является ли class MyTuple(tuple): pass изменчивым или нет для целей этой гарантии. - person max; 17.04.2012
comment
@max: похоже, что значение по умолчанию __new__ для MyTuple создает новые объекты, даже если оно происходит от неизменяемого класса, хотя я не смог найти гарантии этого в документах для __new__. - person Fred Foo; 17.04.2012
comment
@max: вы можете устанавливать атрибуты для своих объектов (a = MyTuple(); a.foo = 1), поэтому объекты MyTuple изменяемы, и каждый экземпляр имеет свой идентификатор. - person Luper Rouch; 17.04.2012
comment
@max: вы можете перегрузить __new__, чтобы получить необходимую вам гарантию, но, честно говоря, я бы поискал другой способ решить проблему, которую вы пытаетесь решить. - person Fred Foo; 17.04.2012
comment
@LuperRouch Да; но если я это сделаю, я могу отказаться от проверок личности и просто добавить уникальный идентификатор в качестве атрибута. - person max; 17.04.2012
comment
@larsmans Перегрузка __new__ для обеспечения различных кортежей была бы неэффективной, поскольку я не могу использовать tuple.__new__ (который может возвращать один и тот же объект дважды). Я надеялся, что мой класс DistinctTuple гарантированно создаст отдельные объекты, но если это не так, я действительно буду искать другое решение. - person max; 17.04.2012
comment
@larsmans Теперь я согласен с тем, что рекомендуется другой способ решения проблемы. Смотрите мое обновление 2 к вопросу. - person max; 18.04.2012

Если Python гарантирует, что кортежи, созданные в разное время, являются разными объектами, я мог бы просто создать подкласс tuple, чтобы переопределить равенство для обозначения идентичности.

Вы, кажется, не понимаете, как работает подкласс: если B является подклассом A, то B получает возможность использовать все методы A[1], но методы A будут работать с экземплярами B, а не A. Это верно даже для __new__:

--> class Node(tuple):
...   def __new__(cls):
...     obj = tuple.__new__(cls)
...     print(type(obj))
...     return obj
...
--> n = Node()
<class '__main__.Node'>

Как указал @larsman в справочнике по Python :

для неизменяемых типов операции, которые вычисляют новые значения, могут фактически возвращать ссылку на любой существующий объект с тем же типом и значением, в то время как для изменяемых объектов это не разрешено.

Однако имейте в виду, что в этом отрывке речь идет о встроенных типах Python, а не о типах, определяемых пользователем (которые могут сойти с ума по любому поводу).


Я понимаю приведенный выше отрывок, чтобы гарантировать, что Python не вернет новый изменяемый объект, который совпадает с существующим объектом, а классы, определяемые пользователем и созданные в коде Python, по своей сути изменяемы (опять же, см. примечание выше о сумасшедшем пользователе). определенные классы).

Более полный класс Node (обратите внимание, что вам не нужно явно ссылаться на tuple.__hash__):

class Node(tuple):
    __slots__ = tuple()
    __hash__ = tuple.__hash__
    def __eq__(self, other):
        return self is other
    def __ne__(self, other):
        return self is not other

--> n1 = Node()
--> n2 = Node()
--> n1 is n2
False
--> n1 == n2
False
--> n1 != n2
True

--> n1 <= n2
True
--> n1 < n2
False

Как видно из двух последних сравнений, вы можете также переопределить методы __le__ и __ge__.

[1] Единственное исключение, о котором я знаю, это __hash__ - если __eq__ определено в подклассе, но подкласс хочет, чтобы родительский класс "__hash__" он должен был явно указать это (это изменение Python 3).

person Ethan Furman    schedule 18.04.2012