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
Calling help
executes @classmethod @property decorated methods
#89519
Comments
I noticed some strange behaviour when calling from time import sleep
from abc import ABC, ABCMeta, abstractmethod
class MyMetaClass(ABCMeta):
@classmethod
@property
def expensive_metaclass_property(cls):
"""This may take a while to compute!"""
print("computing metaclass property"); sleep(3)
return "Phew, that was a lot of work!"
class MyBaseClass(ABC, metaclass=MyMetaClass):
@classmethod
@property
def expensive_class_property(cls):
"""This may take a while to compute!"""
print("computing class property .."); sleep(3)
return "Phew, that was a lot of work!"
@property
def expensive_instance_property(self):
"""This may take a while to compute!"""
print("computing instance property ..."); sleep(3)
return "Phew, that was a lot of work!"
class MyClass(MyBaseClass):
"""Some subclass of MyBaseClass"""
help(MyClass) Calling The other two properties, Secondly, only The problem is also present in '3.10.0rc2 (default, Sep 28 2021, 17:57:14) [GCC 10.2.1 20210110]' Stack Overflow thread: https://stackoverflow.com/questions/69426309 |
I updated the script with dome more info. The class-property gets actually executed 5 times when calling
|
See also: https://bugs.python.org/issue44904 |
Randolf, what specific behaviors do you consider to be bugs that should be fixed. What would a test of the the changed behavior look like? This should perhaps be closed as a duplicate of bpo-44904. Randolf, please check and say what you thing. |
On current 3.9, 3.10, 3.11, on Windows running in IDLE, I see |
@terry I think the problem boils down to the fact that Calling mappingproxy({'__module__': '__main__',
'expensive_class_property': <classmethod at 0x7f893e95dd60>,
'expensive_instance_property': <property at 0x7f893e8a5860>,
'__dict__': <attribute '__dict__' of 'MyBaseClass' objects>,
'__weakref__': <attribute '__weakref__' of 'MyBaseClass' objects>,
'__doc__': None,
'__abstractmethods__': frozenset(),
'_abc_impl': <_abc._abc_data at 0x7f893fb98740>}) Two main issues:
isinstance(func, property) or (`isinstance(func, classmethod)` and `isinstance(func.__func__, property)`
MyBaseClass.expensive_class_property = 2
MyBaseClass().expensive_instance_property = 2 Then the first line erroneously executes, such that MyBaseClass.dict['expensive_class_property'] is now |
If fact, in the current state it seem that it is impossible to implement real class-properties, for a simple reason: descriptor.__set__ is only called when setting the attribute of an instance, but not of a class!! import math
class TrigConst:
const = math.pi
def __get__(self, obj, objtype=None):
print("__get__ called")
return self.const
def __set__(self, obj, value):
print("__set__ called")
self.const = value
class Trig:
const = TrigConst() # Descriptor instance Trig().const # calls TrigConst.__get__
Trig().const = math.tau # calls TrigConst.__set__
Trig.const # calls TrigConst.__get__
Trig.const = math.pi # overwrites TrigConst attribute with float. |
I'm merging bpo-44904 into this one because they are related. |
It may have been a mistake to allow this kind of decorator chaining.
We either need to undo the Python 3.9 feature from bpo-19072, or we need to put serious thought into making all these descriptors composable in a way that would match people's expectations. |
Dear Raymond, I think decorator chaining is a very useful concept. At the very least, if all decorators are functional (following the The question is: can we get similar behaviour when allowing decoration with stateful objects, i.e. classes? This seems a lot more difficult. At the very least, in the case of ### Expectation
Using your pure-python versions of property / classmethod from https://docs.python.org/3.9/howto/descriptor.html, I was able to write down a variant of def ClassMethod(func):
class Wrapped(type(func)):
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.__func__), '__get__'):
return self.__func__.__get__(cls)
return MethodType(self.__func__, cls)
return Wrapped(func) I attached a full MWE. Unfortunately, this doesn't fix the ### Some further Proposals / Ideas
|
Some thoughts from me, as an unqualified but interested party: Like Randolph, I very much like having class properties in the language, and have used them in several projects since their introduction in 3.9. I find they're especially useful with Enums. However, while the bug in doctest, for example, is relatively trivial to fix (see my PR in bpo-44904), it seems to me plausible that bugs might crop up in other standard library modules as well. As such, leaving class properties in the language might mean that several more bugfixes relating to this feature would have to be made in the future. So, I can see the argument for removing them. It seems to me that Randolph's idea of changing
This would at least mean that Note that this change wouldn't fix the bugs in abc and doctest, nor does it fix the issue with class-property setter logic that Randolph identified. With regards to the point that |
@alex Regarding my proposal, the crucial part is the desiderata, not the hacked up implementation I presented. And I really believe that at least that part I got hopefully right. After all, what does Regarding code breakage - yes that is a problem, but code is already broken and imo Python needs to make a big decision going forward:
At this point however I want to emphasize that I am neither a CS major nor a python expert (I only started working with the language 3 years ago), so take everything I say as what it is - the opinion of some random stranger from the internet (: |
I've put up a PR; I'm not sure it's the best way to fix it. I will look more into it and will try to post some details about the PR later today. |
I missed that this is assigned to Raymond, hope we didn't duplicate any effort (it only took me a short while to do the PR). Apologies.. |
I've looked more into it, the issue is that even before an object can be tested with So I think my PR is going in the right direction, adding guards in the inspect function and in two |
added Graham Dumpleton to nosy list in case he has some useful insight here, I think the PR from bpo-19072 may have been adapted from grahamd's patch originally? |
Too much to grok right now. There is already a convention for what a decorator wraps. It is __wrapped__. Line 70 in 3405792
Don't use __func__ as that has other defined meaning in Python related to bound methods and possibly other things as well and overloading on that will break other stuff. In part I suspect a lot of the problems here are because things like classmethod and functools style decorators are not proper transparent object proxies, which is the point of what the wrapt package was trying to solve so that accessing stuff on the wrapper behaves as much as possible as if it was done on what was wrapped, including things like isinstance checks. |
Also see: https://bugs.python.org/issue42073 The classmethod pass through broke some existing code and the "fix" for it looks dubious: if hasattr(type(self.f), '__get__'):
return self.f.__get__(cls, cls) |
I propose deprecating classmethod chaining. It has become clear that it doesn't really do what people wanted and can't easily be made to work. By even suggesting that some stateful decorators are composable, we've ventured onto thin ice. Wrapping property in a classmethod doesn't produce something that behaves like a real property. Mixing staticmethod and property doesn't work at all. Putting abstractmethod in the mix doesn't work well either. The ecosystem of code inspection tools, like help() in this issue, is wholly unprepared for recognizing and working around these combinations. The latest "fix" for classmethod chaining looks weird and worriesome as well: self.f.__get__(cls, cls). Classmethod chaining is relatively new, so we will affect very little code by deprecating it. Any of the possible use cases can be served in other ways like the wrapt package or by explicit code in __getattribute__. |
It makes me sad that the stdlib will no longer provide a way to compose classmethods with other descriptors. However, I agree that deprecating classmethod chaining is probably the correct course of action, given the complications this feature has caused, and the backwards-compatibility issues it raises. This is probably a conversation for another BPO issue or the python-ideas mailing list, but I hope some consideration can be given in the future as to whether a new classmethod-like feature could possibly be added to functools that would enable this kind of decorator chaining without the same code-breakage concerns that this feature has had. |
Probably >90% of the use-cases for chaining classmethod are a read-only class property. Perhaps the general case of classmethod(descriptor(...)) should be treated separately from the common case?
If classmethod(property(f)) is going to be removed, can an implementation of classproperty be considered as a replacement? Or perhaps support via the other ordering, property(classmethod(f))? |
In Python 3.10, classmethod() added a __wrapped__ attribute. Presumably, any use case for implicit chaining can now be accomplished in an explicit and controlled manner. |
See also: https://bugs.python.org/issue46764 |
@serhiy-storchaka Do you know if there is a way to add Here's the relevant code from functionobject.c:
Right now, I thinking this has to be a documentation-only deprecation. |
Seems the problem with the deprecation is that Python functions are descriptors, so in case of normal class methods we pass the same path as for properties. Am I right? I think that instead of emitting a deprecation warning when read a class property, it should be emitted when the class property is created. We can start with emitting warning if the |
randolf-scholz mannequin commentedOct 3, 2021
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: