PR #30694: Fail when np.array and len are called on Tensors.

Imported from GitHub PR #30694

When np.array is called on a symbolic Tensor the result is a `shape=()` numpy array of objects which is basically never what was intended. Similarly, length is not defined and can lead to rather cryptic error messages. This surfaced through #28619; however the fact is that accidentally passing a Tensor rather than and EagerTensor to a package in the NumPy ecosystem can result in very cryptic error messages.

This PR simply makes Tensors fail with clear error messages in such cases. (Similar to the treatment of `__iter__`)

Copybara import of the project:

  - 134b5b3155a839824616626b1f4dc101512cc89f Fail when np.array and len are called on Tensors. by Taylor Robie <taylorrobie@google.com>
  - 1daa5d714de83cf6f1bd37a6fd83deec5347c51a update autograph test by Taylor Robie <taylorrobie@google.com>
  - e01a1ec7f0ddaa274b5fbc49ef62fe860eb04268 fix tests and remove try-catch on self.name by Taylor Robie <taylorrobie@google.com>
  - d32883fe8b7040d7d8f72f00a46e9ee58fef72f4 switch __len__ to an AttributeError by Taylor Robie <taylorrobie@google.com>
  - c43597adb5e2d873339ca9b9059a1e23092adf51 better test graph isolation by Taylor Robie <taylorrobie@google.com>
  - 6e69d651061262b0f4ab9a39ae79d4461ea29916 minor adjustments to tensor checking logic by Taylor Robie <taylorrobie@google.com>
  - 112da8fb8bbaf9a6e5ae1545334236845f3e796c address reviewer comments by Taylor Robie <taylorrobie@google.com>
  - 3cadc8fa89de65a5f3af796e5e84fd37991772dc Merge 112da8fb8bbaf9a6e5ae1545334236845f3e796c into 699e9... by Taylor Robie <taylorrobie@google.com>

COPYBARA_INTEGRATE_REVIEW=https://github.com/tensorflow/tensorflow/pull/30694 from robieta:fix/raise_array_error 112da8fb8bbaf9a6e5ae1545334236845f3e796c
PiperOrigin-RevId: 260994937
This commit is contained in:
Taylor Robie 2019-07-31 14:08:06 -07:00 committed by TensorFlower Gardener
parent 2cc96948e4
commit ebca088d52
6 changed files with 44 additions and 7 deletions

View File

@ -510,8 +510,7 @@ class ApiTest(test.TestCase):
opts = converter.ConversionOptions(internal_convert_user_code=False)
# f should not be converted, causing len to error out.
with self.assertRaisesRegexp(Exception,
'object of type \'Tensor\' has no len()'):
with self.assertRaisesRegexp(Exception, 'len is not well defined'):
api.converted_call(f, opts, (constant_op.constant([0]),), {})
# len on the other hand should work fine.

View File

@ -728,6 +728,15 @@ class Tensor(_TensorLike):
# with ndarrays.
__array_priority__ = 100
def __array__(self):
raise NotImplementedError("Cannot convert a symbolic Tensor ({}) to a numpy"
" array.".format(self.name))
def __len__(self):
raise TypeError("len is not well defined for symbolic Tensors. ({}) "
"Please call `x.shape` rather than `len(x)` for "
"shape information.".format(self.name))
@staticmethod
def _override_operator(operator, func):
_override_helper(Tensor, operator, func)

View File

@ -188,6 +188,25 @@ class TensorAndShapeTest(test_util.TensorFlowTestCase):
r"\(op: 'Add(V2)?'\) with input shapes: \[1,2,3\], \[4,5,6\]."):
_ = a + b
def testNumpyArray(self):
with ops.Graph().as_default():
x = array_ops.ones((3, 4), name="test_ones")
with self.assertRaisesRegexp(NotImplementedError,
r"Cannot convert a symbolic.+test_ones"):
np.array(x)
with self.assertRaisesRegexp(TypeError, "not well defined.+test_ones"):
len(x)
# EagerTensors should still behave as numpy arrays.
with context.eager_mode():
x = array_ops.ones((3, 4))
self.assertAllEqual(x, np.ones((3, 4)))
self.assertAllEqual(np.array(x), np.ones((3, 4)))
self.assertEqual(len(x), 3)
class IndexedSlicesTest(test_util.TensorFlowTestCase):

View File

@ -333,6 +333,11 @@ def _AssertCompatible(values, dtype):
def _is_array_like(obj): # pylint: disable=invalid-name
"""Check if a given object is array-like."""
if isinstance(obj, ops.Tensor) and not isinstance(obj, ops._EagerTensorBase): # pylint: disable=protected-access
# Tensor implements __array__ only so it can inform the user that it is not
# a valid array.
return False
# TODO(slebedev): an object could also implement C-level array interface.
if (callable(getattr(obj, "__array__", None)) or
isinstance(getattr(obj, "__array_interface__", None), dict)):

View File

@ -475,9 +475,14 @@ def standardize_input_data(data,
Raises:
ValueError: in case of improperly formatted user-provided data.
"""
try:
data_len = len(data)
except TypeError:
# For instance if data is `None` or a symbolic Tensor.
data_len = None
if not names:
if (data is not None and hasattr(data, '__len__') and len(data) and
not isinstance(data, dict)):
if data_len and not isinstance(data, dict):
raise ValueError(
'Error when checking model ' + exception_prefix + ': '
'expected no data, but got:', data)

View File

@ -2602,10 +2602,10 @@ def conv_transpose(input, # pylint: disable=redefined-builtin
"""
with ops.name_scope(name, "conv_transpose",
[input, filter, output_shape]) as name:
if isinstance(output_shape, collections.Sized):
n = len(output_shape) - 2
elif isinstance(output_shape, ops.Tensor):
if tensor_util.is_tensor(output_shape):
n = output_shape.shape[0] - 2
elif isinstance(output_shape, collections.Sized):
n = len(output_shape) - 2
else:
raise ValueError("output_shape must be a tensor or sized collection.")