Skip to content

Return NotImplemented for Expr and GenExpr operators to simplify#1182

Open
Zeroto521 wants to merge 29 commits intoscipopt:masterfrom
Zeroto521:expr/notimplemented
Open

Return NotImplemented for Expr and GenExpr operators to simplify#1182
Zeroto521 wants to merge 29 commits intoscipopt:masterfrom
Zeroto521:expr/notimplemented

Conversation

@Zeroto521
Copy link
Contributor

Python prefers to use NotImplemented to handle the unknown input types. It's clear.

Here is an example.
A.__add__ can only receive int type. But a + b can work. When Python calls A.__add__(B), it returns NotImplemented. Then Python will call B.__radd__(A). If both of them are NotImplemented, Python will raise a TypeError.

So we use NotImplemented to simplify Expr, GenExpr, and MatrixExpr.

from __future__ import annotations


class A:
    def __init__(self, value: int):
        self.value = value

    def __add__(self, other: int) -> int:
        if not isinstance(other, int):
            return NotImplemented
        return self.value + other


class B:
    def __init__(self, value: int):
        self.value = value

    def __add__(self, other: A) -> B:
        if not isinstance(other, A):
            return NotImplemented
        return self.value + other.value

    def __radd__(self, other: A) -> int:
        if not isinstance(other, A):
            return NotImplemented
        return self.value + other.value


if __name__ == "__main__":
    a = A(5)
    b = B(10)

    print(a + 3)  # print 8
    print(a + b)  # print 15
    print(a + "test")  # raises TypeError
# Traceback (most recent call last):
#   File "test.py", line 35, in <module>
#     print(a + "test")  # raises TypeError
#           ~~^~~~~~~~
# TypeError: unsupported operand type(s) for +: 'A' and 'str'

Zeroto521 and others added 19 commits January 22, 2026 13:09
Improves the _expr_richcmp function by using explicit type checks, handling numpy arrays and numbers more robustly, and leveraging Python C API comparison constants. This refactor enhances error handling and code readability, and ensures unsupported types raise clear exceptions.
Replaces repeated isinstance checks for numeric types with a shared NUMBER_TYPES tuple. This improves maintainability and consistency in type checking within _expr_richcmp and related code.
Modified __mul__ and __add__ methods in Expr and GenExpr classes to return NotImplemented when the operand is not an instance of EXPR_OP_TYPES, improving type safety and operator behavior.
Replaces the explicit type tuple with EXPR_OP_TYPES in the type check for 'other' in _expr_richcmp, raising TypeError for unsupported types. This improves maintainability and consistency in type validation.
Enhanced type validation in expression operator overloads by returning NotImplemented for unsupported types and raising TypeError in buildGenExprObj for invalid inputs. Removed special handling for numpy arrays and simplified code paths for better maintainability and clearer error reporting.
Simplified the implementation of __truediv__ and __rtruediv__ by directly using the division operator instead of calling __truediv__ explicitly on generated expression objects.
Replaces manual timing with the timeit module for more accurate performance measurement in matrix operation tests. Updates assertions to require the optimized implementation to be at least 25% faster, and reduces test parameterization to n=100 for consistency.
Replaces random matrix generation with a stacked matrix of zeros and ones in test_matrix_dot_performance to provide more controlled test data.
Replaces the custom _is_number function with isinstance checks against NUMBER_TYPES for improved clarity and consistency in type checking throughout expression operations.
Simplifies type checking and arithmetic operations in Expr and GenExpr classes by removing unnecessary float conversions and reordering type checks. Also improves error handling for exponentiation and constraint comparisons.
Documented that Expr and GenExpr now return NotImplemented when they cannot handle other types in calculations.
Replaces EXPR_OP_TYPES with GENEXPR_OP_TYPES in GenExpr and related functions to distinguish between Expr and GenExpr operations. Removes special handling for GenExpr in Expr arithmetic methods, simplifying type logic and improving consistency.
Clarified the changelog note to specify that NotImplemented is returned for Expr and GenExpr operators when they can't handle input types in calculations.
Corrects error messages in Expr and GenExpr exponentiation to display the correct variable. Removes an unnecessary assertion in Model and replaces a call to _is_number with isinstance for type checking in readStatistics.
Replaces EXPR_OP_TYPES with GENEXPR_OP_TYPES in the type check to ensure correct type validation in the _expr_richcmp method.
Comment on lines -93 to -96
elif isinstance(other, np.ndarray):
return _expr_richcmp(other, self, 2)
Copy link
Contributor Author

@Zeroto521 Zeroto521 Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MatrixExpr can handle a vector and a scalar well.

Comment on lines -174 to -212
elif isinstance(expr, np.ndarray):
GenExprs = np.empty(expr.shape, dtype=object)
for idx in np.ndindex(expr.shape):
GenExprs[idx] = buildGenExprObj(expr[idx])
return GenExprs.view(MatrixExpr)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MatrixExpr is a vector container. It could take care of vector and scalar.
Expr and GenExpr should only care about scalar types.

Casts the exponent base to float in GenExpr's __pow__ method to prevent type errors when reformulating expressions using log and exp.
@Zeroto521 Zeroto521 changed the title Return NotImplemented for Expr and GenExpr operators Return NotImplemented for Expr and GenExpr operators to simplify Jan 31, 2026
Explicitly converts 'other' to float in the exponentiation method to avoid type errors when using non-float numeric types.
The _is_number symbol was removed from the list of incomplete stubs in scip.pyi, likely because it is no longer needed or has been implemented.
Ensures that multiplication of Expr by numeric types consistently uses float conversion, preventing potential type errors when multiplying terms.
Copilot AI review requested due to automatic review settings February 1, 2026 02:28
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors operator overloading in Expr and GenExpr classes to follow Python's standard protocol by returning NotImplemented for unsupported operand types instead of explicitly checking types and raising errors. This allows Python's reflection mechanism to properly handle mixed-type operations.

Changes:

  • Removed the _is_number() helper function and replaced it with explicit isinstance() checks against new type tuples (NUMBER_TYPES, EXPR_OP_TYPES, GENEXPR_OP_TYPES)
  • Modified operators (__add__, __iadd__, __mul__, __truediv__, __rtruediv__, __rpow__) to return NotImplemented for unsupported types
  • Updated _expr_richcmp() to return NotImplemented for numpy arrays
  • Simplified buildGenExprObj() by removing numpy array handling and adding explicit type validation
  • Updated type annotations to clarify expected input types

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/pyscipopt/scip.pyi Removed _is_number import from type stub
src/pyscipopt/scip.pxi Removed assertion using _is_number in setObjective(), replaced _is_number() call with isinstance() check in readStatistics() (but incorrectly)
src/pyscipopt/expr.pxi Main refactoring: removed _is_number() function, added type tuples, updated all operator methods to return NotImplemented, simplified type handling logic
CHANGELOG.md Documented the change
Comments suppressed due to low confidence (1)

src/pyscipopt/expr.pxi:245

  • The __pow__ method should return NotImplemented for unsupported types before attempting to call float(other). Currently, if an invalid type is passed, it will raise a TypeError from float() instead of allowing Python to try the reflected operation on the other operand. Add a type check at the beginning of the method similar to other operators.
    def __pow__(self, other, modulo):
        if float(other).is_integer() and other >= 0:
            exp = int(other)
        else: # need to transform to GenExpr
            return buildGenExprObj(self)**other

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Refactored the value assignment in readStatistics to use a try-except block for float conversion, ensuring non-numeric values are handled gracefully. This simplifies the logic and improves robustness when parsing statistics.
Fix a bug where the method returned the original 'terms' variable instead of the newly built 'res' dictionary. The change ensures Expr is constructed from the computed aggregation (res), preventing stale/incorrect expression data after the operation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants