A way to have the default value for a parameter as the default of another function: the Default singleton

FWIW here is a real-world example from a large library that wraps Numpy for GPU support. I used None here because that is what the wrapped API uses, and we wanted to match it precisely. But usually I wanted to be more careful in similar situations, I would define an Unset class to use.

def setflags(
    self,
    write: Union[bool, None] = None,
    align: Union[bool, None] = None,
    uic: Union[bool, None] = None,
) -> None:

    ...

    # Be a bit more careful here, and only pass params that are explicitly
    # set by the caller. The numpy interface specifies only bool values,
    # despite its None defaults.
    kws = {}
    if write is not None:
        kws["write"] = write
    if align is not None:
        kws["align"] = align
    if uic is not None:
        kws["uic"] = uic

    self.__array__().setflags(**kws)

I don’t really see what a built-in Default (or Unset or whatever) would buy here. If the goal is to avoid passing unset parameters in order that called-functions use their own defaults, then you still have to go through the work to actually exclude the unset values, since in general you wouldn’t be able to assume the function you call inside to know anything about Default or how to handle it.

What might be useful is a way to get all the passed params as a dict, so that it could just be easily filtered. Then you could write the above:

def setflags(
    self,
    write: Union[bool, None] = None,
    align: Union[bool, None] = None,
    uic: Union[bool, None] = None,
) -> None:

    kws = {k: v for k, v in __kwargs__.items() if v is not None} # or Unset, etc
    self.__array__().setflags(**kws)

where “__kwargs__” means roughly: “all this functions params in one dict regardless of how they were supplied”.

2 Likes

If called at the top of your function, does locals() achieve this for you?

Ha, yes it just might. Sometimes an obvious solution hides in plain sight.

3 Likes

I think I finally got it. Try this:

import inspect
from enum import Enum


class Default(Enum):
    pass


def parametrized(dec):
    def layer(*args, **kwargs):
        def repl(f):
            return dec(f, *args, **kwargs)

        return repl

    return layer


@parametrized
def mimic_defaults(func, *to_mimic):
    d = tuple(
        v.default
        for f in to_mimic
        for k, v in inspect.signature(f).parameters.items()
        if v.default is not inspect.Parameter.empty
        and k in inspect.signature(func).parameters
    )
    func.__defaults__ = d
    return func


@mimic_defaults(sorted)
def mysorted(it, key=Default, reverse=Default):
    # some code
    return sorted(it, key=key, reverse=reverse)


mysorted.__defaults__

Output:

(None, False)

Am I wrong or inspect is not done to be used in prod?

Not sure, why shouldn’t it be?

This is a real world example (censored):

# lot of imports

class MyStreamer(TimelineStreamer):
    """
    Objects of this class can read data and can send 
    them to a client through the associated websocket.
    
    @author Marco Sulla
    @date Dec 16, 2015
    """
    
    def send(self, *args, end_date=None, data=None, binary=True, **kwargs):
        if data is None:
            data = self.getData(*args, end_date=end_date, **kwargs)[:]

        # lot of code
        
        res = super(MyStreamer, self).send(
            *args, 
            data = data_to_send, 
            binary = binary, 
            end_date = end_date, 
            **kwargs
        )
        
        return res

As you can see, the overridden send method has end_date and binary parameters, that are used by two different methods.

So you can’t simply use kwargs or make all the default None and filter them, because you have also to split the parameters in two groups.

1 Like

Note

Some callables may not be introspectable in certain implementations of Python. For example, in CPython, some built-in functions defined in C provide no metadata about their arguments.

Correct, you are wrong. inspect is fine to use in production.

We already have one of those: None. 9 times out of 10, None stands for “missing value”, but the other 1 time out of 10 None stands for itself.

The same will apply to your Default object: 9 times out of 10 it will stand for your missing value with semantics “replace this with the default”. But the rest of the time it will stand for itself, an object and therefore a value.

Since Default is a value, then it has an ID, and we have to be able to pass it to id() to retrieve that ID. Since it is an object, it has a type, and we have to be able to pass it to type() to retrieve that type. And get its repr, and convert to a string. We have to be able to write Python functions which can inspect the object itself, such as vars(), dir(), functions in the inspect module, etc. So it becomes up to the function itself to decide whether Default stands for a missing value, or itself.

Exactly like None.

And just like None, which is a value, it must be possible to put it inside collections (lists, sets, dicts):

args = [21, "Hello", Default, None]
kw = dict(colour='red', size=Default)
func(*args, **kw)

If Default can be the value of some key in a dict, then it can be the value of some key in an instance or class __dict__, which means it can be an attribute.

All this is just a long-winded way of saying that Default will be a value just like None, with exactly the same issues as using None as the default.

The idea that we can bless one specific value to be “not a value” doesn’t work. We already have one of those, None, and its not enough. Two (None + Default) will not be enough either.

Nor will three, or four, or five, or five thousand: any values that are not values will always suffer the same fate that sometimes we will need to treat them as themselves, and not a stand-in for something else.

1 Like

Yes, I know what inspect.signature does. Why would you think it shouldn’t be used in prod?

Yes. What’s your point?

Some sequences don’t have an append method. Some numeric types don’t have a bit_length method. Does that mean you can’t use floats in production?

There are over 80 functions and classes in the inspect module. What is so special about that one function that it disqualifies you from using the entire module in production code?

Question: Do they have the same default? Will they always have the same default? And if it’s not possible to answer those questions, does it even make sense to have a single send() parameter doing double duty, without choosing a default for send itself?

Also, still not sure why it’s so special to reference the default value, but you don’t mind duplicating the full list of parameter names. It’s MUCH more common for a function to grow a new (keyword) parameter than for it to change the default for an existing one, and your Default magic non-object wouldn’t help at all with that.

Then how about using a hash map to update default argument of the enclosing function using parameters of the inner function while fully maintaining the original order?

import inspect


def parametrized(dec):
    def layer(*args, **kwargs):
        def repl(f):
            return dec(f, *args, **kwargs)

        return repl

    return layer


@parametrized
def mimic_defaults(func, to_mimic):
    dict_outer = dict(inspect.signature(func).parameters.items())
    dict_inner = dict(inspect.signature(to_mimic).parameters.items())

    # Update the parameters hash map of the outer function using information from the
    # function located inside
    dict_outer.update((k, dict_inner[k]) for k in dict_outer.keys() & dict_inner.keys())

    func.__defaults__ = tuple(
        v.default for k, v in dict_outer.items() if v.default is not inspect.Parameter.empty
    )
    return func


@mimic_defaults(sorted)
def mysorted(it, key=None, reverse=None):
    # some code
    return sorted(it, key=key, reverse=reverse)

Very clever, but it seems like a lot of effort for very little gain.

It seems to me that the annoyance of dealing with default values might be lessened a little by including something like this in functools, but in my experience, nine times out of ten the annoyance of having to use a decorator and an import will be greater than the annoyance it is solving.

1 Like

Yes, I think either adding something like this to functools, or, alternatively, to well-maintained functional programming library like funcy wouldn’t hurt if for your specific use case you run into “I wish I would just mimic defaults for my functions without much hassle” a lot. Inflating a number of built-in objects for a rather niche use case of “None but for a specific margin case of handling defaults” wouldn’t be the right approach.

Thank you for trying to find a solution that let you to code less and it’s elegant, but the problem is inspect.signature is not always reliable, as I wrote before.

The workaround of Zachary works, but it’s not very elegant IMHO.

Sorry to not reply to every other posts, but no time for pointless discussions.

1 Like

Yes, Zachary already posted a solution in which you can use None. As I wrote, just too much verbose IMHO.

Well, if this is just pointless discussion to you, then I guess this thread is over. Use whatever method you like out of the ones that already work, because you haven’t convinced many people that this is even worth pursuing.

Sorry Chris, don’t take it personal. Mine was just a general consideration, since I observed that threads in Idea sections tends to degenerate in metaphysical or marginal discussion.

I agree with you that it seems no much people is interested in the matter, so I doubt that Default will be ever implemented in CPython. Personally I think I’ll use the Zachary solution in my code (if I ever return to code in Python and see goodbye to Java), even if the solution of Vladimir was really interesting.

1 Like