Dhiwakar Ravikumar
Dhiwakar Ravikumar

Reputation: 2217

The right way to use **kwargs in Python

I took a look at this question but it doesn't exactly answer my question. As an example, I've taken a simple method to print my name.

def call_me_by_name(first_name):
    print("Your name is {}".format(first_name))

Later on, I realized that optionally, I would also like to be able to print the middle name and last name. I made the following changes to accommodate that using **kwargs fearing that in the future, I might be made to add more fields for the name itself (such as a 3rd, 4th, 5th name etc.)

I decided to use **kwargs

def call_me_by_name(first_name,**kwargs):

    middle_name = kwargs['middle_name'] if kwargs.get('middle_name') else ""
    last_name = kwargs['last_name'] if kwargs.get('last_name') else ""

    print("Your name is {} {} {}".format(first_name,middle_name,last_name))

My only concern here is that as I continue to implement support for more names, I end up writing one line of code for every single keyword argument that may or may not come my way. I'd like to find a solution that is as pythonic as possible. Is there a better way to achieve this ?

EDIT 1

I want to use keyword arguments since this is just an example program. The actual use case is to parse through a file. The keyword arguments as of now would support parsing a file from

1) A particular byte in the file.
2) A particular line number in the file.

Only one of these two conditions can be set at any given point in time (since it's not possible to read from a particular byte offset in the file and from a line number at the same time.) but there could be more such conditions in the future such as parse a file from the first occurrence of a character etc. There could be 10-20 different such conditions my method should support BUT only one of those conditions would ever be set at any time by the caller. I don't want to have 20-30 different IF conditions unless there's no other option.

Upvotes: 1

Views: 2038

Answers (6)

Danilo Souza Morães
Danilo Souza Morães

Reputation: 1593

You have two separate questions with two separate pythonic ways of answering those questions.

1- Your first concern was that you don't want to keep adding new lines the more arguments you start supporting when formatting a string. The way to work around that is using a defaultdict so you're able to return an empty string when you don't provide a specific keyword argument and str.format_map that accepts a dict as a way to input keyword arguments to format. This way, you only have to update your string and what keyword arguments you want to print:

from collections import defaultdict
def call_me_by_name(**kwargs):
    default_kwargs = defaultdict(str, kwargs)
    print("Your name is {first_name} {second_name} {third_name}".format_map(default_kwargs))

2- If, on the other hand and answering your second question, you want to provide different behavior depending on the keyword arguments, like changing the way a string looks or providing different file lookup functionalities, without using if statements, you have to add different functions/methods and call them from this common function/method. Here are two ways of doing that:

OOP:

class FileLookup:

    def parse(self, **kwargs):
        return getattr(self, next(iter(kwargs)))(**kwargs)

    def line_number(self, line_number):
        print('parsing with a line number: {}'.format(line_number))

    def byte_position(self, byte_position):
        print('parsing with a byte position: {}'.format(byte_position))

fl = FileLookup()
fl.parse(byte_position=10)
fl.parse(line_number=10)

Module:

def line_number(line_number):
    print('parsing with a line number: {}'.format(line_number))

def byte_position(byte_position):
    print('parsing with a byte position: {}'.format(byte_position))

def parse(**kwargs):
    return globals()[next(iter(kwargs))](**kwargs)

parse(byte_position=29)
parse(line_number=29)

Upvotes: 2

Frank from Frankfurt
Frank from Frankfurt

Reputation: 218

I think that your call_me_by_name is no good example for **kwargs. But if you want to avoid omitting some exotic, unconsidered name fields, call_me_by_name could look like:

def call_me_by_name(first_name, last_name, middle_name='', **kwargs):
    s = "Your name is {} {} {}".format(first_name,middle_name,last_name)
    if kwargs:
        s += " (" + ", ".join(["{}: {}".format(k,v) for k,v in kwargs.items()]) + ")"
    print(s)

Test:

name = {'first_name': 'Henry', 'last_name': 'Ford', 'ordinal': 'II', 'nickname': 'Hank the Deuce'}
call_me_by_name(**name)
>>> Your name is Henry  Ford (ordinal: II, nickname: Hank the Deuce)

Upvotes: 0

Take_Care_
Take_Care_

Reputation: 2154

I will post it as a answer then :

You can instantly unpack the kwargs values to the format function like this :

"Your name is {} {} {}".format(first_name , *kwargs)

But as a User @PM 2Ring mentioned You must be aware that doesn't guarantee that the names will be in the correct order.

Upvotes: 0

Venkatesh
Venkatesh

Reputation: 31

You can simplify it by:

middle_name = kwargs.get('middle_name', '')

Upvotes: 1

user2201041
user2201041

Reputation:

If your function care so specifically about keyword arguments, this is probably not the right tool. In this case, you can get the same effect with default arguments:

def call_me_by_name(first_name, middle_name="", last_name=""):
    print("Your name is {} {} {}".format(first_name,middle_name,last_name))

It's better for when you want a sort of "grab-bag" of options. E.g.

def configure(**kwargs):
    if 'color' in kwargs:
        set_color(kwargs['color'])

etc.

Upvotes: 1

Alexandru Chirila
Alexandru Chirila

Reputation: 2362

Seem to me your better off not using kwargs and not even a function, you can simply do something like this:

print("Your name is", " ".join([first_name, middle_name, last_name]))

Or if you do want a function:

def call_me_by_name(*args):
     print("Your name is", " ".join(args))

Upvotes: 1

Related Questions