2

I have a search function. This function takes 4 different parameters that can be either a list of strings or a string. For each parameter, if it's a string I convert it to a list of strings.

def search(name: Union[str, List[str], group: Union[str, List[str], ...):
    if isinstance(name, str):
        name = [name]
    if isinstance(group, str):
        group = [group]
    ...

As this function will be part of a framework that will be publicly released, I'm wondering if it's not better to have two different functions ? One where every parameters take a string and the other one a list of strings?

If the best solution is to keep only one function, what about the name of the parameters? Does they need to be plural or not...? I don't want the function to be confusing

I had a look at some other framework like django. There filter function use a lookup attribute in addition to the default parameter like name__in=['a', 'b'] which take a list of the type of name and name='a' that take the default type of the value. But it seems a bit complicated to put in place even if really efficient.

Does anyone encounter this in there development ? How did you manage it on your side ?

2 Answers 2

2

It might be confusing to users of this library if the function takes parameters as a single string or list of strings. Search logic is prone to complexity due to the number of criteria. Introducing a parameter object gives users of this library something concrete to use when calling the search function. Since you can search by a single name or multiple names, really you can search by multiple names. Sometimes there is only one name to search by. Same thing for groups:

args = SearchParameters()

args.add_name("...")
args.add_group("...)

results = search(args)

The name of the parameter class is dependent on what you are searching for, so choose an appropriate name for your use case.

Another option is to create a fluent API for searching. This is more complicated, and might be more idiomatic of languages like Java or C#. You would create one or more classes that can be used to build a search query programmatically:

results = foo.search()
             .where_names_are("...", "...")
             .where_groups_are("...", "...")
             .fetch()

While this is more complicated to implement, it might be easier to use. This is something to consider if making this library public.

4
  • That's Joshua Blochs Builder Pattern. It's only idiomatic in languages that need to simulate named arguments like Java, but not Python or C#. You'll see code that looks vaguely like that in Python and C# when you advance to a DSL. This lets you enforce construction rules prior to run time. Jan 27 at 15:33
  • Thaks for your answer and your comment @candied. The examples you came up with are pretty interesting. I will have a look closely to the second solution you mentioned. If other people that read this have other ideas let us know. Thanks
    – CyDevos
    Jan 27 at 16:02
  • @candied_orange: I find Joshua Bloch's builder pattern useful in languages that support named arguments, as well. That variation of the builder pattern gives you a much more expressive syntax in cases where named arguments are not enough. If you get more than one "." deep (e.g. foo.bar().baz()) then the builder pattern is better. There is no way to capture that in quite the same detail with named arguments. Jan 27 at 19:08
  • I fail to see how "deep" comes into this. Joshua Bloch's builder is explicitly about simulating named arguments. That doesn't mean there aren't useful fluent interfaces that do more than simulate named arguments. But I don't see you doing that more here. If you are I missed it. Would you mind pointing it out? Jan 27 at 19:18
3

I don't particularly see an issue with allowing either a string or a list of strings as an argument, as long as you handle both cases gracefully. For example, the pandas.DataFrame.groupby method does exactly that, allowing you to call it as any of the following:

  • df.groupby('foo')
  • df.groupby(by='foo')
  • df.groupby(['foo', 'bar'])
  • df.groupby(by=['foo', 'bar'])

(and even a few other data types!)

In my opinion, the intent is clear: you can either group data by one or multiple columns. Same as you can search data by one or multiple values for each parameter.

You could even define a decorator that handles the conversion for you, so that, you don't need to worry about cluttering the body of your search function with a bunch of isinstance checks:

def listify_args(func):
    def f(*args, **kwargs):
        args = list(args)
        for i, arg in enumerate(args):
            if isinstance(arg, str):
                args[i] = [arg]
        for key, value in kwargs.items():
            if isinstance(value, str):
                kwargs[key] = [value]
        return func(*args, **kwargs)
    return f


@listify_args
def search(a, b, c, d):
    print(a)
    print(b)
    print(c)
    print(d)


search('John', ['Users', 'Admins'], c='foo', d=['bar', 'baz'])
# output:
# ['John']
# ['Users', 'Admins']
# ['foo']
# ['bar', 'baz']

Of course, if your search functionality ever evolves into something much more complex than what we're discussing here, a more structured alternative (e.g. as suggested by Greg) may be better. But I'd try to keep things simple at first.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.