2

I'm writing a program that takes a permutations "model" of strings, and outputs all permutations according to that model. The model looks something like this:

model = Mix([
    [
        "this",
        PickOne(["is", "isn't"])
    ],
    PickOne([
        Mix([
            "absolutely",
            "great"
        ])
    ])
])

Within the outputted permutations,

  1. list objects will output the containing objects in sequence
  2. Mix objects will output the containing objects in every possible order with ever possible max length (including zero)
  3. PickOne objects will output only one of its containing objects at a time

So, the desired output of the above example would be:

[
    ["this", "is"],
    ["this", "isn't"],
    ["this", "is", "absolutely"],
    ["this", "is", "great"],
    ["this", "isn't", "absolutely"],
    ["this", "isn't", "great"],
    ["absolutely"],
    ["great"],
    ["absolutely", "this", "is"],
    ["great", "this", "is"],
    ["absolutely", "this", "isn't"],
    ["great", "this", "isn't"],
    []
]

So far, I've implemented the permutations for the Mix class like this:

class Mix(list):
    def __init__(self, *args, **kwargs):
        super(Mix, self).__init__(*args, **kwargs)
        self.permutations = []
        for L in range(0, len(self)+1):
            for subset in itertools.combinations(self, L):
                subset_permutations = itertools.permutations(subset)
                self.permutations.extend(subset_permutations)

and my PickOne class is simply this

class PickOne(list):
    pass

which I'm planning on just testing for in my main function and handling accordingly (if type(obj) is PickOne: ...).

itertools provides simplification and efficiency for simpler use-cases, however I'm at a loss on how it will help me for this implementation (beyond what I already implemented in Mix). Any ideas of how itertools can be used to help in a Pythonic implementation of the above?

Julien
  • 5,243
  • 4
  • 34
  • 35
  • I'm having a hard time wrapping my head around this.So even a plain "list", if it contains other "permutation models", should yield the product of all the possible results of those models, right? – tobias_k Jun 03 '17 at 16:53
  • That's correct. – Julien Jun 03 '17 at 20:22

1 Answers1

1

I'm having some troubles wrapping my head around all those combinations, but I think your Mix class also has to yield the product of all those permutations. I've created a similar model to test this. This might work a bit different than yours, but it should be easy to adapt:

class CombModel:
    def __init__(self, *options):
        self.options = options

class Atom(CombModel):
    def __iter__(self):
        for x in self.options:
            yield x

class All(CombModel):
    def __iter__(self):
        for prod in itertools.product(*self.options):
            yield prod

class One(CombModel):
    def __iter__(self):
        for x in self.options:
            for y in x:
                yield y

class Mix(CombModel):
    def __iter__(self):
        for n in range(len(self.options) + 1):
            for perm in itertools.permutations(self.options, n):
                for prod in itertools.product(*perm):
                    yield prod

The CombModel only provides a var-arg constructor for all the sub-classes. Atom is a "leaf" in the model and it's there so that I can "iterate" even over single strings or integers. This saves some if/else logic in all the other classes. All is what seem to be plain lists in your model, yielding the product of the different options. One and Mix correspond to your PickOne and Mix.

Using this rather simple model comb = Mix(All(Atom(1), Atom(21, 22)), One(Atom(31, 32), Atom(4))) I get the these combinations:

()
((1, 21),)
((1, 22),)
(31,)
(32,)
(4,)
((1, 21), 31)
((1, 21), 32)
((1, 21), 4)
((1, 22), 31)
((1, 22), 32)
((1, 22), 4)
(31, (1, 21))
(31, (1, 22))
(32, (1, 21))
(32, (1, 22))
(4, (1, 21))
(4, (1, 22))

Your model translates to this:

model = Mix(
    All(
        Atom("this"),
        One(Atom("is"), Atom("isn't"))
    ),
    One(
        Mix(
            Atom("absolutely"),
            Atom("great")
        )
    )
)

And gives those combinations:

()
(('this', 'is'),)
(('this', "isn't"),)
((),)
(('absolutely',),)
(('great',),)
(('absolutely', 'great'),)
(('great', 'absolutely'),)
(('this', 'is'), ())
(('this', 'is'), ('absolutely',))
(('this', 'is'), ('great',))
(('this', 'is'), ('absolutely', 'great'))
(('this', 'is'), ('great', 'absolutely'))
(('this', "isn't"), ())
(('this', "isn't"), ('absolutely',))
(('this', "isn't"), ('great',))
(('this', "isn't"), ('absolutely', 'great'))
(('this', "isn't"), ('great', 'absolutely'))
((), ('this', 'is'))
((), ('this', "isn't"))
(('absolutely',), ('this', 'is'))
(('absolutely',), ('this', "isn't"))
(('great',), ('this', 'is'))
(('great',), ('this', "isn't"))
(('absolutely', 'great'), ('this', 'is'))
(('absolutely', 'great'), ('this', "isn't"))
(('great', 'absolutely'), ('this', 'is'))
(('great', 'absolutely'), ('this', "isn't"))

Note that those are still nested lists (or tuples, actually), but those can easily be flattened afterwards. Also, there are a few pseudo-duplicates (that would be duplicates when flattened) due to the "zero or more" nature of Mix. But apart from that, this looks rather close to your expected output.


On closer inspection, it might indeed be necesary to flatten first, so that PickOne will select only one from the Mix and not the entire Mix...

class All(CombModel):
    def __iter__(self):
        for prod in itertools.product(*self.options):
            yield flat(prod) # flatten here

class One(CombModel):
    def __iter__(self):
        for x in self.options:
            for y in flat(x): # flatten here
                yield y

class Mix(CombModel):
    def __iter__(self):
        for n in range(len(self.options) + 1):
            for perm in itertools.permutations(self.options, n):
                for prod in itertools.product(*perm):
                    yield flat(prod) # flatten here

After weeding out the duplicates, the result is this:

()
('absolutely',)
('absolutely', 'this', 'is')
('absolutely', 'this', "isn't")
('great',)
('great', 'this', 'is')
('great', 'this', "isn't")
('this', 'is')
('this', 'is', 'absolutely')
('this', 'is', 'great')
('this', "isn't")
('this', "isn't", 'absolutely')
('this', "isn't", 'great')
tobias_k
  • 81,265
  • 12
  • 120
  • 179
  • 1
    Hmm, this doesn't seem to work when a `One` contains an `All` object- instead of picking the `All` in its entirety, it picks one of its inner contents. Here's a gist: https://gist.github.com/flux627/a30bf5a3da08730c7cf2e0083046d694 – Julien Jun 04 '17 at 08:21
  • @Julien Isn't this how it's supposed to work? At least that's consistent with how One(Mix(...)) worked in your example. Also, my first version, without the flattening, should work like this, with the One picking all of the All's content. – tobias_k Jun 04 '17 at 09:57
  • You're right. My specification was not perfect, but I got this to do what I need by simply omitting the `flatten` in `All`. Thanks again – Julien Jun 04 '17 at 14:59