-1

Is it possible to replace the function foo below with real code such that

def foo():
    #calling_scope()['a']=2
def bar(a):
    print(a)
    foo()
    print(a)
bar(1)

prints

1
2

?


To avoid an XY problem, what I really want is to have foo be part of an external module that summarizes common parsing code. At the moment, I solve the parsing as follows: I have a module parse_args that contains a bunch of functions such as

def bool(arg):
  if arg in [True,'True','true']:
     return True
  if arg in [False,'False','false']:
     return False
  raise ValueError('Did not understand boolean value')
def nonnegative(arg):
  if arg<0:
    raise ValueError('Argument cannot be negative')
  return arg  

and I do the following in bar:

def bar(arg1,arg2)
    arg1=parse_args.bool(arg1)
    arg2=parse_args.nonnegative(arg2)

What I would like to be able to do instead is:

def bar(arg1,arg2)
    parse_args(arg1='bool',arg2='nonnegative')

where the pseudocode of parse_args is

def parse_args(**kwargs)
    #cs=calling_scope()
    #for arg in kwargs:
    #    cs[arg]=globals()[kwargs[arg]](cs[arg])

I know this is only marginally less wordy, and I understand that there are probably reasons to favor my current approach over what I aim for, but as someone still learning Python, I am really mainly interested in the feasibility here.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Bananach
  • 2,016
  • 26
  • 51
  • A downvote is not equivalent to 'No'. And asking stupid questions is not equivalent to asking bad questions. – Bananach Sep 20 '17 at 07:44

2 Answers2

2

No, it's not possible. You can't alter the local namespace of a function from further down the stack, because the CPython implementation has highly optimised the local namespace in such a way that makes any manipulation impossible.

You are going about your problem the wrong way. Rather than rely on the standard local namespace, create your own in the form of a dictionary. You can then just pass that dictionary around:

def bar(arg1, arg2)
    namespace = {'arg1': arg1, 'arg2': arg2}
    parse_args(namespace, arg1='bool', arg2='nonnegative')
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Works, but that is even more wordy than what I am doing right now, as it requires writing each variable name three times (or five times if I afterwards want to have `arg1`,`arg2` bound to the results of parse_args) – Bananach Sep 20 '17 at 07:42
  • 1
    @Bananach: then generate that namespace as early as possible, and centralise the validation parameters. Pass around dictionaries from there on. – Martijn Pieters Sep 20 '17 at 07:54
  • Sorry, I really don't know what that last comment means – Bananach Sep 20 '17 at 07:56
  • When you take those parameters in as input, you want to produce a dictionary from that (the `namespace` dictionary in my example). The `arg1='bool'` validation configuration has to be centralised somewhere to ease re-use. Everything else can be dynamic and rely on the central configuration from there on out. – Martijn Pieters Sep 20 '17 at 08:01
  • I found a solution to my problem that avoids having to access the local namespace with the use of decorators. If you are interested, I would be happy about some feedback (see EDIT of my question) – Bananach Nov 02 '17 at 08:54
  • @Bananach: please do not use your question as a scratchpad for communication; the idea is that posts here are useful for future visitors and by changing your question you broke the question-answer pairing. I'm going to have to roll back that change, sorry. – Martijn Pieters Nov 02 '17 at 08:55
  • That's why I added an `EDIT` section and left everything above untouched. I have been following this practice over on `math.stackexchange` for years and noone has ever complained. Would you be happy if I had added a date to the `EDIT`? – Bananach Nov 02 '17 at 08:58
  • @Bananach: but yes, using a decorator to control what is passed in is a much better approach. – Martijn Pieters Nov 02 '17 at 08:59
  • @Bananach: but it is a new question, and one that would bee too broad for SO (but code review might accept it if there is more code to review). Stack Overflow is a different site from Math, if the community there is fine with the practice then that's their choice, but that doesn't necessarily extend here. :-) – Martijn Pieters Nov 02 '17 at 09:01
  • I don't think it was a new question but a solution to the old question, which is why I now posted it as an answer, which I hope is more appropriate. My comment here was indeed q question to you how you like the solution, which again, is probably better addressed by you commenting (or not) on my now added answer. – Bananach Nov 02 '17 at 09:05
0

I found a way to do the parsing that I want in a way that is more beautiful than I hoped for, using function annotations and decorators. (Effectively this actually modifies variable bindings in the function where I want them modified, technically this is possible because through the use of decorators the caller becomes the callee.)

I now write

@validated()
def f(a:'nonnegative integer',b:'bool|function', c:'float in_range(0,1)', d:'{float}', e:'{integer:any}',f:'bool'):  
    print(a,b,c,d,e,f)

and get

>>f(a=2,b=lambda x: x>2, c=0,d=[1.2, 2.3], e={1:'foo',2:'bar'},f='true')
2 <function <lambda> at 0x7f4779ae8400> 0.0 [1.2, 2.3] {1: 'foo', 2: 'bar'} True

Note that the integer argument c=0 got converted to the float 0.0 and the string 'true' got converted to the bool True. Curly braces in the annotations indicate any iterable, with colons they indicate dictionaries. Vertical bars indicate alternatives. Different requirements can be concatenated by spaces, as done for parameters a and c. If any of the specifications is not met, for example a=-1, a well-founded error is thrown:

swutil.validation.ValidationError: Invalid argument for parameter 'a': -1 was rejected by 'nonnegative integer' (-1 was rejected by 'nonnegative')

Finally, the decorator takes arguments that can be used to specify which arguments must be passed. For example, @validated('a^b') and @validated('a|b') specify that (exactly) one of a and b must be passed. The other one will be filled with a NotPassed instance.

If anyone is interested in the code, leave a comment and I'll share.

Bananach
  • 2,016
  • 26
  • 51
  • Yes, inverting the problem by using a decorator is a much better approach. If this is a design spec for your decorator then I do think it sounds rather a complicated spec; I'd probably use callables to pass the values through rather than strings to parse. And unless this is handling user input (config, etc.) I'd not do validation like this at all. – Martijn Pieters Nov 02 '17 at 09:08
  • This is not a design, I already implemented it. I felt strings were more natural, but you are right (if I understand you right), doing `All[Nonnegative,Integer]` or `All[Float,Range(0,1)]` would have saved me a lot of parsing code. I don't really know which parts of my code will be user code and which won't. I'm mostly doing medium-size half-interactive scientific computations in ipython, reusing little bits of my code every then and now throughout the years. – Bananach Nov 02 '17 at 10:07