Skip to content

Commit 155764a

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 828451d commit 155764a

File tree

9 files changed

+137
-11
lines changed

9 files changed

+137
-11
lines changed

Doc/c-api/hash.rst

Lines changed: 26 additions & 0 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,6 +45,28 @@ 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.

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
@@ -1252,6 +1252,9 @@ New Features
12521252
* Add :c:func:`Py_HashPointer` function to hash a pointer.
12531253
(Contributed by Victor Stinner in :gh:`111545`.)
12541254

1255+
* Add :c:func:`Py_HashDouble` function to hash a C double number.
1256+
(Contributed by Victor Stinner in :gh:`111545`.)
1257+
12551258

12561259
Porting to Python 3.13
12571260
----------------------

Include/cpython/pyhash.h

Lines changed: 2 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 */
@@ -37,3 +38,4 @@ typedef struct {
3738
PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void);
3839

3940
PyAPI_FUNC(Py_hash_t) Py_HashPointer(const void *ptr);
41+
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
@@ -77,3 +78,46 @@ def python_hash_pointer(x):
7778
# Py_HashPointer((void*)(uintptr_t)-1) doesn't return -1 but -2
7879
VOID_P_MAX = -1 & (2 ** (8 * SIZEOF_VOID_P) - 1)
7980
self.assertEqual(hash_pointer(VOID_P_MAX), -2)
81+
82+
def test_hash_double(self):
83+
# Test Py_HashDouble()
84+
hash_double = _testcapi.hash_double
85+
86+
def check_number(value, expected):
87+
self.assertEqual(hash_double(value), (1, expected))
88+
89+
# test some integers
90+
integers = [
91+
*range(1, 30),
92+
2**30 - 1,
93+
2 ** 233,
94+
int(sys.float_info.max),
95+
]
96+
for x in integers:
97+
with self.subTest(x=x):
98+
check_number(float(x), hash(x))
99+
check_number(float(-x), hash(-x))
100+
101+
# test positive and negative zeros
102+
check_number(float(0.0), 0)
103+
check_number(float(-0.0), 0)
104+
105+
# test +inf and -inf
106+
inf = float("inf")
107+
check_number(inf, sys.hash_info.inf)
108+
check_number(-inf, -sys.hash_info.inf)
109+
110+
# special float values: compare with Python hash() function
111+
special_values = (
112+
math.nextafter(0.0, 1.0), # smallest positive subnormal number
113+
sys.float_info.min, # smallest positive normal number
114+
sys.float_info.epsilon,
115+
sys.float_info.max, # largest positive finite number
116+
)
117+
for x in special_values:
118+
with self.subTest(x=x):
119+
check_number(x, hash(x))
120+
check_number(-x, hash(-x))
121+
122+
# test not-a-number (NaN)
123+
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: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
#include "parts.h"
22
#include "util.h"
33

4+
5+
static PyObject *
6+
long_from_hash(Py_hash_t hash)
7+
{
8+
assert(hash != -1);
9+
10+
Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash));
11+
return PyLong_FromLongLong(hash);
12+
}
13+
14+
415
static PyObject *
516
hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
617
{
@@ -54,14 +65,28 @@ hash_pointer(PyObject *Py_UNUSED(module), PyObject *arg)
5465
}
5566

5667
Py_hash_t hash = Py_HashPointer(ptr);
57-
Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash));
58-
return PyLong_FromLongLong(hash);
68+
return long_from_hash(hash);
69+
}
70+
71+
72+
static PyObject *
73+
hash_double(PyObject *Py_UNUSED(module), PyObject *args)
74+
{
75+
double value;
76+
if (!PyArg_ParseTuple(args, "d", &value)) {
77+
return NULL;
78+
}
79+
80+
Py_hash_t hash;
81+
int res = Py_HashDouble(value, &hash);
82+
return Py_BuildValue("iN", res, long_from_hash(hash));
5983
}
6084

6185

6286
static PyMethodDef test_methods[] = {
6387
{"hash_getfuncdef", hash_getfuncdef, METH_NOARGS},
6488
{"hash_pointer", hash_pointer, METH_O},
89+
{"hash_double", hash_double, METH_VARARGS},
6590
{NULL},
6691
};
6792

Python/pyhash.c

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

86-
Py_hash_t
87-
_Py_HashDouble(PyObject *inst, double v)
86+
int
87+
Py_HashDouble(double v, Py_hash_t *result)
8888
{
8989
int e, sign;
9090
double m;
9191
Py_uhash_t x, y;
9292

9393
if (!Py_IS_FINITE(v)) {
94-
if (Py_IS_INFINITY(v))
95-
return v > 0 ? _PyHASH_INF : -_PyHASH_INF;
96-
else
97-
return _Py_HashPointer(inst);
94+
if (Py_IS_INFINITY(v)) {
95+
*result = (v > 0 ? _PyHASH_INF : -_PyHASH_INF);
96+
return 1;
97+
}
98+
else {
99+
assert(Py_IS_NAN(v));
100+
*result = _PyHASH_NAN;
101+
return 0;
102+
}
98103
}
99104

100105
m = frexp(v, &e);
@@ -126,7 +131,20 @@ _Py_HashDouble(PyObject *inst, double v)
126131
x = x * sign;
127132
if (x == (Py_uhash_t)-1)
128133
x = (Py_uhash_t)-2;
129-
return (Py_hash_t)x;
134+
*result = (Py_hash_t)x;
135+
return 1;
136+
}
137+
138+
Py_hash_t
139+
_Py_HashDouble(PyObject *obj, double v)
140+
{
141+
assert(obj != NULL);
142+
143+
Py_hash_t hash;
144+
if (Py_HashDouble(v, &hash) == 0) {
145+
hash = Py_HashPointer(obj);
146+
}
147+
return hash;
130148
}
131149

132150
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)