Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

gh-119127: functools.partial placeholders#119827

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
rhettinger merged 78 commits intopython:mainfromdg-pb:implement-119127-again
Sep 26, 2024

Conversation

dg-pb
Copy link
Contributor

@dg-pbdg-pb commentedMay 31, 2024
edited
Loading

As I already had implementation I though PR might be helpful for others to see and evaluate.

From all the different extensions offunctools.partial I think this one is the best. It is relatively simple and exposes all missing functionality. Otherpartial extensions that I have seen lack functionality and would not provide complete argument ordering capabilities and/or are too complicated in relation to what they offer.

Implementation can be summarised as follows:
a) Trailing placeholders are not allowed. (Makes things simpler)
b) Throws exception if not all placeholders are filled on call
c) retains optimization benefits of application on otherpartial instances.

Performance penalty compared to currentfunctools.partial is minimal for extension class. + 20-30 ns for initialisation and <4 ns when called with or without placeholders.

To put it simply, new functionality extendsfunctools.partial so that it has flexibility oflambda /def approach (in terms of argument ordering), but call overhead is 2x smaller.

The way I see it is that this could only be justified if this extension provided completeness and no new functionality is going to be needed anywhere near in the future. I have thought about it and tried various alternatives and I think there is a good chance that this is the case. Personally, I don't think I would ever need anything more frompartial class.

Current implementation functions reliably.

Benchmark

There is nothing new here in terms of performance. The performance after this PR will be (almost) the same as the performance ofpartial until now.Placeholders only provide flexibility for taking advantage of performance benefits where it is important.

So far I have identified 2 such cases:

  1. More flexible predicate construction for functions inoperator module. This allows for new strategies in making performantiterator recipes.
  2. Partializing input target function. Examples of this are optimizers and similar. I.e. cases where the function will be called over and over within the routine with number of arguments. But the input target function needs partial substitution for positionals and keywords.

Good example of this isscipy.optimize.minimize.

Its signature is:scipy.optimize.minimize(fun, x0, args=(), ...)

Note, it does not havekwds. Why? I don't know. But good reason for it could be:

fun=lambdax:f(x,**kwds)

will need to expand**kwds on every call (even if it is empty), whilepartial will make the most optimal call. (see benchmarks below). So theminimize function can leave outkwds given there is a good way to source callable with already substituted keywords.

This extension allows pre-substituting both positionals and keywords. This allows optimizer signature to leave out bothkwds andargs resulting in simpler interfacescipy.optimize.minimize(fun, x0, ...) and gaining slightly better performance - function calls are at the center of such problems after all.

Benchmark Results for__call__

Code for Cases

dct= {'a':1}kwds= {'c':1,'d':2}kwds_empty= {}args1= (1,)args3= (1,2,4)opr_sub=opr.subopr_contains=opr.containsopr_sub_lambda=lambdab:opr_sub(1,b)opr_sub_partial=ftl.partial(opr_sub,1)opr_contains_lambda=lambdab:opr_contains(dct,b)opr_contains_partial=ftl.partial(opr_contains,dct)defpos2(a,b):passdefpos6(a,b,c,d,e,f):passdefpos2kw2(a,b,c=1,d=2):passpos2_lambda=lambdab:pos2(1,b)pos2_partial=ftl.partial(pos2,1)pos6_lambda=lambdab,c,d:pos6(1,2,3,b,c,d)pos6_partial=ftl.partial(pos6,1,2,3)pos2kw2_kw_lambda=lambdab:pos2kw2(1,b,**kwds)pos2kw2_kw_partial=ftl.partial(pos2kw2,1,**kwds)pos2kw2_kwe_lambda=lambdab:pos2kw2(1,b,**kwds_empty)pos2kw2_kwe_partial=ftl.partial(pos2kw2,1,**kwds_empty)opr_sub_partial_ph=ftl.partial(opr_sub,PH,1)opr_contains_partial_ph=ftl.partial(opr_contains,PH,'a')pos2_partial_ph=ftl.partial(pos2,PH,1)pos6_partial_ph=ftl.partial(pos6,PH,2,PH,4,PH,6)pos2kw2_kw_partial_ph=ftl.partial(pos2kw2,PH,1,**kwds)pos2kw2_kwe_partial_ph=ftl.partial(pos2kw2,PH,1,**kwds_empty)# Placeholder versionsfromfunctoolsimportPlaceholderasPHopr_sub_partial_ph=ftl.partial(opr_sub,PH,1)opr_contains_partial_ph=ftl.partial(opr_contains,PH,'a')pos2_partial_ph=ftl.partial(pos2,PH,1)pos6_partial_ph=ftl.partial(pos6,PH,2,PH,4,PH,6)pos2kw2_kw_partial_ph=ftl.partial(pos2kw2,PH,1,**kwds)pos2kw2_kwe_partial_ph=ftl.partial(pos2kw2,PH,1,**kwds_empty)

CPython Results

C Implementation----------------    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┃   5 repeats, 1,000,000 times     ┃    ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃    Units: ns    lambda   partial ┃    ┃              ┏━━━━━━━━━━━━━━━━━━━┫    ┃      opr_sub ┃  50 ± 4    40 ± 2 ┃    ┃ opr_contains ┃  53 ± 3    43 ± 3 ┃    ┃         pos2 ┃  50 ± 1    64 ± 1 ┃    ┃ pos2(*args1) ┃  69 ± 5    73 ± 5 ┃    ┃         pos6 ┃  58 ± 1   103 ± 5 ┃    ┃ pos6(*args3) ┃  77 ± 3    99 ± 5 ┃    ┃   pos2kw2_kw ┃ 240 ± 4   259 ± 7 ┃    ┃  pos2kw2_kwe ┃ 134 ± 6    69 ± 3 ┃    ┗━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━┛    With Placeholders    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┃           5 repeats, 1,000,000 times              ┃    ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃    Units: ns     lambda    partial   Placeholders ┃    ┃              ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃      opr_sub ┃  50 ±  2    39 ±  1        44 ±  4 ┃    ┃ opr_contains ┃  61 ±  2    44 ±  2        49 ±  2 ┃    ┃         pos2 ┃  54 ±  2    58 ±  3        64 ±  2 ┃    ┃ pos2(*args1) ┃  67 ±  3    72 ±  9        69 ±  3 ┃    ┃         pos6 ┃  63 ±  3   102 ±  3        99 ±  2 ┃    ┃ pos6(*args3) ┃  75 ±  3   101 ±  2        94 ±  4 ┃    ┃   pos2kw2_kw ┃ 242 ±  7   259 ± 10       260 ±  7 ┃    ┃  pos2kw2_kwe ┃ 131 ±  4    64 ±  1        69 ±  2 ┃    ┗━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛Python Implementation---------------------    Current    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┃    5 repeats, 1,000,000 times      ┃    ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃    Units: ns     lambda    partial ┃    ┃              ┏━━━━━━━━━━━━━━━━━━━━━┫    ┃      opr_sub ┃  48 ±  1   373 ± 13 ┃    ┃ opr_contains ┃  51 ±  1   377 ± 12 ┃    ┃         pos2 ┃  51 ±  4   378 ±  5 ┃    ┃ pos2(*args1) ┃  63 ±  5   354 ±  7 ┃    ┃         pos6 ┃  59 ±  1   437 ±  5 ┃    ┃ pos6(*args3) ┃  75 ±  2   410 ±  7 ┃    ┃   pos2kw2_kw ┃ 239 ±  4   517 ±  5 ┃    ┃  pos2kw2_kwe ┃ 133 ±  3   408 ± 49 ┃    ┗━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━┛    With Placeholders    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┃           5 repeats, 1,000,000 times              ┃    ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃    Units: ns     lambda    partial   Placeholders ┃    ┃              ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃      opr_sub ┃  49 ±  1   392 ± 13       547 ± 12 ┃    ┃ opr_contains ┃  54 ±  2   393 ±  9       605 ± 78 ┃    ┃         pos2 ┃  55 ±  9   398 ±  7       544 ±  5 ┃    ┃ pos2(*args1) ┃  66 ±  2   373 ±  5       533 ±  8 ┃    ┃         pos6 ┃  58 ±  5   462 ±  4       652 ±  3 ┃    ┃ pos6(*args3) ┃  74 ±  2   428 ± 11       635 ±  9 ┃    ┃   pos2kw2_kw ┃ 240 ±  5   533 ±  4       696 ± 10 ┃    ┃  pos2kw2_kwe ┃ 134 ±  2   406 ±  4       555 ±  3 ┃    ┗━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

PyPy Results

PyPy----    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┃     5 repeats, 10,000 times        ┃    ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃    Units: ns     lambda    partial ┃    ┃              ┏━━━━━━━━━━━━━━━━━━━━━┫    ┃      opr_sub ┃ 122 ± 15   266 ± 70 ┃    ┃ opr_contains ┃ 147 ±  7   248 ± 64 ┃    ┃         pos2 ┃ 114 ± 17   204 ± 49 ┃    ┃ pos2(*args1) ┃ 156 ± 24   202 ± 28 ┃    ┃         pos6 ┃ 124 ± 14   268 ± 39 ┃    ┃ pos6(*args3) ┃ 147 ± 36   225 ± 21 ┃    ┃   pos2kw2_kw ┃ 259 ± 17   436 ± 66 ┃    ┃  pos2kw2_kwe ┃ 180 ± 14   243 ± 43 ┃    ┗━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━┛    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┃   5 repeats, 1,000,000 times     ┃    ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃     Units: ns   lambda   partial ┃    ┃               ┏━━━━━━━━━━━━━━━━━━┫    ┃       opr_sub ┃  1 ± 0     3 ± 1 ┃    ┃  opr_contains ┃ 13 ± 0    16 ± 2 ┃    ┃          pos2 ┃  1 ± 0     3 ± 1 ┃    ┃  pos2(*args1) ┃  2 ± 0     2 ± 0 ┃    ┃          pos6 ┃  1 ± 0     2 ± 0 ┃    ┃  pos6(*args3) ┃  2 ± 0     2 ± 0 ┃    ┃    pos2kw2_kw ┃ 42 ± 1    72 ± 2 ┃    ┃   pos2kw2_kwe ┃  2 ± 0     2 ± 0 ┃    ┗━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━┛PyPy Placeholder----------------    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┃             5 repeats, 10,000 times                 ┃    ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃    Units: ns      lambda     partial   Placeholders ┃    ┃              ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃      opr_sub ┃ 114 ±   5   256 ±  82      719 ± 170 ┃    ┃ opr_contains ┃ 142 ±   7   538 ± 536      787 ± 145 ┃    ┃         pos2 ┃ 125 ±  19   239 ±  54      679 ± 116 ┃    ┃ pos2(*args1) ┃ 130 ±  30   199 ±  17      638 ±  48 ┃    ┃         pos6 ┃ 115 ±  16   237 ±  43      785 ± 176 ┃    ┃ pos6(*args3) ┃ 138 ±  25   214 ±  14      703 ±  19 ┃    ┃   pos2kw2_kw ┃ 260 ±  24   382 ±  67      850 ±  92 ┃    ┃  pos2kw2_kwe ┃ 179 ±  28   223 ±  44      661 ±  32 ┃    ┗━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┃          5 repeats, 1,000,000 times             ┃    ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃    Units: ns    lambda   partial   Placeholders ┃    ┃              ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫    ┃      opr_sub ┃  1 ±  0    3 ±  1       156 ±  4 ┃    ┃ opr_contains ┃ 13 ±  0   15 ±  1       173 ±  3 ┃    ┃         pos2 ┃  2 ±  0    3 ±  1       154 ±  7 ┃    ┃ pos2(*args1) ┃  2 ±  0    2 ±  0       148 ±  3 ┃    ┃         pos6 ┃  2 ±  0    3 ±  1       200 ±  2 ┃    ┃ pos6(*args3) ┃  2 ±  0    3 ±  0       217 ± 39 ┃    ┃   pos2kw2_kw ┃ 43 ±  1   71 ±  1       240 ±  2 ┃    ┃  pos2kw2_kwe ┃  2 ±  0    2 ±  0       149 ±  2 ┃    ┗━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Setup:

  • First 2 columns are identical calls - one usinglambda otherpartial.
  • 3rd column is using placeholder to expose 1st argument as opposed to 2nd (or different places for 6-arg case).

CPython:

PyPy:

  • Usage ofPlaceholders results in very poor performance. However, this has no material implication aslambda is more performant thanpartial in all cases and is an optimal choice.

Benchmark Results for__new__

INIT='import functools as ftl; g = lambda a, b, c, d, e, f: (a, b, c, d, e, f);'# CURRENT$PY_MAIN -m timeit -s$INIT'ftl.partial(g, 0, 1, 2)'# 160# PLACEHOLDERSINIT2="$INIT PH=ftl.Placeholder;"$PY_MAIN -m timeit -s$INIT2'ftl.partial(g, 0, 1, 2)'# 170$PY_MAIN -m timeit -s$INIT2'ftl.partial(g, 0, 1, 2, 3, 4, 5)'# 190$PY_MAIN -m timeit -s$INIT2'ftl.partial(g, PH, 1, PH, 3, PH, 5)'# 200
  • There is small performance decrease for initialization without placeholders.
  • Initializing it with placeholders is slower for the same number of arguments (excluding placeholders).
  • But it is not much slower if placeholders are counted as arguments.

To sum up

This extension:

  1. allows extracting current performance benefits ofpartial to few more important (at least from my POV) cases.
  2. seems to allow for certain simplifications to happen by bringing it more in line withlambda/def behaviour. Thus, allowingpartial to be used forpartialmethod application which allows for some simplifications in handling these in other parts of the library - i.e.inspect.

📚 Documentation preview 📚:https://cpython-previews--119827.org.readthedocs.build/

nineteendo and jab reacted with thumbs up emoji
@rhettingerrhettinger self-assigned thisJun 6, 2024
Copy link
Member

@picnixzpicnixz left a comment
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I personally would advocate forPLACEHOLDER instead ofPlaceholder to stress that 1) this is not a global constant, just a module constant, 2) this is not a class, 3) this is named similarly todataclasses.KW_ONLY. (see#119827 (comment))

Copy link
Member

@serhiy-storchakaserhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Sorry for more nitpicks.

dg-pb reacted with thumbs up emoji
@dg-pb
Copy link
ContributorAuthor

Not very convenient to match exact exception message string, which, at least from my experience, is more common case.#123013

@rhettinger
Copy link
Contributor

I just looked at the Placeholder singleton logic and think it is fine because it mirrors the None singleton. It is a little complicated but not in a way that will impair maintenance.

@rhettingerrhettingerenabled auto-merge (squash)September 26, 2024 00:52
@rhettingerrhettinger merged commitd929652 intopython:mainSep 26, 2024
37 checks passed
@bedevere-bot
Copy link

⚠️⚠️⚠️ Buildbot failure⚠️⚠️⚠️

Hi! The buildbotAMD64 CentOS9 NoGIL Refleaks 3.x has failed when building commitd929652.

What do you need to do:

  1. Don't panic.
  2. Checkthe buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/1610/builds/60) and take a look at the build logs.
  4. Check if the failure is related to this commit (d929652) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/1610/builds/60

Failed tests:

  • test__interpchannels
  • test_capi
  • test__interpreters
  • test_ast
  • test_importlib

Test leaking resources:

  • test_importlib: references
  • test_ast: references
  • test__interpchannels: references
  • test__interpreters: memory blocks
  • test_capi: memory blocks
  • test_capi: references
  • test__interpreters: references
  • test_importlib: memory blocks
  • test_ast: memory blocks
  • test__interpchannels: memory blocks

Summary of the results of the build (if available):

==

Click to see traceback logs
Traceback (most recent call last):  File"<string>", line10, in<module>  File"<frozen importlib._bootstrap>", line813, inmodule_from_spec  File"<frozen importlib._bootstrap_external>", line1046, increate_module  File"<frozen importlib._bootstrap>", line488, in_call_with_frames_removedImportError:module _test_module_state_shared does not support loading in subinterpretersxpected failure

@bedevere-bot
Copy link

⚠️⚠️⚠️ Buildbot failure⚠️⚠️⚠️

Hi! The buildbotARM64 MacOS M1 Refleaks NoGIL 3.x has failed when building commitd929652.

What do you need to do:

  1. Don't panic.
  2. Checkthe buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/1368/builds/1855) and take a look at the build logs.
  4. Check if the failure is related to this commit (d929652) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/1368/builds/1855

Failed tests:

  • test__interpchannels
  • test_capi
  • test__interpreters
  • test_ast
  • test_importlib

Test leaking resources:

  • test_importlib: references
  • test_ast: references
  • test__interpchannels: references
  • test__interpreters: memory blocks
  • test_capi: memory blocks
  • test_capi: references
  • test__interpreters: references
  • test_importlib: memory blocks
  • test_ast: memory blocks
  • test__interpchannels: memory blocks

Summary of the results of the build (if available):

==

Click to see traceback logs
Traceback (most recent call last):  File"<string>", line10, in<module>  File"<frozen importlib._bootstrap>", line813, inmodule_from_spec  File"<frozen importlib._bootstrap_external>", line1046, increate_module  File"<frozen importlib._bootstrap>", line488, in_call_with_frames_removedImportError:module _test_module_state_shared does not support loading in subinterpretersxpected failure

@rhettinger
Copy link
Contributor

@dg-pb Congratulations on landing a new feature.

tim-one reacted with thumbs up emojidg-pb and JelleZijlstra reacted with hooray emoji

@dg-pb
Copy link
ContributorAuthor

Thank you@rhettinger.

And thank you everyone for your help. Especially@serhiy-storchaka.

I hope this will prove to be a useful feature.

Was pleasure working with you all.

tim-one reacted with thumbs up emoji

@dg-pbdg-pb deleted the implement-119127-again branchSeptember 26, 2024 05:02
@vstinner
Copy link
Member

The deallocator which marks the Placeholder as an immortal object is causing memory leaks:#124586. I propose to use a regular singleton by just storing a strong reference in the module state:#124601. My PR fix the leak. I don't think that immortal object is needed here.

rhettinger reacted with thumbs up emoji

@bswck
Copy link
Contributor

I'm a very often user ofpartial. This looks so useful, thank you!

dg-pb reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@rhettingerrhettingerrhettinger left review comments

@serhiy-storchakaserhiy-storchakaserhiy-storchaka left review comments

@zwarezwarezware left review comments

@picnixzpicnixzpicnixz left review comments

@erlend-aaslanderlend-aaslanderlend-aasland left review comments

@XuehaiPanXuehaiPanXuehaiPan requested changes

@1st11st1Awaiting requested review from 1st1

@asvetlovasvetlovAwaiting requested review from asvetlov

@gvanrossumgvanrossumAwaiting requested review from gvanrossum

@willingcwillingcAwaiting requested review from willingc

Assignees

@rhettingerrhettinger

Labels
Projects
Archived in project
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

10 participants
@dg-pb@picnixz@rhettinger@serhiy-storchaka@bedevere-bot@vstinner@bswck@zware@erlend-aasland@XuehaiPan

[8]ページ先頭

©2009-2025 Movatter.jp