0

Suppose I have the following functions:

class Foo:
   @classmethod
   def bar(cls, a, b): # do not want to use *args or **kwargs
      return 'a b'
 
   @classmethod
   def advanced_bar(cls, a, b): # do not want to use *args or **kwargs
      ab = cls.bar(a, b)
      return ab.upper()

And my question is: how do I, in a streamlined, somewhat robust way, capture the calling scope args as a *args-type object and write advanced_bar as:

   @classmethod
   def bar(cls, a, b): # did not define any *args or *kwargs bins for arguments to overflow into
      return 'a b'

   @classmethod
   def advanced_bar(cls, a, b):
      ...
      x = 'y'  # added in a local variable
      
      ab = cls.bar(*this_functions_args)
      return ab.upper()

Where I added some extra things that I think will break the jankiest solution to this question.

Specifically, I want to avoid the redundant, verbose noise:

    def advanced_bar(cls, a, b): # specifically defined args (I do not want to accept *args or **kwargs here)
       args = (a,b)
       ab = cls.bar(*args) # haven't really gained anything
       return ab.upper()

So, in light of that solution, I think a decorator is a sufficing answer.


The reason why I am not using *args and **kwargs at the entry point of the functions is to leverage the IDE.

I think verbosity is very important in the function signature, and that is my personal style -- I think it is a mistake to drop names from the interface.

So, I am looking for a way to halve my cake and eat it too: it seems like there should be such a way.


One way to have explicitly named function arguments, no *args or **kwargs holes in the code, define as many local variables before hand as desired, and have the *args syntactical tool might be to decorate the function, internally, somehow...

# alternatively, I can use the idiom
def foo:
    args = locals()
Chris
  • 28,822
  • 27
  • 83
  • 158
  • 2
    You'll have to be clearer as to what you want, because `def advanced_bar(cls, *args): x = 'y'; ab = cls.bar(*args)` will work fine. (What do you *do* with `x`?) – chepner Feb 15 '21 at 15:37
  • @chepner ok I'll rule that out: I *ABSOLUTELY DO NOT WANT* to define a function that accepts `*args` – Chris Feb 15 '21 at 15:38
  • 2
    `locals` is way jankier than you think. You're likely to have your function mysteriously break when you run it in a debugger if you take that approach, among other nasty pitfalls. – user2357112 Feb 15 '21 at 15:50
  • 1
    I don't get what the desired goal of this is. What are you trying to gain? Why not call ``cls.bar(a, b)``? Are you trying to pass the arguments implicitly, i.e. pass all local variables matching the target function's argument names automatically? How would you expect to handle anonymous, positional and variadic arguments? What about multiple-dispatch functions with different signatures? – MisterMiyagi Feb 15 '21 at 15:53
  • @MisterMiyagi If I have several functions with the exact same function signature, then I can use them and chain them easily. This is a standard functional programming benefit when you have consistently typed functions (function type is determined by the function signature) that do different and complementary things. Anyways, I would like to leverage this archetype in `python`, but it appears to be difficult. – Chris Feb 15 '21 at 15:56
  • @MisterMiyagi "I would like to leverage this archetype in python" in a way that is a bit more organized and structured than `*args`, `**kwargs`, where anything is possible. – Chris Feb 15 '21 at 16:01
  • @Chris That doesn't really answer my question. I mean concretely what you expect to do inside ``advanced_bar`` that you aren't doing already. What you are showing does not appear to be function chaining, so I don't know how it relates to "leverage this archetype". – MisterMiyagi Feb 15 '21 at 16:02
  • @user2357112supportsMonica thanks man -- I would never use locals. I am listing it here to avoid that answer: it is not an answer I'll accept. I think the solution may be some sort of decorator pattern. – Chris Feb 15 '21 at 16:02
  • @MisterMiyagi I don't know man, I have minimally and viably illustrated the semantic barrier I am facing. What do you care what I do inside those semantics? I am obviously not going to be transitioning a string to upper case...and I am obviously not naming my functions `foo`, `bar` with arguments `a` and `b`. – Chris Feb 15 '21 at 16:04
  • @MisterMiyagi and as to the rest of your quesiton, a great example of solution spaces ideal for function chaining are aerospace, chemical solutions, physics, maxwells equations, etc: all have consistent parameters you can toss around from black box to black box for calculation and solving. Another great example of function spaces with consistent interfaces are in the domain of image mutation, transformation, blurring etc. where subsequent calls will have the same argument structure and output structure. This isn't rocket science and is the basic idea of functional programming and math. – Chris Feb 15 '21 at 16:08
  • @Chris I care what you do inside those semantics because a) that is what you are asking about, and b) you haven't defined some things that would impact possible solutions. So, again, "Are you trying to pass the arguments implicitly, i.e. pass all local variables matching the target function's argument names automatically?" Can you always rely on the target having a consistent signature? – MisterMiyagi Feb 15 '21 at 16:10
  • @MisterMiyagi a larger scale problem which may be a point of overlap with our backgrounds will be docker. Docker leverages a docker file. Always the same input, but you call it over and over to do different things. Other configurable tools work this way. It is almost safe to say this functional architecture I am describing is the *only* architecture that exists (with everything else being specialized details) – Chris Feb 15 '21 at 16:10
  • @MisterMiyagi that is exactly what I am trying to do: pass Only the exact arguments supplied to the outer function straight through to the inner function Without leveraging an open-ended set mechanic like `*args` and `**kwargs` so as 1) to leverage the IDE, and 2) to keep my code doing what it is supposed to do – Chris Feb 15 '21 at 16:11
  • Okay, so do you want to pass the parameters of the outer function along or the local values of the variables with the same name? Are you looking for syntax, as in ``cls.bar()`` or are you fine with actively applying decorators or macros to the function? Do you actually need methods/descriptors or just functions (i.e. does ``self``/``cls`` need special handling)? – MisterMiyagi Feb 15 '21 at 16:19
  • @MisterMiyagi "Particle Physicist" funny, I was a physicist in the field of electronic warfare and aerospace simulation. If programming language theory interests you, then you are a lot like me. Let's talk on the same level: you have function signature `(a:str,b:str)-> (str)` in the first and second function. That means they are functionally the same `type`. In the second function `advanced_bar` I would like to solve `bar` and add onto it. `upper()` illustrates that. The parameters are the same. I need a tight expression for capturing *JUST* the parameters of the function (`a`, and `b`) – Chris Feb 15 '21 at 16:21
  • @MisterMiyagi I am fine with `@decorator`, fine with a macro (python has macros?), but obviously `bar()` is ideal. The scope here is I want **just** the arguments outlined in the expression `def foo(these,arguments,only)` – Chris Feb 15 '21 at 16:23
  • @MisterMiyagi and, well, as one former pen-and-paper guy to (maybe) another (or maybe you have always been in computing): my bad, if there has been unintentional whatever – Chris Feb 15 '21 at 16:26
  • Need to think this through a bit, but it's probably possible to cook something up. Note though that the idiomatic thing is just to explicitly call ``cls.bar(a, b)``. Also, if you are looking for general function composition/chaining, there are patterns to do composition as ``advanced_bar = bar * str.upper`` – basically as a monad hiding a trampoline loop. – MisterMiyagi Feb 15 '21 at 17:37
  • @MisterMiyagi Cool -- yeah I think there was a comment down there, but the idea is to introduce a mechanic similar to that found in javascript, where there is special access to the functions arguments. It may not sound like much, but it tends to empower a person reasoning about code dramatically. The `*args, **kwargs` method impairs and obfuscates the "reasoning", and explicitly listing out the attributes requires the mind to see `foo(some,super,long,arg,list)` and know that it is the same as the calling context. – Chris Feb 15 '21 at 17:55

3 Answers3

1

This is possible, but a bad idea, because you're relying on CPython internals. Suppose you just want to handle *args, not **kwargs:

import sys

def get_local_args():
    calling_frame = sys._getframe().f_back
    caller = calling_frame.f_code
    relevant_vars = caller.co_varnames[:caller.co_argcount]
    return [calling_frame.f_locals[name] for name in relevant_vars]

def foo(a, b):
    if (a, b) == (1, 2):
        return "a b"
    return "c d"

def bar(a, b):
    c = 23
    assert foo(*get_local_args()) == "a b"

bar(1, 2)
L3viathan
  • 26,748
  • 2
  • 58
  • 81
0

The standard idiom for passing all function args (both positional and named) would be:

   @classmethod
   def bar(cls, a, b): # did not define any *args or *kwargs bins for arguments to overflow into
      return 'a b'

   @classmethod
   def advanced_bar(cls, *args, **kwargs):
      ...
      x = 'y'  # added in a local variable
      
      ab = cls.bar(*args, **kwargs)
      return ab.upper()

If that doesn't meet your requirements, please clarify your question.

Thomas
  • 174,939
  • 50
  • 355
  • 478
  • I understand that is the idiomatic way to write some things, I am attempting to avoid creating that sort of open ended input. I am explicitly asking for the solution to the question I posed above -- no modification of the function input signatures – Chris Feb 15 '21 at 15:40
  • I'll accept this, but it will warp my question into two more questions: how do I mark up `advanced_bar` so that the IDE knows what args it requires, and how do I secure the function parameters (the *args, **kwargs) in `advanced_bar` so that only the parameters I think are going in will be going in? (or is there just no way) – Chris Feb 15 '21 at 15:58
  • I don't think there is a way but it depends on your IDE. Some IDEs might support parsing docstrings, in which there is a convention for listing argumens. To validate the arguments, you can of course check `len(args)` and so on, but then the cure is worse than the ailment of simply repeating the args. – Thomas Feb 15 '21 at 16:16
-1

How about using *args in your function signature and destructuring it into components in the function body?

def something_else(a, b):
  print(a, b)

def do_something(*args):
  a, b = args
  print('A', a)
  
  something_else(*args)

EDIT:

Hacky way using locals(), based on https://stackoverflow.com/a/50763376/3080592

def do_something(a, b):
  args = locals()
  something_else(*args)
chvolkmann
  • 524
  • 2
  • 9
  • this is the hacky answer that I was aware of. I defined `x = 'y'` and did not allow the inner function to accept anything other than the args I defined. Do you know of a way to identify only the function arguments with this `locals()` method? – Chris Feb 15 '21 at 15:45
  • I wouldn't recommend this, but you could subtract the module-level locals from the function-level arguments - I'm not sure if that clears all non-function-arguments though. So before the function, `module_locals = locals()`. Inside the function body, `args = set(locals()) - set(module_locals)`. – chvolkmann Feb 15 '21 at 16:09
  • Also if you have a module-level variable `a` and an argument `a`, this breaks – chvolkmann Feb 15 '21 at 16:11
  • 1
    But maybe this could be helpful in your search: JavaScript has this exact feature. You can access all arguments a function received through `let args = Array.from(arguments)`, where [`arguments` is a JS keyword](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments) – chvolkmann Feb 15 '21 at 16:22