Re: A proposal to modify `None` so that it hashes to a constant
Terry Reedy <[email protected]> Fri, 2 Dec 2022 10:53:44 -0500
| Newsgroups | gmane.comp.python.devel |
|---|---|
| Message-ID | <[email protected]> |
On 11/30/2022 8:48 PM, Rob Cliffe via Python-Dev wrote: > Thank you for this very clear analysis, Oscar. > It seems to me that this strengthens the OP's case. I am curious as to > whether others agree. I do. > On 30/11/2022 13:35, Oscar Benjamin wrote: >> On Tue, 29 Nov 2022 at 23:46, Steven D'Aprano <[email protected]> >> wrote: >>> On Tue, Nov 29, 2022 at 08:51:09PM -0000, Yoni Lavi wrote: >>> >>>> It does make your argument invalid though, since it's based on this >>>> assumption that I was asking for a requirement on iteration order >>>> (e.g. like dict's iteration order = insertion order guarantee), which >>>> is not the case. >>> Yoni, I think this answer is disingenious. >> I don't think it is disingenuous. There are just a lot of people >> talking past each other and not quite understanding what each person >> means because there is confusion about even the intended meaning of >> terms like "deterministic". I will expand here with enough detail that >> we should hopefully be able to avoid misunderstanding each other. >> >> There are probably other places where you could find mentions of this >> in the docs but I just took a quick look in the Python 3.5 docs >> (before hash randomisation) to find this mention of dictionary >> iteration order: >> https://docs.python.org/3.5/library/stdtypes.html#dictionary-view-objects >> >> What it says is >> """ >> Keys and values are iterated over in an arbitrary order which is >> non-random, varies across Python implementations, and depends on the >> dictionary’s history of insertions and deletions. >> """ >> The key point is the use of the term "non-random" which here is >> intended to mean that although no particular ordering is guaranteed >> you can expect to rerun the same program and get the same result >> deterministically. A different version or implementation of Python >> might give a different order but rerunning the same program twice >> without changing anything should give the same result even if that >> result depends in some way on the iteration order of some >> dictionaries. I can't immediately find a similar statement about sets >> but in practice the same behaviour applied to sets as well. Note >> carefully that it is this *narrow* form of determinism that Yoni is >> interested in. >> >> Of course there are some caveats to this and the obvious one is that >> this statement does not apply if there are some objects that use >> identity based hashing so this is not deterministic: >> >> class A: >> def __init__(self, data): >> self.data = data >> def __repr__(self): >> return 'A(%s)' % self.data >> >> a1 = A(1) >> a2 = A(2) >> >> for a in {a1, a2}: >> print(a) >> >> Running this gives: >> >> $ python3.5 t.py >> A(2) >> A(1) >> $ python3.5 t.py >> A(1) >> A(2) >> >> On the other hand if all of the hashes themselves are deterministic >> then the program as a whole will be as well so this is deterministic: >> >> class A: >> def __init__(self, data): >> self.data = data >> def __repr__(self): >> return 'A(%s)' % self.data >> def __hash__(self): >> return hash(self.data) >> def __eq__(self): >> return self.data == other.data >> >> a1 = A(1) >> a2 = A(2) >> >> for a in {a1, a2}: >> print(a) >> >> $ python3.5 t.py >> A(1) >> A(2) >> $ python3.5 t.py >> A(1) >> A(2) >> >> So we have two classes of hashable objects: >> >> 1. Those with deterministic hash >> 2. Those with non-deterministic hash >> >> A program that avoids depending on the iteration order of sets or >> dicts containing objects with non-deterministic hash could be >> deterministic. It is not the case that the program would depend on the >> iteration order for its *correctness* but just that the behaviour of >> the program is *reproducible* which is useful in various ways e.g.: >> >> - You could say to someone else "run this code with CPython 3.5 and >> you should be able to reproduce exactly what I see when I run the >> program". It is common practice e.g. in scientific programming to >> record things like random seeds so that someone else can precisely >> reproduce the results shown in a paper or some other work and this in >> general requires that it is at least possible to make everything >> deterministic. >> >> - When debugging it is useful to be able to reproduce an error >> condition precisely. Debugging non-deterministic failures can be >> extremely difficult. In the same way that you might want to reproduce >> correctly functioning code it is also very useful to be able to >> reproduce bugs. >> >> I can list more examples but really it shouldn't be necessary to >> justify from first principles why determinism in programming is >> usually a good thing. There can be reasons sometimes why determinism >> is undesired or cannot or should not be guaranteed. It should not be >> controversial though to say that all things being equal determinism is >> usually a desirable feature and should be preferred by default. I >> don't think that the 3.5 docs I quoted above used the words >> "non-random" casually: it was an intended feature and people were >> aware that breaking that behaviour would be problematic in many >> situations. >> >> Of course in Python 3.6 this determinism was broken with the >> introduction of hash randomisation for strings. It was considered that >> for security purposes it would be better to have some internal >> non-deterministic behaviour to guard against attackers. Specifically >> the hashes of three types (str, bytes and datetime) were made >> non-deterministic between subsequent CPython processes. The effect was >> not only to change purely internal state though but also the >> observable iteration order of dicts and sets which became >> non-deterministic from one run of CPython to another. It was >> anticipated at the time that this might be problematic in some >> situations (it certainly was!) and so an environment variable >> PYTHONHASHSEED was introduced in order to restore determinism for >> cases that needed it. >> >> So now if you want to have reproducible behaviour with Python 3.6+ you >> also need to fix PYTHONHASHSEED as well as avoiding the use of other >> types of non-deterministically hashable objects in sets and dicts. >> This is something that I have personally used mainly for the >> reproducibility of rare bugs e.g. "to reproduce this you should run >> the following Python code using commit abc123 under CPython 3.6 and >> with PYTHONHASHSEED=1234". >> >> Subsequently in Python 3.7 dict iteration order was changed to make it >> always deterministic by having it not depend on the hash values at >> all, with the order depending on the order of insertions into the dict >> instead. This introduced a *stronger* guarantee of determinism: now >> the ordering could be expected to be reproducible even with different >> versions and implementations of Python. For many this seemed to have >> resolved the problems of undefined, implementation-defined etc >> ordering. However this only applied to dicts and not sets and as of >> Python 3.12 any issues about deterministic ordering still remain >> wherever sets are used. >> >> The introduction of hash randomisation means that since Python 3.6 >> there are now three classes of hashable objects: >> >> 1. Objects with deterministic hash (int etc) >> 2. Objects with non-deterministic hash that can be controlled with >> PYTHONHASHSEED (bytes, str and datetime). >> 3. Objects with non-deterministic hash that cannot be controlled >> (id-based hashing). >> >> The question in this thread and others is which of these three classes >> None should belong to. Although None is just one particular value it >> is a very commonly used value and its non-deterministic hash can creep >> through to affect the hash of larger data structures that use >> recursive hash calls (e.g. a tuple of tuples of ... that somewhere >> contains None). Also certain situations such as a type hint like >> Optional[T] as referred to by the OP necessarily use None: >> >> $ python -c 'from typing import Optional; print(hash(Optional[int]))' >> -7631587549837930667 >> $ python -c 'from typing import Optional; print(hash(Optional[int]))' >> -6488475102642892491 >> >> Somehow this affects frozen dataclasses but I haven't used those >> myself so I won't demonstrate how to reproduce the problem with them. >> >> Here is a survey of types from builtins: >> >> NoneType bool bytearray bytes complex dict ellipsis enumerate filter >> float frozenset int list map memoryview object range reversed set >> slice str tuple type zip >> >> We can divide these into the three classes described above plus the >> non-hashable types (I haven't checked this in the code, but just >> experimenting with calling hash): >> >> 1. Deterministic hash: >> bool, complex, float, frozenset, int, range, tuple >> >> 2. Hash depends on hash seed: >> str, bytes >> >> 3. Hash depends on id: >> NoneType, ellipsis, enumerate, filter, memoryview, object, reversed, zip >> >> 4. Non-hashable: >> bytearray, dict, list, set, slice >> >> The question here is whether None belongs in class 3 or class 1. To me >> it seems clear that there is no advantage in having None be in class 3 >> except perhaps to save a few simple lines of code: >> https://github.com/python/cpython/pull/99541/files >> >> There is however a worthwhile advantage in having None be in class 1. >> If None had a deterministic hash then tuples, frozensets etc >> consisting of objects with None as well as other objects with >> deterministic hash could all have deterministic hash. The behaviour of >> iteration order for sets would then be deterministic in the *narrow* >> sense that is referred to by the Python 3.5 docs above. >> >> Some have argued that the fact that some types have a seed dependent >> hash implies that None should not have a deterministic hash but this >> does not follow. It was known at the time that str and bytes were >> moved from class 1 to class 2 that it would be problematic to do so >> which is precisely why PYTHONHASHSEED was introduced. However >> PYTHONHASHSEED does not help here because None is not even in class 2 >> but rather class 3. >> >> If I was going to label any claim made by anyone in these threads as >> disingenuous then it would be the claim that None is somehow not >> "special". Firstly many types have deterministic hash so it isn't >> really that much of a special property. Secondly None is *clearly* a >> special value that is used everywhere! When I open the CPython >> interpreter there are already thousands of references to None before I >> have even done anything: >> >> >>> import sys >> >>> sys.getrefcount(None) >> 4429 >> >> The motivation for this and other threads is to bring determinism in >> the *narrow* sense. Others (including me) have made references to >> other kinds of determinism that have derailed the threads by >> misunderstanding exactly what Yoni is referring to. The *stronger* >> sense of determinism would be useful if possible but it is not the >> intended topic of these threads. >> >> -- >> Oscar >> _______________________________________________ >> Python-Dev mailing list -- [email protected] >> To unsubscribe send an email to [email protected] >> https://mail.python.org/mailman3/lists/python-dev.python.org/ >> Message archived at >> https://mail.python.org/archives/list/[email protected]/message/73KMUPBTS4MIOMPRT3PBQ36HREQFXUUN/ >> Code of Conduct: http://python.org/psf/codeofconduct/ > > _______________________________________________ > Python-Dev mailing list -- [email protected] > To unsubscribe send an email to [email protected] > https://mail.python.org/mailman3/lists/python-dev.python.org/ > Message archived at > https://mail.python.org/archives/list/[email protected]/message/KVY3SRLMEFTAY7CBJHYDXQ4HCHK6P2O4/ > Code of Conduct: http://python.org/psf/codeofconduct/ -- Terry Jan Reedy _______________________________________________ Python-Dev mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/[email protected]/message/MNPGEGH3AQOB75OEEXQNZIARCBZYUSYA/ Code of Conduct: http://python.org/psf/codeofconduct/