Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

un int, une list d'int ou une list de list d'int

+2 votes

J'aime bien me laisser de la marge de manoeuvre quand je définis mes paramètres de fonction, et permettre de passer plusieurs types d'arguments. Mais hier j'ai galéré avec un cas qui j'espère ne m'arrivera plus: je devais pouvoir passer en argument un int, une list d'int ou une list de list d'int (voire une list avec un int et une list d'int, raaah). Tout ça pour pouvoir itérer dessus de manière souple. J'ai fini par comprendre que le format final devrait être le plus contraignant (la list de list), et j'ai géré les autre cas avec ceci:

# Check dir_id is in a list of list
if not isinstance(dir_id,list):
    dir_id = [[dir_id]]
elif not any([isinstance(temp_idx,list) for temp_idx in dir_id]):
    dir_id = [dir_id]

ce qui marche mais est quand même assez moche.
La question est donc: comment feriez-vous?

EDIT: vu que visiblement c'est pas clair: comment feriez-vous pour gérer des entrées de format différent(int, list d'int, list contenant une combinaison d'int et de list d'int) pour les faire converger vers un type d'entrée (list de list d'int)?

demandé 11-Sep-2015 par furankun (1,434 points)
edité 17-Sep-2015 par furankun

4 Réponses

+6 votes
 
Meilleure réponse

Test d'intégrité

Plutôt qu'écrire un code pour chaque cas (car ceci déclenche une alerte rouge : ceci n'est plus de l'informatique, mais de la bricole), pourquoi ne pas tester que la donnée est un iterable contenant soit des entiers uniquement, soit des iterable de même nature uniquement ?

def all_iterint(element):
    if isinstance(element, int): return True
    try:
        return all(all_iterint(e) for e in element)
    except TypeError:  # raised if element is not iterable
        return False

# test cases
a =  (2, 3, 4, 5)
b = ((2, 3),
     (4, 7),
    )
c = ((2, 3),
     ((4, 2), (7, 4)),
    )
d = ((2, 3),
     ((4, 2.), (7, 4)),
    )

print(all_iterint(a))  # True
print(all_iterint(b))  # True 
print(all_iterint(c))  # True
print(all_iterint(d))  # False

Pour la réflexion de fond : une fonction qui prévois plusieurs représentations d'une même donnée, c'est une marque de mauvaise conception. Et si c'est effectivement juste pour t’aménager une zone de confort, non seulement c'est inutile (un peu de rigueur, que diable !), mais c'est aussi que tu va avoir plusieurs utilisations différentes d'une même fonction.
Cette fonction sera probablement inutilement complexe.

Ensuite, il convient de vérifier que tu ais bien besoin de checker le type de ces données.
Parce que python est implémenté avec la notion de duck typing en tête, les données sont considérées selon ce qu'elles font, et non ce qu'elles sont.
Pourquoi ne pas opérer les traitements sur les données directement, et, si ça plante, afficher les données après coups et indiquer la source de l'erreur ?


Conversion de type hétérogènes vers type prédéfini

Le principe serais de convertir "tout type de données hétérogène" vers un type particuliers.
Ici, n'importe quelle composition de liste de données hétérogène et d'entier fait affaire de données hétérogènes, et une double liste d'entier le type ciblé.

1. Bonne chance

Ceci est un sujet de recherche assez ouvert, si mes souvenirs sont bons, et pas mal utilisé en robotique : la fusion de données hétérogènes. C'est beaucoup de maths.
Et cela nécessite d'avoir une compréhension profonde du sens des données hétérogènes utilisées.

2. Sans plus d'information, il est impossible de savoir quoi faire dans beaucoup de cas

Voici un jeu de données correspondant à la définition initiale :

data = [
    [1, 2, [31, 32, [331, 332, 333], 34], 4],
    5,
    [61, [621], [71, [[7211], [7221]], 73], [81, 82, [831, 832]], 9],
]

Il est possible de convertir data de telle manière d'avoir un tableau à deux dimensions, mais cela de plusieurs manières. Très nombreuses en fait.

Une solution simple serais de collecter tous les entiers, et de constituer un tableau d'entier à deux dimensions en essayant d'optimiser la taille des colonnes, de manière à ce que la dernière ligne ai le minimum de "trou" possible :

[  # 4 éléments par ligne, sauf la dernière
        [  1,   2,  31,  32],
        [331, 332, 333,  34],
        [  4,   5,  61, 621],
        [ 71,7211,7221,  73],
        [ 81,  82, 831, 832],
        [9],
]
# NB: avec 3 éléments par lignes, il n'y aurais pas de trous

Mais bien sûr, il existe bien d'autres manière de faire. Il manque juste des spécifications sur le sens des données d'entrées. (voir la réflexion ci-dessus)

3. Implémentation d'un cas qui pourrait être utile

Considérons que les données d'entrées consiste en une liste d'éléments, de telle manière que tout élément est soit une liste d'entier, soit un entier.

Exemple :

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

L'objectif est alors de modifier les données jusqu'à se retrouver avec l'équivalent d'une matrice où tout élément non définit par les données initiales serais égales à zéro.

Dans l'exemple, nous obtiendrions :

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

Le code python permettant de passer ainsi de l'un à l'autre est :

def conv(data):
    # tout élément de data doit être une liste
    ldata = [e if isinstance(e, list) else [e] for e in data]
    # obtention de la taille de la plus grande ligne
    maxlen = max(len(l) for l in ldata)
    # pour chaque ligne ajouter les zéros manquants
    for line in ldata:
        while len(line) < maxlen: line.append(0)
    return ldata
répondu 11-Sep-2015 par lucas (2,292 points)
sélectionné 21-Sep-2015 par furankun

Je plussoie totalement la réflexion ci-dessus.

Merci pour les tuyaux, c'est très cool et je retiens pour une prochaine fois. le truc c'est que je veux en format de sortie une liste de liste, pas un booléen. En fait peu m'importe que les données d'entrées soient d'une forme ou d'une autre (dans mon docstring je précise bien que le format doit être un int, une liste d'int ou une liste contenant une combinaison d'int et de list d'int), ce qui m'importe c'est qu'elles correpsondent à la mise en forme finale qu'il me faut pour le traitement: une list de list d'int.

@furankun J'ai ajouté à ma réponse quelques ligne sur tes précisions.

Je ne peux que m'incliner devant un tel effort de réflexion et compréhension! Ton dernier exemple correspond à ce que je fais dans mon traitement.

+7 votes

je te propose un générateur récursif

-- avec yield from à partir de python 3.3:

def gen(arg):
    if isinstance(arg,list):
        for e in arg:
            yield from gen(e)
    else:
        yield arg

-- avant python 3.3:

def gen(arg):
    if isinstance(arg,list):
        for e1 in arg:
            if isinstance(e1,list):
                for e2 in e1:
                    yield e2
            else:
                yield e1
    else:
        yield arg

Usage:

for arg in [
    5,
    [2,3],
    [[1,2,3],[4,5,6]]
]:
    print('arg:',arg)
    # boucle parcourant l'ensemble des elements quelque soit le type de arg
    for i in gen(arg):
        print(i)

Résultats:

arg: 5
5
arg: [2, 3]
2
3
arg: [[1, 2, 3], [4, 5, 6]]
1
2
3
4
5
6
répondu 11-Sep-2015 par glickind (196 points)
edité 11-Sep-2015 par Lhassa

+1 pour ce magnifique yield from.

+3 votes

j'ai traité un cas similaire sur des arguments exactement comme toi avec une fonction du type

def flatten(seq):
    seq = list(seq)
    i=0
    while i < len(seq):
        if type(seq[i]) in (list,tuple):
            x = seq.pop(i)
            for item in x:
                seq.insert(i,item)
            i += 1
        else:
            i += 1
    return seq

un peu de récursivité doit permettre de l'adapter pour une profondeur infinie

maintenant, la solution de glickind avec un générateur est beaucoup plus propre!!!!!


édité d'après le commentaire de glickind

def flatten(seq):
    seq = list(seq)
    i=0
    while i < len(seq):
        if type(seq[i]) in (list,tuple):
            x = seq.pop(i)
            for item in x:
                seq.insert(i,item)
        i += 1
    return seq
répondu 11-Sep-2015 par Lhassa (794 points)
edité 28-Sep-2015 par Lhassa

il y a quelques maladresses dans ton code:
- remplace while i < len(seq): par for element in seq:
ou for i, element in enumerate(seq): si tu as vraiment besoin de l'index en plus de l'element
voir http://jeffknupp.com/blog/2012/10/04/writing-idiomatic-python/
- si un bout de code est présent avant et après le else
mets le qu 'une fois après la fin du if

if cond:
    # bla bla
    i +=1
else:
    i +=1

doit devenir:

if cond:
   # bla bla
i += 1

effectivement, c'est bien plus pythonesque!

c'est un vieux bout de code, mais j'aurai du le nettoyer avant de le re-publier!

j'édite ça!

OK pour la condition if par contre, le while se justifie dans la mesure où l'on sait que tout ce qu'il y a avant l'indice i a été vérifié (et est donc bien un int/float...) par contre si seq[i] était une liste de liste, alors on doit continuer à applatir à partir de l'indice i...

j'espère avpir été clair

+3 votes

Vérifier qu'on a bien une liste ou un tuple à coup de type() ou insinstance() est toujours une source potentielle d'ennuis futurs. Que se passera-t-il quand on passera un autre type conteneur (set, deque, ...) ?

Voici une proposition qui gère ces situations. Pas super optimisée mais l'idée est-là.

>>> import itertools
>>> def riter(obj):
...     try: return iter(obj)
...     except TypeError: return iter((obj,))
>>> def flatten(seq):
...     res = list(itertools.chain.from_iterable(riter(e) for e in riter(seq)))
...     res2 = list(itertools.chain.from_iterable(riter(e) for e in riter(res)))
...     if res == res2: return res
...     return flatten(res2)
>>> l = ([(1,2), 3], 4, [[[5]]], (6, 7, 8, [9, 10]), {11, 12, (13, 14)})
>>> flatten(l)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14, 11, 12]
répondu 13-Sep-2015 par bubulle (2,106 points)
...