Repository: chainer/chainer

Issue:
A concern about deprecating link-adding interfaces except init_scope
From Chainer v2, `init_scope` is introduced as a new interface to add parameters and child links. While I think this interface is nice in a sense that it helps IDEs and users understand they can be used as attributes, I have a concern about deprecating the old interfaces: `Chain.__call__` and `add_link`.

Let's say I want to wrap two links to make a single link.
Old interface:
```
chain = chainer.Chain(a=LinkA(), b=LinkB())
```

New interface:
```
chain = chainer.Chain()
with chain.init_scope():
    chain.a = LinkA()
    chain.b = LinkB()
```
It's lengthy and unnecessarily stateful.

Another example is to construct a Chain when child links and names are determined at runtime. I guess in this situation IDEs cannot infer attributes anyway.
Old interface:
```
chain = chainer.Chain(**children)  # children is a dict holding child links by their names
# or
chain = chainer.Chain()
for name, link in children:
    chain.add_link(name, link)
```

New interface:
```
chain = chainer.Chain()
with chain.init_scope():
    for name, link in children:
        setattr(chain, name, link)
```
I feel `setattr` is less readable to humans than `add_link`.

I guess Chain is mainly designed to be used via inheritance, not by direct instantiation, but defining a new class for just wrapping other links seems overkill to me. I want it to work as a container as well.

I don't mean Chainer should stop deprecating the old interfaces, as I guess the developer team already had discussions on it. I just want to show my concern and hear opinions on it from other users as well as from the developer team.


Discussion:
Recently I have been thinking about the same thing (on `add_link` vs `setattr`) and actually about to raise an issue.
`setattr` is not only less readable but also cannot detect unintended value type.
I'm +1 for reviving it.

Same for `Link.add_param`, but with single `Parameter` argument (we can still preserve compatibility).
When we discussed deprecating the old interface, we did not consider the container-style usage (and only considered the OOP-style one). I understand it turns out that sometimes OOP style model definition is overkill (or simply does not fit to the model design well), in which case the container-style definition is more natural, so it makes sense to reconsider the old interface. We should be aware of the difference in recommended situations to use either of them and it should be clearly noted in the documentation.

One note on un-deprecating the interface: I think `Chain.__init__` should be improved so that it also accepts parameters to register (like `setattr` inside `init_scope` does) so that users do not have to use `Link` and `Chain` differently (I think most users will then just use `Chain`, which is simple and easier to understand).
This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 30 days if no further activity occurs. Thank you for your contributions.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 30 days if no further activity occurs. Thank you for your contributions.


Resolving patch:
diff --git a/chainer/link.py b/chainer/link.py
--- a/chainer/link.py
+++ b/chainer/link.py
@@ -120,11 +120,12 @@ def forward(self, x):
        forward propagation.
 
     Args:
-        params: *(deprecated since v2.0.0)* Names, shapes, and optional dtypes
-            of initial parameters. The keywords are used as the parameter
-            names and the corresponding values consist either of the shape or
-            a tuple of shape and a dtype ``(shape, dtype)``. If only the shape
-            is supplied, the default dtype will be used.
+        params:
+            Names, shapes, and optional dtypes of initial parameters.
+            The keywords are used as the parameter names and the corresponding
+            values consist either of the shape or a tuple of shape and a dtype
+            ``(shape, dtype)``.
+            If only the shape is supplied, the default dtype will be used.
 
     Attributes:
         name (str): Name of this link, given by the parent chain (if exists).
@@ -142,7 +143,6 @@ def __init__(self, **params):
         self.name = None
 
         for name, value in six.iteritems(params):
-            # Note: deprecation warning will be raised in add_param
             shape, dtype = _ensure_shape_dtype(value)
             self.add_param(name, shape, dtype=dtype)
 
@@ -268,26 +268,6 @@ def add_param(self, name, shape=None, dtype=numpy.float32,
                   initializer=None):
         """Registers a parameter to the link.
 
-        .. deprecated:: v2.0.0
-
-           Assign a :class:`~chainer.Parameter` object directly to an
-           attribute within :meth:`~chainer.Link.init_scope` instead.
-           For example, the following code
-
-           .. code-block:: python
-
-               link.add_param('W', shape=(5, 3))
-
-           can be replaced by the following assignment.
-
-           .. code-block:: python
-
-               with link.init_scope():
-                   link.W = chainer.Parameter(None, (5, 3))
-
-           The latter is easier for IDEs to keep track of the attribute's
-           type.
-
         Args:
             name (str): Name of the parameter. This name is also used as the
                 attribute name.
@@ -301,11 +281,6 @@ def add_param(self, name, shape=None, dtype=numpy.float32,
                 ignored.
 
         """
-        warnings.warn('''\
-Parameter registeration via Link.__init__ and Link.add_param are deprecated.
-Assign a Parameter object directly to an attribute within a \
-"with Link.init_scope():" block instead.
-''', DeprecationWarning)
         if name in self.__dict__:
             raise AttributeError(
                 'cannot register a new parameter %s: attribute exists'
@@ -884,10 +859,6 @@ def forward(self, x):
         links: Child links. The keywords are used as their names. The names are
             also set to the links.
 
-            .. deprecated:: v2.0.0
-
-               Assign child links directly to attributes instead.
-
     """
 
     def __init__(self, **links):
@@ -917,37 +888,12 @@ def __delattr__(self, name):
     def add_link(self, name, link):
         """Registers a child link to this chain.
 
-        .. deprecated:: v2.0.0
-
-           Assign the child link directly to an attribute within
-           :meth:`~chainer.Chain.init_scope` instead.
-           For example, the following code
-
-           .. code-block:: python
-
-              chain.add_link('l1', L.Linear(3, 5))
-
-           can be replaced by the following line.
-
-           .. code-block:: python
-
-              with chain.init_scope():
-                  chain.l1 = L.Linear(3, 5)
-
-           The latter is easier for IDEs to keep track of the attribute's
-           type.
-
         Args:
             name (str): Name of the child link. This name is also used as the
                 attribute name.
             link (Link): The link object to be registered.
 
         """
-        warnings.warn('''\
-Child link registeration via Chain.__init__ and Chain.add_link are deprecated.
-Assign a Link object directly to an attribute within a \
-"with link.init_scope():" block instead.
-        ''', DeprecationWarning)
         if name in self.__dict__:
             raise AttributeError(
                 'cannot register a new link %s: attribute exists' % name)


Test patch:
diff --git a/tests/chainer_tests/test_link.py b/tests/chainer_tests/test_link.py
--- a/tests/chainer_tests/test_link.py
+++ b/tests/chainer_tests/test_link.py
@@ -18,9 +18,8 @@ class TestLink(unittest.TestCase):
     def setUp(self):
         x_shape_0 = 2
         x_shape_1 = numpy.int64(3)
-        with testing.assert_warns(DeprecationWarning):
-            self.link = chainer.Link(x=((x_shape_0, x_shape_1), 'd'),
-                                     u=(None, 'd'))
+        self.link = chainer.Link(x=((x_shape_0, x_shape_1), 'd'),
+                                 u=(None, 'd'))
         with self.link.init_scope():
             self.link.y = chainer.Parameter(shape=(2,))
             self.link.v = chainer.Parameter()
@@ -97,45 +96,38 @@ class InjectedLink(chainer.Link, CallMixin):
         assert ret == call.return_value
 
     def test_add_param(self):
-        with testing.assert_warns(DeprecationWarning):
-            self.link.add_param('z', (2, 3))
+        self.link.add_param('z', (2, 3))
         self.check_param_init('z', (2, 3), 'f')
 
-        with testing.assert_warns(DeprecationWarning):
-            self.link.add_param('w', (2, 3), dtype='d')
+        self.link.add_param('w', (2, 3), dtype='d')
         self.check_param_init('w', (2, 3), 'd')
 
-        with testing.assert_warns(DeprecationWarning):
-            self.link.add_param('r')
+        self.link.add_param('r')
         self.check_param_uninit('r')
         self.link.r.initialize((2, 3))
         self.check_param_init('r', (2, 3), 'f')
 
-        with testing.assert_warns(DeprecationWarning):
-            self.link.add_param('s', dtype='d')
+        self.link.add_param('s', dtype='d')
         self.check_param_uninit('s')
         self.link.s.initialize((2, 3))
         self.check_param_init('s', (2, 3), 'd')
 
         initializer = initializers.Zero('d')
-        with testing.assert_warns(DeprecationWarning):
-            self.link.add_param('t', initializer=initializer)
+        self.link.add_param('t', initializer=initializer)
         self.check_param_uninit('t', initializer)
         self.link.t.initialize((2, 3))
         self.check_param_init('t', (2, 3), 'd', 0)
 
     def test_add_param_direct_initialization(self):
         z = numpy.random.rand(2, 3).astype('f')
-        with testing.assert_warns(DeprecationWarning):
-            self.link.add_param('z', initializer=z)
+        self.link.add_param('z', initializer=z)
         self.assertIsInstance(self.link.z.data, numpy.ndarray)
         numpy.testing.assert_array_equal(self.link.z.data, z)
 
     def test_add_param_duplicated_with_persistent(self):
         self.link.add_persistent('z', 'abc')
         with self.assertRaises(AttributeError):
-            with testing.assert_warns(DeprecationWarning):
-                self.link.add_param('z', (2, 3))
+            self.link.add_param('z', (2, 3))
 
     def test_add_persistent(self):
         self.assertTrue(hasattr(self.link, 'p'))
@@ -666,8 +658,7 @@ def setUp(self):
         self.c1 = chainer.Chain()
         with self.c1.init_scope():
             self.c1.l1 = self.l1
-        with testing.assert_warns(DeprecationWarning):
-            self.c1.add_link('l2', self.l2)
+        self.c1.add_link('l2', self.l2)
         self.c2 = chainer.Chain()
         with self.c2.init_scope():
             self.c2.c1 = self.c1