from functools import wraps
def defaults(**default_kwargs): # Decorator factory
def decorator(func): # Decorator function
@wraps(func)
def wrapper(*args, **kwargs): # Actual wrapper
# Iterate over all default keyword arguments
for name in default_kwargs.keys():
# And if not specified in the passed in kwargs use the default
kwargs[name] = kwargs.get(name, default_kwargs[name]())
# Call the actual function
return func(*args, **kwargs)
return wrapper
return decorator
# Note that the default argument value should be a callable to avoid the pitfall with mutable arguments
@defaults(bar=lambda: [1, 2, 3])
def foo(bar):
print(bar)
foo()https://gist.github.com/softwaredoug/bbfa2541d48c685908ebd6a...
def foo(bar: list):
print(bar)
that type checking will be happy.Both in terms of enhancing one's understanding of python's memory model, and in terms of it being an incredibly useful feature, e.g. for introducing static/persistent variables to a function (without the need for global declarations)
I would not want to see this "fixed"
def myfun(person = Person()): ...
will evaluate `Person()` only once. If you want it to be evaluated every time, you have to say: def myfun(person = None):
if person is None: person = Person()
...
What's being suggested, is to change the language so that the default-arg code: def myfun(person = Person()): ...
will evaluate `Person()` on every call. If you wanted it evaluated only once, you would have to say: __default_person = Person()
def myfun(person = __default_person): ...
In either case, it's pretty easy to obtain the other case, and the question is what the best default is. My impression is that "evaluate every time" is both a lot less surprising and more commonly what you want, so it would be the better default.It makes sense if you realize that the function is defined once, and for the function need to be defined, its signature needs to be defined with it. Default arguments are part of the signature, not part of the function body.
It would make sense to pass a callable without calling it, like
def myfun(person = Person): ...
but if the call occurs in the function signature, it doesn't really make sense that the call would somehow happen again and again. You're not defining a function in the signature; you're specifying a value right there.The current PEP proposal is to enable the "evaluate at function call time" ability with new syntax, which I think makes the most sense semantically and also doesn't break existing code.
There are things besides functions and values! They're called "expressions" or "code blocks", and in an impure language like Python they can produce a different value each time you evaluate them. In my second example, "Person()" is neither a value nor a function -- it's an expression that would be evaluated each time the function is run. This isn't that weird; in `while condition: body`, `body` is a block that's evaluated multiple times. It would need to capture variables from the local environment though, which makes it more complicated to implement.
> The current PEP proposal is to enable the "evaluate at function call time" ability with new syntax, which I think makes the most sense semantically and also doesn't break existing code.
Oh yeah I wasn't suggesting breaking existing code. Just that if I was designing a language from scratch, it would probably avoid the Python foot-gun and have default arguments evaluated each time.
When performing Python interviews I usually try to have that behavior come up in the coding question - not to quiz for trivia so much as to get a feeling for adding on battle-scar bonus points.
In practice, for any non plain-ol-data defaults I just have it default to None and handle it in logic.
Totally understand why it may be useful for introducing a persistent store to a function, but as a matter of taste I like to do that in an explicit decorator if needed.
> Both in terms of enhancing one's understanding of python's memory model...
In much the same way as having your company bricked by a buffer-overflow ransomware attack does in the C world? I'm not a big fan of elucidation by gotcha.
> [...and] for introducing static/persistent variables to a function (without the need for global declarations)
Except when you are at risk of someone calling with their own variable(s) that do not have the static-singleton semantics that the default would have.
(let ((my-list '(1 2 3)))
(defun foo (&optional (l my-list))
(setf (car l) 'hello)
l))
or, more often, just make it a special variable of some sort: (defvar *my-list* '(1 2 3))
(defun foo (&optional (l *my-list*))
(setf (car l) 'hello)
l))
This seems to work fine, and brings attention to "I have an object that supports mutation, and I'm actually going to mutate it," which is an unusual enough case to be worth calling out. def fn(s, n=>len(s)):
"=>" just kind of bugs me, for some reason. (I love me some bikeshedding.)Of the alternatives they offer, ":=" kind of looks nicest to me:
def fn(s, n:=len(s)):
Though it doesn't really line up with the semantics of the existing walrus operator. Also, as the PEP mentions, it could look confusing when mixed with type annotations.Semantically, the "?=" alternative might be best, since it kind of makes it clear it's a default, and doesn't conflict with anything else in the language:
def fn(s, n?=len(s)):
Kind of reads like "present? else = ...".edit: At this point I'd shift from "might be best" to "almost certainly is best". I'm now a zealous "?=" advocate.
`x ?= default` is perfect IMHO. There's already prior art (Makefiles) that this means exactly "present? else = ..." with lazy evaluation of RHS expression.
It also would allow eliminating the "if x is SENTINEL" pattern, where None is a valid non-default argument.
def foo(..., cache={}):
...
to implement cached functions, or functions which need an internal cache for whatever reason. Later versions of Python provide an lru cache decorator, but that doesn't help with the internal cache and can be inefficient when you don't want your cache to forget any results.The only potential downside is that standard, function definition-time default arguments also imply "present? else = ...". But I think of all the options available, this one makes the most sense.
I'm not sure it handles the use case op is talking about.
IE def foo(bar=list()): # this creates a mutable list shard by all calls of the function. Very unlikely something you want.
Maybe the pep would allow for def foo(bar=>list()):
But the pep does not address this use case at all
>Function parameters can have default values which are calculated during function definition and saved. This proposal introduces a new form of argument default, defined by an expression to be evaluated at function call time.
Doing it the if-statement way is the clearest and easiest for other programmers to follow, therefore it is the best way.
If you make the functions to supply late-bound defaults arguments to the decorator, its pretty doable; here’s what I just threw together.
from inspect import signature, Parameter
def with_deferred_defaults(*args, **kwargs):
def decorate(fun):
parameters = signature(fun).parameters
positional_parameters = [parameter for parameter in parameters.values() if parameter.kind in [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD]]
defaults = list(args)
kwdefaults = kwargs
def wrapper(*args, **kwargs):
args = list(args)
if len(args) < len(defaults):
args = args + [arg() for arg in defaults[len(args):]]
new_kwargs = {k: v() for k, v in kwdefaults.items()}
new_kwargs.update(kwargs)
for i, p in enumerate(positional_parameters):
if i >= len(args):
if p.name in new_kwargs:
args.extend([None]*(1+i-len(args)))
args[i] = new_kwargs[p.name]
del new_kwargs[p.name]
elif p.default is Parameter.empty:
raise TypeError('Missing argument: {}'.format(p.name))
else:
if p.name in new_kwargs:
del new_kwargs[p.name]
return fun(*args, **new_kwargs)
return wrapper
return decorateI'd also like whatever a solution is to work well with type annotations.
I'm surprised there isn't a pep to improve the situation. (Maybe there is and I haven't found it!)
def foo(Bar=Bar):
bar = Bar()
...
Pass in the class or any callable and then call it in the body to make an instance.This is commonly used to implement logging callbacks e.g.
from collections.abc import Callable
def log_fn(message: str = ''):
print(f'This is your log: {message}')
def run_read_file(file_to_read:str, log_handler: Callable=log_fn):
contents = open(file_to_read).readlines()
for line in contents:
log_handler(message=line)https://docs.python.org/3/library/dataclasses.html#dataclass...
Functions are in fact objects as well, so there might be some way to do this sort of behavior with decorators.
I would actually contend that "if x is None, x =Bar()" is actually a superior pattern in some/many cases. I used to use "x or Bar()" or "x if x is not None else Bar()" quite a lot, due to its compactness. However I learned that actually sometimes code coverage fails to ensure proper coverage of these ternary assignments. If your default argument is just because you need to initialize a mutable object, ternary works fine. However if the presence or lack of a parameter is an important part of your state space, you probably want explicit coverage.
https://stackoverflow.com/questions/47958166/code-coverage-a...
Anyways, my point is that I can write `if X is None: X = bla` and be confident it will be correct in almost all situations. For `X = X or Y` there are some nasty corner cases where the distinction between `[]` and `""` and `{}` is somehow important, or the distinction between `is` and `==` is important, or different falsy values have different annotations attached to them, or or or....
Which is why I just do `if param is None:... ` and not worry about the few extra lines of code and move on.
The trick does have the nice property of delaying execution for evaluating what the default parameter though. The only other way to do that would involve writing lambdas which is even more code? The sibling comment suggesting a package looks like even more code to write as a user. I think any really elegant fix would have to be done in the language itself.
To me it's like complaining that (++a) is a "trick" that lets you use secret temp values that are hard to understand. I mean.. _I guess_, but it's C so it seems like the problem is that you don't like C.
Why? This is like someone saying "I don't like declaring my variables in C at the top of the function".
It is a good and readable solution to a somewhat rare problem. Passing mutable objects as default function parameters is a common code smell in Python.
And no, adding a special syntax to an already syntax-heavy language to address this is not good.
It’s boilerplate that distracts from the purpose of the function, this getting in the way of readability.
It’s also just more code prone to a programmer screwing up from time to time, despite how simple it is.
Functions are not a good tool for storing stateful objects in Python. That's why classes exist.
def foo(bar=None):
bar = bar or Bar()
if you know that bar can't evaluate to false in edge cases.In general I just try my best to avoid immutable, or dynamic defaults, but sometimes they're necessary and I make that same if block at the top of the function. If I catch myself copy/pasting the same setup to multiple functions, then it might be time for a class or a decorator.
Apart from the common solutions (initialize to None and check, etc.) this is exactly what this PEP is about: https://www.python.org/dev/peps/pep-0671/
What is your list of "these little hang ups you only know about once you’ve pulled your hair out" if you don't mind sharing?
That said, it is not always desirable that a new object is created each time the function is called without the arg. One obvious example is when the default is an immutable object that's either large or expensive to construct.
Or an empty list.
Or any other falsish value.
For example, this doesn't do what people expect:
Because this creates exactly one Bar at function definition time, not a Bar per function call.Instead what people usually want is:
I hate having to have a block of `if bar is None` at the top of my functions.I wonder if there's a library that helps make creating functions with default args easier? Or any tips people have?