1

Maybe I found something strange on pytorch, which result in property setter not working. Below is a minimal example that demonstrates this:

import torch.nn as nn

class A(nn.Module):
    def __init__(self):
        super(A, self).__init__()
        self.aa = 1
        self.oobj = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    @property
    def obj(self):
        print('get attr [obj]: {0}'.format(self.oobj))
        return self.oobj
    @obj.setter
    def obj(self, val):
        print('set attr [obj] to {0}'.format(val))
        self.oobj = val

class B(nn.Module):
    def get_attr(self):
        print('no any attr.')

class C:
    def get_attr(self):
        print('no any attr.')

b = A()             # set obj, and prints my setter message

b.obj               # get obj using my getter

# respectively run the following 3 lines, only the last line not call the setter I defined explicitly.
b.obj = C()         # set obj, and prints my setter message

# b.obj = [1, 2, 3]   # set obj, and prints my setter message

# b.obj = B()         # set obj, but it doesn't print my setter message

The last line doesn't call property setter I defined on class A, but call setter on torch.nn.Module. Because A regard B as a nn.Module, call the setter on nn.Module to set attr [obj] as a Module, but it still strange, why not call the setter I explicitly defined on class A?

And my project needs to set a nn.Module attribute via setter I defined explicitly, which causes BUG( because it failed). Now I change my code solved the BUG, but still puzzle with the problem.

1 Answers1

1

It may not look obvious at first, but up until you set b.obj as a nn.Module object, you are defining a normal attribute; but once you set b.obj as a nn.Module object, then you can "only" replace b.obj with another nn.Module, because you registered it to _modules. Let me walk you through the code and you'll get it.

nn.Module()'s __setattr__ implementation can be found here.

First, you defined a new nn.Module:

b = A()  # btw, why not a = A() :)

Then, you set (I'll skip unnecessary steps to reproduce the behavior):

b.obj = [1, 2, 3]

In this case, because

Then, this line will be execute:

object.__setattr__(self, name, value)

which is nothing but a normal attribute set, which calls your setter.

Now, when you set:

b.obj = B()

Then, because B() is a nn.Module, the following block will be executed instead:

modules = self.__dict__.get('_modules')
if isinstance(value, Module):
    if modules is None:
        raise AttributeError(
            "cannot assign module before Module.__init__() call")
    remove_from(self.__dict__, self._parameters, self._buffers)
    modules[name] = value

So, now you are actually registering a nn.Module to self.__dict__.get('_modules') (print it before and after and you'll see... do it before and after setting [1,2,3] as well).

After this point, if you are not setting a nn.Parameter, and you try to set .obj again, then it will fall into this block:

elif modules is not None and name in modules:
    if value is not None:
        raise TypeError("cannot assign '{}' as child module '{}' "
                        "(torch.nn.Module or None expected)"
                        .format(torch.typename(value), name))
    modules[name] = value

That is: you already have modules['obj'] set to something and from now on you need to provide another nn.Module or None if you want to set it again. And, as you can see, because you are providing a list if you try to set b.obj = [1,2,3] again, you'll get the error message in the block above, and that is what you get.

If you really want set it to something else, then you have to delete it before:

b.obj = B()
del b.obj
b.obj = [1,2,3]
Berriel
  • 12,659
  • 4
  • 43
  • 67
  • Yeah, you are right! But what I mainly puzzle with is the setter I explicitly defined failed to set nn.Module as a property. If nn.Module attribute can't set by my setter, there may be two property have the same name? One is @property I defined, another is a Module name? There is no conflict? – Weiming Xiong Apr 10 '20 at 04:26
  • @WeimingXiong as I said, **all** your calls first go to `__setattr__` of `nn.Module`. This is how Python works. As I tried to explain, it just happens that when the value is not a `nn.Module`'s object that **your** setter is used (because it is called from `nn.Module`). If you're not familiar with the order that Python uses for attributes (set and get), check this [answer](https://stackoverflow.com/a/15751159/4228275). – Berriel Apr 10 '20 at 14:07
  • @WeimingXiong if this answer was useful somehow, consider upvoting it – Berriel Apr 10 '20 at 14:08
  • @WeimingXiong I still can't see the _bug_. Everything is working as expected. – Berriel Apr 10 '20 at 14:13