3

I am writing a class for a lazy square root, allowing me to use it in fractions for linear algebra without getting a messy float. I've run into two specific problems both related to pythons 'private' class attributes. The first is that I am getting a soft error from pycharm for accessing an unresolved class attribute. My constructor looks something like this:

class SquareRoot(Real):
    def __new__(cls, operand, coefficient=None):
        self = super().__new__(cls)
        self._value = (here i parse the value of operand)
        self._scalar = (here i parse the value of the coefficient if there is one)
        return self

Obviously a lot more happens with handling the input, and in the case that I get a non-integer, I try to return just sqrt(operand), throwing an error if this doesn't work, so my __new__() function will sometimes not return a SquareRoot object, but most often will.

My first problem is with my properties:

@property
def radicand(self):
    return self._value

Here I get a soft error about an unresolved attribute to SquareRoot._value. When I change the method to

@property
def radicand(self: 'SquareRoot'):
    return self._value

with the type hint, the error goes away. If I change the constructor so that it always returns SquareRoot types this does not change the error, so there shouldn't be a problem with the construction. From what I understand of 'private' attributes in Python, I should expect PyCharm (or any IDE) to throw a soft error if I do this:

x = SquareRoot(2)
x._value

but that I should be able to reference self._value anywhere within my class without any warnings. I have a .pyi stub file and whenever I let PyCharm try to correct my 'unresolved attribute' it tries to put an __init__() method in the stub with self._value initialized inside, so I'm wondering if somehow my stub file (which has absolutely nothing but type hints for methods) is making my .py file think its missing an attribute initialization.

My second problem is more of a concern that I have done something wrong. When I create an instance of SquareRoot like this:

x = SquareRoot(2)
print(x) # 'sqrt(2)'
SquareRoot._value = 5
print(x) # 'sqrt(5)'

I am somehow able to change the value of x._value like this, but from my understanding of class attributes, I should only be able to do this if value was a class attribute, shared across all instances, but it is created only within my __new__() method. My question here is more of: is this expected and if so, why?

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
  • 1
    I think PyCharm only looks in the `__init__` method for attributes, not `__new__` – Barmar Sep 10 '21 at 21:43
  • You're only getting this warning from PyCharm, the script runs as expected, right? – Barmar Sep 10 '21 at 21:47
  • `SquareRoot._value = 5` is possible by design. You can set any attribute at runtime in Python, even if it is not defined in the class/module/object at all – Marat Sep 10 '21 at 21:48
  • @Barmar yes the script runs as expected. all errors are soft warnings – Andrew Carpenter Sep 10 '21 at 21:56
  • Unfortunately, it doesn't seem like there's any way to tell PyCharm about attributes that are created outside the `__init__()` method. – Barmar Sep 10 '21 at 21:59
  • @Barmar that makes a lot of sense thank you. I'll probably use the type hints so its not throwing an error – Andrew Carpenter Sep 10 '21 at 22:00
  • @Marat thank you i hadnt learned that yet! – Andrew Carpenter Sep 10 '21 at 22:01
  • Is there a reason you are setting the attributes in `__new__` and *not* in `__init__`? – chepner Sep 11 '21 at 03:23
  • @chepner because my SquareRoot object is immutable and (as far as I know) ```__new__``` is preferred for immutables – Andrew Carpenter Sep 11 '21 at 03:33
  • It's not *really* immutable. `__new__` is *necessary* when you need to perform initialization via the parent class's constructor. You are adding attributes to the already constructed object; you can do that in `__init__`. – chepner Sep 11 '21 at 13:07
  • @chepner that makes a lot of sense. I thought about it and was leaning towards using ```__init__``` but I remembered a big reason I need ```__new__``` is because in situations where I get an irrational coefficient I want to return a float (equivalent of just using my SquareRoot object as ```math.sqrt()``` ) which I can't do with ```__init__``` but you're right I'm not actually doing anything with the constructor – Andrew Carpenter Sep 12 '21 at 05:41
  • Another question: what exactly does `SquareRoot` provide that an ordinary `float` doesn't? *Every* `float` is the square root of some other value (precision issues aside). Your class should encapsulate some behavior that doesn't already exist. For example, you might define an `Radical` class that stores two integers, an index `n` and a radicand `r` such that `Radical(r=3, n=2)` reprenstents the square root of 3 *exactly*. This class could override `__add__` et al. to provide exact arithmetic operations on `Radicals`. – chepner Sep 12 '21 at 13:29
  • @chepner I need a lazy square root so that I can put it in the numerator of a fraction. If it's evaluated as a float it's irrational and cannot be put into a fraction. I need this mostly for orthonormalized bases in linear algebra. And yes my class overrides all operators (although I don't override ```pow(base, power, mod)``` since mod is pretty tricky with irrationals). – Andrew Carpenter Sep 12 '21 at 14:59

0 Answers0