Repository: numpy/numpy

Issue:
numpy.poly1d.__eq__ method fails with AttributeError
I called the `inspect.signature` function on a `numpy.poly1d` object:
```python
import numpy as np
import inspect
poly = np.poly1d([1, 2, 3])
print(inspect.signature(poly))
```
and got an unexpected exception:
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/python3.5/inspect.py", line 2987, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "/python3.5/inspect.py", line 2737, in from_callable
    follow_wrapper_chains=follow_wrapped)
  File "/python3.5/inspect.py", line 2228, in _signature_from_callable
    if _signature_is_builtin(obj):
  File "/python3.5/inspect.py", line 1785, in _signature_is_builtin
    obj in (type, object))
  File "/python3.5/site-packages/numpy/lib/polynomial.py", line 1203, in __eq__
    if self.coeffs.shape != other.coeffs.shape:
AttributeError: type object 'type' has no attribute 'coeffs'
```

So `numpy.poly1d` objects can be compared only to objects with special attributes structure.

The current`__eq__` implementation from [v.1.12.x](https://github.com/numpy/numpy/blob/maintenance/1.12.x/numpy/lib/polynomial.py#L1201-L1204):
```python
def __eq__(self, other):
    if self.coeffs.shape != other.coeffs.shape:
        return False
    return (self.coeffs == other.coeffs).all()
```

numpy version 1.12.0


Discussion:
That should probably have a `if not isinstance(other, poly1d): return NotImplemented`. In fact, a whole bunch of the `__(.*)__` methods should have those lines
I think `if not isinstance(other, poly1d): return False` is better.
With `raise NotImplemented` we still will have the same problem.

Is there any reason to not return False in such cases?
> With raise NotImplemented we still will have the same problem.

@bondarevts: Indeed, that is why I suggested `return NotImplemented` and not `raise NotImplementedError`. `NotImplemented` here just means "I have no idea, ask the other guy", and does not result in an error

> Indeed, that is why I suggested return NotImplemented

@eric-wieser You're right. I read it wrong. Thank you for the clarification.

Resolving patch:
diff --git a/numpy/lib/polynomial.py b/numpy/lib/polynomial.py
--- a/numpy/lib/polynomial.py
+++ b/numpy/lib/polynomial.py
@@ -1036,17 +1036,47 @@ class poly1d(object):
     poly1d([ 1, -3,  2])
 
     """
-    coeffs = None
-    order = None
-    variable = None
     __hash__ = None
 
-    def __init__(self, c_or_r, r=0, variable=None):
+    @property
+    def coeffs(self):
+        """ The polynomial coefficients """
+        return self._coeffs
+
+    @property
+    def variable(self):
+        """ The name of the polynomial variable """
+        return self._variable
+
+    # calculated attributes
+    @property
+    def order(self):
+        """ The order or degree of the polynomial """
+        return len(self._coeffs) - 1
+
+    @property
+    def roots(self):
+        """ The roots of the polynomial, where self(x) == 0 """
+        return roots(self._coeffs)
+
+    # alias attributes
+    r = roots
+    c = coef = coefficients = coeffs
+    o = order
+
+    def __init__(self, c_or_r, r=False, variable=None):
         if isinstance(c_or_r, poly1d):
-            for key in c_or_r.__dict__.keys():
-                self.__dict__[key] = c_or_r.__dict__[key]
+            self._variable = c_or_r._variable
+            self._coeffs = c_or_r._coeffs
+
+            if set(c_or_r.__dict__) - set(self.__dict__):
+                msg = ("In the future extra properties will not be copied "
+                       "across when constructing one poly1d from another")
+                warnings.warn(msg, FutureWarning, stacklevel=2)
+                self.__dict__.update(c_or_r.__dict__)
+
             if variable is not None:
-                self.__dict__['variable'] = variable
+                self._variable = variable
             return
         if r:
             c_or_r = poly(c_or_r)
@@ -1056,11 +1086,10 @@ def __init__(self, c_or_r, r=0, variable=None):
         c_or_r = trim_zeros(c_or_r, trim='f')
         if len(c_or_r) == 0:
             c_or_r = NX.array([0.])
-        self.__dict__['coeffs'] = c_or_r
-        self.__dict__['order'] = len(c_or_r) - 1
+        self._coeffs = c_or_r
         if variable is None:
             variable = 'x'
-        self.__dict__['variable'] = variable
+        self._variable = variable
 
     def __array__(self, t=None):
         if t:
@@ -1199,29 +1228,17 @@ def __rdiv__(self, other):
     __rtruediv__ = __rdiv__
 
     def __eq__(self, other):
+        if not isinstance(other, poly1d):
+            return NotImplemented
         if self.coeffs.shape != other.coeffs.shape:
             return False
         return (self.coeffs == other.coeffs).all()
 
     def __ne__(self, other):
+        if not isinstance(other, poly1d):
+            return NotImplemented
         return not self.__eq__(other)
 
-    def __setattr__(self, key, val):
-        raise ValueError("Attributes cannot be changed this way.")
-
-    def __getattr__(self, key):
-        if key in ['r', 'roots']:
-            return roots(self.coeffs)
-        elif key in ['c', 'coef', 'coefficients']:
-            return self.coeffs
-        elif key in ['o']:
-            return self.order
-        else:
-            try:
-                return self.__dict__[key]
-            except KeyError:
-                raise AttributeError(
-                    "'%s' has no attribute '%s'" % (self.__class__, key))
 
     def __getitem__(self, val):
         ind = self.order - val
@@ -1237,10 +1254,9 @@ def __setitem__(self, key, val):
             raise ValueError("Does not support negative powers.")
         if key > self.order:
             zr = NX.zeros(key-self.order, self.coeffs.dtype)
-            self.__dict__['coeffs'] = NX.concatenate((zr, self.coeffs))
-            self.__dict__['order'] = key
+            self._coeffs = NX.concatenate((zr, self.coeffs))
             ind = 0
-        self.__dict__['coeffs'][ind] = val
+        self._coeffs[ind] = val
         return
 
     def __iter__(self):


Test patch:
diff --git a/numpy/lib/tests/test_polynomial.py b/numpy/lib/tests/test_polynomial.py
--- a/numpy/lib/tests/test_polynomial.py
+++ b/numpy/lib/tests/test_polynomial.py
@@ -213,6 +213,15 @@ def test_poly_int_overflow(self):
         v = np.arange(1, 21)
         assert_almost_equal(np.poly(v), np.poly(np.diag(v)))
 
+    def test_poly_eq(self):
+        p = np.poly1d([1, 2, 3])
+        p2 = np.poly1d([1, 2, 4])
+        assert_equal(p == None, False)
+        assert_equal(p != None, True)
+        assert_equal(p == p, True)
+        assert_equal(p == p2, False)
+        assert_equal(p != p2, True)
+
 
 if __name__ == "__main__":
     run_module_suite()