Python'da kapsayıcı aralıkları nasıl ele almalıyım?

Aralıkların geleneksel olarak kapsayıcı biçimde tanımlandığı bir alanda çalışıyorum. Her iki son noktayı içeren aralıkları temsil eden A'dan B'ye gibi insan tarafından okunabilen açıklamalarım var; 2 ila 4 , 2, 3, 4 anlamına gelir.

Python kodunda bu aralıklarla çalışmanın en iyi yolu nedir? Aşağıdaki kod, kapsayıcı tamsayı aralıkları oluşturmak için çalışır, ancak kapsayıcı dilim işlemleri de yapmam gerekiyor:

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

Gördüğüm tek tam çözüm, range veya dilim notasyonu her kullandığımda açıkça + 1 (veya - 1 ) kullanmaktır. > aralık (A, B + 1) , l [A: B + 1] , aralığı (B, A - 1, -1) ). Bu tekrarlama gerçekten kapsayıcı aralıklar ile çalışmak için en iyi yol mu?

Edit: Thanks to L3viathan for answering. Writing an inclusive_slice function to complement inclusive_range is certainly an option, although I would probably write it as follows:

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

... here represents code to handle negative indices, which are not straightforward when used with slices - note, for example, that L3viathan's function gives incorrect results if slice_to == -1.

Bununla birlikte, bir inclusive_slice işlevinin kullanımı oldukça garip görünüyor - l [inclusive_slice (A, B)] gerçekten l [A: B'den daha iyi 1] ?

Kapsayıcı aralıkları kullanmanın daha iyi bir yolu var mı?

Edit 2: Thank you for the new answers. I agree with Francis and Corley that changing the meaning of slice operations, either globally or for certain classes, would lead to significant confusion. I am therefore now leaning towards writing an inclusive_slice function.

Önceki düzenlemeden kendi soruma cevap vermek için, böyle bir işlevi kullanmanın (örn. l [inclusive_slice (A, B)] ) elle 1 eklemekten/çıkarmadan daha iyi olacağı sonucuna vardım. örneğin, l [A: B + 1] ), kenar durumlarına izin vereceğinden ( B == -1 ve B == Yok ) tek bir yerde kullanılması. Fonksiyonu kullanırken garipliği azaltabilir miyiz?

Edit 3: I have been thinking about how to improve the usage syntax, which currently looks like l[inclusive_slice(1, 5, 2)]. In particular, it would be good if the creation of an inclusive slice resembled standard slice syntax. In order to allow this, instead of inclusive_slice(start, stop, step), there could be a function inclusive that takes a slice as a parameter. The ideal usage syntax for inclusive would be line 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

Ne yazık ki, buna yalnızca [] içinde : sözdiziminin kullanılmasına izin veren Python tarafından izin verilmez. dahil bu nedenle 2 veya 3 sözdizimi kullanılarak çağrılmalıdır (burada s_ numpy tarafından sağlanan sürüm )

Diğer olasılıklar dahil 'u __getitem__ olan bir nesneye yerleştirmek, 4 sözdizimine izin vermek veya Dilimin stop parametresi, 5 sözdiziminde olduğu gibi. Maalesef dahil adım değeri hakkında bilgi gerektirdiğinden ikincisinin çalışabileceğine inanmıyorum.

Çalışılabilir sözdizimleri (orijinal l [dahil) [(5, 2)] , artı 2 , 3 ve 4/code>), hangisi kullanılacak en iyi olurdu? Yoksa daha iyi bir seçenek var mı?

Final Edit: Thank you all for the replies and comments, this has been very interesting. I have always been a fan of Python's "one way to do it" philosophy, but this issue has been caused by a conflict between Python's "one way" and the "one way" proscribed by the problem domain. I have definitely gained some appreciation for TIMTOWTDI in language design.

İlk ve en yüksek oyu alan cevabı verdiğim için, lütuf L3viathan'a verilir.

34
katma yazar wim, kaynak
@qarma - Evet, tıpkı range() işlevinde olduğu gibi, yalnızca tamsayı aralıklarıyla ilgilenmem gerekiyor.
katma yazar user200783, kaynak
@Rcynic - Dahili range işlevi için, pozitif adımlı, aralığı (a, b, adım) == aralığı (a, b) [:: adım] . Aynısı inclusive_range için de geçerli olmalıdır; bu, inclusive_range (10, 20, 3) == [10, 13, 16, 19] anlamına gelir. Bu, kapsayıcı aralık öğesinin her zaman olmadığı, yalnızca potansiyel olarak olmadığı anlamına gelir. Belki de closed_range daha iyi bir isim olabilir mi?
katma yazar user200783, kaynak
@MikeSamuel - Sorunuzu anladığımdan emin değilim. Yerleşik range() işlevi gibi, yalnızca tam sayı aralıklarını kullanmam gerekir. Bu ayrıklık anlamına gelmez mi?
katma yazar user200783, kaynak
"Kapsayıcı tamsayı aralıkları oluştur, ancak kapsayıcı dilim işlemleri gerçekleştirmem gerekiyor" diyorsunuz; bu, sürekli aralıklar değil, belirli aralıklar olduğunu gösterir. Bu böyle mi? Soruyorum, çünkü gerçek aralıkları gibi sürekli bir çizgide genel aralıkları temsil etmek, iyi tanımlanmış bir halefinizin olduğu ayrı bir çizgide aralıkları temsil etmekten daha karmaşık bir sorundur.
katma yazar Mike Samuel, kaynak
@PaulBaker, Evet, bu ayrıklık anlamına gelir ve soruma cevap verir. Emin değildim, çünkü "kapsayıcı tamsayı aralıkları üretmeye çalışıyor ama aynı zamanda ... dilimlere de ihtiyacım var" tamsayıları öneriyor, ancak sayı aralıkları kesirli adımlarla çalışıyor, bu yüzden kontrol etmeyi düşündüm.
katma yazar Mike Samuel, kaynak
Yazdığınız fonksiyonun yanlış olduğunu unutmayın. İkinci argüman stop + step , kapsayıcı menzili olması gerekenden daha fazla artırma potansiyeline sahiptir. stop + 1 olmalıdır. Örneğin. aralığı (0, 7, 3) [0, 3, 6] , ancak işleviniz [0, 3, 6, 9] .
katma yazar Shashank, kaynak
Menzil içindeki öğeler her zaman etki alanınızda tamsayılar mıdır? Örneğin 2 ila 4 , [2,3,4] veya 2: 00,2: 01, ..., 4:00 anlamına gelir .
katma yazar Dima Tisnek, kaynak
Açıkça aralık işlevindeki adımı söylüyorsunuz - bu, girişinizin de bir adım belirtebileceği anlamına mı geliyor? örneğin: "Her 2'de 10 ila 20", yani [10, 12, 14, 16, 18, 20]. eğer öyleyse, her 3'te 10'dan 20'ye kadar olan durumlarda ne olur? Yoksa bu asla olmayacak mı?
katma yazar Rcynic, kaynak
bir başka olasılık ... list 'ı alt sınıflamak ve dizileri/grupları işlemek için __getitem__ işlevselliğini genişletmek. daha sonra, çıkartma nesneniz olarak bir aralıkta geçebilirsiniz. Örneğin. şu anda l = [0,1,2,3,4] , l [2] 2 verir, ancak l [2,3,4] bir hatadır. l [2,3,4] [2,3,4] 'ı verecek şekilde destek eklemek mevcut hiçbir işlevselliği bozmaz (sanmıyorum) ve Size yardımcı olursa, l [inclusive_range (2,4)] gibi şeyleri yapmanıza izin verin.
katma yazar Corley Brigman, kaynak

9 cevap

Kapsayıcı dilimler için ek bir işlev yazın ve bunu dilimleme yerine kullanın. Mesela; alt sınıf bir dilim nesnesine tepki gösteren bir __getitem__ listesini uygulayın ve uygulayın, çünkü kodunuz bir yıl içinde sizden başka bir şey için ve muhtemelen de sizin için beklentilere aykırı davranacaktır.

inclusive_slice could look like this:

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]

Şahsen yapmak istediğim, yalnızca belirttiğiniz "tamamlama" çözümünü kullanmaktır ( aralığı (A, B + 1) , l [A: B + 1] ) ve iyi yorum yapın.

13
katma
[n: -1] öğesinden dilimleme burada [n: 0] 'a çevrilecektir, bu da tamamen farklı bir şey anlamına gelir
katma yazar wim, kaynak
Ayrıca slice_to, TypeError ile çarpmadan None 'ı alabilmelidir
katma yazar wim, kaynak
Önemsiz olarak varsayılan bir adım = 1, burada argüman ekleyebilirsiniz.
katma yazar Andy Hayden, kaynak
@FrancisColas Bu gibi mi?
katma yazar L3viathan, kaynak
@FrancisColas Yanlış olduğunu hatırladım, bir dilim nesnesine __getitem__ yönteminde tepki göstermek demek.
katma yazar L3viathan, kaynak
@wim artık hoş bir iki gömlek değil, düzeltildi.
katma yazar L3viathan, kaynak
Kodunuz hala step negatif değerleri için anlamsal olarak doğru değil; bunu düzeltmek güzel olurdu.
katma yazar Francis Colas, kaynak
Uygulanacak bir __slice__ yöntemi hakkında ne demek istiyorsunuz? Bir belgeye işaret eder misiniz, bulamadım. Ayrıca, olumsuz adımlar için mevcut çözümünüz OP'lerin aksine çalışmaz.
katma yazar Francis Colas, kaynak

Python'da bitiş endeksi her zaman özel olduğundan, "Python-conility" değerlerini her zaman dahili olarak kullanmayı düşünmeye değer. Bu şekilde, kodunuzdaki ikisini karıştırmaktan kendinizi kurtaracaksınız.

Özel dışa aktarma alt yordamları aracılığıyla yalnızca "dış temsil" ile ilgilenin:

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)

Alternatif olarak, "olağandışı" gösterimi tutan değişkenleri gerçek Macarca notasyonu ile işaretleyebilirsiniz.

8
katma
Katılmıyorum. Çünkü bugün bu şekilde yapar, gelecekte asla yapmaması gerektiği anlamına gelmez. Pek çok dilde kapsayıcı çeşitler var, ayrıca seçkinler de var çünkü çoğu zaman tam olarak neye ihtiyacınız var. (Ruby ve Swift & Perl hemen akla geliyor)
katma yazar uchuugaka, kaynak
@ uchuugaka Öngörülebilir bir gelecekte böyle bir değişikliğin gerçekleşmesi etkili bir şekilde imkansızdır. 1) Python aygıtları geriye dönük uyumluluk konusunda çok dikkatlidir. Bunu kesinlikle bir sonraki ana sürüme geçtikten sonra değiştirmeyeceklerdir. 2) Python'un odağı basitlik ve bakımdır - bu bir Pandora'nın kutusu olacaktır. 3) 0 ... N-1 indekslemenin 1'den daha uygun olduğunu belirleyen temel bilgi işlem kavramları (Neumann mimarisi, ikili sistem). Yani, YAGNI ilkesine göre, Python çözümlerinde bu konuda endişelenmenize gerek yok.
katma yazar ivan_pozdeev, kaynak

Adım boyutunu belirlemek yerine adım sayısını belirtmek istemiyorsanız, başlangıç ​​ve bitiş noktalarını içeren numpy.linspace kullanma seçeneği vardır.

import numpy as np

np.linspace(0,5,4)
# array([ 0.        ,  1.66666667,  3.33333333,  5.        ])
4
katma
linspace 'ı hatırlatmak için thx: ancak verilen aralıkta tamsayıları döndüren bir sürüm var mı? Aksi halde, bu konuda gerekli olan diğer bazı cevaplar kadar dönüşüm işi de vardır.
katma yazar javadba, kaynak

Kendi sınıfını yazmadan, fonksiyon gitmek için yol gibi görünüyor. En çok düşünebildiğim şey gerçek listeleri saklama değil, sadece ilgilendiğiniz aralığa göre üreteçleri iade etmektir. Şimdi kullanım sözdizimi hakkında konuştuğumuzdan - burada yapabilecekleriniz

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

Ve sonra kullanın:

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

Elbette, başka birisi bu işlevi kullanacaksa, diğer kötü girdi biçimlerini de kontrol etmeniz gerekir.

4
katma

Standart cevabın sadece ihtiyaç duyduğu her yerde + 1 veya -1 kullanmak olduğuna inanıyorum.

Dilimlerin anlaşılma şeklini genel olarak değiştirmek istemezsiniz (bu, çok fazla kod kıracaktır), ancak başka bir çözüm, dilimlerin dahil edilmesini istediğiniz nesneler için bir sınıf hiyerarşisi oluşturmak olacaktır. Örneğin, bir listesi için:

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]

Elbette, __setitem__ ve __delitem__ ile aynı şeyi yapmak istersiniz.

(Bir listesi kullandım, ancak bu Sıra veya MutableSequence .)

4
katma
Bence böyle bir sınıf kullanmak, işleri yardım ettiğinden daha fazla karıştırıyor. Örneğin, InclusiveList (aralık (11)) ila include 11 olmasını bekleyebilirim.
katma yazar tobias_k, kaynak
Peki range (11) 11 içermez ve yalnızca listeyi başlatmak için kullanılır. Burada range 'in kullanılmasının kafa karıştırıcı hale geldiğini anlıyorum, örneği değiştirdim.
katma yazar Francis Colas, kaynak

Geleneksel olmayan veya liste gibi veri türlerini genişleten API oluşturmak yerine, yerleşik dilim üzerinde bir sarmalayıcı oluşturmak için Dilim işlevini kullanmak idealdir. dilimleme gerektiren herhangi bir yere geçirin. Python, bazı istisnai durumlar için bu yaklaşımı desteklemektedir ve bu istisna vakası için garanti verebileceğiniz dava. Örnek olarak, kapsayıcı bir dilim,

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

And you can use it for any sequence types

>>> 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)

Son olarak, kapsayıcı dilimi tamamlamak için irange adlı kapsayıcı bir aralık oluşturabilirsiniz (OPs satırlarında yazılmıştır).

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

Bu temel kavramları aşmak zor ve muhtemelen akıllıca değil. Inclusivelist sınıfında, b-a + 1'deki len (l [a: b]) karışıklığa yol açabilir. Doğal piton hissini korumak için, BASIC tarzında okunabilirlik verirken, sadece şunları tanımlayın:

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

daha sonra doğal python mantığını koruyarak istediğiniz gibi yönetebilirsiniz:

>>>>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
katma
Negatif sayıları doğru işlemez
katma yazar wim, kaynak
@wim: teşekkürler, şimdi düzeltildi.
katma yazar B. M., kaynak

En iyi sözdizimi için isteğinize odaklanarak, hedefleme ne olacak:

l[1:UpThrough(5):2]

Bunu __index__ yöntemini kullanarak elde edebilirsiniz. :

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

Artık özel bir liste sınıfına bile ihtiyacınız yok (ve değişiklik yapmanıza gerek yok) ya da küresel tanım):

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

Çok kullanırsanız, upIncl , downIncl daha kısa adlarını kullanabilirsiniz. In ve InRev .

Dilimleri kullanmaktan başka, onlar da bu sınıfları oluşturabilirler. gerçek dizin gibi davran:

def __int__(self):
    return self.stop
3
katma
Basit işlevlerden ziyade sınıflar olarak UpThrough ve DownThrough tanımlamanızın bir nedeni var mı?
katma yazar user200783, kaynak
Bunun nedeni, __int__ ve/veya __float__ 'u kullanmak olacaktır, böylece int (UpThrough (5)) == 5 ancak dilimde doğru sonuç alınacaktır indeks. Aritmetik işleçler de ekleyebilirsiniz.
katma yazar shaunc, kaynak

Yveyaum yapacak mıydı, ama cevap olarak kod yazmak daha kolay, bu yüzden ...

ÇOK net olmadığı sürece, dilimlemeyi yeniden tanımlayan bir sınıf yazmazdım. Bit dilimleme ile ints temsil eden bir sınıf var. Bağlamlarımda, '4: 2' çok açık bir şekilde kapsayıcıdır ve ints zaten dilimleme için herhangi bir kullanımına sahip değildir, bu yüzden (zar zveya) kabul edilebilir (imho ve bazıları katılmıyveyaum).

Listeler için, böyle bir şey yapacaksınız.

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

ve sonra kodunuzda

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

ve bunu yapmak çok kolay bir hatadır, çünkü liste zaten iyi tanımlanmış bir kullanıma sahiptir ... aynı gözükürler, ama farklı davranırlar ve bu nedenle, özellikle yazmamışsanız hata ayıklamak için çok kafa karıştırıcı olacaktır.

Bu tamamen kaybolduğun anlamına gelmez ... dilimleme uygun, ama sonuçta, bu sadece bir işlev. Ve bu işlevi bu gibi bir şeye ekleyebilirsiniz, bu nedenle bunu elde etmenin daha kolay bir yolu olabilir:

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]

Ancak, bu çözüm, diğerleri gibi (tamamlanmamış olmanın yanı sıra ... biliyveyaum) aşillerin topuğu, basit dilimleme notasyonu kadar basit değil, listeyi geçirmekten biraz daha basit. tartışma, ancak yine de [4: 2] 'den daha zveya. Bunu gerçekleştirmenin tek yolu dilime farklı , farklı bir şekilde yveyaumlanabilecek bir şey iletmektir, böylece kullanıcı yaptıklarını okumayı öğrenir ve yine de basit olabilir.

Bir ihtimal ... kayan nokta sayıları. Onlar farklı, bu yüzden onları görebilirsiniz ve onlar 'basit' sözdiziminden çok daha zveya değiller. Bu yerleşik değil, bu yüzden hala bazı 'sihir' var, ancak sözdizimsel şeker kadar kötü değil ....

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]

3.0, herhangi bir python programcısına 'hey, veyaada alışılmadık bir şeyler oluyveya' diyecek kadar yeterli olmalı ... ... mutlaka ne olduğunu değil, ama en azından 'garip' davranması şaşırtıcı değil.

Listelerde benzersiz olan bir şey olmadığını unutmayın ... bunu herhangi bir sınıf için yapabilecek bir dekveyaatör yazabilirsiniz:

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)

veya

@inclusiveclass
class inclusivelist(list):
    pass

The first fveyam is probably mveyae useful though.

3
katma