| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
|
Removed keyword Placeholder restriction from this and will issue a separate PR after. Felt like too much is packed into 1 PR. |
Sorry, something went wrong.
|
@dg-pb Is this PR still relevant or have you opened PRs for the different components? If so, can we close this one? |
Sorry, something went wrong.
|
It is still relevant. I could factor "allowing trailing placeholders" into a separate one if it is preferred. |
Sorry, something went wrong.
I have not looked at all the changes in detail, but the PR seems big and that could be a reason this PR has not yet been reviewed. In the description at least 3 changes are mentioned (allowing placeholders, performance, refactor for partialmethod). If possible, I would advice to split the PR into multiple PRs. |
Sorry, something went wrong.
There are 2 really. Performance benefit is a consequence of "allowing trailing placeholders". I don't mind making changes, splitting as desired etc, but I would like these to be called by reviewer. Otherwise, I already have experience by trying to guess what reviewer might prefer, making changes per suggestions of others, etc and when final reviewer comes he desires to be different again and I need to keep changing things more times than necessary. And either way these would need to be considered at the same time. I.e. allowing or not allowing trailing placeholders are both ok. There is a slight advantage for allowing them as it makes it a bit more flexible and explicit. While looking at partial from the POV of using it on methods, the advantages and rationale for allowing them can be seen more clearly putting it on a favourable side (at least this is my conclusion). Also, if I split now, then I have PRs hanging on unmerged code.
It isn't that big. Most of it is Pure Python rewrite of partialmethod and the implementation is much simpler to follow than the previous one. |
Sorry, something went wrong.
|
As this series of partial related PRs started slowly moving, the path became a bit clearer and splitting this into 2 seems to be the best option to me now. Will do that shortly. |
Sorry, something went wrong.
|
@serhiy-storchaka, just a gentle reminder. |
Sorry, something went wrong.
|
@dg-pb, I am reviewing, I just was very busy last days with other issue. |
Sorry, something went wrong.
|
@serhiy-storchaka, I think it would be good to merge this. Otherwise, it will be sitting there for another version. inspect simplification is what I think has most value here. |
Sorry, something went wrong.
|
This PR is stale because it has been open for 30 days with no activity. |
Sorry, something went wrong.
|
@serhiy-storchaka, I think this can be merged regardless of #128644. And maybe we could reopen #128644 so that it has another chance for consideration? |
Sorry, something went wrong.
There was a problem hiding this comment.
There are some behavior changes that need to be addressed (or documented).
Reproducers:
from functools import partialmethod, Placeholder
class C: # main: TypeError here
m = partialmethod(print, Placeholder) # PR: accepted silently
C().m # PR: TypeError raised only now,
# from inside functools.__make_method
And
from functools import partialmethod
class D: # a descriptor that needs no binding
def __get__(self, obj, cls=None): return self
def __call__(self, *a, **kw): return a
class C:
m = partialmethod(D(), 42)
C().m() # main: (<C object>, 42) PR: (42,) <- instance gone
And
Freezing of partialmethod"""Reproducer 3 for PR 124788 (gh-124652).
On current main, partialmethod.__get__ rebuilds the bound callable on every
access, and the generated closure reads self.func / self.args / self.keywords
at CALL time -- so mutating those public attributes is reflected immediately.
With the PR, the method object is built once and cached (self.method), so the
partialmethod is effectively frozen after its first access: later mutation is
silently ignored. Note the cache is a *public* attribute (`.method`), which
makes such mutation easy to reach.
"""
from functools import partialmethod
def base(self, *args, **kwargs):
return ('base', args, kwargs)
def other(self, *args, **kwargs):
return ('other', args, kwargs)
class C:
m = partialmethod(base, 1, kw='before')
pm = C.__dict__['m']
c = C()
first = c.m()
print(f'1st access (baseline) : {first!r}')
# --- mutate every documented public attribute of partialmethod -------------
pm.keywords = {'kw': 'after'}
after_keywords = c.m()
print(f'after pm.keywords = ... : {after_keywords!r}')
pm.args = (99,)
after_args = c.m()
print(f'after pm.args = (99,) : {after_args!r}')
pm.func = other
after_func = c.m()
print(f'after pm.func = other : {after_func!r}')
frozen = (after_keywords == first
and after_args == first
and after_func == first)
# Is the stale callable being reused, and is the cache publicly reachable?
cached = getattr(pm, 'method', None)
print()
print(f'public .method cache attr : {cached!r}')
print(f'declared __slots__ : '
f'{getattr(partialmethod, "__slots__", None)!r}')
if cached is not None:
# The slot named in __slots__ is "wrapper", but the code stores the cache
# in "method" -- so it lands in __dict__ and the slot goes unused.
print(f'cache stored in __dict__ : '
f'{"method" in getattr(pm, "__dict__", {})}')
Sorry, something went wrong.
|
Thank you for looking into this.
I added the check so that after this PR this is also raised at definition. I think it is good to keep it in sync with partial and it is an easy one to catch early. |
Sorry, something went wrong.
Made this PR to behave the same as it was. |
Sorry, something went wrong.
If you look at pure python partial - these are writable as well, but in C implementation these are read only. To make partial the same in both versions, all what would be needed is to make read only properties. In this case one can still modify contents of partial.keywords['kw'] = new_val and see the effect. I made it this way purely for performance - reconstructing on each call is fairly expensive.
Would be good to know if being able to modify partial.keywords has found any use cases or is it purely the consequence of not having frozedict at a time? Perspective of someone with historical context would be very helpful here. |
Sorry, something went wrong.
Lets also see what others think. |
Sorry, something went wrong.
|
So ok, I looked around a bit and it seems that ability to modify keywords of partial has become a feature. So in terms of restriction and not breaking expected (even though undocumented) contract likely the most sensible thing to do here is to replicate what partial does. If someone has been depending on the fact that func/args/keywords can be replaced by new objects, I think this is something that is ok to put a stop to. So my solution is as follows: class partialmethod:
...
@property
def func(self):
return self._func
@property
def args(self):
return self._args
@property
def keywords(self):
method = self._cachedmethod
if method is None:
return self._keywords
if isinstance(method, (staticmethod, classmethod)):
method = method.__wrapped__
return method.keywordsSo in case the method is cached, give keywords of underlying partial object. An in case descriptor is not yet built or is in state of being built on every call, then return partialmethod._keywords. Subclass optimization also works correctly via: temp = partial(lambda *_, **__: None, *func._args, **func.keywords) |
Sorry, something went wrong.
Made changes. Now:
|
Sorry, something went wrong.
|
There are some more behavior changes from functools import partialmethod
class Desc:
"""Standard descriptor idiom: return self on class access."""
def __get__(self, obj, cls=None):
if obj is None:
return self
return lambda a, b: ("bound", a, b)
def __call__(self, *args):
return ("Desc.__call__", args)
class A:
pd = partialmethod(Desc(), 1)
a = A()
print("before class access:", a.pd(2))
A.pd # any class-level access, e.g. inspect.getmembers(A)
print("after class access: ", a.pd(2))
and import inspect
from functools import partialmethod
async def coro(self, a): ...
class B:
pm = partialmethod(coro, 1)
ps = partialmethod(staticmethod(coro), 1)
pc = partialmethod(classmethod(coro), 1)
for name in ("pm", "ps", "pc"):
print(name, inspect.iscoroutinefunction(getattr(B, name)),
inspect.iscoroutinefunction(getattr(B(), name)))
|
Sorry, something went wrong.
We could also build the descriptor if keywords is accessed. A bit slower for some cases, but maybe safer. |
Sorry, something went wrong.
Ok, I have contained caching only to intended set of types. I think this solves all discrepancies. But would appreciate another look.
Specialised descriptor would return something different than dict and this would break existing type(pm.keywords) is dict. I think I have preserved full backwards compatibility now - even ability to set func/args/keywods. Although I think it would be ideal to keep it consistent with partial, I think it is best to limit scope of this to what was initially intended. |
Sorry, something went wrong.
|
@dg-pb There are some more issues with the current iteration. I will add (AI generated) reproducers in the details section below. Getting this right turns out to be trickly. I suggest to split this PR into multiple smaller ones: i) add the regression tests found so far (at least the cases where we do not want to change behaviour) ii) Placeholder binding plus inspect cleanup (should make the code cleaner) iii) some form of caching for performance. Reproducers ``` from functools import partialmethoddef f(self, a, b=0): return ("f", a, b) class A: class B: import pickle class A: bound = A().pm import inspect class Desc: class A: print("instance:", A().pd(2)) </details> |
Sorry, something went wrong.
|
@eendebakpt thank you for pushing on this. Indeed it did get tricky from another angle as well -- in the end I couldn't find a way to deal with keywords without forbidding setting new func/args/keywords attributes. I think by this time the promise of "simplification" in the title is failing badly and I agree - let's leave caching out. And it might be a better call in general - after all, now, at least for simple cases, partial+Placeholder should be preferred - explicit, cached by default and much faster. So I think tidying things up while not breaking stuff is the right scope. So:
Take a look, I think this is getting close to something that actually works. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.