En Python, quand deux objets sont-ils identiques?

, Il semble que 2 is 2 et 3 is 3 sera toujours vrai en python, et en général, toute référence à un entier est le même que toute autre référence à la même entier. La même chose arrive à None (c'est-à-dire None is None). Je sais que cela n'arrive pas aux types définis par l'utilisateur, ou aux types mutables. Mais il échoue parfois aussi sur les types immuables:

>>> () is ()
True
>>> (2,) is (2,)
False

C'est-à-dire: deux constructions indépendantes du tuple vide donnent des références au même objet en mémoire, mais deux constructions indépendantes des tuples identiques d'un élément(immuable) finissent par créer deux objets identiques. J'ai testé, et frozenset fonctionne d'une manière similaire aux tuples.

Qu'est-ce qui détermine si un objet sera dupliqué en mémoire ou aura une seule instance avec beaucoup de références? Cela dépend-il si l'objet est "atomique" dans un certain sens? Varie-t-il en fonction de la mise en œuvre?

35
demandé sur fonini 2016-04-27 22:14:23

2 réponses

Python a certains types qu'il garantit aura seulement un exemple. Des exemples de ces cas sont None, NotImplemented, et Ellipsis. Ce sont (par définition) des singletons et donc des choses comme None is None sont garanties pour retourner True car il n'y a aucun moyen de créer une nouvelle instance de NoneType.

, Il fournit également quelques doubletons 1True, False 2 -- Toutes les références à True pointent vers le même objet. Encore une fois, c'est parce qu'il n'y a aucun moyen de créer une nouvelle instance de bool.

Les choses ci-dessus sont toutes garanties par le langage python. Cependant, comme vous l'avez remarqué, il existe certains types (tous immuables) qui stockent certaines instances pour les réutiliser. Ceci est autorisé par le langage, mais différentes implémentations peuvent choisir d'utiliser cette allocation ou non-en fonction de leurs stratégies d'optimisation. Quelques exemples qui entrent dans cette catégorie sont les petits entiers (-5 - > 255), les tuple vides et frozenset vides.

Enfin, Disponible interns certains objets immuables pendant l'analyse...

E. g. si vous exécutez le script suivant avec Disponible, vous verrez qu'il retourne True:

def foo():
    return (2,)

if __name__ == '__main__':
    print foo() is foo()

Cela semble vraiment bizarre. L'astuce que joue Cpython est que chaque fois qu'il construit la fonction foo, Il voit un tuple-littéral qui contient d'autres littéraux simples (immuables). Plutôt que de créer ce tuple (ou ses équivalents) encore et encore, python le crée juste une fois. Il n'y a aucun danger que cet objet soit changé depuis l'accord de l'ensemble est immuable. Cela peut être une grande victoire pour la performance où la même boucle serrée est appelée encore et encore. De petites cordes sont également internées. La vraie victoire ici est dans les recherches de dictionnaire. Python peut faire une comparaison de pointeur (incroyablement rapide), puis se replier sur des comparaisons de chaînes plus lentes lors de la vérification des collisions de hachage. Comme une grande partie de python est construite sur des recherches de dictionnaire, cela peut être une grande optimisation pour le langage dans son ensemble.


1j' peut-être composé que de mots ... Mais j'espère que vous obtenez l'idée...
2dans des circonstances normales, vous n'avez pas besoin d' vérifier si l'objet est une référence à True -- Habituellement, vous n'soins si l'objet est "truthy" -- par exemple, si if some_instance: ... exécutera la branche. Mais, je l'ai mis ici juste pour l'exhaustivité.


Notez que is peut être utilisé pour comparer des choses qui ne sont pas des singletons. Une utilisation courante est de créer une sentinelle valeur:

sentinel = object()
item = next(iterable, sentinel)
if items is sentinel:
   # iterable exhausted.

Ou:

_sentinel = object()
def function(a, b, none_is_ok_value_here=_sentinel):
    if none_is_ok_value_here is sentinel:
        # Treat the function as if `none_is_ok_value_here` was not provided.

La morale de cette histoire est de toujours dire ce que tu veux dire., Si vous voulez vérifier si une valeur de est une autre valeur, puis utiliser le is opérateur. Si vous voulez vérifier si une valeur est égale à une autre valeur (mais éventuellement distincte), utilisez ==. Pour plus de détails sur la différence entre is et == (et à utiliser), consultez l'un des postes suivants:


Additif

Nous avons parlé de ces détails D'implémentation de CPython et nous avons affirmé qu'il s'agissait d'optimisations. Ce serait bien d'essayer de mesurer exactement ce que nous obtenons de toute cette optimisation (à part une petite confusion supplémentaire lorsque nous travaillons avec l'opérateur is).

String" interning " et recherches de dictionnaire.

Voici un petit script que vous pouvez exécuter pour voir à quel point les recherches de dictionnaire sont plus rapides si vous utilisez la même chaîne pour rechercher la valeur au lieu d'une chaîne différente. Notez que j'utilise le terme "interned" dans les noms de variables - ces valeurs ne sont pas nécessairement internées (bien qu'elles puissent l'être). Je l'utilise juste pour indiquer que la chaîne "internée" est la chaîne dans le dictionnaire.

import timeit

interned = 'foo'
not_interned = (interned + ' ').strip()

assert interned is not not_interned


d = {interned: 'bar'}

print('Timings for short strings')
number = 100000000
print(timeit.timeit(
    'd[interned]',
    setup='from __main__ import interned, d',
    number=number))
print(timeit.timeit(
    'd[not_interned]',
    setup='from __main__ import not_interned, d',
    number=number))


####################################################

interned_long = interned * 100
not_interned_long = (interned_long + ' ').strip()

d[interned_long] = 'baz'

assert interned_long is not not_interned_long
print('Timings for long strings')
print(timeit.timeit(
    'd[interned_long]',
    setup='from __main__ import interned_long, d',
    number=number))
print(timeit.timeit(
    'd[not_interned_long]',
    setup='from __main__ import not_interned_long, d',
    number=number))

Les valeurs exactes ici ne devraient pas avoir trop d'importance, mais sur mon ordinateur, les chaînes courtes montrent environ 1 partie dans 7 plus rapide. Les chaînes longues sont presque 2 fois plus rapides (car la comparaison de chaînes prend plus de temps si la chaîne a plus de caractères à comparer). Les différences ne sont pas aussi frappantes sur python3.x, mais ils sont toujours bel et bien là.

Tuple "stage"

Voici un petit script avec lequel vous pouvez jouer:

import timeit

def foo_tuple():
    return (2, 3, 4)

def foo_list():
    return [2, 3, 4]

assert foo_tuple() is foo_tuple()

number = 10000000
t_interned_tuple = timeit.timeit('foo_tuple()', setup='from __main__ import foo_tuple', number=number)
t_list = (timeit.timeit('foo_list()', setup='from __main__ import foo_list', number=number))

print(t_interned_tuple)
print(t_list)
print(t_interned_tuple / t_list)
print('*' * 80)


def tuple_creation(x):
    return (x,)

def list_creation(x):
    return [x]

t_create_tuple = timeit.timeit('tuple_creation(2)', setup='from __main__ import tuple_creation', number=number)
t_create_list = timeit.timeit('list_creation(2)', setup='from __main__ import list_creation', number=number)
print(t_create_tuple)
print(t_create_list)
print(t_create_tuple / t_create_list)

Celui - ci est un peu plus délicat au temps (et je suis heureux de prendre de meilleures idées pour le chronométrer dans les commentaires). L'essentiel, c'est que sur en moyenne (et sur mon ordinateur), un tuple prend environ 60% autant de temps à créer qu'une liste. Cependant, foo_tuple() prend en moyenne environ 40% du temps que foo_list() prend. Cela montre que nous gagnons vraiment un peu d'accélération de ces stagiaires. Les économies de temps semblent augmenter à mesure que le tuple devient plus grand (la création d'une liste plus longue prend plus de temps-la "création" du tuple prend un temps constant puisqu'elle a déjà été créée).

Notez Également que j'ai appelé ce "stage". Il n'est pas réellement (à moins pas dans le même sens que les chaînes sont internées). Nous pouvons voir la différence dans ce script simple:

def foo_tuple():
    return (2,)

def bar_tuple():
    return (2,)

def foo_string():
    return 'foo'

def bar_string():
    return 'foo'

print(foo_tuple() is foo_tuple())  # True
print(foo_tuple() is bar_tuple())  # False

print(foo_string() is bar_string())  # True

Nous voyons que les chaînes sont vraiment "internées" - différentes invocations utilisant la même notation littérale renvoient le même objet. Le tuple "stage" semble être spécifique à une seule ligne.

36
répondu mgilson 2017-05-23 12:33:42

Cela varie en fonction de l'implémentation.

CPython met en cache Certains objets immuables en mémoire. Cela est vrai des "petits" entiers comme 1 et 2 (-5 à 255, comme indiqué dans les commentaires ci-dessous). CPython le fait pour des raisons de performance; les petits entiers sont couramment utilisés dans la plupart des programmes, donc il économise de la mémoire pour n'avoir qu'une seule copie créée (et est sûr car les entiers sont immuables).

Cela est également vrai des objets "singleton" comme None; Il n'y a qu'un seul None existant à un moment donné.

D'autres objets (tels que le tuple vide, ()) peuvent être implémentés en tant que singletons, ou ils peuvent ne pas l'être.

En général, vous ne devriez pas nécessairement supposer que les objets immuables seront implémentés de cette façon. CPython le fait pour des raisons de performance, mais d'autres implémentations peuvent ne pas le faire, et CPython peut même arrêter de le faire à un moment donné dans le futur. (La seule exception pourrait être None, car x is None est un idiome Python commun et est susceptible d'être implémenté à travers différents interprètes et versions.)

, Habituellement, vous souhaitez utiliser == au lieu de is. L'opérateur is de Python n'est pas souvent utilisé, sauf pour vérifier si une variable est None.

20
répondu mipadi 2016-04-27 19:24:31