-
-
Notifications
You must be signed in to change notification settings - Fork 30.7k
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
Make inspect.Parameter.__hash__ use _default instead of default #102302
Comments
Can you expand on your motivation for wanting to subclass |
"Consistency with |
Subclassing Parameter is just as useful as subclassing Signature, and Signature is expected to be subclassable. Do we need special permission before subclassing something builtin ? In my experience, classes resisting inheritance are the exception rather than the norm, and those would deserve a special warning, instead of an explicit grant whenever subclassing is allowed. |
You haven't addressed my question :) You don't need permission to subclass anything, of course, but any change to the standard library needs a strong motivation. Can I ask again what your motivation is for subclassing |
The idea is to manually parse a language we defined where things take parameters. However, unlike Python, we don't evaluate defaults at parse time, but at call time, which means the "default" field of parameters are really a string evaluating to their actual default value, or P.empty (or None in our previous implementation) for required parameters. As life goes on, we find ourselves in the need to manipulate signatures with literal defaults, and signatures with evaluated defaults. We tried an implementation with two different Signature subclasses for these two cases but they share the same Parameter class (the native one, at this point), so if we somehow need to manipulate both kinds of signatures in the same function, there's a chance a mixup happens between the two kinds of parameter. To solve that, subclassing Parameter would be useful, so we can have a literalParameter on one side, and a evaledParameter on the other (the latter probably being the native class, but whatever). Then, for performance reasons, I had the idea of only computing the evaled value of the default when required, and memorizing it after, which means that the default property evaluates the literal string the first time it's accessed, and then caches it and returns the cached value (the example I used on top of the thread). |
This is a very specific use case that doesn't work currently:
All of these are reasonable, but I wonder if you couldn't just cache results in an alternative way in your code? class Foo(Parameter):
__slots__ = ("_default",)
@property
def default(self):
try:
return self._default
except AttributeError:
self._default = my_expensive_function_call()
return self._default This alternative solution also avoids well-known pitfalls around using |
You need to rename Yes, I guess I could do that. But I also think the original recursion error is obscure and easily fixable, and comes from an inconsistency which, if not a good enough reason by itself to change the code, certainly doesn't go in favor of leaving it as it is. Solving the inconsistency would unlock the other ways of implementing "my" feature, for no cost that I can see. I mean, I understand the fear of changing working code, sure, but what bad consequence would the change cause ? If it has any, it means that a property was overriding Parameter's native ones and returning something other than the underlying values ( |
I'm afraid you're approaching this from the wrong angle. The burden is on you to explain why this change would be generally useful to users of One of the issues I'm having is that there's little point fixing a bug or adding a feature unless we include a test; otherwise, the behaviour we're trying to enable could easily be broken accidentally in the future if somebody fixes a different bug or refactors the code for some other reason. So what would a test look like here? It could look something like this: def test_Signature_can_be_subclassed_and_functools_cache_can_be_used_on_properties_in_the_subclass(self):
class ParameterSubclass(inspect.Parameter):
__slots__ = ()
@property
@functools.cache
def default(self):
return super().default
self.assertEqual(ParameterSubclass("x", inspect.Parameter.POSITIONAL_ONLY, default=3).default, 3) ...But that seems like such an absurdly specific test that it makes me wonder whether this really is a problem where fixing it would be generally useful to the Python community at large, or whether this would be code churn for little gain. |
My reasons to want this change are that I prefer a dict-like cache solution, such as the one I had with a WeakKeyDict bound to the class, which does not reproduce the property-functools-cache pitfalls but still requires hashing to be independent of the method or property you're caching. Solving the inconsistency between the dunders would unlock using hash(self) within property calls for any other purpose. It would also disable incorrect hash behaviors relying on inconstant overridden properties and yielding variable hashes for the same instance - which is a violation of the data model as far as I understand it. Sumup, which could end up being the news entry:
So, I would write the test that way : class ParameterSubclass(inspect.Parameter):
__slots__ = ()
@property
def default(self):
return random.random()
# same for the other properties
inst = ParameterSubclass("x", POSITIONAL_ONLY, default=5)
self.assertEqual(hash(inst), hash(Parameter("x", POSITIONAL_ONLY, default=5))) # hash doesn't rely on properties but on the actual values
self.assertEqual(hash(inst), hash(inst)) # constant hash for the same instance
# same with the instances themselves to test __eq__, and possibly pickle if we want to test __reduce__ too |
Thanks. I'm afraid I'm still unconvinced here. It just doesn't seem to me that Note that even if we did change things here, we likely wouldn't backport it, since this is arguably a feature rather than a bug. I'll leave this open for a few days in case another core dev feels differently. |
If the change is not done in the end, then I suggest warning against subclassing Parameter in the doc, so that the inconstant hash problem is covered (which doesn't raise an exception, unlike my cache use case). |
Subclassing |
I still disagree with the rationale put forward in this issue thread for making the change, but I was persuaded to merge the linked PR anyway due to a microbenchmark showing a decent speedup. |
* main: (21 commits) pythongh-102192: Replace PyErr_Fetch/Restore etc by more efficient alternatives in sub interpreters module (python#102472) pythongh-95672: Fix versionadded indentation of get_pagesize in test.rst (pythongh-102455) pythongh-102416: Do not memoize incorrectly loop rules in the parser (python#102467) pythonGH-101362: Optimise PurePath(PurePath(...)) (pythonGH-101667) pythonGH-101362: Check pathlib.Path flavour compatibility at import time (pythonGH-101664) pythonGH-101362: Call join() only when >1 argument supplied to pathlib.PurePath() (python#101665) pythongh-102444: Fix minor bugs in `test_typing` highlighted by pyflakes (python#102445) pythonGH-102341: Improve the test function for pow (python#102342) Fix unused classes in a typing test (pythonGH-102437) pythongh-101979: argparse: fix a bug where parentheses in metavar argument of add_argument() were dropped (python#102318) pythongh-102356: Add thrashcan macros to filter object dealloc (python#102426) Move around example in to_bytes() to avoid confusion (python#101595) pythonGH-97546: fix flaky asyncio `test_wait_for_race_condition` test (python#102421) pythongh-96821: Add config option `--with-strict-overflow` (python#96823) pythongh-101992: update pstlib module documentation (python#102133) pythongh-63301: Set exit code when tabnanny CLI exits on error (python#7699) pythongh-101863: Fix wrong comments in EUC-KR codec (pythongh-102417) pythongh-102302 Micro-optimize `inspect.Parameter.__hash__` (python#102303) pythongh-102179: Fix `os.dup2` error reporting for negative fds (python#102180) pythongh-101892: Fix `SystemError` when a callable iterator call exhausts the iterator (python#101896) ...
* main: (37 commits) pythongh-102192: Replace PyErr_Fetch/Restore etc by more efficient alternatives in sub interpreters module (python#102472) pythongh-95672: Fix versionadded indentation of get_pagesize in test.rst (pythongh-102455) pythongh-102416: Do not memoize incorrectly loop rules in the parser (python#102467) pythonGH-101362: Optimise PurePath(PurePath(...)) (pythonGH-101667) pythonGH-101362: Check pathlib.Path flavour compatibility at import time (pythonGH-101664) pythonGH-101362: Call join() only when >1 argument supplied to pathlib.PurePath() (python#101665) pythongh-102444: Fix minor bugs in `test_typing` highlighted by pyflakes (python#102445) pythonGH-102341: Improve the test function for pow (python#102342) Fix unused classes in a typing test (pythonGH-102437) pythongh-101979: argparse: fix a bug where parentheses in metavar argument of add_argument() were dropped (python#102318) pythongh-102356: Add thrashcan macros to filter object dealloc (python#102426) Move around example in to_bytes() to avoid confusion (python#101595) pythonGH-97546: fix flaky asyncio `test_wait_for_race_condition` test (python#102421) pythongh-96821: Add config option `--with-strict-overflow` (python#96823) pythongh-101992: update pstlib module documentation (python#102133) pythongh-63301: Set exit code when tabnanny CLI exits on error (python#7699) pythongh-101863: Fix wrong comments in EUC-KR codec (pythongh-102417) pythongh-102302 Micro-optimize `inspect.Parameter.__hash__` (python#102303) pythongh-102179: Fix `os.dup2` error reporting for negative fds (python#102180) pythongh-101892: Fix `SystemError` when a callable iterator call exhausts the iterator (python#101896) ...
Feature or enhancement
Enable subclasses of Parameter to add a
default
property accessing hash(self) rather than having hash rely on thedefault
propertyPitch
I wanted to make a subclass of Parameter which would store as _default the string eval-ing to the actual default value, and which would only eval it when accessed, and memorize it at that time.
So, I coded this :
(not using @cached_property because I want to keep the empty slots)
This doesn't work, because
cache
tries to hashself
, and Parameter.__hash__ accessesself.default
, rather thanself._default
(so it does an infinite recursion loop).The last edit to the hash method was purportedly to "fix a discrepancy between eq and hash". However, the eq still accesses the raw _default, and hash did so too before the "fix".
Overall, apart from my selfish use-case, I think it makes more sense for an immutable class to hash relative to the actual value instead of the less-reliable propertied value.
And there's a minor performance bonus too.
Linked PRs
inspect.Parameter.__hash__
#102303The text was updated successfully, but these errors were encountered: