web-dev-qa-db-fra.com

Comment dois-je gérer les plages inclusives en Python?

Je travaille dans un domaine dans lequel les gammes sont classiquement décrites de manière inclusive. J'ai des descriptions lisibles par l'homme telles que from A to B, Qui représentent des plages comprenant les deux extrémités - par ex. from 2 to 4 Signifie 2, 3, 4.

Quelle est la meilleure façon de travailler avec ces plages dans le code Python?)? Le code suivant permet de générer des plages intégrales d'entiers, mais je dois également effectuer des opérations de tranche incluses:

def inclusive_range(start, stop, step):
    return range(start, (stop + 1) if step >= 0 else (stop - 1), step)

La seule solution complète que je vois est d’utiliser explicitement + 1 (Ou - 1) Chaque fois que j’utilise la notation range ou slice (par exemple range(A, B + 1), l[A:B+1], range(B, A - 1, -1)). Cette répétition est-elle vraiment la meilleure façon de travailler avec des gammes inclusives?

Edit: Merci à L3viathan pour sa réponse. Écrire une fonction inclusive_slice Pour compléter inclusive_range Est certainement une option, même si je l’écrirais probablement comme suit:

def inclusive_slice(start, stop, step):
    ...
    return slice(start, (stop + 1) if step >= 0 else (stop - 1), step)

... Représente ici le code permettant de gérer les index négatifs, qui ne sont pas simples lorsqu'ils sont utilisés avec des tranches. Notez, par exemple, que la fonction de L3viathan donne des résultats incorrects si slice_to == -1.

Cependant, il semble qu'une fonction inclusive_slice Serait difficile à utiliser - est-ce que l[inclusive_slice(A, B)] est vraiment meilleur que l[A:B+1]?

Existe-t-il une meilleure façon de gérer les gammes inclusives?

Edit 2: Merci pour les nouvelles réponses. Je conviens avec Francis et Corley que changer le sens des opérations sur les tranches, que ce soit globalement ou pour certaines classes, créerait une grande confusion. Je suis donc maintenant enclin à écrire une fonction inclusive_slice.

Pour répondre à ma propre question de la précédente édition, je suis parvenu à la conclusion que l’utilisation d’une telle fonction (par exemple l[inclusive_slice(A, B)]) serait préférable à l’ajout/soustraction manuel de 1 (par exemple l[A:B+1]), Car cela permettrait aux cas Edge (tels que B == -1 et B == None) d'être traités à un seul endroit. Peut-on réduire la gêne liée à l'utilisation de la fonction?

Edit 3: J'ai réfléchi à la façon d'améliorer la syntaxe d'utilisation, qui ressemble actuellement à l[inclusive_slice(1, 5, 2)]. En particulier, il serait bon que la création d'une tranche inclusive ressemble à la syntaxe de la tranche standard. Afin de permettre cela, au lieu de inclusive_slice(start, stop, step), il pourrait y avoir une fonction inclusive qui prend une tranche en tant que paramètre. La syntaxe d'utilisation idéale pour inclusive serait la ligne 1:

l[inclusive(1:5:2)]          # 1
l[inclusive(slice(1, 5, 2))] # 2
l[inclusive(s_[1:5:2])]      # 3
l[inclusive[1:5:2]]          # 4
l[1:inclusive(5):2]          # 5

Malheureusement, cela n’est pas autorisé par Python, qui autorise uniquement l’utilisation de la syntaxe : Dans []. inclusive devrait donc être appelé en utilisant la syntaxe 2 ou 3 (où s_ agit comme la version fournie par numpy ).

Il est également possible de transformer inclusive en objet avec __getitem__, En autorisant la syntaxe 4, Ou d’appliquer inclusive uniquement au paramètre stop. de la tranche, comme dans la syntaxe 5. Malheureusement, je ne crois pas que ce dernier puisse fonctionner car inclusive nécessite la connaissance de la valeur step.

Parmi les syntaxes utilisables (l'original l[inclusive_slice(1, 5, 2)], plus 2, 3 Et 4), Quelle serait la meilleure utilisation? Ou y a-t-il une autre meilleure option?

Final Edit: Merci à tous pour vos réponses et vos commentaires, cela a été très intéressant. J'ai toujours été fan de la philosophie "une façon de faire" de Python, mais ce problème a été causé par un conflit entre la "méthode à une" de Python et la "méthode à une" interdite par le domaine problématique. J'ai définitivement acquis une certaine appréciation pour TIMTOWTDI dans la conception de la langue.

Pour avoir donné la première réponse et le vote le plus élevé, j'attribue la prime à L3viathan.

43
user200783

Ecrivez une fonction supplémentaire pour slice inclusive et utilisez-la au lieu de slicing. Bien qu’il serait possible, par exemple, de Sous-classe et implémenter un __getitem__ réagissant à un objet slice, je vous le déconseille, car votre code se comportera contrairement aux attentes de quiconque sauf vous - et probablement de vous aussi dans un an.

inclusive_slice Pourrait ressembler à ceci:

def inclusive_slice(myList, slice_from=None, slice_to=None, step=1):
    if slice_to is not None:
        slice_to += 1 if step > 0 else -1
    if slice_to == 0:
        slice_to = None
    return myList[slice_from:slice_to:step]

Ce que je ferais personnellement, c’est simplement d’utiliser la solution "complète" que vous avez mentionnée (range(A, B + 1), l[A:B+1]) Et de bien commenter.

14
L3viathan

Etant donné qu'en Python, l'index final est toujours exclusif, il est utile de toujours utiliser les valeurs "convention Python" en interne. De cette façon, vous éviterez de mélanger les deux dans votre code.

Ne traitez que la "représentation externe" par le biais de sous-routines de conversion dédiées:

def text2range(text):
    m = re.match(r"from (\d+) to (\d+)",text)
    start,end = int(m.groups(1)),int(m.groups(2))+1

def range2text(start,end):
    print "from %d to %d"%(start,end-1)

Vous pouvez également marquer les variables contenant la représentation "inhabituelle" avec le signe notation hongroise vraie .

9
ivan_pozdeev

Si vous ne souhaitez pas spécifier la taille de l'étape mais le nombre d'étapes, vous pouvez utiliser numpy.linspace qui inclut les points de départ et d'arrivée

import numpy as np

np.linspace(0,5,4)
# array([ 0.        ,  1.66666667,  3.33333333,  5.        ])
5
plonser

Sans écrire votre propre classe, la fonction semble être la voie à suivre. Ce à quoi je peux penser tout au plus, ce n’est pas de stocker des listes réelles, mais simplement de renvoyer des générateurs pour la gamme qui vous tient à cœur. Puisque nous parlons maintenant de la syntaxe d'utilisation, voici ce que vous pourriez faire.

def closed_range(slices):
    slice_parts = slices.split(':')
    [start, stop, step] = map(int, slice_parts)
    num = start
    if start <= stop and step > 0:
        while num <= stop:
            yield num
            num += step
    # if negative step
    Elif step < 0:
        while num >= stop:
            yield num
            num += step

Et ensuite utiliser comme:

list(closed_range('1:5:2'))
[1,3,5]

Bien sûr, vous devrez également rechercher d'autres formes de mauvaise saisie si quelqu'un d'autre va utiliser cette fonction.

4
Rcynic

Je crois que la réponse standard est d'utiliser simplement +1 ou -1 partout où c'est nécessaire.

Vous ne voulez pas modifier globalement la manière dont les tranches sont comprises (cela briserait beaucoup de code), mais une autre solution consisterait à créer une hiérarchie de classes pour les objets pour lesquels vous souhaitez que les tranches soient inclusives. Par exemple, pour un list:

class InclusiveList(list):
    def __getitem__(self, index):
        if isinstance(index, slice):
            start, stop, step = index.start, index.stop, index.step
            if index.stop is not None:
                if index.step is None:
                    stop += 1
                else:
                    if index.step >= 0:
                        stop += 1
                    else:
                        if stop == 0: 
                            stop = None # going from [4:0:-1] to [4::-1] since [4:-1:-1] wouldn't work 
                        else:
                            stop -= 1
            return super().__getitem__(slice(start, stop, step))
        else:
            return super().__getitem__(index)

>>> a = InclusiveList([1, 2, 4, 8, 16, 32])
>>> a
[1, 2, 4, 8, 16, 32]
>>> a[4]
16
>>> a[2:4]
[4, 8, 16]
>>> a[3:0:-1]
[8, 4, 2, 1]
>>> a[3::-1]
[8, 4, 2, 1]
>>> a[5:1:-2]
[32, 8, 2]

Bien sûr, vous voulez faire la même chose avec __setitem__ et __delitem__.

(J'ai utilisé un list mais cela fonctionne pour tout Sequence ou MutableSequence.)

4
Francis Colas

J'allais commenter, mais c'est plus facile d'écrire du code comme réponse, alors ...

Je n'écrirais PAS un cours qui redéfinit le découpage en tranches, à moins que ce soit TRÈS clair. J'ai une classe qui représente les entiers avec tranchage de bits. Dans mes contextes, '4: 2' est très clairement inclusif et ints n'a déjà aucune utilité pour le découpage en tranches, il est donc (à peine) acceptable (à mon humble avis, et certains seraient en désaccord).

Pour les listes, vous avez le cas que vous ferez quelque chose comme

list1 = [1,2,3,4,5]
list2 = InclusiveList([1,2,3,4,5])

et plus tard dans votre code

if list1[4:2] == test_list or list2[4:2] == test_list:

et c’est une erreur très facile à faire, puisque list a déjà un usage bien défini .. ils ont l’air identique, mais agissent différemment, et donc cela sera très déroutant pour le débogage, surtout si vous ne l’écrivez pas.

Cela ne veut pas dire que vous êtes complètement perdu ... le découpage est pratique, mais après tout, ce n'est qu'une fonction. Et vous pouvez ajouter cette fonction à quelque chose comme ça, alors cela pourrait être un moyen plus facile d'y accéder:

class inc_list(list):
    def islice(self, start, end=None, dir=None):
        return self.__getitem__(slice(start, end+1, dir))

l2 = inc_list([1,2,3,4,5])
l2[1:3]
[0x3,
 0x4]
l2.islice(1,3)
[0x3,
 0x4,
 0x5]

Cependant, cette solution, comme beaucoup d’autres, (en plus d’être incomplète ... je sais) a le talon d’Achille dans le sens où elle n’est pas aussi simple que la notation de tranche simple ... c’est un peu plus simple que de passer la liste à argument, mais toujours plus difficile que juste [4: 2]. La seule façon de le faire est de transmettre quelque chose différent à la tranche, qui pourrait être interprété différemment, de sorte que l'utilisateur puisse le lire en lisant ce ils l'ont fait, et cela pourrait toujours être aussi simple.

Une possibilité ... des nombres à virgule flottante. Ils sont différents, vous pouvez donc les voir, et ils ne sont pas plus difficiles que la syntaxe "simple". Ce n'est pas intégré, donc il y a encore de la "magie", mais en ce qui concerne le sucre syntaxique, ce n'est pas mal ...

class inc_list(list):
    def __getitem__(self, x):
        if isinstance(x, slice):
            start, end, step = x.start, x.stop, x.step
            if step == None:
                step = 1
            if isinstance(end, float):
                end = int(end)
                end = end + step
                x = slice(start, end, step)
            return list.__getitem__(self, x)

l2 = inc_list([1,2,3,4,5])
l2[1:3]
[0x2,
 0x3]
l2[1:3.0]
[0x2,
 0x3,
 0x4]

La 3.0 devrait suffire à dire à tout python programmeur 'hé, quelque chose d'inhabituel s'y passe' ... pas nécessairement quoi se passe, mais au moins, il n’est pas surprenant qu’il ait un comportement "bizarre".

Notez qu'il n'y a rien d'unique à ce sujet dans les listes ... vous pouvez facilement écrire un décorateur qui pourrait le faire pour n'importe quelle classe:

def inc_getitem(self, x):
    if isinstance(x, slice):
        start, end, step = x.start, x.stop, x.step
        if step == None:
            step = 1
        if isinstance(end, float):
            end = int(end)
            end = end + step
            x = slice(start, end, step)
    return list.__getitem__(self, x)

def inclusiveclass(inclass):
    class newclass(inclass):
        __getitem__ = inc_getitem
    return newclass

ilist = inclusiveclass(list)

ou

@inclusiveclass
class inclusivelist(list):
    pass

La première forme est probablement plus utile cependant.

3
Corley Brigman

Il est difficile et probablement pas sage de surcharger de tels concepts de base. avec une nouvelle classe inclusive, len (l [a: b]) dans b-a + 1, ce qui peut entraîner des confusions.
Pour préserver le sens naturel python, tout en offrant la lisibilité dans un style BASIC, définissez simplement:

STEP=FROM=lambda x:x
TO=lambda x:x+1 if x!=-1 else None 
DOWNTO=lambda x:x-1 if x!=0 else None

alors vous pouvez gérer comme vous voulez, en gardant la logique naturelle python:

>>>>l=list(range(FROM(0),TO(9)))
>>>>l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>l[FROM(9):DOWNTO(3):STEP(-2)] == l[9:2:-2]
True
3
B. M.

En vous concentrant sur votre demande de la meilleure syntaxe, qu’en est-il du ciblage:

l[1:UpThrough(5):2]

Vous pouvez y parvenir en utilisant le __index__ méthode:

class UpThrough(object):
    def __init__(self, stop):
        self.stop = stop

    def __index__(self):
        return self.stop + 1

class DownThrough(object):
    def __init__(self, stop):
        self.stop = stop

    def __index__(self):
        return self.stop - 1

Maintenant, vous n'avez même pas besoin d'une classe de liste spécialisée (et vous n'avez pas besoin de modifier la définition globale non plus):

>>> l = [1,2,3,4]
>>> l[1:UpThrough(2)]
[2,3]

Si vous en utilisez beaucoup, vous pouvez utiliser des noms plus courts upIncl, downIncl ou même In et InRev.

Vous pouvez également créer ces classes de sorte que, autres que leur utilisation dans slice, elles agissent comme l’index réel:

def __int__(self):
    return self.stop
3
shaunc

Au lieu de créer une API non conventionnelle ou d’étendre des types de données tels que list, il serait idéal de créer une fonction Slice qui encapsule le slice intégré afin de pouvoir la transmettre à tout où, une tranche est exigeant. Python) prend en charge cette approche dans certains cas exceptionnels, et le cas que vous possédez peut être justifié dans ce cas exceptionnel. Par exemple, une tranche inclusive devrait ressembler à

def islice(start, stop = None, step = None):
    if stop is not None: stop += 1
    if stop == 0: stop = None
    return slice(start, stop, step)

Et vous pouvez l'utiliser pour tout types de séquence

>>> range(1,10)[islice(1,5)]
[2, 3, 4, 5, 6]
>>> "Hello World"[islice(0,5,2)]
'Hlo'
>>> (3,1,4,1,5,9,2,6)[islice(1,-2)]
(1, 4, 1, 5, 9, 2)

Enfin, vous pouvez également créer une plage inclusive appelée irange pour compléter la tranche inclusive (écrite en lignes de points d’opération).

def irange(start, stop, step):
    return range(start, (stop + 1) if step >= 0 else (stop - 1), step)
3
Abhijit

Je ne sais pas si cela est déjà couvert, voici comment je l'ai traité pour vérifier si ma variable se situe dans une plage définie:

my var=10 # want to check if it is in range(0,10) as inclusive
limits = range(0,10)
limits.append(limits[-1]+1)
if(my_var in limits):
    print("In Limit")
else:
    print("Out of Limit")

Ce code renverra "In Limit" puisque j'ai élargi ma plage de 1, ce qui la rend inclusive.

0
Raghu