Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assigning attribute to instance method causes AttributeError #100057

Open
ahopkins opened this issue Dec 6, 2022 · 8 comments
Open

Assigning attribute to instance method causes AttributeError #100057

ahopkins opened this issue Dec 6, 2022 · 8 comments
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@ahopkins
Copy link

ahopkins commented Dec 6, 2022

Bug report

Assigning (and reassigning) attributes to functions works as expected as with any object.

def bar(self):
    ...


bar.something = 1
print(bar.something)
# prints: 1

bar.something = 2
print(bar.something)
# prints: 2

However, the experience is not consistent with class methods. When dealing with the class, you can assign and reassign similar to regular functions.

class Foo:
    def bar(self):
        ...


Foo.bar.something = 1
print(Foo.bar.something)
# prints: 1

Foo.bar.something = 2
print(Foo.bar.something)
# prints: 2

This even carries through after instantiation:

foo = Foo()
print(foo.bar.something)
# prints: 2

We can clearly see the value exists in the __dict__:

print(foo.bar.__dict__)
# {'something': 2}

However, trying to assign to the method from the instance fails:

foo.bar.something = 2
# AttributeError: 'method' object has no attribute 'something'

While I guess I can see why this might be (since you would be modifying an object on the class and not the instance--if this indeed is the rationale), this is not intuitive. It seems inconsistent with setting attributes otherwise.

If this behavior is intended and not a bug, then perhaps the error should be more explicit. The error stating that the method has no attribute named something is not correct. It does have the attribute and we can otherwise interact with it, just not set it.

Either we should be able to assign back to that attribute from the instance, or we should receive a more explicit error why this is not possible.

Your environment

  • CPython versions tested on: 3.7.12, 3.8.11, 3.9.9, 3.10.8, 3.11.0rc1
  • Operating system and architecture:
▶ uname -a           
Linux hopkins 6.0.2-arch1-1 #1 SMP PREEMPT_DYNAMIC Sat, 15 Oct 2022 14:00:49 +0000 x86_64 GNU/Linux
@ahopkins ahopkins added the type-bug An unexpected behavior, bug, or error label Dec 6, 2022
@ahopkins
Copy link
Author

ahopkins commented Dec 6, 2022

For backreference: sanic-org/sanic#2581

@eryksun
Copy link
Contributor

eryksun commented Dec 6, 2022

A method object wraps a callable, which is usually a function object. The real attributes of a method object include its __class__, __func__, and __self__ data and its __getattribute__(), __hash__(), __eq__(), __ne__(), __reduce__(), __repr__() methods (also __gt__, __ge__, __lt__, and __le__, but these comparisons aren't supported). For other attribute names, its __getattribute__() method tries to get the attribute from the wrapped callable.

For a wrapped function object, the latter includes __annotations__, __builtins__, __closure__, __code__, __defaults__, __dict__, __doc__, __globals__, __kwdefaults__, __module__, __name__, and __qualname__. It also includes all instance attributes set in the function's __dict__.

A method object does not have its own __dict__ for instance attributes, and it does not override the default __setattr__() to implement setting attributes on the wrapped callable. So, for example, you can get the value of foo.bar.something, but you can't set it that way. You'd have to set foo.bar.__func__.something.

@eryksun eryksun added the interpreter-core (Objects, Python, Grammar, and Parser dirs) label Dec 6, 2022
@ahopkins
Copy link
Author

ahopkins commented Dec 6, 2022

Thanks @eryksun. I was not aware of __func__. But, I think my point still stands that the exception is misleading at best.

@eryksun
Copy link
Contributor

eryksun commented Dec 6, 2022

Any class that provides dynamic attributes by overriding __getattribute__() or __getattr__() can lead to the same problem. For example:

class C:
    __slots__ = ()
    def __getattr__(self, name):
        return 'spam'
>>> c = C()
>>> c.x
'spam'
>>> c.x = 'eggs'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'C' object has no attribute 'x'

That said, I don't see a reason to not override the __setattr__() method of method objects to allow setting attributes on the wrapped callable, or at least to implement a less confusing error message than the generic message.

@eryksun
Copy link
Contributor

eryksun commented Dec 8, 2022

@vstinner, do you think it would be helpful to implement a custom __settattr__() for method objects that either allows setting attributes on the wrapped callable (i.e. __func__) or raises an AttributeError that's more informative?

@vstinner
Copy link
Member

vstinner commented Dec 8, 2022

There are many "read-only type", especially ones implemented in C, which cannot be modified, whereas types defined in Python can be modified. If you want to change that, I suggest to open a wider discussion on the python-ideas list or in the Core Development category in: https://discuss.python.org/

In Python 3.10, Py_TPFLAGS_IMMUTABLETYPE flag was added to make this feature a little bit more explicit and visible:
https://docs.python.org/dev/c-api/typeobj.html#Py_TPFLAGS_IMMUTABLETYPE

In short, it's also a deliberate choice that some types cannot be modified. See: #88074 (comment)

@eryksun
Copy link
Contributor

eryksun commented Dec 8, 2022

@vstinner, I wasn't suggesting to allow modification of a method object's real attributes (e.g. __func__ and __self__). They should remain read only.

This issue is in regards to trying to modify the attributes of the underlying callable, which method_getattro() exposes as dynamic attributes. Trying to set a dynamic attribute fails with the somewhat confusing error message that the method has no such attribute. Technically the latter is true, but it's not helpful to someone who doesn't understand that the method is a separate object that proxies the attributes of the underlying callable.

A tp_setattro function could be implemented that calls PyObject_SetAttr(im->im_func, name), such that the attributes of the underlying callable are seamlessly proxied on the method. Or it could at least raise an attribute error with a more informative error message that lets a developer know that the attribute has to be set on the wrapped callable instead of on the method.

@ahopkins
Copy link
Author

ahopkins commented Dec 8, 2022

Correct. The practical side of me wishes that it would pass thru so that it would "just work" in that a function or an instance method could be passed around and used (and potentially modified with dynamic attributes) without knowing what it is.

But, I certainly understand that it is a bit of drawing lines in the sand: what is modifiable? what is not? Since the behavior is not intuitive the error: AttributeError: 'C' object has no attribute 'x' is not really correct. Or, at least fails to point someone like me in the right direction: that attribute 'x' does exist, but it is in fact a read-only or proxy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants