Skip to content

Commit 5be68f0

Browse files
committed
pythongh-111545: Add Py_HashDouble() function
* Add again the private _PyHASH_NAN constant. * Add tests: Modules/_testcapi/hash.c and Lib/test/test_capi/test_hash.py.
1 parent 9560e0d commit 5be68f0

File tree

9 files changed

+131
-12
lines changed

9 files changed

+131
-12
lines changed

Doc/c-api/hash.rst

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ PyHash API
55

66
See also the :c:member:`PyTypeObject.tp_hash` member.
77

8+
Types
9+
^^^^^
10+
811
.. c:type:: Py_hash_t
912
1013
Hash value type: signed integer.
1114

1215
.. versionadded:: 3.2
1316

17+
1418
.. c:type:: Py_uhash_t
1519
1620
Hash value type: unsigned integer.
@@ -41,11 +45,30 @@ See also the :c:member:`PyTypeObject.tp_hash` member.
4145
.. versionadded:: 3.4
4246

4347

48+
Functions
49+
^^^^^^^^^
50+
51+
.. c:function:: int Py_HashDouble(double value, Py_hash_t *result)
52+
53+
Hash a C double number.
54+
55+
* Set *\*result* to the hash and return ``1`` if *value* is finite or is
56+
infinity.
57+
* Set *\*result* to :data:`sys.hash_info.nan <sys.hash_info>` (``0``) and
58+
return ``0`` if *value* is not-a-number (NaN).
59+
60+
*result* must not be ``NULL``.
61+
62+
.. note::
63+
Only rely on the function return value to distinguish the "not-a-number"
64+
case. *\*result* can be ``0`` if *value* is finite. For example,
65+
``Py_HashDouble(0.0, &result)`` sets *\*result* to 0.
66+
67+
.. versionadded:: 3.13
68+
69+
4470
.. c:function:: PyHash_FuncDef* PyHash_GetFuncDef(void)
4571
4672
Get the hash function definition.
4773
48-
.. seealso::
49-
:pep:`456` "Secure and interchangeable hash algorithm".
50-
5174
.. versionadded:: 3.4

Doc/library/sys.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,13 @@ always available.
10341034

10351035
.. attribute:: hash_info.nan
10361036

1037-
(This attribute is no longer used)
1037+
The hash value returned for not-a-number (NaN).
1038+
1039+
This hash value is only used by the :c:func:`Py_HashDouble` C function if
1040+
the argument is not-a-number (NaN).
1041+
1042+
.. versionchanged:: 3.10
1043+
This hash value is no longer used to hash numbers in Python.
10381044

10391045
.. attribute:: hash_info.imag
10401046

Doc/whatsnew/3.13.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,9 @@ New Features
12471247
:exc:`KeyError` if the key missing.
12481248
(Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)
12491249

1250+
* Add :c:func:`Py_HashDouble` function to hash a C double number.
1251+
(Contributed by Victor Stinner in :gh:`111545`.)
1252+
12501253

12511254
Porting to Python 3.13
12521255
----------------------

Include/cpython/pyhash.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#define _PyHASH_MODULUS (((size_t)1 << _PyHASH_BITS) - 1)
1919
#define _PyHASH_INF 314159
20+
#define _PyHASH_NAN 0
2021
#define _PyHASH_IMAG _PyHASH_MULTIPLIER
2122

2223
/* Helpers for hash functions */
@@ -33,3 +34,5 @@ typedef struct {
3334
} PyHash_FuncDef;
3435

3536
PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void);
37+
38+
PyAPI_FUNC(int) Py_HashDouble(double value, Py_hash_t *result);

Lib/test/test_capi/test_hash.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import math
12
import sys
23
import unittest
34
from test.support import import_helper
@@ -31,3 +32,46 @@ def test_hash_getfuncdef(self):
3132
self.assertEqual(func_def.name, hash_info.algorithm)
3233
self.assertEqual(func_def.hash_bits, hash_info.hash_bits)
3334
self.assertEqual(func_def.seed_bits, hash_info.seed_bits)
35+
36+
def test_hash_double(self):
37+
# Test Py_HashDouble()
38+
hash_double = _testcapi.hash_double
39+
40+
def check_number(value, expected):
41+
self.assertEqual(hash_double(value), (1, expected))
42+
43+
# test some integers
44+
integers = [
45+
*range(1, 30),
46+
2**30 - 1,
47+
2 ** 233,
48+
int(sys.float_info.max),
49+
]
50+
for x in integers:
51+
with self.subTest(x=x):
52+
check_number(float(x), hash(x))
53+
check_number(float(-x), hash(-x))
54+
55+
# test positive and negative zeros
56+
check_number(float(0.0), 0)
57+
check_number(float(-0.0), 0)
58+
59+
# test +inf and -inf
60+
inf = float("inf")
61+
check_number(inf, sys.hash_info.inf)
62+
check_number(-inf, -sys.hash_info.inf)
63+
64+
# special float values: compare with Python hash() function
65+
special_values = (
66+
math.nextafter(0.0, 1.0), # smallest positive subnormal number
67+
sys.float_info.min, # smallest positive normal number
68+
sys.float_info.epsilon,
69+
sys.float_info.max, # largest positive finite number
70+
)
71+
for x in special_values:
72+
with self.subTest(x=x):
73+
check_number(x, hash(x))
74+
check_number(-x, hash(-x))
75+
76+
# test not-a-number (NaN)
77+
self.assertEqual(hash_double(float('nan')), (0, sys.hash_info.nan))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`Py_HashDouble` function to hash a C double number. Patch by
2+
Victor Stinner.

Modules/_testcapi/hash.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "parts.h"
22
#include "util.h"
33

4+
45
static PyObject *
56
hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
67
{
@@ -44,8 +45,27 @@ hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
4445
return result;
4546
}
4647

48+
49+
static PyObject *
50+
hash_double(PyObject *Py_UNUSED(module), PyObject *args)
51+
{
52+
double value;
53+
if (!PyArg_ParseTuple(args, "d", &value)) {
54+
return NULL;
55+
}
56+
57+
Py_hash_t hash;
58+
int res = Py_HashDouble(value, &hash);
59+
assert(hash != -1);
60+
61+
Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash));
62+
return Py_BuildValue("iN", res, PyLong_FromLongLong(hash));
63+
}
64+
65+
4766
static PyMethodDef test_methods[] = {
4867
{"hash_getfuncdef", hash_getfuncdef, METH_NOARGS},
68+
{"hash_double", hash_double, METH_VARARGS},
4969
{NULL},
5070
};
5171

Python/pyhash.c

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,23 @@ static Py_ssize_t hashstats[Py_HASH_STATS_MAX + 1] = {0};
8585

8686
Py_hash_t _Py_HashPointer(const void *);
8787

88-
Py_hash_t
89-
_Py_HashDouble(PyObject *inst, double v)
88+
int
89+
Py_HashDouble(double v, Py_hash_t *result)
9090
{
9191
int e, sign;
9292
double m;
9393
Py_uhash_t x, y;
9494

9595
if (!Py_IS_FINITE(v)) {
96-
if (Py_IS_INFINITY(v))
97-
return v > 0 ? _PyHASH_INF : -_PyHASH_INF;
98-
else
99-
return _Py_HashPointer(inst);
96+
if (Py_IS_INFINITY(v)) {
97+
*result = (v > 0 ? _PyHASH_INF : -_PyHASH_INF);
98+
return 1;
99+
}
100+
else {
101+
assert(Py_IS_NAN(v));
102+
*result = _PyHASH_NAN;
103+
return 0;
104+
}
100105
}
101106

102107
m = frexp(v, &e);
@@ -128,7 +133,20 @@ _Py_HashDouble(PyObject *inst, double v)
128133
x = x * sign;
129134
if (x == (Py_uhash_t)-1)
130135
x = (Py_uhash_t)-2;
131-
return (Py_hash_t)x;
136+
*result = (Py_hash_t)x;
137+
return 1;
138+
}
139+
140+
Py_hash_t
141+
_Py_HashDouble(PyObject *obj, double v)
142+
{
143+
assert(obj != NULL);
144+
145+
Py_hash_t hash;
146+
if (Py_HashDouble(v, &hash) == 0) {
147+
hash = _Py_HashPointer(obj);
148+
}
149+
return hash;
132150
}
133151

134152
Py_hash_t

Python/sysmodule.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1514,7 +1514,7 @@ get_hash_info(PyThreadState *tstate)
15141514
PyStructSequence_SET_ITEM(hash_info, field++,
15151515
PyLong_FromLong(_PyHASH_INF));
15161516
PyStructSequence_SET_ITEM(hash_info, field++,
1517-
PyLong_FromLong(0)); // This is no longer used
1517+
PyLong_FromLong(_PyHASH_NAN));
15181518
PyStructSequence_SET_ITEM(hash_info, field++,
15191519
PyLong_FromLong(_PyHASH_IMAG));
15201520
PyStructSequence_SET_ITEM(hash_info, field++,

0 commit comments

Comments
 (0)