Алгоритм простой - рекурсивно генерируйте все разбиения.
Текущий предмет может пойти или в одну из занятых кучек, или в первую пустую, если они есть.
Так, первый предмет обязательно пойдет в первую группу. Второй может пойти в ту же или во вторую. И т.д.
Чтобы не было пустых групп, элемент обязательно кладется в первую пустую, если их осталось столько же, сколько осталось элементов распределить. Ну, и, нельзя создавать новую группу, их уже, сколько надо. Удобнее распологать элементы с конца.
Что-то вроде такого:
def partition(n, k, answer):
if n == 0 :
yield answer
cur_len = len(answer)
if k-cur_len < n:
for i in range(cur_len):
answer[i].append(n)
yield from partition(n-1, k, answer)
answer[i].pop()
if cur_len < k:
answer.append([n])
yield from partition(n-1,k, answer)
answer.pop()
for x in partition(4, 2, []):
print(x)
Вроде как set_partitions из пакета more-itertools делает именно то, что вам надо, но так-то алгоритм - всего несколько строк.
Этот алгоритм переберет все разбиения без повторов, потому что групировка элементов однозначно задает и порядок групп (группа с 1 - всегда первая. Потом группа с минимальным элементом - вторая и т.д.)
Edit:
Если же вам надо разбить предметы по 1, 2 и т.д. групп сразу же, а не только на фиксированные k групп, то надо чуть поменять условия в коде - надо сделать return в начале, после yield, и можно будет ставить предмет в любую группу или в новую всегда. Параметр k будет не нужен.