Re: A proposal to modify `None` so that it hashes to a constant
Rob Cliffe via Python-Dev <[email protected]> Thu, 1 Dec 2022 01:48:40 +0000
| Newsgroups | gmane.comp.python.devel |
|---|---|
| Message-ID | <[email protected]> |
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. Best wishes Rob Cliffe 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/