1

Is there a way in python to check for the number of output arguments a function is called with from inside of the function being called?

For example:

a,b = Fun() #-> number of output arguments would be 2
a,b,c = Fun() #-> number of output arguments would be 3

In matlab this would be done using nargout I know that the "normal way" of doing it is to unpack the unwanted values to the _ variable:

def f():
    return 1, 2, 3

_, _, x = f()

What I am trying to accomplish is simple. I have a function that will return a single object if called with some arguments or two objects otherwise:

def f(a,b=None):
    if b is None:
        return 1
    else:
        return 1,2

But I would like to force the tuple unpacking not to happen and force an error, for example:

x = f(a) #-> Fine
x,y = f(a,b) #-> Fine
x,y = f(a) #-> Will throw native error: ValueError: need more than Foo values to unpack
x = f(a,b) #-> Want to force this to throw an error and not default to the situation where x will be a tuple.
Community
  • 1
  • 1
Mac
  • 3,397
  • 3
  • 33
  • 58
  • You may want to skim through the archives of [python-ideas](http://mail.python.org/mailman/listinfo/python-ideas) from a few months ago. Someone suggested adding an "unpack protocol" separate from the iterator protocol, which would make something like this almost trivial. IIRC, the idea was shot down, but some "hacky implementations in today's Python" were suggested along the way. – abarnert May 10 '13 at 19:06

2 Answers2

2

As Ashwini pointed out this seems to work:

import inspect,dis

def expecting():
    """Return how many values the caller is expecting"""
    f = inspect.currentframe()
    f = f.f_back.f_back
    c = f.f_code
    i = f.f_lasti
    bytecode = c.co_code
    instruction = bytecode[i+3]
    if instruction == dis.opmap['UNPACK_SEQUENCE']:
        howmany = bytecode[i+4]
        return howmany
    elif instruction == dis.opmap['POP_TOP']:
        return 0
    return 1

def cleverfunc():
    howmany = expecting()
    if howmany == 0:
        print("return value discarded")
    if howmany == 2:
        return 1,2
    elif howmany == 3:
        return 1,2,3
    return 1

def test():
    cleverfunc()
    x = cleverfunc()
    print(x)
    x,y = cleverfunc()
    print(x,y)
    x,y,z = cleverfunc()
    print(x,y,z)

test()
Mac
  • 3,397
  • 3
  • 33
  • 58
  • Note that this depends on bytecode, which is only guaranteed to exist in the reference implementation, CPython, and may not work in other Python implementations. Even in CPython, opcodes and their semantics can change between feature releases without notice. In particular, the instruction size for all opcodes changed in Python 3.6. Additionally, this does not correctly handle the case for extended unpacking (e.g. with `a, *b, c = func()`), in which case you would want to return as many results as you have (or document that your function is not designed to be called that way). – jcgoble3 Apr 25 '20 at 04:10
1

If you use Python 3, you can use extended iterable unpacking on the LH:

>>> def f(n):
...    return tuple(range(n))
... 
>>> f(3)
(0, 1, 2)
>>> a,b,*c=f(20)
>>> a
0
>>> b
1
>>> c
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

With your function, you can do this:

>>> def f(a,b=None):
...     if b is None:
...         return 1
...     else:
...         return 1,2
>>> rtr,*_=f(1),None
>>> rtr
1
>>> rtr,*_=f(1,True),None
>>> rtr
(1, 2)

In either case, _ will be None and rtr will either be 1 or (1,2)

You may want to explicitly return a tuple from f to avoid later ambiguity:

>>> def f(a,b=None):
...    return (1,2) if b else (1,)
... 
>>> rtr,*_=f(1),None
>>> rtr
(1,)
>>> rtr,*_=f(1,True),None
>>> rtr
(1,2)

And if you do it that way -- it works under Python 2X as well:

Python 2.7:

>>> def f(a,b=None):
...    return (1,2) if b else (1,)
... 
>>> x,y=f(1),None
>>> x
(1,)
>>> x,y=f(1,True),None
>>> z
>>> x
(1, 2)
dawg
  • 98,345
  • 23
  • 131
  • 206