Author: Antonio Cuni <anto.c...@gmail.com> Branch: identity-dict-strategy Changeset: r45805:76c609d60ebd Date: 2011-07-21 12:11 +0200 http://bitbucket.org/pypy/pypy/changeset/76c609d60ebd/
Log: kill the global versioning logic, and add a big comment which explains why it's not needed diff --git a/pypy/objspace/std/identitydict.py b/pypy/objspace/std/identitydict.py --- a/pypy/objspace/std/identitydict.py +++ b/pypy/objspace/std/identitydict.py @@ -1,3 +1,6 @@ +## ---------------------------------------------------------------------------- +## dict strategy (see dict_multiobject.py) + from pypy.rlib import rerased from pypy.objspace.std.dictmultiobject import (AbstractTypedStrategy, DictStrategy, @@ -5,39 +8,6 @@ _UnwrappedIteratorMixin) -# a global (per-space) version counter to track live instances which "compare -# by identity" (i.e., whose __eq__, __cmp__ and __hash__ are the default -# ones). The idea is to track only classes for which we checked the -# compares_by_identity() status at least once: we increment the version if its -# status might change, e.g. because we set one of those attributes. The -# actual work is done by W_TypeObject.mutated() and objecttype:descr_setclass - -def bump_global_version(space): - if space.config.objspace.std.withidentitydict: - space.fromcache(ComparesByIdentityVersion).bump() - -def get_global_version(space): - if space.config.objspace.std.withidentitydict: - return space.fromcache(ComparesByIdentityVersion).get() - return None - -class ComparesByIdentityVersion(object): - - def __init__(self, space): - self.bump() - - def bump(self): - from pypy.objspace.std.typeobject import VersionTag - self._version = VersionTag() - - def get(self): - return self._version - - - -## ---------------------------------------------------------------------------- -## dict strategy (see dict_multiobject.py) - # this strategy is selected by EmptyDictStrategy.switch_to_correct_strategy class IdentityDictStrategy(AbstractTypedStrategy, DictStrategy): """ @@ -45,11 +15,48 @@ default unless you override __hash__, __eq__ or __cmp__). The storage is just a normal RPython dict, which has already the correct by-identity semantics. + + Note that at a first sight, you might have problems if you mutate the + class of an object which is already inside an identitydict. Consider this + example:: + + class X(object): + pass + d = {x(): 1} + X.__eq__ = ... + d[y] # might trigger a call to __eq__? + + We want to be sure that x.__eq__ is called in the same cases as in + CPython. However, as long as the strategy is IdentityDictStrategy, the + __eq__ will never be called. + + It turns out that it's not a problem. In CPython (and in PyPy without + this strategy), the __eq__ is called if ``hash(y) == hash(x)`` and ``x is + not y``. Note that hash(x) is computed at the time when we insert x in + the dict, not at the time we lookup y. + + Now, how can hash(y) == hash(x)? There are two possibilities: + + 1. we write a custom __hash__ for the class of y, thus making it a not + "compares by reference" type + + 2. the class of y is "compares by reference" type, and by chance the + hash is the same as x + + In the first case, the getitem immediately notice that y is not of the + right type, and switches the strategy to ObjectDictStrategy, then the + lookup works as usual. + + The second case is completely non-deterministic, even in CPython. + Depending on the phase of the moon, you might call the __eq__ or not, so + it is perfectly fine to *never* call it. Morever, in practice with the + minimar GC we never have two live objects with the same hash, so it would + never happen anyway. """ - _erase_tuple, _unerase_tuple = rerased.new_erasing_pair("identitydict") - _erase_tuple = staticmethod(_erase_tuple) - _unerase_tuple = staticmethod(_unerase_tuple) + erase, unerase = rerased.new_erasing_pair("identitydict") + erase = staticmethod(erase) + unerase = staticmethod(unerase) def wrap(self, unwrapped): return unwrapped @@ -57,18 +64,6 @@ def unwrap(self, wrapped): return wrapped - def erase(self, d): - current_version = get_global_version(self.space) - return self._erase_tuple((current_version, d)) - - def unerase(self, dstorage): - version, d = self._unerase_tuple(dstorage) - return d - - def get_current_version(self, dstorage): - version, d = self._unerase_tuple(dstorage) - return version - def get_empty_storage(self): return self.erase({}) diff --git a/pypy/objspace/std/objecttype.py b/pypy/objspace/std/objecttype.py --- a/pypy/objspace/std/objecttype.py +++ b/pypy/objspace/std/objecttype.py @@ -43,9 +43,6 @@ assert isinstance(w_oldcls, W_TypeObject) if w_oldcls.get_full_instance_layout() == w_newcls.get_full_instance_layout(): w_obj.setclass(space, w_newcls) - if space.config.objspace.std.withidentitydict: - if w_oldcls.compares_by_identity() and not w_newcls.compares_by_identity(): - identitydict.bump_global_version(space) else: raise operationerrfmt(space.w_TypeError, "__class__ assignment: '%s' object layout differs from '%s'", diff --git a/pypy/objspace/std/test/test_identitydict.py b/pypy/objspace/std/test/test_identitydict.py --- a/pypy/objspace/std/test/test_identitydict.py +++ b/pypy/objspace/std/test/test_identitydict.py @@ -2,7 +2,7 @@ from pypy.conftest import gettestobjspace from pypy.conftest import option -class AppTestTrackVersion: +class AppTestComparesByIdentity: def setup_class(cls): from pypy.objspace.std import identitydict @@ -13,15 +13,6 @@ return space.wrap(w_cls.compares_by_identity()) cls.w_compares_by_identity = cls.space.wrap(interp2app(compares_by_identity)) - def get_version(space): - v = cls.versions.setdefault(identitydict.get_global_version(space), - len(cls.versions)) - return space.wrap(v) - cls.w_get_version = cls.space.wrap(interp2app(get_version)) - - def setup_method(self, m): - self.__class__.versions = {} - def test_compares_by_identity(self): class Plain(object): pass @@ -53,56 +44,6 @@ del X.__eq__ assert self.compares_by_identity(X) - def test_versioning(self): - class X(object): - pass - - class Y(object): - def __eq__(self, other): - pass - - assert self.get_version() == 0 - X.__eq__ = lambda x: None - # modifying a class for which we never checked the - # compares_by_identity() status does not increase the version - assert self.get_version() == 0 - - del X.__eq__ - assert self.compares_by_identity(X) # now we check it - X.__add__ = lambda x: None - assert self.get_version() == 0 # innocent change - # - X.__eq__ = lambda x: None - assert self.get_version() == 1 # BUMP! - - del X.__eq__ - assert self.compares_by_identity(X) - X.__bases__ = (object,) - assert self.get_version() == 2 # BUMP! - - # modifying a class which is already "bad" does not increase the - # version - Y.__eq__ = lambda x: None - assert self.get_version() == 2 - - def test_change___class__(self): - class X(object): - pass - - class Y(object): - pass - - class Z(object): - def __eq__(self, other): - pass - - x = X() - assert self.compares_by_identity(X) - assert self.get_version() == 0 - x.__class__ = Y - assert self.get_version() == 0 - x.__class__ = Z - assert self.get_version() == 1 class AppTestIdentityDict(object): diff --git a/pypy/objspace/std/typeobject.py b/pypy/objspace/std/typeobject.py --- a/pypy/objspace/std/typeobject.py +++ b/pypy/objspace/std/typeobject.py @@ -178,8 +178,6 @@ if (key is None or key == '__eq__' or key == '__cmp__' or key == '__hash__'): w_self.compares_by_identity_status = UNKNOWN - if did_compare_by_identity: - identitydict.bump_global_version(w_self.space) if space.config.objspace.std.newshortcut: w_self.w_bltin_new = None _______________________________________________ pypy-commit mailing list pypy-commit@python.org http://mail.python.org/mailman/listinfo/pypy-commit