4

I am writing a small simulation in python, which is supposed to aggregate results in different ways depending on command line arguments.

Once the simulation has run and the Simulation object contains the raw results, I want to use the Simulation.sample(list_of_objects) method or the Simulation.sample_differently() method to generate some outputs for each specified sampler. list_of_objects should either be a range(N) or a list explicitly specified on the command line.

For example, I would like the following calculations to happen.

$ simulation --sample 5
[Simulation.sample(range(5))]
$ simulation --sample-objects 0 1 2 3 a
[Simulation.sample([0, 1, 2, 3, "a"])]
$ simulation --sample 4 --sample-objects 1 3 "b"
[Simulation.sample(range(4)), Simulation.sample([1, 3, "b"])]
$ simulation --sample-differently --sample-objects 1
[Simulation.sample_differently(), Simulation.sample([1])]
$ simulation --sample-objects 0 1 2 --sample-objects 3 4
[Simulation.sample([0, 1, 2]), Simulation.sample([3, 4])]

I thought I would do it as follows.

def parse_objects_to_sampler(object_strings):
    objects = []
    for entry in object_strings:
        try:
            objects.append(int(entry))
        except ValueError:
            objects.append(entry)
    return lambda simulation: simulation.sample(objects))

parser = argparse.ArgumentParser()
parser.add_argument(
    "--sample", action=append,
    type=lambda x: lambda simulation: simulation.sample(range(int(x))))
parser.add_argument(
    "--sample-differently", action="append_const", dest="sample",
    const=Simulation.sample_differently)
parser.add_argument(
    "--sample-objects", nargs="*", action="append", dest="sample",
    type=parse_objects_to_sampler)

for sampler in parser.parse().sample:
    sampler(Simulation)

Unfortunately, the type constructor operates on each individual command line argument, not on the list of several of them generated for nargs≠None, so the approach above does not work.

What is the best pythonic way to achieve the behaviour sketched above?

0

2 Answers 2

1

type should focus on testing the inputs and converting them to basic inputs. It accepts one string as input, and returns some object, or raises an error if the string is invalid. To handle the items of a nargs* list as an aggregate you need to process later, after parsing.

The Pythonic way (or in general good programming) is to break the task into pieces. Use argparse to just parse the inputs, and subsequent code to construct the final list of simulation objects.

For example, I think this parser will accept all of your inputs (I haven't tested it):

parser = argparse.ArgumentParser()
parser.add_argument("--sample", type=int, action='append')
parser.add_argument("--sample-differently", action="store_true")
parser.add_argument("--sample-objects", nargs="*", action="append")
args = parser.parse_args()

The focus is on accepting an integer with --sample, a list of strings with --sample-objects and a True/False value with --sample-differently.

Then I can construct the list of ranges and simulation objects from those arguments (again not tested):

alist = []
if args.sample_differently:
    alist.append(Simulation.sample_differently())
for i in args.sample:
    # is a number
    alist.append(Simulation.sample(range(i)))
for i in args.sample_objects:
    # i will be a list of strings
    def foo(i):
        # conditionally convert strings to integers
        res = []
        for j in i:
            try:
                j = int(j)
            except ValueError:
                pass
        res.append(j)
        return res
    alist.append(Simulation.sample(foo(i))

If I've done things right alist should match your desired lists.

You could create a custom Action class that would perform this kind of addition with the Simulation.sample. The Action gets the whole list as values, which it can process and add to the namespace. But it doesn't save any coding compared to what I outlined.

===============

This pair of definitions might fix your '--samples-objects' argument:

def intorstr(astr):
        # conditionally convert strings to integers
        try:
            astr = int(astr)
        except ValueError:
            pass
        return astr

class SamplesAction(argparse._AppendAction):
    # adapted from _AppendAction
    def __call__(self, parser, namespace, values, option_string=None):
         values = Simulation.sample(values)
         items = _copy.copy(_ensure_value(namespace, self.dest, []))
         items.append(values)
         setattr(namespace, self.dest, items)

parser.add_argument("--sample-objects", nargs="*", 
    action=SamplesAction, dest="sample", type=intorstr)

I'm ignoring the reasons why you are using lambda simulation: simulation..... I think it would be less confusing if you rewrote that as a function or class definition. Too many lambdas cloud the code.

Sign up to request clarification or add additional context in comments.

Comments

1

I think someone with more experience than me could talk about "most pythonic", but the way I would approach this is to accept a CSV string and then use the parse_objects_to_sampler function to split the string and do further logic.

So:

def parse_objects_to_sampler(input_string):
object_string = input_string.split(",")

objects = []
for entry in object_strings:
    try:
        objects.append(int(entry))
    except ValueError:
        objects.append(entry)
return lambda simulation: simulation.sample(objects))

Then you would call, for example:

simulation --sample-objects "0,1,2,3,a"

Hopefully this should get the result you want!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.