Package: dynamic-default-args
Python Dynamic Default ArgumentsThis package provides facilities to make default arguments of Python's functions dynamic (in an elegant manner).ContextThe package solves a problem that was also mentioned in
thisstackoverflow thread.The common approach is to define a function that retrieves the value of thedefaultargument stored somewhere:class_empty(type):pass# placeholderB='path/to/heaven'defget_default_b():# function that retrieves the 'default' valuereturnBdeffoo(a,b=_empty):ifbis_empty:b=get_default_b()send_to(a,destination=b)# do somethingdefmain():globalBB='path/to/hell'foo('Putin')The old standard way is ok, but we should be aware of numbers of function calls when there are many arguments to be made
dynamically default.
But the point is, it doesn't look nice.This module's solution limits to a single wrapper function, which iscompiled from string to minimize overheads on
runtime.RequirementsPython 3Installationdynamic-default-argsis available on PyPI, this is a pure Python
package.pipinstalldynamic-default-argsUsageThis package provides two components:named_default: A object that has a name and contains a value.
This is a singleton-like object, which means any initialization with the same name will return the first one with the
same registered name.dynamic_default_args: A function decorator for substituting any givennamed_defaultwith its value when function
is called.Creating anamed_default:There are 3 ways to initialize anamed_default:Pass a pair of positional argumentsnamed_default([name], [value])Pass the two keywordsnamed_default(name=[name], value=[value])Pass a single keyword argumentnamed_default([name]=[value]).fromdynamic_default_argsimportnamed_default# method 1x=named_default('x',1)# method 2y=named_default(name='y',value=2)# method 3z=named_default(z=3)It is not necessary to keep the reference of this object as you can always recover them when callingnamed_defaultagain with the same name. New value passed to the constructor will be ignored.fromdynamic_default_argsimportnamed_defaultprint(named_default('x').value)named_default('x').value=1e-3Trying to access an unregistered name will raise Exception.fromdynamic_default_argsimportnamed_defaultprint(named_default('an_unregistered_name').value)# ValueError: an_unregistered_name has not been registered.Decorating functions withdynamic_default_args:Here is an example inexample.pyon Python 3.8+:fromdynamic_default_argsimportdynamic_default_args,named_default# Note that even non-dynamic default args can be formatted because# both are saved for populating positional-only defaults args@dynamic_default_args(format_doc=True)deffoo(a0,a1=named_default(a1=5),a2=3,/,a3=named_default(a3=slice(0,3)),a4=-1,*a5,a6=None,a7=named_default(a7='python'),**a8):"""A Foo function that has dynamic default arguments.Args:a0: Required Positional-only argument a0.a1: Positional-only argument a1. Dynamically defaults to a0={a1}.a2: Positional-only argument a1. Defaults to {a2}.a3: Positional-or-keyword argument a2. Dynamically defaults to a3={a3}.a4: Positional-or-keyword argument a4. Defaults to {a4}*a5: Varargs a5.a6: Keyword-only argument a5. Defaults to {a6}.a7: Keyword-only argument a6. Dynamically defaults to {a7}.**a8: Varkeywords a8."""print(f'Called with: a0={a0}, a1={a1}, a2={a2}, a3={a3}, 'f'a4={a4}, a5={a5}, a6={a6}, a7={a7}, a8={a8}')# test output:foo(0)# Called with: a0=0, a1=5, a2=3, a3=slice(0, 3, None), a4=-1, a5=(), a6=None, a7=python, a8={}How it works?Internally, the auto generated wrapper with similar signature for this function will be (without formatting):defwrapper(a0,a1=a1_,a2=a2_,a3=a3_,a4=a4_,*a5,a6=a6_,a7=a7_,**a8):func(a0,a1._valueifisinstance(a1,default)elsea1,a2,a3._valueifisinstance(a3,default)elsea3,a4,*a5,a6=a6,a7=a7._valueifisinstance(a7,default)elsea7,**a8)whose defaults are set to those offunc(=foo), but the containednamed_defaults will be type checked and have
their values forwarded instead.
How the arguments are forwared depend on the type of arguments:Positional-only: with its name, e.g.a0,a1,a2Keyword-or-position: with its name, e.g.a3,a4Varargs: with an asterisk operator, e.g.*a5Keyword-only: with its name as key, e.g.a6=a6,a7=a7Varkeywords: with double asterick operator, e.g.**a8Note:For those who don't know, the type of argument depends on its position relative to the 3 syntax's/,*,
and**:deff(po0,___,/,pok0,____,*args,kw0,kw1,_____,**kwargs):#    ---------     -----------    |   ----------------     |#    |             |              |   |                    |#    |             Positional -   |   |                Varkeywords#    |             or -keyword    |   Keyword - only#    Positional - only         Varargs...Note:The aliaseswrapper, func, defaultare assured to be different from the original arguments' names.Docstring formattingBy configuring@named_default_args(format_doc=True)(which is the default behavior), the decorator will try to bind
the default values of arguments with names defined in format keys{[argument_name]}in the docstring.
Any modification to thevalueproperty ofnamed_defaultwill update the docstring with an event.named_default('a1').value*=2named_default('a3').value=range(10)named_default('a7').value='rust'help(foo)Output:(even normal default arguments will be formatted)foo(a0, a1=10, a2=3, /, a3=range(0, 10), a4=-1, *a5, a6=None, a7='rust', **a8)
    A Foo function that has dynamic default arguments.
    
    Args:
        a0: Required Positional-only argument a0.
        a1: Positional-only argument a1. Dynamically defaults to a0=10.
        a2: Positional-only argument a1. Defaults to 3.
        a3: Positional-or-keyword argument a2. Dynamically defaults to a3=range(0, 10).
        a4: Positional-or-keyword argument a4. Defaults to -1
        *a5: Varargs a5.
        a6: Keyword-only argument a5. Defaults to None.
        a7: Keyword-only argument a6. Dynamically defaults to rust.
        **a8: Varkeywords a8.BindingThenamed_defaultobject will emit an event to all registered listeners when itsvalueproperty is modified.
You can register your own handler by calling.connectmethod:fromdynamic_default_argsimportnamed_defaultvariable=named_default('my_variable',None)defon_variable_changed(value):print(f'Changed to{value}')variable.connect(on_variable_changed)# modifying the slot has no effect# but accessing its value is much faster this wayvariable._value='this doesn\'t work'variable.value='this works!'# Changed to this works!LimitationsThis solution relies on function introspection provided by theinspectmodule, which does not work on built-in
functions (including C/C++ extensions).
However, you can wrap them with your own Python function.ForCythonusers, adeforcpdef(which might be inspected incorrectly) function defined in.pyxfiles can be
decorated by settingbinding=True.importcythonfromdynamic_default_argsimportdynamic_default_args,named_default@dynamic_default_args(format_doc=True)@cython.binding(True)defadd(x:float=named_default(x=0.),y:float=named_default(y=0.)):"""``cython.binding`` will add docstring to the wrapped function,so we can format it later.Args:x: First argument, dynamically defaults to {x}y: Second argument, dynamically defaults to {y}Returns:The sum of x and y"""returnx+yAlso, it is clear that decorators are not lazily initialized.Further improvements:Modifying thefunc.__defaults__should be more performant.LicenseThe code is released under MIT-0 license. SeeLICENSE.txtfor details.
Feel free to do anything, I would be surprised if anyone does use this 😐.