beartype / numerary

A pure-Python codified rant aspiring to a world where numbers and types can work together.
https://posita.github.io/numerary/latest/
Other
38 stars 1 forks source link

RealLike doesn't detect a class implementing numbers.Real #14

Closed copperfield42 closed 2 years ago

copperfield42 commented 2 years ago

consider this quick and dirty example

from numerary import RealLike
from numbers import Real

class MyNumber(Real):

    def __init__(self,value=0):
        self.value = value

    def __repr__(self):
        return f"{type(self).__name__}({self.value})"

    def __float__(self):
        return type(self)(self.value)

    def __neg__(self):
        return type(self)(self.value)

    def __rtruediv__(self, otro):
        return type(self)(self.value)

    def __pos__(self):
        return type(self)(self.value)

    def __abs__(self):
        return type(self)(self.value)

    def __rmul__(self, otro):
        return type(self)(self.value)

    def __floor__(self):
        return type(self)(self.value)

    def __eq__(self, otro):
        return type(self)(self.value)

    def __floordiv__(self, otro):
        return type(self)(self.value)

    def __le__(self, otro):
        return type(self)(self.value)

    def __mul__(self, otro):
        return type(self)(self.value)

    def __radd__(self, otro):
        return type(self)(self.value)

    def __add__(self, otro):
        return type(self)(self.value)

    def __truediv__(self, otro):
        return type(self)(self.value)

    def __pow__(self, otro):
        return type(self)(self.value)

    def __rfloordiv__(self, otro):
        return type(self)(self.value)

    def __round__(self, ndigits=None):
        return type(self)(self.value)

    def __rmod__(self, otro):
        return type(self)(self.value)

    def __rpow__(self, otro):
        return type(self)(self.value)

    def __lt__(self, otro):
        return type(self)(self.value)

    def __ceil__(self):
        return type(self)(self.value)

    def __mod__(self, otro):
        return type(self)(self.value)

    def __trunc__(self):
        return type(self)(self.value)

n = MyNumber(23)

print(n)

assert isinstance(n,Real),"isn't a Real"
assert isinstance(n,RealLike),"isn't a RealLike"

gives

MyNumber(23)
Traceback (most recent call last):
  File "C:\Users\ThecnoMacVZLA\Desktop\mypy_tests\mynumber.py", line 89, in <module>
    assert isinstance(n,RealLike),"isn't a RealLike"
AssertionError: isn't a RealLike

I wonder why?

posita commented 2 years ago

I don't think you've implemented all the requisite methods of Real. Specifically, you're missing an implementation for the __hash__ method. Consider adding something like this:

class MyNumber(Real):
    # …
    def __hash__(self) -> int:
        return hash(self.value)
    # …

Mypy probably would/should have detected this for you, possibly surfacing an error like Cannot instantiate abstract class "MyNumber" with abstract attribute "__hash__" [abstract] for the line n = MyNumber(23).

copperfield42 commented 2 years ago

oh my hash :o like the abc does not ask for it never occur to my that it was missing...

posita commented 2 years ago

like the abc does not ask for it never occur to my that it was missing...

It's kind of buried. Note this (cf. this). You'll probably be hard pressed to write true numerics that aren't hash-able, but you're right: numerary's …Like protocols make defining a __hash__ method an explicit requirement. I should probably highlight that better in the docs.

copperfield42 commented 2 years ago

I see, yeah is a little buried. Went I'm done with the required method from the abc and does what I want it to do I just call it a day, and the hash thing never crossed my mind...

posita commented 2 years ago

I see, yeah is a little buried. Went I'm done with the required method from the abc and does what I want it to do I just call it a day, and the hash thing never crossed my mind...

I don't know if it's theoretically a strict requirement, but it's probably a practical one. I'll experiment with getting rid of it.

posita commented 2 years ago

To be clearer, in most cases, it's desirable to use numbers with sets. Practically speaking, that means they need to be hashable. Consider:

>>> import poly
>>> r5 = poly.constante.RadicalConstante(5)
>>> poly.constante.RadicalConstante(5) in {r5}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'RadicalConstante'
copperfield42 commented 2 years ago

I don't know if it's theoretically a strict requirement, but it's probably a practical one. I'll experiment with getting rid of it.

or you could add a new protocol that is more loose, that only check for the most basic of thing like idk, just the basic operations of + - / * and call it NumberLike

and that can be the functional equivalent to numbers.Numbers, after all Numbers itself is very loose, like extremely soo XD

>>> import numbers
>>> class N(numbers.Number):
...     pass
... 
>>> N()
<__main__.N object at 0x000001E95677FD30>
>>> isinstance(N(),numbers.Number)
True
>>> 

To be clearer, in most cases, it's desirable to use numbers with sets. Practically speaking, that means they need to be hashable.

I guess, but I made it more for symbolic calculation like the sample Fibonnaci function

def fib(n):
    """calculate the nth number in the fibbonacci sequence"""
    r5 = RadicalConstante(5) #sqrt(5)
    g  = (r5+1)//2 #golden ratio
    return (g**n - (1-g)**n)//r5

That is the Binet's formula calculated without the pesky floating point problems so the result is exact no matter how big n is, so there is no need to touch floats because you know that the square root of 5 will banish at the end.

Or other such thing and you want to keep the root around or something and you don't want it to actualize into its decimal representation, so making it hashable was the least of my concern so I never notice it until now...

posita commented 2 years ago

or you could add a new protocol that is more loose, that only check for the most basic of thing like idk, just the basic operations of + - / * and call it NumberLike

This is something numerary clients can do for themselves by imitating the …Like protocols (e.g., RealLike):

from beartype.typing import Protocol, TypeVar, runtime_checkable
from numerary.types import CachingProtocolMeta, Supports…
T_co = TypeVar("T_co", covariant=True)

@runtime_checkable
class NumberType(
  SupportsAbs[T_co],
  SupportsRealOps[T_co],
  SupportsComplexOps[T_co],
  SupportsComplexPow[T_co],
  Protocol[T_co],
  metaclass=CachingProtocolMeta,
):
  pass  # no __hash__ method required

one: NumberType = 1
assert isinstance(one, NumberType)
one = "1"  # properly results in assignment error
assert not isinstance(one, NumberType)
posita commented 2 years ago

But, in reference to your MyNumber subclass of Real example above, be aware that numbers.Number and derivatives also require a __hash__ method (cf. this).