Aplatir une liste de listes irrégulière

440

Oui, je sais que ce sujet a déjà été abordé ( ici , ici , ici , ici ), mais pour autant que je sache, toutes les solutions, sauf une, échouent sur une liste comme celle-ci:

L = [[[1, 2, 3], [4, 5]], 6]

Où la sortie souhaitée est

[1, 2, 3, 4, 5, 6]

Ou peut-être encore mieux, un itérateur. La seule solution que j'ai vue qui fonctionne pour une imbrication arbitraire se trouve dans cette question :

def flatten(x):
    result = []
    for el in x:
        if hasattr(el, "__iter__") and not isinstance(el, basestring):
            result.extend(flatten(el))
        else:
            result.append(el)
    return result

flatten(L)

Est-ce le meilleur modèle? Ai-je oublié quelque chose? Des problèmes?

telliott99
la source
16
Le fait qu'il y ait autant de réponses et tant d'action sur cette question suggère vraiment que cela devrait être une fonction intégrée quelque part, non? C'est particulièrement dommage que le compiler.ast ait été supprimé de Python 3.0
Mittenchops
3
Je dirais que ce dont Python a vraiment besoin, c'est d'une récursion ininterrompue plutôt que d'un autre intégré.
terre battue du
2
@Mittenchops: en total désaccord, le fait que les personnes qui travaillent avec les API / structures de données trop compliqué évidemment mauvais (juste une note: lists destinés à être homogène) ne signifie pas que c'est la faute d'un Python et nous avons besoin d' une commande intégrée pour une telle tâche
Azat Ibrakov
1
Si vous pouvez vous permettre d'ajouter un package à votre projet - je suppose que la solution more_itertools.collapse le fera le mieux. De cette réponse: stackoverflow.com/a/40938883/3844376
viddik13

Réponses:

382

L'utilisation des fonctions du générateur peut rendre votre exemple un peu plus facile à lire et probablement augmenter les performances.

Python 2

def flatten(l):
    for el in l:
        if isinstance(el, collections.Iterable) and not isinstance(el, basestring):
            for sub in flatten(el):
                yield sub
        else:
            yield el

J'ai utilisé l' ABC Iterable ajouté en 2.6.

Python 3

En Python 3, le basestringn'est plus, mais vous pouvez utiliser un tuple de stret bytespour y obtenir le même effet.

L' yield fromopérateur renvoie un élément d'un générateur un à la fois. Cette syntaxe de délégation à un sous-générateur a été ajoutée en 3.3

def flatten(l):
    for el in l:
        if isinstance(el, collections.Iterable) and not isinstance(el, (str, bytes)):
            yield from flatten(el)
        else:
            yield el
Cristian
la source
6
De toutes les suggestions sur cette page, c'est la seule qui aplatit cette liste l = ([[chr(i),chr(i-32)] for i in xrange(ord('a'), ord('z')+1)] + range(0,9))en un clin d'œil quand je l'ai fait list(flatten(l)). Tous les autres commenceraient à travailler et prendraient une éternité!
nemesisfixx
7
Cela aplatit également les dictionnaires. Peut-être que vous souhaitez utiliser à la collections.Sequenceplace de collections.Iteratable?
josch
1
Cela ne fonctionne pas avec des choses qui ne sont pas des listes au départ, par exemple for i in flatten(42): print (i). Cela pourrait être corrigé en déplaçant le isinstance-test et la clause else en dehors de la for elboucle. (Ensuite, vous pourriez y jeter n'importe quoi, et cela en ferait une liste aplatie)
RolKau
6
Pour Python 3.7, l'utilisation collections.Iterableest déconseillée. Utilisez collections.abc.Iterableplutôt.
dawg
5
En effet, la récursivité n'est jamais nécessaire. Dans ce cas spécifique, l'utilisation de la récursivité n'est pas la meilleure solution car elle se plantera sur des listes profondément imbriquées (profondeur> 1000). Mais si vous ne visez pas à avoir quelque chose de sûr, alors les fonctions récursives oui sont meilleures car elles sont beaucoup plus faciles à lire / écrire.
cglacet
50

Ma solution:

import collections


def flatten(x):
    if isinstance(x, collections.Iterable):
        return [a for i in x for a in flatten(i)]
    else:
        return [x]

Un peu plus concis, mais à peu près la même chose.

Josh Lee
la source
5
Vous pouvez le faire sans importer quoi que ce soit si vous voulez juste try: iter(x)tester s'il est itérable… Mais je ne pense pas que devoir importer un module stdlib soit un inconvénient à éviter.
abarnert
8
À noter que cette solution ne fonctionne que si tous les éléments sont de typeint
alfasin
1
Pourrait le rendre plus concis, def flatten(x): return [a for i in x for a in flatten(i)] if isinstance(x, collections.Iterable) else [x]- mais la lisibilité pourrait être subjective ici.
Zero
4
cela ne fonctionne pas sur les chaînes car les chaînes sont également itérables. Remplacez la condition parif isinstance(x, collections.Iterable) and not isinstance(x, basestring)
aandis
remplacer collections.Iterableparlist
noobninja il y a
36

Générateur utilisant la récursion et le typage canard (mis à jour pour Python 3):

def flatten(L):
    for item in L:
        try:
            yield from flatten(item)
        except TypeError:
            yield item

list(flatten([[[1, 2, 3], [4, 5]], 6]))
>>>[1, 2, 3, 4, 5, 6]
dansalmo
la source
1
Merci, cela fonctionne bien pour Python 3. Pour 2.x, le précédent est nécessaire: for i in flatten(item): yield i
dansalmo
list (flatten ([['X'], 'Y']))) échoue sur la variante 2.X
sten
@ user1019129 voir mon commentaire au-dessus du vôtre
dansalmo
oui ça échoue avec le cycle .. je pense car une chaîne est aussi un "tableau" -de-chars
sten
35

Version génératrice de la solution non récursive de @ unutbu, comme l'a demandé @Andrew dans un commentaire:

def genflat(l, ltypes=collections.Sequence):
    l = list(l)
    i = 0
    while i < len(l):
        while isinstance(l[i], ltypes):
            if not l[i]:
                l.pop(i)
                i -= 1
                break
            else:
                l[i:i + 1] = l[i]
        yield l[i]
        i += 1

Version légèrement simplifiée de ce générateur:

def genflat(l, ltypes=collections.Sequence):
    l = list(l)
    while l:
        while l and isinstance(l[0], ltypes):
            l[0:1] = l[0]
        if l: yield l.pop(0)
Alex Martelli
la source
c'est une traversée en pré-commande de l'arbre formé par les listes imbriquées. seules les feuilles sont retournées. Notez que cette implémentation consommera la structure de données d'origine, pour le meilleur ou pour le pire. Cela pourrait être amusant d'en écrire un qui conserve à la fois l'arborescence d'origine, mais n'a pas non plus à copier les entrées de la liste.
Andrew Wagner
6
Je pense que vous devez tester les chaînes - par exemple, ajouter "et non isinstance (l [0], basestring)" comme dans la solution de Cristian. Sinon, vous obtenez une boucle infinie autour de l [0: 1] = l [0]
c-urchin
C'est un bon exemple de création d'un générateur, mais comme le mentionne c-urchin, l'algorithme lui-même échoue lorsque la séquence contient des chaînes.
Daniel 'Dang' Griffith
28

Voici ma version fonctionnelle d'aplatir récursif qui gère à la fois les tuples et les listes, et vous permet d'ajouter n'importe quel mélange d'arguments positionnels. Renvoie un générateur qui produit la séquence entière dans l'ordre, arg par arg:

flatten = lambda *n: (e for a in n
    for e in (flatten(*a) if isinstance(a, (tuple, list)) else (a,)))

Usage:

l1 = ['a', ['b', ('c', 'd')]]
l2 = [0, 1, (2, 3), [[4, 5, (6, 7, (8,), [9]), 10]], (11,)]
print list(flatten(l1, -2, -1, l2))
['a', 'b', 'c', 'd', -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
échantillon de biais
la source
1
grande solution, serait toutefois beaucoup plus utile si vous avez ajouté quelques commentaires pour décrire e, a, nreportez - vous à
Kristof Pal
2
@WolfgangKuehne: Essayez argspour n, intermediate(ou le plus court midou vous préférez peut-être element) pour aet resultpour e, donc:flatten = lambda *args: (result for mid in args for result in (flatten(*mid) if isinstance(mid, (tuple, list)) else (mid,)))
Pause jusqu'à nouvel ordre.
C'est nettement plus rapide que compiler.ast.flatten. Grand code compact, fonctionne pour tout type d'objet (je pense).
bcdan
Wow, cela devrait être la réponse la plus votée et acceptée ... fonctionne comme un charme!
U10-Forward
27

Cette version flattenévite la limite de récursivité de python (et fonctionne donc avec des itérables imbriqués profondément arbitraires). C'est un générateur qui peut gérer des chaînes et des itérables arbitraires (même infinis).

import itertools as IT
import collections

def flatten(iterable, ltypes=collections.Iterable):
    remainder = iter(iterable)
    while True:
        first = next(remainder)
        if isinstance(first, ltypes) and not isinstance(first, (str, bytes)):
            remainder = IT.chain(first, remainder)
        else:
            yield first

Voici quelques exemples démontrant son utilisation:

print(list(IT.islice(flatten(IT.repeat(1)),10)))
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

print(list(IT.islice(flatten(IT.chain(IT.repeat(2,3),
                                       {10,20,30},
                                       'foo bar'.split(),
                                       IT.repeat(1),)),10)))
# [2, 2, 2, 10, 20, 30, 'foo', 'bar', 1, 1]

print(list(flatten([[1,2,[3,4]]])))
# [1, 2, 3, 4]

seq = ([[chr(i),chr(i-32)] for i in range(ord('a'), ord('z')+1)] + list(range(0,9)))
print(list(flatten(seq)))
# ['a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'E', 'f', 'F', 'g', 'G', 'h', 'H',
# 'i', 'I', 'j', 'J', 'k', 'K', 'l', 'L', 'm', 'M', 'n', 'N', 'o', 'O', 'p', 'P',
# 'q', 'Q', 'r', 'R', 's', 'S', 't', 'T', 'u', 'U', 'v', 'V', 'w', 'W', 'x', 'X',
# 'y', 'Y', 'z', 'Z', 0, 1, 2, 3, 4, 5, 6, 7, 8]

Bien qu'il flattenpuisse gérer des générateurs infinis, il ne peut pas gérer l'imbrication infinie:

def infinitely_nested():
    while True:
        yield IT.chain(infinitely_nested(), IT.repeat(1))

print(list(IT.islice(flatten(infinitely_nested()), 10)))
# hangs
unutbu
la source
1
un consensus sur l'opportunité d'utiliser ABC Iterable ou ABC Sequence?
wim
sets, dicts, deques, listiterators, generators, Et les classes descripteurs de fichiers personnalisés avec __iter__définis sont toutes les instances de collections.Iterable, mais pas collections.Sequence. Le résultat de l'aplatissement d'un dictest un peu incertain, mais sinon, je pense que collections.Iterablec'est un meilleur défaut que collections.Sequence. C'est certainement le plus libéral.
unutbu
@wim: Un problème avec l'utilisation collections.Iterableest que cela inclut des générateurs infinis. J'ai changé ma réponse pour gérer ce cas.
unutbu
1
Cela ne semble pas fonctionner pour les 3e et 4e exemples. Ça jette StopIteration. En outre, il while True: first = next(remainder) pourrait être remplacé par for first in remainder:.
Georgy
@Georgy cela pourrait être résolu en encapsulant le corps d'aplatir dans un try-except StopIteration block.
baduker
12

Voici une autre réponse encore plus intéressante ...

import re

def Flatten(TheList):
    a = str(TheList)
    b,crap = re.subn(r'[\[,\]]', ' ', a)
    c = b.split()
    d = [int(x) for x in c]

    return(d)

Fondamentalement, il convertit la liste imbriquée en chaîne, utilise une expression régulière pour supprimer la syntaxe imbriquée, puis reconvertit le résultat en une liste (aplatie).

argile
la source
Si vous essayez de généraliser cela à autre chose que des valeurs int, ce sera amusant avec, par exemple, [['C=64', 'APPLE ]['], ['Amiga', 'Mac', 'ST']]:) D'un autre côté, étant donné une liste qui se contient, cela fera un peu mieux que les autres réponses, soulevant un exception au lieu de simplement boucler jusqu'à épuisement de la mémoire / récurrence jusqu'à épuisement de la pile…
abarnert
L'invite d'origine concernait l'aplatissement d'une liste d'entiers. Si vous modifiez simplement la compréhension de la liste en d = [x pour x en c], cela devrait fonctionner correctement pour votre échantillon.
terre battue du
Tout d'abord, [x for x in c]est-ce juste une façon lente et verbeuse d'en faire une copie c, alors pourquoi feriez-vous cela? Deuxièmement, votre code va clairement se convertir 'APPLE ]['en 'APPLE ', car il ne gère pas les guillemets, il suppose simplement que tous les crochets sont des crochets de liste.
abarnert
Ha! La façon dont votre commentaire a été formaté sur mon ordinateur, je ne savais même pas que c'était censé être Apple II tel qu'il apparaissait sur les anciens ordinateurs. En tout cas, ma réponse à vos deux questions est que cet exercice - pour moi - n'est qu'une expérience pour trouver une solution créative pour aplatir une liste. Je ne suis pas sûr de le généraliser pour aplatir chaque liste.
terre battue
Vous en avez juste besoin arr_str = str(arr)et puis [int(s) for s in re.findall(r'\d+', arr_str)]vraiment. Voir github.com/jorgeorpinel/flatten_nested_lists/blob/master/…
Jorge Orpinel
10
def flatten(xs):
    res = []
    def loop(ys):
        for i in ys:
            if isinstance(i, list):
                loop(i)
            else:
                res.append(i)
    loop(xs)
    return res
kev
la source
8

Vous pouvez utiliser à deepflattenpartir du package tiers iteration_utilities:

>>> from iteration_utilities import deepflatten
>>> L = [[[1, 2, 3], [4, 5]], 6]
>>> list(deepflatten(L))
[1, 2, 3, 4, 5, 6]

>>> list(deepflatten(L, types=list))  # only flatten "inner" lists
[1, 2, 3, 4, 5, 6]

Il s'agit d'un itérateur, vous devez donc l'itérer (par exemple en l'enveloppant listou en l'utilisant en boucle). En interne, il utilise une approche itérative au lieu d'une approche récursive et il est écrit en extension C afin qu'il puisse être plus rapide que les approches python pures:

>>> %timeit list(deepflatten(L))
12.6 µs ± 298 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
>>> %timeit list(deepflatten(L, types=list))
8.7 µs ± 139 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

>>> %timeit list(flatten(L))   # Cristian - Python 3.x approach from https://stackoverflow.com/a/2158532/5393381
86.4 µs ± 4.42 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

>>> %timeit list(flatten(L))   # Josh Lee - https://stackoverflow.com/a/2158522/5393381
107 µs ± 2.99 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

>>> %timeit list(genflat(L, list))  # Alex Martelli - https://stackoverflow.com/a/2159079/5393381
23.1 µs ± 710 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Je suis l'auteur de la iteration_utilitiesbibliothèque.

MSeifert
la source
7

C'était amusant d'essayer de créer une fonction qui pourrait aplatir une liste irrégulière en Python, mais bien sûr, c'est à cela que sert Python (pour rendre la programmation amusante). Le générateur suivant fonctionne assez bien avec certaines mises en garde:

def flatten(iterable):
    try:
        for item in iterable:
            yield from flatten(item)
    except TypeError:
        yield iterable

Il va aplatir les types de données que vous voudrez peut - être laissé seul (comme bytearray, byteset strobjets). De plus, le code repose sur le fait que demander un itérateur à un non-itérable soulève a TypeError.

>>> L = [[[1, 2, 3], [4, 5]], 6]
>>> def flatten(iterable):
    try:
        for item in iterable:
            yield from flatten(item)
    except TypeError:
        yield iterable


>>> list(flatten(L))
[1, 2, 3, 4, 5, 6]
>>>

Éditer:

Je ne suis pas d'accord avec la mise en œuvre précédente. Le problème est que vous ne devriez pas pouvoir aplatir quelque chose qui n'est pas itérable. C'est déroutant et donne une mauvaise impression de l'argument.

>>> list(flatten(123))
[123]
>>>

Le générateur suivant est presque le même que le premier mais n'a pas le problème d'essayer d'aplatir un objet non itérable. Il échoue comme on pourrait s'y attendre lorsqu'un argument inapproprié lui est donné.

def flatten(iterable):
    for item in iterable:
        try:
            yield from flatten(item)
        except TypeError:
            yield item

Le test du générateur fonctionne correctement avec la liste fournie. Cependant, le nouveau code lèvera un TypeErrorquand un objet non itérable lui sera donné. Des exemples sont présentés ci-dessous du nouveau comportement.

>>> L = [[[1, 2, 3], [4, 5]], 6]
>>> list(flatten(L))
[1, 2, 3, 4, 5, 6]
>>> list(flatten(123))
Traceback (most recent call last):
  File "<pyshell#32>", line 1, in <module>
    list(flatten(123))
  File "<pyshell#27>", line 2, in flatten
    for item in iterable:
TypeError: 'int' object is not iterable
>>>
Noctis Skytower
la source
5

Bien qu'une réponse élégante et très pythonique ait été choisie, je présenterais ma solution juste pour la revue:

def flat(l):
    ret = []
    for i in l:
        if isinstance(i, list) or isinstance(i, tuple):
            ret.extend(flat(i))
        else:
            ret.append(i)
    return ret

Veuillez dire à quel point ce code est bon ou mauvais?

Xolve
la source
1
Utilisez isinstance(i, (tuple, list)). L'initialisation de variables vides est un indicateur pour moi de chercher des structures de code alternatives, généralement des compréhensions, des générateurs, une récursivité, etc.
dansalmo
3
return type(l)(ret)vous récupérera également le même type de conteneur que celui transmis. :)
dash-tom-bang
@ dash-tom-bang Pouvez-vous expliquer ce que cela signifie en détail.
Xolve
1
Si vous passez une liste, vous voudrez probablement une liste de retour. Si vous passez un tuple, vous voulez probablement un tuple de retour. Si vous passez dans un méli-mélo des deux, vous obtiendrez ce que l'enveloppe extérieure était.
dash-tom-bang
4

Je préfère des réponses simples. Pas de générateurs. Aucune récursivité ou limite de récursivité. Juste itération:

def flatten(TheList):
    listIsNested = True

    while listIsNested:                 #outer loop
        keepChecking = False
        Temp = []

        for element in TheList:         #inner loop
            if isinstance(element,list):
                Temp.extend(element)
                keepChecking = True
            else:
                Temp.append(element)

        listIsNested = keepChecking     #determine if outer loop exits
        TheList = Temp[:]

    return TheList

Cela fonctionne avec deux listes: une boucle for interne et une boucle while externe.

La boucle for interne parcourt la liste. S'il trouve un élément de liste, il (1) utilise list.extend () pour aplatir ce niveau d'imbrication de première partie et (2) les commutateurs keepChecking à True. keepchecking est utilisé pour contrôler la boucle while externe. Si la boucle externe est définie sur true, elle déclenche la boucle interne pour un autre passage.

Ces passes continuent de se produire jusqu'à ce qu'aucune liste imbriquée ne soit trouvée. Lorsqu'un passage se produit finalement là où aucun n'est trouvé, keepChecking n'est jamais déclenché sur true, ce qui signifie que listIsNested reste faux et que la boucle while externe se ferme.

La liste aplatie est ensuite renvoyée.

Essai

flatten([1,2,3,4,[100,200,300,[1000,2000,3000]]])

[1, 2, 3, 4, 100, 200, 300, 1000, 2000, 3000]

argile
la source
J'aime aussi simple. Dans ce cas cependant, vous parcourez la liste autant de fois qu'il y a d'imbrication ou de niveau. Pourrait coûter cher.
telliott99
@ telliott99: Vous avez raison si vos listes sont vraiment grandes et / ou imbriquées à de grandes profondeurs. Cependant, si ce n'est pas le cas, la solution la plus simple fonctionne tout aussi bien et sans la magie profonde de certaines des autres réponses. Il y a une place pour les compréhensions de générateur récursif à plusieurs étapes, mais je ne suis pas convaincu que ce devrait être là que vous regardez en premier. (Je suppose que vous savez où je tombe dans le débat "Le pire c'est mieux".)
clay
@ telliott99: Ou pour dire les choses autrement, vous n'aurez pas à "essayer de Grok" ma solution. Si les performances ne sont pas un goulot d'étranglement, qu'est-ce qui vous importe le plus en tant que programmeur?
terre battue
Les solutions plus simples ont moins de logique. La récursivité est une construction de programmation assez fondamentale avec laquelle quiconque se considère comme un programmeur devrait être complètement à l'aise. Les générateurs sont très bien le Python Way et (avec les compréhensions) sont quelque chose que tout programmeur Python professionnel devrait grogner instantanément.
dash-tom-bang
1
Je suis d'accord sur la récursivité. Quand j'ai écrit ma réponse, python a encore cassé la récursivité à 1000 cycles. Ont-ils changé cela? Quant à être un programmeur professionnel en python, je ne le suis pas. De plus, j'imagine que beaucoup de gens qui programment en python ne le font pas à plein temps.
terre battue
4

Voici une fonction simple qui aplatit les listes de profondeur arbitraire. Aucune récursivité, pour éviter un débordement de pile.

from copy import deepcopy

def flatten_list(nested_list):
    """Flatten an arbitrarily nested list, without recursion (to avoid
    stack overflows). Returns a new list, the original list is unchanged.

    >> list(flatten_list([1, 2, 3, [4], [], [[[[[[[[[5]]]]]]]]]]))
    [1, 2, 3, 4, 5]
    >> list(flatten_list([[1, 2], 3]))
    [1, 2, 3]

    """
    nested_list = deepcopy(nested_list)

    while nested_list:
        sublist = nested_list.pop(0)

        if isinstance(sublist, list):
            nested_list = sublist + nested_list
        else:
            yield sublist
Wilfred Hughes
la source
Oui! Très similaire à mon code sur github.com/jorgeorpinel/flatten_nested_lists/blob/master/…
Jorge Orpinel
3

Je suis surpris que personne n'y ait pensé. Maudite récursivité Je n'ai pas les réponses récursives que les gens avancés ici ont faites. de toute façon voici ma tentative à ce sujet. la mise en garde est qu'il est très spécifique au cas d'utilisation de l'OP

import re

L = [[[1, 2, 3], [4, 5]], 6]
flattened_list = re.sub("[\[\]]", "", str(L)).replace(" ", "").split(",")
new_list = list(map(int, flattened_list))
print(new_list)

production:

[1, 2, 3, 4, 5, 6]
Sion
la source
3

Je ne suis pas passé par toutes les réponses déjà disponibles ici, mais voici une ligne unique que j'ai trouvée, empruntant à la manière de lisp de traiter en premier et en liste de repos

def flatten(l): return flatten(l[0]) + (flatten(l[1:]) if len(l) > 1 else []) if type(l) is list else [l]

voici un cas simple et un cas pas si simple -

>>> flatten([1,[2,3],4])
[1, 2, 3, 4]

>>> flatten([1, [2, 3], 4, [5, [6, {'name': 'some_name', 'age':30}, 7]], [8, 9, [10, [11, [12, [13, {'some', 'set'}, 14, [15, 'some_string'], 16], 17, 18], 19], 20], 21, 22, [23, 24], 25], 26, 27, 28, 29, 30])
[1, 2, 3, 4, 5, 6, {'age': 30, 'name': 'some_name'}, 7, 8, 9, 10, 11, 12, 13, set(['set', 'some']), 14, 15, 'some_string', 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
>>> 
Shreyas
la source
Ce n'est pas un paquebot. Peu importe combien vous essayez de l'adapter en un seul, def foo():c'est une ligne distincte. C'est également très illisible.
cs95
J'ai de-one-line-ified le code, et fait quelques refactoring supplémentaires. (La modification est en attente d'examen par les pairs au moment où j'écris ceci) Cette méthode particulière m'a semblé très lisible, bien que le code d'origine ait eu besoin d'être refactorisé.
Emilio M Bumachar
3

Lorsque vous essayez de répondre à une telle question, vous devez vraiment donner les limites du code que vous proposez comme solution. S'il ne s'agissait que de performances, cela ne me dérangerait pas trop, mais la plupart des codes proposés comme solution (y compris la réponse acceptée) ne parviennent pas à aplatir une liste dont la profondeur est supérieure à 1000.

Quand je dis la plupart des codes, je veux dire tous les codes qui utilisent n'importe quelle forme de récursivité (ou appellent une fonction de bibliothèque standard qui est récursive). Tous ces codes échouent car pour chaque appel récursif effectué, la pile (d'appel) augmente d'une unité et la pile d'appel python (par défaut) a une taille de 1000.

Si vous n'êtes pas trop familier avec la pile d'appels, alors peut-être que ce qui suit vous aidera (sinon vous pouvez simplement faire défiler jusqu'à la mise en œuvre ).

Taille de la pile d'appels et programmation récursive (analogie avec les donjons)

Trouver le trésor et sortir

Imaginez que vous entrez dans un immense donjon avec des chambres numérotées , à la recherche d'un trésor. Vous ne connaissez pas l'endroit mais vous avez des indications sur la façon de trouver le trésor. Chaque indication est une énigme (la difficulté varie, mais vous ne pouvez pas prédire à quel point elles seront difficiles). Vous décidez de réfléchir un peu à une stratégie pour gagner du temps, vous faites deux constats:

  1. Il est difficile (long) de trouver le trésor car vous devrez résoudre des énigmes (potentiellement difficiles) pour y arriver.
  2. Une fois le trésor trouvé, le retour à l'entrée peut être facile, il vous suffit d'utiliser le même chemin dans l'autre sens (même si cela nécessite un peu de mémoire pour rappeler votre chemin).

En entrant dans le donjon, vous remarquez un petit cahier ici. Vous décidez de l'utiliser pour noter chaque pièce que vous quittez après avoir résolu une énigme (lorsque vous entrez dans une nouvelle pièce), de cette façon, vous pourrez retourner à l'entrée. C'est une idée géniale, vous ne dépenserez même pas un centime pour mettre en œuvre votre stratégie.

Vous entrez dans le donjon, résolvant avec grand succès les 1001 premières énigmes, mais voici quelque chose que vous n'aviez pas planifié, vous n'avez plus de place dans le cahier que vous avez emprunté. Vous décidez d' abandonner votre quête car vous préférez ne pas avoir le trésor que d'être perdu pour toujours à l'intérieur du donjon (cela a l'air intelligent en effet).

Exécuter un programme récursif

Fondamentalement, c'est exactement la même chose que de trouver le trésor. Le donjon est la mémoire de l' ordinateur , votre objectif n'est plus de trouver un trésor mais de calculer une fonction (trouver f (x) pour un x donné ). Les indications sont simplement des sous-routines qui vous aideront à résoudre f (x) . Votre stratégie est la même que la stratégie de pile d'appels , le bloc-notes est la pile, les salles sont les adresses de retour des fonctions:

x = ["over here", "am", "I"]
y = sorted(x) # You're about to enter a room named `sorted`, note down the current room address here so you can return back: 0x4004f4 (that room address looks weird)
# Seems like you went back from your quest using the return address 0x4004f4
# Let's see what you've collected 
print(' '.join(y))

Le problème que vous avez rencontré dans le donjon sera le même ici, la pile d'appels a une taille finie (ici 1000) et donc, si vous entrez trop de fonctions sans revenir en arrière, vous remplirez la pile d'appels et aurez une erreur qui ressemblera comme « aventurier Cher, je suis désolé , mais votre ordinateur portable est pleine » : RecursionError: maximum recursion depth exceeded. Notez que vous n'avez pas besoin de récursivité pour remplir la pile d'appels, mais il est très peu probable qu'un programme non récursif appelle 1000 fonctions sans jamais revenir. Il est important de comprendre également qu'une fois que vous êtes revenu d'une fonction, la pile d'appels est libérée de l'adresse utilisée (d'où le nom "pile", l'adresse de retour est poussée avant d'entrer dans une fonction et retirée lors du retour). Dans le cas particulier d'une récursion simple (une fonctionfqui s'appelle lui-même encore et encore -) vous entrerez fencore et encore jusqu'à ce que le calcul soit terminé (jusqu'à ce que le trésor soit trouvé) et reviendrez fjusqu'à ce que vous retourniez à l'endroit où vous avez appelé fen premier lieu. La pile d'appels ne sera jamais libérée de quoi que ce soit jusqu'à la fin où elle sera libérée de toutes les adresses de retour l'une après l'autre.

Comment éviter ce problème?

C'est en fait assez simple: "n'utilisez pas la récursivité si vous ne savez pas jusqu'où elle peut aller". Ce n'est pas toujours vrai car dans certains cas, la récursivité des appels de queue peut être optimisée (TCO) . Mais en python, ce n'est pas le cas, et même une fonction récursive "bien écrite" n'optimisera pas l' utilisation de la pile. Il y a un article intéressant de Guido sur cette question: Tail Recursion Elimination .

Il existe une technique que vous pouvez utiliser pour rendre toute fonction récursive itérative, cette technique que nous pourrions appeler apporter votre propre cahier . Par exemple, dans notre cas particulier, nous explorons simplement une liste, entrer dans une pièce équivaut à entrer une sous-liste, la question que vous devez vous poser est de savoir comment puis-je revenir d'une liste à sa liste parente? La réponse n'est pas si complexe, répétez ce qui suit jusqu'à ce que le stacksoit vide:

  1. pousser la liste actuelle addresset indexdans un stacklors de la saisie d'une nouvelle sous-liste (notez qu'une adresse de liste + index est également une adresse, nous utilisons donc exactement la même technique utilisée par la pile des appels);
  2. chaque fois qu'un élément est trouvé, yieldil (ou les ajouter dans une liste);
  3. une fois qu'une liste est entièrement explorée, retournez à la liste parent en utilisant le stack retour address(et index) .

Notez également que cela équivaut à un DFS dans une arborescence où certains nœuds sont des sous-listes A = [1, 2]et certains sont des éléments simples: 0, 1, 2, 3, 4(pour L = [0, [1,2], 3, 4]). L'arbre ressemble à ceci:

                    L
                    |
           -------------------
           |     |     |     |
           0   --A--   3     4
               |   |
               1   2

La pré-commande de parcours DFS est: L, 0, A, 1, 2, 3, 4. N'oubliez pas que pour implémenter un DFS itératif, vous avez également besoin d'une pile. L'implémentation que j'ai proposée auparavant aboutit à avoir les états suivants (pour le stacket le flat_list):

init.:  stack=[(L, 0)]
**0**:  stack=[(L, 0)],         flat_list=[0]
**A**:  stack=[(L, 1), (A, 0)], flat_list=[0]
**1**:  stack=[(L, 1), (A, 0)], flat_list=[0, 1]
**2**:  stack=[(L, 1), (A, 1)], flat_list=[0, 1, 2]
**3**:  stack=[(L, 2)],         flat_list=[0, 1, 2, 3]
**3**:  stack=[(L, 3)],         flat_list=[0, 1, 2, 3, 4]
return: stack=[],               flat_list=[0, 1, 2, 3, 4]

Dans cet exemple, la taille maximale de la pile est 2, car la liste d'entrée (et donc l'arborescence) a la profondeur 2.

la mise en oeuvre

Pour l'implémentation, en python, vous pouvez simplifier un peu en utilisant des itérateurs au lieu de simples listes. Les références aux (sous) itérateurs seront utilisées pour stocker les adresses de retour des sous-listes (au lieu d'avoir à la fois l'adresse de liste et l'index). Ce n'est pas une grande différence mais je pense que c'est plus lisible (et aussi un peu plus rapide):

def flatten(iterable):
    return list(items_from(iterable))

def items_from(iterable):
    cursor_stack = [iter(iterable)]
    while cursor_stack:
        sub_iterable = cursor_stack[-1]
        try:
            item = next(sub_iterable)
        except StopIteration:   # post-order
            cursor_stack.pop()
            continue
        if is_list_like(item):  # pre-order
            cursor_stack.append(iter(item))
        elif item is not None:
            yield item          # in-order

def is_list_like(item):
    return isinstance(item, list)

Notez également que dans is_list_likeI have isinstance(item, list), qui pourrait être modifié pour gérer plus de types d'entrée, ici, je voulais juste avoir la version la plus simple où (itérable) n'est qu'une liste. Mais vous pouvez aussi le faire:

def is_list_like(item):
    try:
        iter(item)
        return not isinstance(item, str)  # strings are not lists (hmm...) 
    except TypeError:
        return False

Cela considère les chaînes comme des «éléments simples» et, par conséquent, flatten_iter([["test", "a"], "b])sera renvoyé ["test", "a", "b"]et non ["t", "e", "s", "t", "a", "b"]. Remarquez que dans ce cas, iter(item)est appelé deux fois sur chaque élément, supposons que c'est un exercice pour le lecteur de rendre ce nettoyeur.

Tests et remarques sur d'autres implémentations

En fin de compte, n'oubliez pas que vous ne pouvez pas imprimer une liste imbriquée à l'infini en Lutilisant print(L)car en interne, il utilisera des appels récursifs à __repr__( RecursionError: maximum recursion depth exceeded while getting the repr of an object). Pour la même raison, les solutions à l' flattenimplication stréchoueront avec le même message d'erreur.

Si vous devez tester votre solution, vous pouvez utiliser cette fonction pour générer une liste imbriquée simple:

def build_deep_list(depth):
    """Returns a list of the form $l_{depth} = [depth-1, l_{depth-1}]$
    with $depth > 1$ and $l_0 = [0]$.
    """
    sub_list = [0]
    for d in range(1, depth):
        sub_list = [d, sub_list]
    return sub_list

Ce qui donne: build_deep_list(5)>>> [4, [3, [2, [1, [0]]]]].

cglacet
la source
2

Voici l' compiler.ast.flattenimplémentation de 2.7.5:

def flatten(seq):
    l = []
    for elt in seq:
        t = type(elt)
        if t is tuple or t is list:
            for elt2 in flatten(elt):
                l.append(elt2)
        else:
            l.append(elt)
    return l

Il existe des méthodes meilleures et plus rapides (si vous avez atteint ici, vous les avez déjà vues)

Notez également:

Déconseillé depuis la version 2.6: le package du compilateur a été supprimé dans Python 3.

pradyunsg
la source
2

totalement hacky mais je pense que cela fonctionnerait (selon votre data_type)

flat_list = ast.literal_eval("[%s]"%re.sub("[\[\]]","",str(the_list)))
Joran Beasley
la source
2

Utilisez simplement une funcybibliothèque: pip install funcy

import funcy


funcy.flatten([[[[1, 1], 1], 2], 3]) # returns generator
funcy.lflatten([[[[1, 1], 1], 2], 3]) # returns list
ADR
la source
1
Pour info: il utilise une solution récursive: lien vers la source
Georgy
1

Voici une autre approche py2, je ne sais pas si c'est la plus rapide ou la plus élégante ni la plus sûre ...

from collections import Iterable
from itertools import imap, repeat, chain


def flat(seqs, ignore=(int, long, float, basestring)):
    return repeat(seqs, 1) if any(imap(isinstance, repeat(seqs), ignore)) or not isinstance(seqs, Iterable) else chain.from_iterable(imap(flat, seqs))

Il peut ignorer tout type spécifique (ou dérivé) que vous souhaitez, il renvoie un itérateur, vous pouvez donc le convertir en n'importe quel conteneur spécifique tel que liste, tuple, dict ou simplement le consommer afin de réduire l'empreinte mémoire, pour le meilleur ou pour le pire. il peut gérer les objets non itérables initiaux tels que int ...

Notez que la plupart du gros du travail se fait en C, car pour autant que je sache, c'est comment les outils itertools sont implémentés, donc bien qu'il soit récursif, AFAIK il n'est pas limité par la profondeur de récursivité python puisque les appels de fonction se produisent en C, bien que cela ne signifie pas que vous êtes limité par la mémoire, spécialement sous OS X où sa taille de pile a une limite stricte à partir d'aujourd'hui (OS X Mavericks) ...

il existe une approche légèrement plus rapide, mais une méthode moins portable, ne l'utilisez que si vous pouvez supposer que les éléments de base de l'entrée peuvent être déterminés explicitement sinon, vous obtiendrez une récursion infinie, et OS X avec sa taille de pile limitée, lancer un défaut de segmentation assez rapidement ...

def flat(seqs, ignore={int, long, float, str, unicode}):
    return repeat(seqs, 1) if type(seqs) in ignore or not isinstance(seqs, Iterable) else chain.from_iterable(imap(flat, seqs))

ici, nous utilisons des ensembles pour vérifier le type, il faut donc O (1) vs O (nombre de types) pour vérifier si un élément doit être ignoré ou non, bien que toute valeur avec un type dérivé des types ignorés indiqués échoue , c'est pourquoi son utilisation str, unicodealors utilisez-le avec prudence ...

tests:

import random

def test_flat(test_size=2000):
    def increase_depth(value, depth=1):
        for func in xrange(depth):
            value = repeat(value, 1)
        return value

    def random_sub_chaining(nested_values):
        for values in nested_values:
            yield chain((values,), chain.from_iterable(imap(next, repeat(nested_values, random.randint(1, 10)))))

    expected_values = zip(xrange(test_size), imap(str, xrange(test_size)))
    nested_values = random_sub_chaining((increase_depth(value, depth) for depth, value in enumerate(expected_values)))
    assert not any(imap(cmp, chain.from_iterable(expected_values), flat(chain(((),), nested_values, ((),)))))

>>> test_flat()
>>> list(flat([[[1, 2, 3], [4, 5]], 6]))
[1, 2, 3, 4, 5, 6]
>>>  

$ uname -a
Darwin Samys-MacBook-Pro.local 13.3.0 Darwin Kernel Version 13.3.0: Tue Jun  3 21:27:35 PDT 2014; root:xnu-2422.110.17~1/RELEASE_X86_64 x86_64
$ python --version
Python 2.7.5
Samy Vilar
la source
1

Sans utiliser de bibliothèque:

def flat(l):
    def _flat(l, r):    
        if type(l) is not list:
            r.append(l)
        else:
            for i in l:
                r = r + flat(i)
        return r
    return _flat(l, [])



# example
test = [[1], [[2]], [3], [['a','b','c'] , [['z','x','y']], ['d','f','g']], 4]    
print flat(test) # prints [1, 2, 3, 'a', 'b', 'c', 'z', 'x', 'y', 'd', 'f', 'g', 4]
alfasin
la source
1

En utilisant itertools.chain:

import itertools
from collections import Iterable

def list_flatten(lst):
    flat_lst = []
    for item in itertools.chain(lst):
        if isinstance(item, Iterable):
            item = list_flatten(item)
            flat_lst.extend(item)
        else:
            flat_lst.append(item)
    return flat_lst

Ou sans enchaîner:

def flatten(q, final):
    if not q:
        return
    if isinstance(q, list):
        if not isinstance(q[0], list):
            final.append(q[0])
        else:
            flatten(q[0], final)
        flatten(q[1:], final)
    else:
        final.append(q)
Saksham Varma
la source
1

J'ai utilisé récursif pour résoudre la liste imbriquée avec n'importe quelle profondeur

def combine_nlist(nlist,init=0,combiner=lambda x,y: x+y):
    '''
    apply function: combiner to a nested list element by element(treated as flatten list)
    '''
    current_value=init
    for each_item in nlist:
        if isinstance(each_item,list):
            current_value =combine_nlist(each_item,current_value,combiner)
        else:
            current_value = combiner(current_value,each_item)
    return current_value

Donc, après avoir défini la fonction combine_nlist, il est facile d'utiliser cette fonction pour l'aplatissement. Ou vous pouvez le combiner en une seule fonction. J'aime ma solution car elle peut être appliquée à n'importe quelle liste imbriquée.

def flatten_nlist(nlist):
    return combine_nlist(nlist,[],lambda x,y:x+[y])

résultat

In [379]: flatten_nlist([1,2,3,[4,5],[6],[[[7],8],9],10])
Out[379]: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Vieux jeune
la source
"liste imbriquée avec n'importe quelle profondeur" pas vrai. Essayez juste, vous verrez: current_value = combiner(current_value,each_item) RecursionError: maximum recursion depth exceeded
cglacet
hmmm j'essaye d'aplatir la liste avec plus de 1000 couches?
Oldyoung
Bien sûr, c'est tout l'intérêt de la discussion sur les solutions récursives vs itératives. Si vous savez à l'avance que le nombre de couches est inférieur à 1000, la solution la plus simple fonctionnera. Lorsque vous dites "n'importe quelle profondeur", cela inclut une liste avec une profondeur> 1000.
cglacet
1

La manière la plus simple est d'utiliser la bibliothèque de morphing à l' aide de pip install morph.

Le code est:

import morph

list = [[[1, 2, 3], [4, 5]], 6]
flattened_list = morph.flatten(list)  # returns [1, 2, 3, 4, 5, 6]
YPCrumble
la source
1

Je suis conscient qu'il existe déjà de nombreuses réponses impressionnantes, mais je voulais ajouter une réponse qui utilise la méthode de programmation fonctionnelle pour résoudre la question. Dans cette réponse, j'utilise la double récursivité:

def flatten_list(seq):
    if not seq:
        return []
    elif isinstance(seq[0],list):
        return (flatten_list(seq[0])+flatten_list(seq[1:]))
    else:
        return [seq[0]]+flatten_list(seq[1:])

print(flatten_list([1,2,[3,[4],5],[6,7]]))

production:

[1, 2, 3, 4, 5, 6, 7]
Leo wahyd
la source
1

Je ne sais pas si c'est nécessairement plus rapide ou plus efficace, mais voici ce que je fais:

def flatten(lst):
    return eval('[' + str(lst).replace('[', '').replace(']', '') + ']')

L = [[[1, 2, 3], [4, 5]], 6]
print(flatten(L))

La flattenfonction transforme ici la liste en une chaîne, supprime tous les crochets, rattache les crochets aux extrémités et la transforme en liste.

Bien que, si vous saviez que vous auriez des crochets dans votre liste sous forme de chaînes [[1, 2], "[3, 4] and [5]"], vous devriez faire autre chose.

diligar
la source
Cela n'a aucun avantage sur la solution simple car cela ne parvient pas à traiter les listes profondes, c'est-à-dire "RecursionError: profondeur de récursivité maximale dépassée lors de la récupération d'un objet".
cglacet
1

Ceci est un simple implémentation d'aplatir sur python2

flatten=lambda l: reduce(lambda x,y:x+y,map(flatten,l),[]) if isinstance(l,list) else [l]

test=[[1,2,3,[3,4,5],[6,7,[8,9,[10,[11,[12,13,14]]]]]],]
print flatten(test)

#output [1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
Statham
la source
1

Cela aplatira une liste ou un dictionnaire (ou une liste de listes ou de dictionnaires de dictionnaires, etc.). Il suppose que les valeurs sont des chaînes et crée une chaîne qui concatène chaque élément avec un argument séparateur. Si vous le souhaitez, vous pouvez utiliser le séparateur pour diviser le résultat en un objet liste par la suite. Il utilise la récursivité si la valeur suivante est une liste ou une chaîne. Utilisez l'argument clé pour indiquer si vous souhaitez les clés ou les valeurs (définir la clé sur faux) de l'objet dictionnaire.

def flatten_obj(n_obj, key=True, my_sep=''):
    my_string = ''
    if type(n_obj) == list:
        for val in n_obj:
            my_sep_setter = my_sep if my_string != '' else ''
            if type(val) == list or type(val) == dict:
                my_string += my_sep_setter + flatten_obj(val, key, my_sep)
            else:
                my_string += my_sep_setter + val
    elif type(n_obj) == dict:
        for k, v in n_obj.items():
            my_sep_setter = my_sep if my_string != '' else ''
            d_val = k if key else v
            if type(v) == list or type(v) == dict:
                my_string += my_sep_setter + flatten_obj(v, key, my_sep)
            else:
                my_string += my_sep_setter + d_val
    elif type(n_obj) == str:
        my_sep_setter = my_sep if my_string != '' else ''
        my_string += my_sep_setter + n_obj
        return my_string
    return my_string

print(flatten_obj(['just', 'a', ['test', 'to', 'try'], 'right', 'now', ['or', 'later', 'today'],
                [{'dictionary_test': 'test'}, {'dictionary_test_two': 'later_today'}, 'my power is 9000']], my_sep=', ')

rendements:

just, a, test, to, try, right, now, or, later, today, dictionary_test, dictionary_test_two, my power is 9000
Matt Farguson
la source
0

Si vous aimez la récursivité, cela pourrait être une solution qui vous intéresse:

def f(E):
    if E==[]: 
        return []
    elif type(E) != list: 
        return [E]
    else:
        a = f(E[0])
        b = f(E[1:])
        a.extend(b)
        return a

J'ai en fait adapté cela à partir d'un code de schéma d'entraînement que j'avais écrit il y a quelque temps.

Prendre plaisir!

inspecteurG4dget
la source
0

Je suis nouveau sur python et viens d'un arrière-plan lisp. Voici ce que j'ai trouvé (consultez les noms var pour lulz):

def flatten(lst):
    if lst:
        car,*cdr=lst
        if isinstance(car,(list,tuple)):
            if cdr: return flatten(car) + flatten(cdr)
            return flatten(car)
        if cdr: return [car] + flatten(cdr)
        return [car]

Semble fonctionner. Tester:

flatten((1,2,3,(4,5,6,(7,8,(((1,2)))))))

Retour:

[1, 2, 3, 4, 5, 6, 7, 8, 1, 2]
Michael Puckett
la source