Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add public functions PyLong_IsPositive(), PyLong_IsNegative() and PyLong_IsZero() #29

Open
serhiy-storchaka opened this issue Jun 13, 2024 · 28 comments
Labels

Comments

@serhiy-storchaka
Copy link

int PyLong_IsPositive(PyObject *);
int PyLong_IsNegative(PyObject *);
int PyLong_IsZero(PyObject *);

They should be identical to existing private functions _PyLong_IsPositive(), _PyLong_IsNegative() and _PyLong_IsZero(), except that the argument is PyObject * instead of PyLongObject *.

The private functions are much more used in the CPython code than _PyLong_Sign (numbers are not accurate):

26 _PyLong_IsZero
 7 _PyLong_IsPositive
71 _PyLong_IsNegative
 8 _PyLong_Sign

Some of cases that use _PyLong_Sign could use _PyLong_IsNegative etc instead. It is expected that they will be more used in the user code too.

They are faster than PyLong_GetSign because they have less code.

They are also more convenient, because you do not need to introduce a variable for result and can use them in expression. For example:

if (PyLong_IsNegative(obj)) {

vs

int sign;
(void)PyLong_GetSign(obj, &sign);
if (sign < 0) {
@vstinner
Copy link

What are return values? Especially when the argument is not a Python int?

@serhiy-storchaka
Copy link
Author

1 for true, 0 for false.

They should be only used with a Python int, like PyUnicode_IsIdentifier() can be only used with a Python str.

@skirpichev
Copy link

0 for false

You meant for non-int's too, right?

They should be only used with a Python int

Then using PyLongObject seems more logical. If so, I don't see why this is better than single

int PyLong_Sign(PyLongobject *obj);

interface, already rejected in #19

PS:

int sign;
(void)PyLong_GetSign(obj, &sign);
if (sign < 0) {

Some compilers (e.g. recent gcc) also emit warning on the first line.

@encukou
Copy link
Collaborator

encukou commented Jun 14, 2024

New public API should take PyObject*, and raise TypeError on bad types. These don't do it. Is there any new reason to make an exception to that guideline? I'd hate to repeat the conversation in #19.

Most of the usage in CPython is in unicodeobject.c implemeting PyLong itself. I don't think that's representative for third-party usage.

The functions quite easy to implement using PyLong_GetSign. Is it really worth adding three extra API functions?

@serhiy-storchaka
Copy link
Author

Even excluding Objects/longobject.c, there are much more use cases for these functions than for _PyLong_Sign (note that some of these cases for _PyLong_Sign can use _PyLong_IsNegative).

14 _PyLong_IsZero
 2 _PyLong_IsPositive
24 _PyLong_IsNegative
 6 _PyLong_Sign

They are faster and more convenient. They are used in many different files:

Modules/posixmodule.c
Modules/mathmodule.c
Modules/_tkinter.c
Modules/_decimal/_decimal.c
Objects/typeobject.c
Objects/sliceobject.c
Objects/abstract.c
Objects/rangeobject.c
Python/structmember.c
Python/bytecodes.c
Python/optimizer_symbols.c
Python/ast_opt.c
Python/marshal.c

If you insist, we can make them returning 0 or setting an error and returning -1 if the argument is not a Python int. They will still be fast and convenient.

@vstinner
Copy link

New public API should take PyObject*, and raise TypeError on bad types. These don't do it. Is there any new reason to make an exception to that guideline? I'd hate to repeat the conversation in #19.

PyLong already has exceptions: PyUnstable_Long_IsCompact() and PyUnstable_Long_CompactValue() parameter type is const PyLongObject* instead of PyObject*.

Maybe PyLong_IsPositive(), PyLong_IsNegative() and PyLong_IsZero() belong to the PyUnstable API instead.

@serhiy-storchaka
Copy link
Author

All uses outside of Objects/longobject.c currently require the argument to be cast to PyLongObject *. I am sure this will be the same in the user code.

When we use other pointer type than PyObject * in C, this makes the code less type safe.

@skirpichev
Copy link

Is it really worth adding three extra API functions?

As author of the PyLong_Sign() proposal, I would like to see these functions instead of the implemented PyLong_GetSign().

Original idea was to offer GMP-like interface - as mpz_sgn() - that take single PyLongObject* argument (or PyObject* and not raise errors on int's). That's just as convenient and readable as PyLong_Is* functions (at cost of one function, instead of several), e.g.:

if (PyLong_Sign(obj) < 0) {

But with PyLong_GetSign() - this is impossible.

With API like

/* Returns 1 if the object obj is negative integer, and 0 otherwise.   On failure,
   return -1 with an exception set.  This function always succeeds if obj is
   a PyLongObject or its subtype. */
int PyLong_IsNegative(PyObject obj);

most consumers could use one-liners as in the issue description and not worry about calling PyErr_Occurred().

The functions quite easy to implement using PyLong_GetSign.

That's true. But I worry that all consumers of that API will end in implementing such wrappers (or PyLong_Sign-like one).

The gmpy2 project has just one place, that will call this API and this doesn't matter. But not all C API users so lucky.

Even excluding Objects/longobject.c, there are much more use cases for these functions than for _PyLong_Sign (note that some of these cases for _PyLong_Sign can use _PyLong_IsNegative).

To me it looks, that across the whole CPython codebase - only three calls to the _PyLong_Sign() do make sense: floatobject.c, _pickle.c and sliceobject.c. All could be converted to using _PyLong_Is* without loss in readability and, I think, with minimal impact on performance.

@encukou
Copy link
Collaborator

encukou commented Jul 1, 2024

That's true. But I worry that all consumers of that API will end in implementing such wrappers (or PyLong_Sign-like one).
The gmpy2 project has just one place, that will call this API and this doesn't matter. But not all C API users so lucky.

For reference, which/how many other consumers are we talking about? I thought gmpy2 is the main user.

@serhiy-storchaka
Copy link
Author

CPython itself uses such functions much more than the sign function.

@vstinner
Copy link

vstinner commented Jul 1, 2024

If the main consumer is CPython, we can use the internal C API, no?

@serhiy-storchaka
Copy link
Author

CPython is just an example. Looking at the list of files where they are used you can see that they are usable in many different fields.

@vstinner
Copy link

vstinner commented Jul 1, 2024

As I wrote previously, if you want PyLongObject* argument, I suggest to add functions to the PyUnstable API.

@skirpichev
Copy link

I thought gmpy2 is the main user.

Probably, sage is. gmpy2 is much less popular project.

https://github.com/sagemath/sage/blob/b7dd28d2456307802ce4ddcfc692331744f9b0db/src/sage/libs/gmp/pylong.pyx#L95

CPython itself uses such functions much more than the sign function.

But for CPython sometimes _PyLong_Sign() does make sense, e.g.:
https://github.com/python/cpython/blob/92893fd8dc803ed7cdde55d29d25f84ccb5e3ef0/Modules/_pickle.c#L2127

For external projects, I think that Positive/Zero/Negative predicates will handle all use cases.

@encukou
Copy link
Collaborator

encukou commented Jul 2, 2024

Based on that link, it looks like sage uses this in combination with other private API, to import/export int data without conversion. We should add API for that whole use case, not for one part of it.

@skirpichev
Copy link

it looks like sage uses this in combination with other private API, to import/export int data without conversion.

Just as gmpy2 or everyone else.

We should add API for that whole use case, not for one part of it.

It's not easy to do this in one shot;) #31 address other parts, perhaps all.

@vstinner
Copy link

vstinner commented Sep 5, 2024

Based on that link, it looks like sage uses this in combination with other private API, to import/export int data without conversion. We should add API for that whole use case, not for one part of it.

I proposed a whole API to import-export Python int in #35.

@vstinner
Copy link

@serhiy-storchaka: You didn't reply to my previous question. Would you be ok with an unstable API with PyLongObject* parameter? It would avoid the need for error checking since we know that the argument is a Python int (or a subclass).

int PyUnstable_Long_IsPositive(PyLongObject *);
int PyUnstable_Long_IsNegative(PyLongObject *);
int PyUnstable_Long_IsZero(PyLongObject *);

@vstinner
Copy link

vstinner commented Sep 10, 2024

I would prefer a single int PyUnstable_Long_GetSign(PyLongObject *), you can use it directly in comparison:

int is_positive = (PyUnstable_Long_GetSign(obj) >= 0);
int is_zero = (PyUnstable_Long_GetSign(obj) == 0);
int is_negative = (PyUnstable_Long_GetSign(obj) < 0);

@serhiy-storchaka
Copy link
Author

No, I do not want PyLongObject * argument. It is inconvenient to use outside of longobject.c and maybe even in it. Requiring a cast to PyLongObject * at every use of this API will just make the code less safe. It should be PyObject *, as in all public C API.

PyUnstable_Long_GetSign() would add more overhead.

As for the PyUnstable_ prefix, well, if it helps to bring this API to users.

@vstinner
Copy link

It should be PyObject *, as in all public C API.

In this case, I would require the caller to handle errors if the argument is not a Python int. Which is not the API that you are asking for.

No, I do not want PyLongObject * argument. It is inconvenient to use outside of longobject.c and maybe even in it.

The idea is that the caller is responsible to do the cast and so takes the responsibility of invalid casts.

@skirpichev
Copy link

In this case, I would require the caller to handle errors if the argument is not a Python int.

I think it's fine if we document that for ints (+subtypes) - no exceptions will be raised.

@vstinner
Copy link

vstinner commented Sep 19, 2024

So what we can have are these 3 functions:

int PyLong_IsPositive(PyObject *);
int PyLong_IsNegative(PyObject *);
int PyLong_IsZero(PyObject *);
  • Returns 1 if the object obj is, respectively, positive, negative or zero.
  • Returns 0 otherwise.
  • If the argument is not a Python int, raise a TypeError and return -1.

So these functions always succeed if the argument is a Python int or its subtype.

@serhiy-storchaka @skirpichev: Would you be ok with such API?

@skirpichev
Copy link

Yep, this definitely better than PyLong_GetSign() API (which should be removed). Based on above statistics, maybe PyLong_IsNegative and PyLong_IsZero - enough.

@vstinner
Copy link

vstinner commented Oct 7, 2024

Ok, let me open a vote on these 3 functions (API):

Moreover, if we add these 3 functions, should we remove PyLong_GetSign()?

@vstinner vstinner added the vote label Oct 7, 2024
@encukou
Copy link
Collaborator

encukou commented Oct 7, 2024

I still think that projects that need these more than once or twice should implement them using PyLong_GetSign, handling the four possible results in the way that's most appropriate for them.

@vstinner
Copy link

@zooba: Would you mind to vote on these APIs?

@zooba
Copy link

zooba commented Oct 14, 2024

Done. I don't mind a soft/docs-only deprecation of GetSign, but I see no reason to remove it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants