Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Missing 1 Required Keyword-Only Argument

When I try to enter a dataframe as a function parameter in Python 3.6, I'm getting the error 'Missing 1 Required Keyword-Only Argument' for the following function, where df is a dataframe and rel_change is an array:

def get_mu(*rel_change, df):

    row_count = len(df.index)
    print("mu count")
    print(row_count)
    mu_sum = 0
    for i in range (0, len(rel_change)):
        mu_sum += rel_change[i]
    mu = (mu_sum) / row_count
    return mu

Then I access it like

mu = get_mu(g, df) 

which gives the error.

I've also tried writing the dataframe access in another function that just calculates row_count, and passing that into mu, but that gives the same error. What could I be doing wrong?

like image 691
amita00 Avatar asked Feb 15 '18 19:02

amita00


2 Answers

You have defined a function with a variable amount of positional arguments, *rel_change, which can only ever be followed by keyword only arguments. In this case, you have to pass df by name like so:

mu = get_mu(g, df=df)

Or redefine get_mu() such that df appears before *rel_change.

like image 60
Adam Barnes Avatar answered Nov 02 '22 23:11

Adam Barnes


You should flip your arguments to be get_mu(df, *rel_change). Don't forget to flip the function call as well: get_mu(df, g). Optional positional arguments (often called star args in reference to the conventional name for the parameter, *args) need to go after keyword arguments.

For more detail, I strongly recommend the book "Effective Python: 59 Specific Ways to Write Better Python" by Brett Slatkin. Here's an excerpt on that topic following the break:


Item 18: Reduce Visual Noise with Variable Positional Arguments

Accepting optional positional arguments (often called star args in reference to the conventional name for the parameter, *args) can make a function call more clear and remove visual noise.

For example, say you want to log some debug information. With a fixed number of arguments, you would need a function that takes a message and a list of values.

def log(message, values):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('My numbers are', [1, 2])
log('Hi there', [])

>>>
My numbers are: 1, 2

Hi there Having to pass an empty list when you have no values to log is cumbersome and noisy. It’d be better to leave out the second argument entirely. You can do this in Python by prefixing the last positional parameter name with *. The first parameter for the log message is required, whereas any number of subsequent positional arguments are optional. The function body doesn’t need to change, only the callers do.

def log(message, *values):  # The only difference
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('My numbers are', 1, 2)
log('Hi there')  # Much better

>>>
My numbers are: 1, 2

Hi there

If you already have a list and want to call a variable argument function like log, you can do this by using the * operator. This instructs Python to pass items from the sequence as positional arguments.

favorites = [7, 33, 99]
log('Favorite colors', *favorites)

>>>
Favorite colors: 7, 33, 99

There are two problems with accepting a variable number of positional arguments.

The first issue is that the variable arguments are always turned into a tuple before they are passed to your function. This means that if the caller of your function uses the * operator on a generator, it will be iterated until it’s exhausted. The resulting tuple will include every value from the generator, which could consume a lot of memory and cause your program to crash.

def my_generator():
    for i in range(10):
        yield i

def my_func(*args):
    print(args)

it = my_generator()
my_func(*it)

>>>
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

Functions that accept *args are best for situations where you know the number of inputs in the argument list will be reasonably small. It’s ideal for function calls that pass many literals or variable names together. It’s primarily for the convenience of the programmer and the readability of the code.

The second issue with *args is that you can’t add new positional arguments to your function in the future without migrating every caller. If you try to add a positional argument in the front of the argument list, existing callers will subtly break if they aren’t updated.

def log(sequence, message, *values):
    if not values:
        print('%s: %s' % (sequence, message))
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s: %s' % (sequence, message, values_str))

log(1, 'Favorites', 7, 33)      # New usage is OK
log('Favorite numbers', 7, 33)  # Old usage breaks

>>>
1: Favorites: 7, 33
Favorite numbers: 7: 33

The problem here is that the second call to log used 7 as the message parameter because a sequence argument wasn’t given. Bugs like this are hard to track down because the code still runs without raising any exceptions. To avoid this possibility entirely, you should use keyword-only arguments when you want to extend functions that accept *args (see Item 21: “Enforce Clarity with Keyword-Only Arguments”).

Things to Remember

  • Functions can accept a variable number of positional arguments by using *args in the def statement.
  • You can use the items from a sequence as the positional arguments for a function with the * operator.
  • Using the * operator with a generator may cause your program to run out of memory and crash.
  • Adding new positional parameters to functions that accept *args can introduce hard-to-find bugs.
like image 37
ZaxR Avatar answered Nov 02 '22 23:11

ZaxR