Repository: chainer/chainer

Issue:
The behavior of Function#parameters= is inconsistent with FunctionSet#parameters=
At aaf2aae14ad683734964590e6ceee9d775d9d7c5, an assertion was introduced into `Function#parameters=`, which requires the argument `values` to be a list-like object (implementing `__len__` method).
However, this assertion seems to be inconsistent with the behavior of `FunctionSet#parameters=`.
`FunctionSet#parameters=` passes the iterator of its argument `params` to its child functions through `Function#parameters=`, and it causes a TypeError exception.

``` python
from chainer import FunctionSet
import chainer.functions as F
func_set = FunctionSet(f=F.Linear(10,10))
params = func_set.parameters
func_set.parameters = params # TypeError: object of type 'tuple_iterator' has no len()
```



Discussion:

Resolving patch:
diff --git a/chainer/function_set.py b/chainer/function_set.py
--- a/chainer/function_set.py
+++ b/chainer/function_set.py
@@ -126,9 +126,16 @@ def parameters(self):
 
     @parameters.setter
     def parameters(self, params):
-        param_iter = iter(params)
-        for _, func in self._get_sorted_funcs():
-            func.parameters = param_iter
+        funcs = self._get_sorted_funcs()
+
+        expected_params_size = sum(len(func.parameters) for _, func in funcs)
+        assert expected_params_size == len(params)
+
+        idx = 0
+        for _, func in funcs:
+            param_size = len(func.parameters)
+            func.parameters = params[idx: idx + param_size]
+            idx += param_size
 
     @property
     def gradients(self):
@@ -142,9 +149,16 @@ def gradients(self):
 
     @gradients.setter
     def gradients(self, grads):
-        grad_iter = iter(grads)
-        for _, func in self._get_sorted_funcs():
-            func.gradients = grad_iter
+        funcs = self._get_sorted_funcs()
+
+        expected_grads_size = sum(len(func.gradients) for _, func in funcs)
+        assert expected_grads_size == len(grads)
+
+        idx = 0
+        for _, func in funcs:
+            grad_size = len(func.gradients)
+            func.gradients = grads[idx: idx + grad_size]
+            idx += grad_size
 
     def _get_sorted_funcs(self):
         return sorted(


Test patch:
diff --git a/tests/chainer_tests/test_function_set.py b/tests/chainer_tests/test_function_set.py
--- a/tests/chainer_tests/test_function_set.py
+++ b/tests/chainer_tests/test_function_set.py
@@ -21,7 +21,31 @@ def __init__(self, shape):
     gradient_names = ('gp', )
 
 
-class TestNestedFunctionSet(unittest.TestCase):
+class TestFunctionSetBase(unittest.TestCase):
+
+    def _check_setter(self, fs, gpu, attribute):
+        expect = getattr(fs, attribute)
+        setattr(fs, attribute, expect)
+        actual = getattr(fs, attribute)
+        if gpu:
+            expect = tuple(p.get() for p in expect)
+            actual = tuple(p.get() for p in actual)
+
+        self.assertEqual(len(expect), len(actual))
+        for e, a in zip(expect, actual):
+            np.testing.assert_array_equal(e, a)
+
+    def _check_setter_invalid(self, fs, diff, xp, attribute):
+        if diff > 0:
+            values = getattr(fs, attribute) + (xp.empty((1,)),) * diff
+        else:
+            values = getattr(fs, attribute)[:diff]
+
+        with self.assertRaises(AssertionError):
+            setattr(fs, attribute, values)
+
+
+class TestNestedFunctionSet(TestFunctionSetBase):
 
     def setUp(self):
         self.fs1 = chainer.FunctionSet(
@@ -30,22 +54,22 @@ def setUp(self):
             fs1=self.fs1,
             b=MockFunction((3, 4)))
 
+        self.p_b = np.zeros((3, 4)).astype(np.float32)
+        self.p_a = np.zeros((1, 2)).astype(np.float32)
+        self.gp_b = np.ones((3, 4)).astype(np.float32)
+        self.gp_a = np.ones((1, 2)).astype(np.float32)
+
     def test_get_sorted_funcs(self):
         six.assertCountEqual(
             self, [k for (k, v) in self.fs2._get_sorted_funcs()], ('b', 'fs1'))
 
     def test_collect_parameters(self):
-        p_b = np.zeros((3, 4)).astype(np.float32)
-        p_a = np.zeros((1, 2)).astype(np.float32)
-        gp_b = np.ones((3, 4)).astype(np.float32)
-        gp_a = np.ones((1, 2)).astype(np.float32)
-
         actual = (self.fs2.parameters, self.fs2.gradients)
         self.assertTrue(list(map(len, actual)) == [2, 2])
-        self.assertTrue((actual[0][0] == p_b).all())
-        self.assertTrue((actual[0][1] == p_a).all())
-        self.assertTrue((actual[1][0] == gp_b).all())
-        self.assertTrue((actual[1][1] == gp_a).all())
+        self.assertTrue((actual[0][0] == self.p_b).all())
+        self.assertTrue((actual[0][1] == self.p_a).all())
+        self.assertTrue((actual[1][0] == self.gp_b).all())
+        self.assertTrue((actual[1][1] == self.gp_a).all())
 
     def test_pickle_cpu(self):
         fs2_serialized = pickle.dumps(self.fs2)
@@ -64,14 +88,102 @@ def test_pickle_gpu(self):
         self.assertTrue((self.fs2.b.p == fs2_loaded.b.p).all())
         self.assertTrue((self.fs2.fs1.a.p == fs2_loaded.fs1.a.p).all())
 
+    def check_getter(self, fs, gpu, attribute):
+        params = getattr(fs, attribute)
+        self.assertEqual(len(params), 2)
+        if gpu:
+            params = tuple(p.get() for p in params)
+        if attribute == 'parameters':
+            np.testing.assert_equal(params[0], self.p_b)
+            np.testing.assert_equal(params[1], self.p_a)
+        elif attribute == 'gradients':
+            np.testing.assert_equal(params[0], self.gp_b)
+            np.testing.assert_equal(params[1], self.gp_a)
+        else:
+            raise ValueError(
+                'attribute should be parameters or gradients')
+
+    def test_parameters_getter_cpu(self):
+        self.check_getter(self.fs2, False, 'parameters')
+
+    @attr.gpu
+    def test_parameters_getter_gpu(self):
+        self.fs2.to_gpu()
+        self.check_getter(self.fs2, True, 'parameters')
+
+    def test_parameters_setter_cpu(self):
+        self._check_setter(self.fs2, False, 'parameters')
+
+    @attr.gpu
+    def test_parameters_setter_gpu(self):
+        self.fs2.to_gpu()
+        self._check_setter(self.fs2, True, 'parameters')
+
+    def test_parameters_setter_invalid_cpu(self):
+        self._check_setter_invalid(self.fs2, 1, np, 'parameters')
+
+    @attr.gpu
+    def test_parameters_setter_invalid_gpu(self):
+        self.fs2.to_gpu()
+        self._check_setter_invalid(self.fs2, 1, cuda.cupy, 'parameters')
+
+    def test_parameters_setter_invalid_2_cpu(self):
+        self._check_setter_invalid(self.fs2, -1, np, 'parameters')
+
+    @attr.gpu
+    def test_parameters_setter_invalid_2_gpu(self):
+        self.fs2.to_gpu()
+        self._check_setter_invalid(self.fs2, -1, cuda.cupy, 'parameters')
+
+    def test_gradients_getter_cpu(self):
+        self.check_getter(self.fs2, False, 'gradients')
+
+    @attr.gpu
+    def test_gradients_getter_gpu(self):
+        self.fs2.to_gpu()
+        self.check_getter(self.fs2, True, 'gradients')
+
+    def test_gradients_setter_cpu(self):
+        self._check_setter(self.fs2, False, 'gradients')
+
+    @attr.gpu
+    def test_gradients_setter_gpu(self):
+        self.fs2.to_gpu()
+        self._check_setter(self.fs2, True, 'gradients')
+
+    def test_gradients_setter_invalid_cpu(self):
+        self._check_setter_invalid(self.fs2, 1, np, 'gradients')
+
+    @attr.gpu
+    def test_gradients_setter_invalid_gpu(self):
+        self.fs2.to_gpu()
+        self._check_setter_invalid(self.fs2, 1, cuda.cupy, 'gradients')
+
+    def test_gradients_setter_invalid_2_cpu(self):
+        self._check_setter_invalid(self.fs2, -1, np, 'gradients')
+
+    @attr.gpu
+    def test_gradients_setter_invalid_2_gpu(self):
+        self.fs2.to_gpu()
+        self._check_setter_invalid(self.fs2, -1, cuda.cupy, 'gradients')
 
-class TestFunctionSet(unittest.TestCase):
+
+class TestFunctionSet(TestFunctionSetBase):
 
     def setUp(self):
         self.fs = chainer.FunctionSet(
             a=F.Linear(3, 2),
             b=F.Linear(3, 2)
         )
+        self.aW = self.fs.a.W
+        self.ab = self.fs.a.b
+        self.bW = self.fs.b.W
+        self.bb = self.fs.b.b
+
+        self.agW = self.fs.a.gW
+        self.agb = self.fs.a.gb
+        self.bgW = self.fs.b.gW
+        self.bgb = self.fs.b.gb
 
     def test_get_sorted_funcs(self):
         six.assertCountEqual(
@@ -146,5 +258,81 @@ def test_getitem_notfoud(self):
         with self.assertRaises(AttributeError):
             self.fs['not_found']
 
+    def check_getter(self, fs, gpu, attribute):
+        params = getattr(fs, attribute)
+        self.assertEqual(len(params), 4)
+        if gpu:
+            params = tuple(p.get() for p in params)
+
+        if attribute == 'parameters':
+            np.testing.assert_array_equal(params[0], self.aW)
+            np.testing.assert_array_equal(params[1], self.ab)
+            np.testing.assert_array_equal(params[2], self.bW)
+            np.testing.assert_array_equal(params[3], self.bb)
+        elif attribute == 'gradients':
+            np.testing.assert_array_equal(params[0], self.agW)
+            np.testing.assert_array_equal(params[1], self.agb)
+            np.testing.assert_array_equal(params[2], self.bgW)
+            np.testing.assert_array_equal(params[3], self.bgb)
+
+    def test_parameters_getter_cpu(self):
+        self.check_getter(self.fs, False, 'parameters')
+
+    @attr.gpu
+    def test_parameters_getter_gpu(self):
+        self.fs.to_gpu()
+        self.check_getter(self.fs, True, 'parameters')
+
+    def test_parameters_setter_cpu(self):
+        self._check_setter(self.fs, False, 'parameters')
+
+    @attr.gpu
+    def test_parameters_setter_gpu(self):
+        self.fs.to_gpu()
+        self._check_setter(self.fs, True, 'parameters')
+
+    def test_parameters_setter_invalid_cpu(self):
+        self._check_setter_invalid(self.fs, 1, np, 'parameters')
+
+    @attr.gpu
+    def test_parameters_setter_invalid_gpu(self):
+        self._check_setter_invalid(self.fs, 1, cuda.cupy, 'parameters')
+
+    def test_parameters_setter_invalid_2_cpu(self):
+        self._check_setter_invalid(self.fs, -1, np, 'parameters')
+
+    @attr.gpu
+    def test_parameters_setter_invalid_2_gpu(self):
+        self._check_setter_invalid(self.fs, -1, cuda.cupy, 'parameters')
+
+    def test_gradients_getter_cpu(self):
+        self.check_getter(self.fs, False, 'gradients')
+
+    @attr.gpu
+    def test_gradients_getter_gpu(self):
+        self.fs.to_gpu()
+        self.check_getter(self.fs, True, 'gradients')
+
+    def test_gradients_setter_cpu(self):
+        self._check_setter(self.fs, False, 'gradients')
+
+    @attr.gpu
+    def test_gradients_setter_gpu(self):
+        self.fs.to_gpu()
+        self._check_setter(self.fs, True, 'gradients')
+
+    def test_gradients_setter_invalid_cpu(self):
+        self._check_setter_invalid(self.fs, 1, np, 'gradients')
+
+    @attr.gpu
+    def test_gradients_setter_invalid_gpu(self):
+        self._check_setter_invalid(self.fs, 1, cuda.cupy, 'gradients')
+
+    def test_gradients_setter_invalid_2_cpu(self):
+        self._check_setter_invalid(self.fs, -1, np, 'gradients')
+
+    @attr.gpu
+    def test_gradients_setter_invalid_2_gpu(self):
+        self._check_setter_invalid(self.fs, -1, cuda.cupy, 'gradients')
 
 testing.run_module(__name__, __file__)