You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is something we like to have to support int<->mpz conversion of "big enough" int's with mpz_import/export. See current code in the gmpy2 (similar approach has Sage): for reading and for writing.
Now we have *From/AsNativeBytes functions to do conversion of "big enough" int's, but this is much more slow than above approach.
Together with PyLong_Sign(), we need functions to access absolute value of "big enough" integer as an array of "digits" (for reading or writing). The GMP supports this kind of API with mpz_limbs_read() and mpz_limbs_write() functions. Perhaps, this is a more simple alternative (on CPython side) to PyInt_Import/Export functions.
Edit, a complete interface:
/* reading */intPyLong_Sign(PyLongObject*obj); // mpz_sgn()Py_ssize_tPyLong_DigitCount(PyLongObject*obj); // mpz_size()constdigit*PyLong_AsDigits(PyLongObject*obj); // mpz_limbs_read()/* writing (former _PyLong_FromDigits, used in _decimal.c) */PyLongObject*PyLong_FromDigits(intnegative, Py_ssize_tdigit_count, digit*digits);
int PyLong_Sign(PyLongObject *obj) wouldn't need to worry about errors.
This is something I was thinking first. Clearly, it's enough for above use case. If this kind of API is acceptable (so far, I see only PyUnstable_* functions of this type) - I would like to adopt one.
Why pass in a PyObject *, rather than a PyLongObject *?
For consistency with other PyLong APIs. Example: long PyLong_AsLong(PyObject *).
Generic PyObject* vs specific type (such as PyLongObject*) was also discussed in the Add PyDict_GetItemRef() function issue. It was decided to stick to PyObject*.
Generic PyObject* vs specific type (such as PyLongObject*) was also discussed in the python/cpython#106004 issue. It was decided to stick to PyObject*.
By the way, I'm surprised that the C API has no function to compare a Python int object to a C integer, something like:
int PyLong_CompareWithLong(PyObject*, long, int *result)
Returns 0 on success. In that case, *result is set to -1 if less than, 0 if equal, +1 if greater than.
Set an exception and return -1 on error.
I'm mentiong such hypothetical function since _PyLong_Sign(obj) is currently used by a few projects to compare a Python int to zero: is it equal to zero? Smaller than zero? Greater than zero?
It could be done with:
intcmp;
if (PyLong_CompareWithLong(number, 0, &cmp) <0) ... handleerror ...
if (cmp==0) ... equaltozeroelseif (cmp<0) ... smallerthanzeroelse ... greaterthanzero
Another data: I just added Py_GetConstantBorrowed(Py_CONSTANT_ZERO) and Py_GetConstantBorrowed(Py_CONSTANT_ONE) to the C API, and these objects can be used with PyObject_RichCompareBool().
There was no formal vote, but most members of the WG decided to use PyObject * rather than concrete types:
All our deliberately public APIs should be PyObject * to minimise abstraction leakage, and do their type checks (raising TypeErrors, not assertions). Fast APIs that skip checks should be the special case, not the default.
Hopefully PyObject_IsTrue is a relatively quick way to compare to zero (assuming you already know you have the right type).
I'm good with adding the function, and I'm about 50/50 between Victor's proposed API and one that returns a "switch"-able result:
switch(PyLong_Sign(o)) {
case PY_LONG_POSITIVE:
case PY_LONG_NEGATIVE:
case PY_LONG_ZERO:
break;
default:
// error raised
}
Though honestly, none of the times when I've needed this have I needed to optimise for a sign check before conversion, especially since for compact values it'll cost about the same to assign *sign as *value. But possibly there's a use for checking without converting at all.
In general I find APIs with output variables awkward, so I want to use them only when necessary. Since the sign function has only four possible outcomes (error, negative, zero, positive) I feel it doesn't really warrant the output variable, so I'm supportive of Steve's solution.
In general I find APIs with output variables awkward, so I want to use them only when necessary. Since the sign function has only four possible outcomes (error, negative, zero, positive) I feel it doesn't really warrant the output variable, so I'm supportive of Steve's solution.
I agree, with -1 for error and non-negative values for the three valid outcomes.
I think it will be confusing in light of the c api convention of <0 indicating error. That's probably stronger in this case than the convention on comparison result being -1,0,1.
PyObject_RichCompareBool() API doesn't have this issue since it takes two arguments and a 3rd is the comparison operator:
/* Perform a rich comparison with integer result. This wraps
PyObject_RichCompare(), returning -1 for error, 0 for false, 1 for true. */
int PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
If we do provide both, PyLong_SignUnchecked is the one that will get used.
I'd be very surprised if users chose the more complex variant over the simpler one.
Reacted by Sergey B Kirpichev, Steve Dower and Erlend E. Aasland
If we were to do both, the latter ought to be PyUnstable_Long_Sign (or PyUnstableLong_Sign? Or PyLongUnstable_Sign?) and probably be defined as an inline function (like the existing private function).
Otherwise, 100% agree with Guido (and 90% with Mark: I think the pointer cast will sometimes be less convenient than chaining conditions (if (...Sign(x, &s) && s > 0 && ...NativeBytes(...)), but for the most part the simpler function will win).
I can live with having just the two-argument version if we rename it to PyLong_GetSign.
Sounds great!
PyLong_SignUnchecked is the one that will get used.
I'd be very surprised if users chose the more complex variant over the simpler one.
That's OK -- with a name that announces that this function is unusual.
(And if we add it, I'd consider it inconsistent to not have the “regular” variant.)
PyUnstable_Long_Sign
I don't think we need to worry about API stability here. IMO, the convenient API is inconsistent with how we want new API to look. While consistency and stability are correlated, I'd rather keep them separate (Unchecked vs. Unstable).
It hardly matters, they aren't getting one that accepts PyLongObject as an argument. The choice will be between putting the error code or the result in an output parameter, and previously we've agreed (in a very rare case of agreement 😄 ) that the error code should be the returned value.
@erlend-aasland: Are you ok with API proposed in #19 (comment) ? Tell me if you need more context/information. And it's ok if you need more time to decide.
I concur with @markshannon. For such low-level function the overhead of checking the type is too high, and it makes the API less convenient.
Also, I think that PyLong_IsPositive(), PyLong_IsNegative() and PyLong_IsZero() are more useful in practice and adds less overhead. So I propose to implement these functions first, and then look if there are enough use cases are left for PyLong_Sign().
These functions are used when performance is very important. When it is not important, you can use PyObject_RichCompareBool().
These functions are used when performance is very important.
In which kind of code the performance of PyLong_GetSign() would matter? I don't expect this function to be called often, like not more than once per integer, and not in "hot code".
PyLong_GetSign() cannot fail if the argument is a Python int. If you know that the parameter is a Python int, you can simply ignore the error handling (maybe using an assertion, just in case).
I do not remember details, it was many years ago, but I used Py_SIZE(x) < 0 instead of _PyLong_Sign(x) < 0 more than once in the past because the difference was noticeable. You can search all occurrences of "_PyLong_Is" and try to replace them with _PyLong_Sign.
BTW, the _PyLong_Is*() functions are used much more (104 occurrences total: 26 _PyLong_IsZero, 7 _PyLong_IsPositive and 71 _PyLong_IsNegative) than _PyLong_Sign() (8). And several of these _PyLong_Sign() calls could be replaced with _PyLong_Is*().
The API proposed here is not only slower, it is less convenient. You need to introduce a variable, and even if you ignore error, you cannot use it in expression.
intsign;
(void)PyLong_Sign(obj, &sign);
if (sign<0) {
instead of
if (PyLong_IsNegative(obj)) {
Multiply this by 112 use cases.
Reacted by Sergey B Kirpichev, Erlend E. Aasland and Mark Shannon
The API proposed here is not only slower, it is less convenient.
These aspects were taken in account in the decision.
This API takes PyObject*. If you consider that it's way too slow and the API is not convenient, you can propose adding a PyUnstable API which takes PyLongObject*. I'm not convinced that it's needed.
Also, this issue is not closed, so I suggest to open a new one.
Activity
gvanrossum commentedon Mar 11, 2024
IIRC @markshannon has opinions on the API for long ints. Please ask him first.
markshannon commentedon Mar 11, 2024
What's the use case for this?
Why pass in a
PyObject *, rather than aPyLongObject *?int PyLong_Sign(PyLongObject *obj)wouldn't need to worry about errors.skirpichev commentedon Mar 12, 2024
This is something we like to have to support int<->mpz conversion of "big enough" int's with mpz_import/export. See current code in the gmpy2 (similar approach has Sage): for reading and for writing.
Now we have
*From/AsNativeBytesfunctions to do conversion of "big enough" int's, but this is much more slow than above approach.Together with
PyLong_Sign(), we need functions to access absolute value of "big enough" integer as an array of "digits" (for reading or writing). The GMP supports this kind of API withmpz_limbs_read()andmpz_limbs_write()functions. Perhaps, this is a more simple alternative (on CPython side) to PyInt_Import/Export functions.Edit, a complete interface:
This is something I was thinking first. Clearly, it's enough for above use case. If this kind of API is acceptable (so far, I see only
PyUnstable_*functions of this type) - I would like to adopt one.vstinner commentedon Mar 15, 2024
@markshannon:
A code search on PyPI top 5,000 projects (at 2023-11-15) finds usage in 10 projects:
Code:
Details
One usage is to ... test the sign of a number :-) Examples:
and
@markshannon:
For consistency with other PyLong APIs. Example:
long PyLong_AsLong(PyObject *).Generic
PyObject*vs specific type (such asPyLongObject*) was also discussed in the Add PyDict_GetItemRef() function issue. It was decided to stick toPyObject*.skirpichev commentedon Mar 20, 2024
JFR, it was discussed rather in the pr thread: python/cpython#106005
Perhaps, this is the relevant issue: capi-workgroup/api-evolution#29
vstinner commentedon Mar 22, 2024
By the way, I'm surprised that the C API has no function to compare a Python int object to a C integer, something like:
int PyLong_CompareWithLong(PyObject*, long, int *result)*resultis set to -1 if less than, 0 if equal, +1 if greater than.I'm mentiong such hypothetical function since
_PyLong_Sign(obj)is currently used by a few projects to compare a Python int to zero: is it equal to zero? Smaller than zero? Greater than zero?It could be done with:
Another data: I just added
Py_GetConstantBorrowed(Py_CONSTANT_ZERO)andPy_GetConstantBorrowed(Py_CONSTANT_ONE)to the C API, and these objects can be used withPyObject_RichCompareBool().But I dislike this code, since it uses a borrowed reference and it requires to call
PyErr_Occurred():-(vstinner commentedon Apr 15, 2024
@encukou @gvanrossum @iritkatriel @zooba: What's your opinion on adding
int PyLong_Sign(PyObject *obj, int *sign)API to Python 3.13?encukou commentedon Apr 15, 2024
+1 from me.
There was no formal vote, but most members of the WG decided to use
PyObject *rather than concrete types:For “evolution”, let's stick to that.
zooba commentedon Apr 15, 2024
Hopefully
PyObject_IsTrueis a relatively quick way to compare to zero (assuming you already know you have the right type).I'm good with adding the function, and I'm about 50/50 between Victor's proposed API and one that returns a "switch"-able result:
Though honestly, none of the times when I've needed this have I needed to optimise for a sign check before conversion, especially since for compact values it'll cost about the same to assign
*signas*value. But possibly there's a use for checking without converting at all.gvanrossum commentedon Apr 15, 2024
In general I find APIs with output variables awkward, so I want to use them only when necessary. Since the sign function has only four possible outcomes (error, negative, zero, positive) I feel it doesn't really warrant the output variable, so I'm supportive of Steve's solution.
iritkatriel commentedon Apr 15, 2024
I agree, with -1 for error and non-negative values for the three valid outcomes.
vstinner commentedon Apr 15, 2024
I'm fine with any API as soon as it doesn't require to call PyErr_Occurred().
If we go with the enum-like approach, I suggest to use more specific names, add "IS_" in the name, and use Py_ prefix rather than PY_:
Usage would be:
The advantage of
int PyLong_Sign(PyObject *obj, int *sign)API is that no constant needs to be defined and sign can be used more naturally:The gmpy project needs to check if a number is negative: https://github.com/aleaxit/gmpy/blob/eb8dfcbd84abcfcb36b4adcb0d5c6d050731dd75/src/gmpy2_convert_gmp.c#L41-L75
So any API would be fine.
gvanrossum commentedon Apr 15, 2024
How about we define the result as follows?
-2error-1negative0zero1positiveiritkatriel commentedon Apr 15, 2024
I think it will be confusing in light of the c api convention of <0 indicating error. That's probably stronger in this case than the convention on comparison result being -1,0,1.
vstinner commentedon Apr 15, 2024
PyObject_RichCompareBool() API doesn't have this issue since it takes two arguments and a 3rd is the comparison operator:
15 remaining items
markshannon commentedon Apr 23, 2024
If we do provide both,
PyLong_SignUncheckedis the one that will get used.I'd be very surprised if users chose the more complex variant over the simpler one.
zooba commentedon Apr 23, 2024
If we were to do both, the latter ought to be
PyUnstable_Long_Sign(orPyUnstableLong_Sign? OrPyLongUnstable_Sign?) and probably be defined as an inline function (like the existing private function).Otherwise, 100% agree with Guido (and 90% with Mark: I think the pointer cast will sometimes be less convenient than chaining conditions (
if (...Sign(x, &s) && s > 0 && ...NativeBytes(...)), but for the most part the simpler function will win).encukou commentedon Apr 24, 2024
Sounds great!
That's OK -- with a name that announces that this function is unusual.
(And if we add it, I'd consider it inconsistent to not have the “regular” variant.)
I don't think we need to worry about API stability here. IMO, the convenient API is inconsistent with how we want new API to look. While consistency and stability are correlated, I'd rather keep them separate (
Uncheckedvs.Unstable).encukou commentedon May 1, 2024
int PyLong_GetSign(PyObject *obj, int *sign)On success, set
*signto sign of integer object obj (0, -1or +1 for zero, negative or positive integer, respectively)
and return 0.
On failure, return -1 with an exception set.
This function always succeeds if obj is a
PyLongObjector its subtype.markshannon commentedon May 1, 2024
Have any of the likely users asked for this two argument form, as opposed to
int PyLong_GetSign(PyLongObject *obj)?zooba commentedon May 1, 2024
It hardly matters, they aren't getting one that accepts
PyLongObjectas an argument. The choice will be between putting the error code or the result in an output parameter, and previously we've agreed (in a very rare case of agreement 😄 ) that the error code should be the returned value.encukou commentedon May 6, 2024
@iritkatriel, what are your thoughts?
vstinner commentedon May 31, 2024
@erlend-aasland: Are you ok with API proposed in #19 (comment) ? Tell me if you need more context/information. And it's ok if you need more time to decide.
erlend-aasland commentedon Jun 1, 2024
I'm fine with the proposed API; I voted.
vstinner commentedon Jun 1, 2024
Thanks all. The SC adopted
int PyLong_GetSign(PyObject *obj, int *sign)API: #19 (comment). I close the issue.serhiy-storchaka commentedon Jun 3, 2024
I concur with @markshannon. For such low-level function the overhead of checking the type is too high, and it makes the API less convenient.
Also, I think that
PyLong_IsPositive(),PyLong_IsNegative()andPyLong_IsZero()are more useful in practice and adds less overhead. So I propose to implement these functions first, and then look if there are enough use cases are left forPyLong_Sign().These functions are used when performance is very important. When it is not important, you can use
PyObject_RichCompareBool().vstinner commentedon Jun 3, 2024
In which kind of code the performance of PyLong_GetSign() would matter? I don't expect this function to be called often, like not more than once per integer, and not in "hot code".
PyLong_GetSign() cannot fail if the argument is a Python int. If you know that the parameter is a Python int, you can simply ignore the error handling (maybe using an assertion, just in case).
serhiy-storchaka commentedon Jun 3, 2024
I do not remember details, it was many years ago, but I used
Py_SIZE(x) < 0instead of_PyLong_Sign(x) < 0more than once in the past because the difference was noticeable. You can search all occurrences of "_PyLong_Is" and try to replace them with_PyLong_Sign.BTW, the
_PyLong_Is*()functions are used much more (104 occurrences total: 26_PyLong_IsZero, 7_PyLong_IsPositiveand 71_PyLong_IsNegative) than_PyLong_Sign()(8). And several of these_PyLong_Sign()calls could be replaced with_PyLong_Is*().The API proposed here is not only slower, it is less convenient. You need to introduce a variable, and even if you ignore error, you cannot use it in expression.
instead of
Multiply this by 112 use cases.
vstinner commentedon Jun 5, 2024
These aspects were taken in account in the decision.
This API takes
PyObject*. If you consider that it's way too slow and the API is not convenient, you can propose adding aPyUnstableAPI which takesPyLongObject*. I'm not convinced that it's needed.Also, this issue is not closed, so I suggest to open a new one.