functools.partial: fix some arguments now
partial(func, *args, **kwargs) returns a new callable with some arguments already filled in. It is the tidy way to adapt a general function to a place that expects a simpler one: a sort key, a callback, a table of converters.
- Build a partial with positional and keyword arguments, and call it with the rest
- Use partials as sort keys and as entries in a table of functions
- Explain why a partial built in a loop keeps its own value while a lambda in a loop may not
Many places want a function of a particular shape: sorted(key=...) wants one argument, a callback might want none, map wants one. Your function often takes more. **functools.partial(func, *args, **kwargs) returns a callable that remembers those arguments; calling it adds the new ones: `partial(func, a)(b, c=1)` runs `func(a, b, c=1)`. Its arguments are fixed when the partial is created**, and you can inspect them: .func, .args, .keywords.
Run each example to see what it prints. Running examples doesn't count toward progress.
from functools import partial def convert(amount, rate, fee=0.0): return amount * rate - fee to_eur = partial(convert, rate=0.92) to_gbp_card = partial(convert, rate=0.79, fee=1.5) parse_hex = partial(int, base=16) print(to_eur(50)) print(to_gbp_card(100)) print(to_eur(50, fee=2)) print(parse_hex("ff"), parse_hex("1A")) print(to_eur.func.__name__, to_eur.args, to_eur.keywords)
A partial is handy wherever a function is passed around. Here one general scoring function becomes a sort key with fixed weights, and a loop builds one filter per nutrient. In a loop, partial captures each value as it is created, which is exactly where a lambda written in the loop goes wrong (the concept check shows it).
from functools import partial def score(work, citations_weight, year_weight): return work["cited_by_count"] * citations_weight + work["publication_year"] * year_weight works = [ {"id": "W3003257820", "publication_year": 2020, "cited_by_count": 38040}, {"id": "W2970971581", "publication_year": 2019, "cited_by_count": 31488}, {"id": "W3021613070", "publication_year": 2020, "cited_by_count": 2362}, ] by_citations = partial(score, citations_weight=1, year_weight=0) by_recent = partial(score, citations_weight=0.001, year_weight=100) print([w["id"] for w in sorted(works, key=by_citations, reverse=True)]) print([w["id"] for w in sorted(works, key=by_recent, reverse=True)]) def above(product, nutrient, limit): return product["nutriments"].get(nutrient, 0) > limit checks = {} for nutrient, limit in {"sugars_100g": 22.5, "salt_100g": 1.5}.items(): checks[nutrient] = partial(above, nutrient=nutrient, limit=limit) corn_flakes = {"nutriments": {"sugars_100g": 10.71, "salt_100g": 1.785}} print({name: check(corn_flakes) for name, check in checks.items()})
When to use it: to adapt a general function to a slot that expects fewer arguments, to build tables of configured functions, and as a clearer alternative to a lambda that only fills in arguments. Mistakes to avoid: fixing a positional argument in the wrong position (partial(convert, 0.92) sets amount, not rate: use keywords); fixing a value that should change between calls; and expecting a partial to have __name__ (it doesn't; look at .func.__name__).
partial(func, *args, **kwargs)returns a callable with those arguments filled in; calls add the rest.- Fix arguments by keyword unless they really are the first positional ones.
- A partial stores values when it is created, which avoids the late-binding trap of lambdas in loops.
- Next: composing functions into pipelines.