Skip to content

Instantly share code, notes, and snippets.

@ptmcg
Last active April 6, 2026 14:41
Show Gist options
  • Select an option

  • Save ptmcg/c7c7f958790940e27800bc6421df34ef to your computer and use it in GitHub Desktop.

Select an option

Save ptmcg/c7c7f958790940e27800bc6421df34ef to your computer and use it in GitHub Desktop.
Deferred evaluation of arithmetic expressions
# Source - https://stackoverflow.com/a/7844038
# Posted by PaulMcG, modified by community. See post 'Timeline' for change history
# Retrieved 2026-04-04, License - CC BY-SA 4.0
"""
Module of classes to implement deferred evaluation of arithmetic expressions.
Based on https://stackoverflow.com/a/7844038
Example:
>>> x = Parameter()
>>> y = -x + 2*x*x + 3*x - x/2
>>> x.value = 10
>>> y.value
215.0
>>> x.value = 20
>>> y.value
830.0
>>> [(x.value, y.value) for x.value in range(5)]
[(0, 0.0), (1, 3.5), (2, 11.0), (3, 22.5), (4, 38.0)]
"""
from __future__ import annotations
import operator
from abc import ABC, abstractmethod
type Numeric = int | float
def as_expression(term: Expr | Numeric) -> Expr:
"""
Ensure the term is an expression by converting numeric values to Constant.
"""
return term if isinstance(term, Expr) else Constant(term)
def make_binary(opfn):
"""
Create a wrapper for a binary operation.
"""
def wrapper(obj: Expr, other) -> Expr:
return BinaryOp(opfn, obj, as_expression(other))
return wrapper
def make_rbinary(opfn):
"""
Create a wrapper for a reflected binary operation.
"""
def wrapper(obj: Expr, other) -> Expr:
return BinaryOp(opfn, as_expression(other), obj)
return wrapper
class Expr(ABC):
"""
Abstract base class for expressions supporting arithmetic operators.
"""
# binary operations
__add__ = make_binary(operator.add)
__sub__ = make_binary(operator.sub)
__mul__ = make_binary(operator.mul)
__mod__ = make_binary(operator.mod)
__truediv__ = make_binary(operator.truediv)
__floordiv__ = make_binary(operator.floordiv)
__pow__ = make_binary(operator.pow)
__radd__ = make_rbinary(operator.add)
__rsub__ = make_rbinary(operator.sub)
__rmul__ = make_rbinary(operator.mul)
__rmod__ = make_rbinary(operator.mod)
__rtruediv__ = make_rbinary(operator.truediv)
__rfloordiv__ = make_rbinary(operator.floordiv)
__rpow__ = make_rbinary(operator.pow)
# unary operation
def __neg__(self) -> UnaryOp:
"""
Negate the expression.
"""
return UnaryOp(operator.neg, self)
@property
def value(self) -> Numeric:
"""
Property to get the evaluated value of the expression.
"""
return self._eval()
@abstractmethod
def _eval(self) -> Numeric:
"""
Evaluate and return an intermediate value.
"""
class Constant(Expr):
"""
An expression that represents a constant value.
Evaluating a Constant merely requires returning its defined value.
"""
def __init__(self, value: Numeric = 0):
self._value = value
def _eval(self) -> Numeric:
return self._value
value = property(_eval, doc="The constant value.")
class Parameter(Constant):
"""
A constant expression whose value can be updated after definition.
"""
@property
def value(self) -> Numeric:
return self._value
@value.setter
def value(self, v: Numeric) -> None:
self._value = v
class BinaryOp(Expr):
"""
An expression representing a binary operation.
"""
def __init__(self, operation, op1, op2):
"""
Initialize the binary operation with an operator and two operands.
"""
self.opn = operation
self.op1 = op1
self.op2 = op2
def _eval(self) -> Numeric:
"""
Evaluate the binary operation by evaluating its operands and applying the operator.
"""
return self.opn(self.op1.value, self.op2.value)
class UnaryOp(Expr):
"""
An expression representing a unary operation.
"""
def __init__(self, operation, op1):
"""
Initialize the unary operation with an operator and one operand.
"""
self.opn = operation
self.op1 = op1
def _eval(self) -> Numeric:
"""
Evaluate the unary operation by evaluating its operand and applying the operator.
"""
return self.opn(self.op1.value)
if __name__ == '__main__':
x = Parameter()
y: Expr = -x + 2*x*x + 3*x - x/2
x.value = 10
print(y.value)
# prints 215
x.value = 20
print(y.value)
# prints 830
# compute a series of x-y values for the function
print([(x.value, y.value) for x.value in range(5)])
# prints [(0, 0.0), (1, 3.5), (2, 11.0), (3, 22.5), (4, 38.0)]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment