ทำไม“ 1000000000000000 ในช่วง (1000000000000001)” เร็วมากใน Python 3


2115

ฉันเข้าใจว่าrange()ฟังก์ชั่นซึ่งจริงๆแล้วเป็นประเภทวัตถุใน Python 3สร้างเนื้อหาในทันทีคล้ายกับเครื่องกำเนิด

นี่เป็นกรณีที่ฉันคาดว่าบรรทัดต่อไปนี้จะใช้เวลามากเกินไปเพราะในการพิจารณาว่า 1 quadrillion อยู่ในช่วงหรือไม่ต้องสร้างค่า quadrillion:

1000000000000000 in range(1000000000000001)

ยิ่งกว่านั้น: ดูเหมือนว่าไม่ว่าฉันจะเพิ่มศูนย์เป็นจำนวนเท่าใดการคำนวณมากหรือน้อยก็ใช้เวลาเท่ากัน

ฉันได้ลองสิ่งนี้เช่นกัน แต่การคำนวณก็ยังเกือบจะทันที:

1000000000000000000000 in range(0,1000000000000000000001,10) # count by tens

ถ้าฉันพยายามที่จะใช้ฟังก์ชั่นพิสัยของตัวเองผลที่ออกมาไม่ค่อยดีเท่าไหร่ !!

def my_crappy_range(N):
    i = 0
    while i < N:
        yield i
        i += 1
    return

อะไรคือสิ่งที่range()วัตถุที่ทำภายใต้ประทุนที่ทำให้มันอย่างรวดเร็ว?


คำตอบ Martijn Pieters'ได้รับเลือกเพื่อความสมบูรณ์ของมัน แต่ยังเห็นคำตอบแรก abarnert ของสำหรับการสนทนาที่ดีของสิ่งที่มันหมายถึงการrangeที่จะเป็นที่เต็มเปี่ยมลำดับในหลาม 3 และข้อมูลบางส่วน / คำเตือนเกี่ยวกับความไม่ลงรอยกันศักยภาพในการ__contains__เพิ่มประสิทธิภาพการทำงานข้ามการใช้งานหลาม . คำตอบอื่น ๆ ของ abarnertมีรายละเอียดเพิ่มเติมและให้ลิงก์สำหรับผู้ที่สนใจในประวัติศาสตร์เบื้องหลังการปรับให้เหมาะสมใน Python 3 (และการขาดการเพิ่มประสิทธิภาพของxrangeใน Python 2) รู้รอบโดยการกระตุ้นและโดย wimจัดเตรียมซอร์สโค้ด C และคำอธิบายที่เกี่ยวข้องสำหรับผู้ที่สนใจ


70
โปรดทราบว่านี่เป็นกรณีเฉพาะในกรณีที่รายการที่เรากำลังตรวจสอบเป็นboolหรือlongประเภทกับวัตถุชนิดอื่น ๆ มันจะบ้าไปแล้ว ลองด้วย:100000000000000.0 in range(1000000000000001)
Ashwini Chaudhary

10
ใครบอกคุณว่าrangeเป็นเครื่องกำเนิดไฟฟ้า
abarnert

7
@abarnert ฉันคิดว่าการแก้ไขที่ฉันทำไว้นั้นยังคงอยู่ในความสับสน
Rick สนับสนุนโมนิก้า

5
@AshwiniChaudhary ไม่ใช่Python2 xrangeเหมือนกับ Python3rangeใช่ไหม
Superbest

28
@Superbest xrange()objects ไม่มี__contains__วิธีการดังนั้นการตรวจสอบรายการจะต้องวนซ้ำทุกรายการ นอกจากนี้ยังมีการเปลี่ยนแปลงอื่น ๆ เล็กน้อยrange()เช่นสนับสนุนการแบ่งส่วน (ซึ่งส่งคืนrangeวัตถุอีกครั้ง) และตอนนี้ยังมีcountและindexวิธีการเพื่อให้เข้ากันได้กับcollections.SequenceABC
Ashwini Chaudhary

คำตอบ:


2170

range()วัตถุPython 3 ไม่ได้สร้างตัวเลขทันที มันเป็นวัตถุลำดับสมาร์ทที่ก่อให้เกิดตัวเลขตามความต้องการ สิ่งที่อยู่ในนั้นคือค่าเริ่มต้นหยุดและขั้นตอนจากนั้นเมื่อคุณวนซ้ำวัตถุจำนวนเต็มถัดไปจะถูกคำนวณในแต่ละการวนซ้ำ

วัตถุยังใช้object.__contains__ฮุกและคำนวณว่าตัวเลขของคุณเป็นส่วนหนึ่งของช่วงหรือไม่ คำนวณเป็น (ใกล้) การดำเนินการเวลาคง* ไม่จำเป็นต้องสแกนผ่านจำนวนเต็มที่เป็นไปได้ทั้งหมดในช่วง

จากrange()เอกสารวัตถุ :

ข้อได้เปรียบของrangeประเภทกว่าปกติlistหรือtupleเป็นที่วัตถุช่วงมักจะใช้เวลาเดียวกัน (เล็ก) จำนวนหน่วยความจำไม่ว่าขนาดของช่วงมันหมายถึง (เป็นเพียงเก็บstart, stopและstepค่าการคำนวณแต่ละรายการและ subranges ตามความจำเป็น).

อย่างน้อยที่สุดrange()วัตถุของคุณจะทำ:

class my_range(object):
    def __init__(self, start, stop=None, step=1):
        if stop is None:
            start, stop = 0, start
        self.start, self.stop, self.step = start, stop, step
        if step < 0:
            lo, hi, step = stop, start, -step
        else:
            lo, hi = start, stop
        self.length = 0 if lo > hi else ((hi - lo - 1) // step) + 1

    def __iter__(self):
        current = self.start
        if self.step < 0:
            while current > self.stop:
                yield current
                current += self.step
        else:
            while current < self.stop:
                yield current
                current += self.step

    def __len__(self):
        return self.length

    def __getitem__(self, i):
        if i < 0:
            i += self.length
        if 0 <= i < self.length:
            return self.start + i * self.step
        raise IndexError('Index out of range: {}'.format(i))

    def __contains__(self, num):
        if self.step < 0:
            if not (self.stop < num <= self.start):
                return False
        else:
            if not (self.start <= num < self.stop):
                return False
        return (num - self.start) % self.step == 0

สิ่งนี้ยังขาดหลายสิ่งที่การrange()สนับสนุนที่แท้จริง(เช่น.index()หรือ.count()วิธีการ hashing การทดสอบความเท่าเทียมกันหรือการแบ่งส่วน) แต่ควรให้แนวคิดแก่คุณ

ฉันทำให้การ__contains__ใช้งานง่ายขึ้นเพื่อเน้นการทดสอบจำนวนเต็มเท่านั้น หากคุณให้ค่าrange()ที่ไม่ใช่จำนวนเต็ม (รวมถึงคลาสย่อยint) ของวัตถุจริงการสแกนแบบช้าจะเริ่มต้นเพื่อดูว่ามีการจับคู่หรือไม่เหมือนกับว่าคุณใช้การทดสอบการบรรจุกับรายการของค่าที่มีทั้งหมด สิ่งนี้ทำเพื่อสนับสนุนประเภทตัวเลขอื่น ๆ ที่เพิ่งเกิดขึ้นเพื่อรองรับการทดสอบความเท่าเทียมกันกับจำนวนเต็ม แต่ไม่คาดว่าจะรองรับการคำนวณทางคณิตศาสตร์จำนวนเต็มเช่นกัน ดูปัญหางูหลามต้นฉบับที่นำมาใช้ทดสอบการบรรจุ


* ใกล้กับเวลาคงที่เนื่องจากจำนวนเต็ม Python ไม่ถูก จำกัด และการดำเนินการทางคณิตศาสตร์ก็เพิ่มขึ้นตามเวลาที่ N เพิ่มขึ้นทำให้การดำเนินการ O (บันทึก N) นี้ เนื่องจากมันถูกประมวลผลทั้งหมดในรหัส C ที่ได้รับการปรับปรุงและ Python เก็บค่าจำนวนเต็มเป็นจำนวน 30 บิตคุณจะมีหน่วยความจำไม่เพียงพอก่อนที่คุณจะเห็นผลกระทบต่อประสิทธิภาพเนื่องจากขนาดของจำนวนเต็มที่เกี่ยวข้องที่นี่


58
สนุกจริง: เพราะคุณมีการใช้งาน__getitem__และ__len__การ__iter__ใช้งานจริงไม่จำเป็น
Lucretiel

2
@ Lucretiel: ใน Python 2.3มีการxrangeiteratorเพิ่มพิเศษโดยเฉพาะเพราะมันไม่เร็วพอ และบางแห่งใน 3.x (ฉันไม่แน่ใจว่ามันคือ 3.0 หรือ 3.2) มันถูกโยนและพวกเขาใช้listiteratorประเภทเดียวกันกับที่listใช้
abarnert

1
ฉันจะกำหนดตัวสร้างเป็นdef __init__(self, *start_stop_step)และแยกมันออกมาจากที่นั่น; วิธีที่การโต้แย้งถูกติดป้ายตอนนี้ทำให้เกิดความสับสน อย่างไรก็ตาม +1; คุณยังคงอธิบายพฤติกรรมอย่างแน่นอน
Cody Piersall

1
@CodyPiersall: น่าเสียดายที่นี่เป็นลายเซ็นต์ของเครื่องมือเริ่มต้นของคลาสจริง rangeเก่ากว่า*args(น้อยกว่าargclinicAPI ที่ให้ฟังก์ชัน C-API มีลายเซ็น Python ที่สมบูรณ์) ไม่กี่ฟังก์ชั่นอื่น ๆ เก่า (และฟังก์ชั่นใหม่ไม่กี่เช่นxrange, sliceและitertools.islice, เพื่อความมั่นคง) วิธีการทำงานเหมือนกัน แต่ส่วนใหญ่กุยและส่วนที่เหลือของ devs หลักดูเหมือนจะเห็นด้วยกับคุณ เอกสาร 2.0+ ยังอธิบายrangeและเพื่อนราวกับว่าพวกเขาเป็น C ++ - สไตล์โอเวอร์โหลดแทนที่จะแสดงลายเซ็นที่สับสนจริง
abarnert

2
@CodyPiersall: ที่จริงแล้วนี่เป็นคำพูดจาก Guido ในargclinicการอภิปรายเมื่อ Nick Coghlan มีวิธีที่จะอนุญาตให้มีการกำหนดrangeอย่างไม่น่าสงสัย: "โปรดอย่าทำให้การคัดลอกการตัดสินใจออกแบบแย่ที่สุดของฉันง่ายขึ้น" ดังนั้นฉันค่อนข้างมั่นใจว่าเขาเห็นด้วยที่rangeทำให้สับสนตามที่เขียนไว้
abarnert

844

ความเข้าใจผิดขั้นพื้นฐานที่นี่คือการคิดว่าrangeเป็นเครื่องกำเนิดไฟฟ้า มันไม่ใช่. ในความเป็นจริงมันไม่ใช่ตัววนซ้ำใด ๆ

คุณสามารถบอกสิ่งนี้ได้อย่างง่ายดาย:

>>> a = range(5)
>>> print(list(a))
[0, 1, 2, 3, 4]
>>> print(list(a))
[0, 1, 2, 3, 4]

หากเป็นเครื่องกำเนิดไฟฟ้าการวนซ้ำจะหมด:

>>> b = my_crappy_range(5)
>>> print(list(b))
[0, 1, 2, 3, 4]
>>> print(list(b))
[]

สิ่งที่rangeเป็นจริงคือเป็นลำดับเช่นเดียวกับรายการ คุณสามารถทดสอบสิ่งนี้ได้:

>>> import collections.abc
>>> isinstance(a, collections.abc.Sequence)
True

ซึ่งหมายความว่าจะต้องปฏิบัติตามกฎทั้งหมดของการเป็นลำดับ:

>>> a[3]         # indexable
3
>>> len(a)       # sized
5
>>> 3 in a       # membership
True
>>> reversed(a)  # reversible
<range_iterator at 0x101cd2360>
>>> a.index(3)   # implements 'index'
3
>>> a.count(3)   # implements 'count'
1

ความแตกต่างระหว่าง a rangeและ a listคือ a rangeเป็นลำดับที่ขี้เกียจหรือไดนามิก มันไม่ได้จำทั้งหมดของค่าของมันก็เพียงแค่จำของมันstart, stopและและสร้างค่านิยมที่ต้องการบนstep__getitem__

(ในฐานะที่เป็นหมายเหตุด้านข้างหากคุณprint(iter(a))คุณจะสังเกตเห็นว่าrangeใช้listiteratorชนิดเดียวกันกับlistวิธีการทำงานอย่างไร A listiteratorไม่ได้ใช้อะไรเป็นพิเศษเกี่ยวกับlistยกเว้นว่ามันมีการใช้งาน C __getitem__ดังนั้นจึงทำงานได้ดีสำหรับrangeเกินไป.)


ตอนนี้ไม่มีอะไรที่บอกว่าSequence.__contains__จะต้องมีเวลาคงที่ในความเป็นจริงสำหรับตัวอย่างที่ชัดเจนของลำดับเช่นlistมันไม่ได้ แต่ไม่มีอะไรที่บอกว่ามันเป็นไปไม่ได้ และง่ายกว่าที่จะใช้range.__contains__เพื่อตรวจสอบทางคณิตศาสตร์ ( (val - start) % stepแต่มีความซับซ้อนเป็นพิเศษในการจัดการกับขั้นตอนเชิงลบ) กว่าที่จะสร้างและทดสอบค่าทั้งหมดดังนั้นทำไมมันไม่ควรทำอย่างนั้นดีกว่า?

แต่ดูเหมือนจะไม่มีอะไรในภาษาที่รับประกันว่าจะเกิดขึ้น ดังที่ Ashwini Chaudhari ชี้ให้เห็นถ้าคุณให้ค่าที่ไม่ครบถ้วนแทนการแปลงเป็นจำนวนเต็มและทำการทดสอบทางคณิตศาสตร์มันจะถอยกลับไปทำซ้ำค่าทั้งหมดและเปรียบเทียบทีละตัว และเนื่องจาก CPython 3.2+ และ PyPy 3.x เวอร์ชันเกิดขึ้นเพื่อเพิ่มประสิทธิภาพนี้และมันเป็นความคิดที่ดีและง่ายที่จะทำไม่มีเหตุผลที่ IronPython หรือ NewKickAssPython 3.x ไม่สามารถละทิ้งมันได้ (และในความเป็นจริง CPython 3.0-3.1 ไม่ได้รวมไว้)


ถ้าrangeเป็นเครื่องกำเนิดไฟฟ้าจริง ๆmy_crappy_rangeแล้วมันก็ไม่มีเหตุผลที่จะทดสอบ__contains__ด้วยวิธีนี้หรืออย่างน้อยก็วิธีที่ทำให้รู้สึกไม่ชัดเจน หากคุณทำซ้ำค่า 3 ค่าแรกแล้ว1ยังเป็นinตัวสร้างหรือไม่ ควรทำการทดสอบเพื่อ1ทำให้เกิดการวนซ้ำและใช้ค่าทั้งหมดจนถึง1(หรือสูงถึงค่าแรก>= 1) หรือไม่


10
นี่เป็นสิ่งสำคัญที่จะต้องพูดให้ตรง ฉันคิดว่าความแตกต่างระหว่าง Python 2 และ 3 อาจนำไปสู่ความสับสนของฉันในจุดนี้ ในกรณีใด ๆ ฉันควรจะได้ตระหนักตั้งแต่rangeเป็น บริษัท จดทะเบียน (พร้อมด้วยlistและtuple) เป็นชนิดลำดับ
Rick สนับสนุนโมนิก้า

4
@RickTeachey: ที่จริงแล้วใน 2.6+ (ฉันคิดว่าอาจจะ 2.5+) xrangeเป็นลำดับเช่นกัน ดู2.7 เอกสาร ในความเป็นจริงมันเกือบจะเป็นลำดับ
abarnert

5
@RickTeachey: จริงๆแล้วฉันคิดผิด ใน 2.6-2.7 (และ 3.0-3.1) มันอ้างว่าเป็นลำดับ แต่ก็ยังเป็นเพียงลำดับเกือบ ดูคำตอบอื่นของฉัน
abarnert

2
มันไม่ใช่ตัววนซ้ำมันเป็นลำดับ (Iterable ในแง่ของ Java, IEnumerable ของ C #) - บางอย่างที่มี.__iter__()เมธอดที่จะคืนค่าตัววนซ้ำ สามารถใช้งานได้เพียงครั้งเดียวเท่านั้น
Smit Johnth

4
@ThomasAhle: เพราะrangeไม่ได้รับการตรวจสอบชนิดเมื่อมันไม่ได้เป็นจำนวนเต็มตั้งแต่มันเสมอชนิดที่เป็นไปได้มีที่เข้ากันได้กับ__eq__ intแน่นอนว่าstrเห็นได้ชัดว่าจะไม่ทำงาน แต่พวกเขาไม่ได้ต้องการสิ่งที่ชะลอตัวลงอย่างชัดเจนโดยการตรวจสอบทุกประเภทที่ไม่สามารถจะอยู่ในนั้น (และหลังจากทั้งหมดstrsubclass สามารถแทนที่__eq__และได้รับการบรรจุอยู่ในrange)
ShadowRanger

377

ใช้แหล่งกำเนิดลุค!

ใน CPython range(...).__contains__(wrapper วิธีการ) ในที่สุดจะมอบหมายให้กับการคำนวณอย่างง่ายซึ่งจะตรวจสอบว่าค่าอาจอยู่ในช่วง เหตุผลสำหรับความเร็วที่นี่คือที่เรากำลังใช้เหตุผลทางคณิตศาสตร์เกี่ยวกับขอบเขตมากกว่าย้ำโดยตรงของวัตถุช่วง เพื่ออธิบายตรรกะที่ใช้:

  1. ตรวจสอบว่าหมายเลขอยู่ระหว่างถึงstartและstopและ
  2. ตรวจสอบว่าค่าก้าวย่างไม่ได้ "ข้าม" หมายเลขของเรา

ตัวอย่างเช่น994เป็นrange(4, 1000, 2)เพราะ:

  1. 4 <= 994 < 1000และ
  2. (994 - 4) % 2 == 0.

รหัส C เต็มรูปแบบรวมอยู่ด้านล่างซึ่งเป็น verbose เพิ่มเติมเล็กน้อยเนื่องจากการจัดการหน่วยความจำและรายละเอียดการอ้างอิงการอ้างอิง แต่แนวคิดพื้นฐานอยู่ที่นั่น:

static int
range_contains_long(rangeobject *r, PyObject *ob)
{
    int cmp1, cmp2, cmp3;
    PyObject *tmp1 = NULL;
    PyObject *tmp2 = NULL;
    PyObject *zero = NULL;
    int result = -1;

    zero = PyLong_FromLong(0);
    if (zero == NULL) /* MemoryError in int(0) */
        goto end;

    /* Check if the value can possibly be in the range. */

    cmp1 = PyObject_RichCompareBool(r->step, zero, Py_GT);
    if (cmp1 == -1)
        goto end;
    if (cmp1 == 1) { /* positive steps: start <= ob < stop */
        cmp2 = PyObject_RichCompareBool(r->start, ob, Py_LE);
        cmp3 = PyObject_RichCompareBool(ob, r->stop, Py_LT);
    }
    else { /* negative steps: stop < ob <= start */
        cmp2 = PyObject_RichCompareBool(ob, r->start, Py_LE);
        cmp3 = PyObject_RichCompareBool(r->stop, ob, Py_LT);
    }

    if (cmp2 == -1 || cmp3 == -1) /* TypeError */
        goto end;
    if (cmp2 == 0 || cmp3 == 0) { /* ob outside of range */
        result = 0;
        goto end;
    }

    /* Check that the stride does not invalidate ob's membership. */
    tmp1 = PyNumber_Subtract(ob, r->start);
    if (tmp1 == NULL)
        goto end;
    tmp2 = PyNumber_Remainder(tmp1, r->step);
    if (tmp2 == NULL)
        goto end;
    /* result = ((int(ob) - start) % step) == 0 */
    result = PyObject_RichCompareBool(tmp2, zero, Py_EQ);
  end:
    Py_XDECREF(tmp1);
    Py_XDECREF(tmp2);
    Py_XDECREF(zero);
    return result;
}

static int
range_contains(rangeobject *r, PyObject *ob)
{
    if (PyLong_CheckExact(ob) || PyBool_Check(ob))
        return range_contains_long(r, ob);

    return (int)_PySequence_IterSearch((PyObject*)r, ob,
                                       PY_ITERSEARCH_CONTAINS);
}

"เนื้อ" ของความคิดที่กล่าวถึงในบรรทัด :

/* result = ((int(ob) - start) % step) == 0 */ 

ในฐานะโน้ตสุดท้าย - ดูที่range_containsฟังก์ชั่นที่ด้านล่างของตัวอย่างโค้ด หากการตรวจสอบประเภทที่แน่นอนล้มเหลวเราจะไม่ใช้อัลกอริทึมที่ฉลาดอธิบายไว้แทนที่จะกลับไปที่การค้นหาซ้ำของช่วงโดยใช้_PySequence_IterSearch! คุณสามารถตรวจสอบพฤติกรรมนี้ในล่าม (ฉันใช้ v3.5.0 ที่นี่):

>>> x, r = 1000000000000000, range(1000000000000001)
>>> class MyInt(int):
...     pass
... 
>>> x_ = MyInt(x)
>>> x in r  # calculates immediately :) 
True
>>> x_ in r  # iterates for ages.. :( 
^\Quit (core dumped)

144

เพื่อเพิ่มคำตอบของ Martijn นี่คือส่วนที่เกี่ยวข้องของแหล่งที่มา (ใน C เป็นวัตถุช่วงที่เขียนในรหัสพื้นเมือง):

static int
range_contains(rangeobject *r, PyObject *ob)
{
    if (PyLong_CheckExact(ob) || PyBool_Check(ob))
        return range_contains_long(r, ob);

    return (int)_PySequence_IterSearch((PyObject*)r, ob,
                                       PY_ITERSEARCH_CONTAINS);
}

ดังนั้นสำหรับPyLongวัตถุ (ซึ่งอยู่intใน Python 3) มันจะใช้range_contains_longฟังก์ชั่นเพื่อกำหนดผลลัพธ์ และฟังก์ชั่นนั้นตรวจสอบว่าobอยู่ในช่วงที่ระบุหรือไม่ (แม้ว่าจะดูซับซ้อนกว่าเล็กน้อยใน C)

หากไม่ใช่intวัตถุมันจะกลับไปทำซ้ำจนกว่าจะพบค่า (หรือไม่)

ตรรกะทั้งหมดสามารถแปลเป็น pseudo-Python ดังนี้:

def range_contains (rangeObj, obj):
    if isinstance(obj, int):
        return range_contains_long(rangeObj, obj)

    # default logic by iterating
    return any(obj == x for x in rangeObj)

def range_contains_long (r, num):
    if r.step > 0:
        # positive step: r.start <= num < r.stop
        cmp2 = r.start <= num
        cmp3 = num < r.stop
    else:
        # negative step: r.start >= num > r.stop
        cmp2 = num <= r.start
        cmp3 = r.stop < num

    # outside of the range boundaries
    if not cmp2 or not cmp3:
        return False

    # num must be on a valid step inside the boundaries
    return (num - r.start) % r.step == 0

11
@ChrisWesseling: ฉันคิดว่านี่เป็นข้อมูลที่ต่างกันพอสมควร (และมากพอ) ที่การแก้ไขคำตอบของ Martijn คงไม่เหมาะสมที่นี่ เป็นการเรียกร้องการตัดสิน แต่คนมักจะทำผิดพลาดโดยไม่ทำการเปลี่ยนแปลงที่รุนแรงกับคำตอบของคนอื่น
abarnert

105

หากคุณสงสัยว่าทำไมการเพิ่มประสิทธิภาพนี้จึงถูกเพิ่มเข้ามาrange.__contains__และทำไมไม่เพิ่มลงxrange.__contains__ใน 2.7:

ขั้นแรกให้เป็น Ashwini Chaudhary ค้นพบปัญหา 1766304[x]range.__contains__ถูกเปิดออกอย่างชัดเจนเพื่อเพิ่มประสิทธิภาพ แพทช์สำหรับเรื่องนี้ได้รับการยอมรับและเช็คอินสำหรับ 3.2แต่ไม่ใช่ backported ที่ 2.7 เพราะ "xrange ทำตัวแบบนี้มานานแล้วที่ฉันไม่เห็นว่ามันจะซื้อเราให้ทำการแพทช์ช้านี้" (2.7 ใกล้จะถึงจุดนั้น)

ในขณะเดียวกัน:

แต่เดิมxrangeเป็นวัตถุที่ค่อนข้างไม่ต่อเนื่อง ในฐานะที่เป็น3.1 เอกสารกล่าวว่า:

วัตถุช่วงมีพฤติกรรมน้อยมาก: รองรับเฉพาะการทำดัชนีการวนซ้ำและlenฟังก์ชัน

เรื่องนี้ไม่เป็นความจริงเลย xrangeวัตถุจริงสนับสนุนสิ่งอื่น ๆ น้อยที่มาโดยอัตโนมัติด้วยการจัดทำดัชนีและlen, *รวมทั้ง__contains__(ผ่านการค้นหาเชิงเส้น) แต่ไม่มีใครคิดว่ามันคุ้มค่าที่จะทำให้พวกเขาเรียงลำดับอย่างเต็มที่ในเวลานั้น

จากนั้นเป็นส่วนหนึ่งของการใช้งานAbstract Base Classes PEP มันเป็นเรื่องสำคัญที่จะต้องพิจารณาว่าประเภทบิวด์อินใดควรทำเครื่องหมายว่าใช้ ABCs ใดและxrange/ / rangeอ้างว่าจะนำไปใช้collections.Sequenceแม้ว่ามันจะยังคงใช้งาน ไม่มีใครสังเกตเห็นปัญหาที่จนกว่าปัญหา 9213 แพทช์สำหรับปัญหาดังกล่าวไม่เพียง แต่เพิ่มindexและcountถึง 3.2 rangeเท่านั้น แต่ยังปรับปรุงการทำงานให้เหมาะสมอีกครั้ง__contains__(ซึ่งใช้คณิตศาสตร์เดียวกันกับindexและใช้งานโดยตรงcount) ** การเปลี่ยนแปลงนี้ดำเนินไปใน 3.2 เช่นกันและไม่ได้กลับไปที่ 2.x เนื่องจาก "เป็นข้อผิดพลาดที่เพิ่มวิธีการใหม่" (ณ จุดนี้ 2.7 สถานะ rc ที่ผ่านมาแล้ว)

ดังนั้นจึงมีโอกาสสองครั้งที่จะได้รับการเพิ่มประสิทธิภาพนี้กลับไปที่ 2.7 แต่พวกเขาทั้งคู่ถูกปฏิเสธ


* ในความเป็นจริงคุณจะได้รับการทำซ้ำได้ฟรีด้วยการทำดัชนีเพียงอย่างเดียว แต่ใน 2.3 xrangeวัตถุมีตัวทำซ้ำที่กำหนดเอง

** รุ่นแรกจริง reimplemented MyIntSubclass(2) in range(5) == Falseมันและมีรายละเอียดที่ไม่ถูกต้องเช่นมันจะทำให้คุณ แต่แพทช์เวอร์ชั่นที่อัปเดตของ Daniel Stutzbach ได้กู้คืนรหัสก่อนหน้าส่วนใหญ่รวมถึงการย้อนกลับไปสู่รุ่นทั่วไปซึ่งช้า_PySequence_IterSearchกว่า pre-3.2 ที่range.__contains__ใช้โดยปริยายเมื่อไม่มีการปรับให้เหมาะสม


4
จากความคิดเห็นที่นี่: ปรับปรุงxrange.__contains__ดูเหมือนว่าพวกเขาไม่ได้ย้อนกลับไปที่ Python 2 เพียงเพื่อปล่อยให้องค์ประกอบของความประหลาดใจสำหรับผู้ใช้และมันก็สายเกินไป o_O เพิ่มcountและindex แพทช์ในภายหลัง ไฟล์ในเวลานั้น: hg.python.org/cpython/file/d599a3f2e72d/Objects/rangeobject.c
Ashwini Chaudhary

12
ผมมีความสงสัยที่น่ากลัวว่าบาง devs หลักหลามมีบางส่วนให้กับ "ความรักที่ยากลำบาก" สำหรับหลาม 2.x เพราะพวกเขาต้องการที่จะส่งเสริมให้คนที่จะสลับไปยัง python3 ไกลดีกว่า :)
Wim

4
นอกจากนี้ฉันเดิมพันว่ามันเป็นภาระอันยิ่งใหญ่ที่จะต้องเพิ่มคุณสมบัติใหม่ให้กับเวอร์ชันเก่า ลองนึกภาพถ้าคุณไปที่ Oracle และพูดว่า "ดูสิฉันใช้ Java 1.4 และฉันสมควรได้รับการแสดงออกแลมบ์ดา!
Rob Grant

2
@RickTeachey ใช่มันเป็นเพียงตัวอย่าง ถ้าฉันบอกว่า 1.7 มันจะยังคงใช้ มันเป็นความแตกต่างเชิงปริมาณไม่ใช่เชิงคุณภาพ โดยทั่วไป devs ที่ค้างชำระจะไม่สามารถสร้างเนื้อหาใหม่ที่ยอดเยี่ยมในแบบ 3.x และย้อนกลับเป็น 2.x สำหรับผู้ที่ไม่ต้องการอัปเกรด มันเป็นภาระที่ยิ่งใหญ่และไร้สาระ คุณคิดว่าเหตุผลของฉันยังคงมีอะไรผิดปกติหรือไม่?
Rob Grant

3
@RickTeachey: 2.7 อยู่ระหว่าง 3.1 และ 3.2 ไม่ใช่ประมาณ 3.3 และนั่นหมายความว่า 2.7 อยู่ใน rc เมื่อการเปลี่ยนแปลงล่าสุดเป็น 3.2 ซึ่งทำให้ความคิดเห็นข้อผิดพลาดง่ายต่อการเข้าใจ อย่างไรก็ตามฉันคิดว่าพวกเขาทำผิดพลาดเล็กน้อยในการหวนกลับ (โดยเฉพาะอย่างยิ่งสมมติว่าผู้คนจะโยกย้ายผ่าน2to3แทนที่จะใช้รหัสสองรุ่นด้วยความช่วยเหลือของไลบรารี่เช่นsixนี้ซึ่งเป็นเหตุผลว่าทำไมเราถึงได้รับสิ่งdict.viewkeysที่ไม่มีใครเคยใช้) การเปลี่ยนแปลงเล็ก ๆ น้อย ๆ ที่เพิ่งจะเกิดขึ้นในช่วง 3.2 แต่ส่วนที่ 2.7 นั้นน่าประทับใจมาก "รุ่นสุดท้าย 2.x เท่าที่เคยมีมา"
abarnert

47

คำตอบอื่น ๆ อธิบายได้ดี แต่ฉันต้องการเสนอการทดสอบอีกครั้งที่แสดงให้เห็นถึงลักษณะของวัตถุช่วง:

>>> r = range(5)
>>> for i in r:
        print(i, 2 in r, list(r))

0 True [0, 1, 2, 3, 4]
1 True [0, 1, 2, 3, 4]
2 True [0, 1, 2, 3, 4]
3 True [0, 1, 2, 3, 4]
4 True [0, 1, 2, 3, 4]

อย่างที่คุณเห็นวัตถุระยะไกลเป็นวัตถุที่จดจำช่วงของมันและสามารถใช้งานได้หลายครั้ง


27

มันคือทั้งหมดที่เกี่ยวกับวิธีการขี้เกียจที่จะประเมินผลและบางส่วนการเพิ่มประสิทธิภาพพิเศษrangeของ ค่าในช่วงไม่จำเป็นต้องคำนวณจนกระทั่งใช้งานจริงหรือเพิ่มเติมเนื่องจากการเพิ่มประสิทธิภาพพิเศษ

โดยวิธีการจำนวนเต็มของคุณไม่ได้ใหญ่พิจารณา sys.maxsize

sys.maxsize in range(sys.maxsize) ค่อนข้างเร็ว

เนื่องจากการเพิ่มประสิทธิภาพ - มันง่ายที่จะเปรียบเทียบจำนวนเต็มที่กำหนดเพียงกับช่วง min และ max

แต่:

Decimal(sys.maxsize) in range(sys.maxsize) ช้าสวย

(ในกรณีนี้ไม่มีการเพิ่มประสิทธิภาพrangeดังนั้นหากงูใหญ่ได้รับทศนิยมที่ไม่คาดคิดงูใหญ่จะเปรียบเทียบตัวเลขทั้งหมด)

คุณควรทราบรายละเอียดการใช้งาน แต่ไม่ควรวางใจเพราะอาจมีการเปลี่ยนแปลงในอนาคต


4
ระวังจำนวนเต็มลอยอย่างระมัดระวัง บนเครื่องมากที่สุดแม้ว่าfloat(sys.maxsize) != sys.maxsize) sys.maxsize-float(sys.maxsize) == 0
holdenweb

18

TL; DR

วัตถุที่ส่งคืนโดยrange()จริง ๆ แล้วเป็นrangeวัตถุ วัตถุนี้ใช้อินเทอร์เฟซตัววนซ้ำเพื่อให้คุณสามารถวนซ้ำค่าตามลำดับเช่นตัวสร้างรายการหรือทูเปิล

แต่มันยังใช้__contains__อินเตอร์เฟสซึ่งเป็นสิ่งที่ถูกเรียกเมื่อวัตถุปรากฏขึ้นทางด้านขวามือของinผู้ปฏิบัติงาน __contains__()ผลตอบแทนที่วิธีการที่boolว่าหรือไม่รายการบนซ้ายมือด้านข้างของinอยู่ในวัตถุ เนื่องจากrangeวัตถุรู้ขอบเขตและก้าวย่างของพวกเขาสิ่งนี้จึงง่ายต่อการนำไปใช้ใน O (1)


0
  1. เนื่องจากการปรับให้เหมาะสมจึงง่ายมากที่จะเปรียบเทียบจำนวนเต็มที่กำหนดด้วยช่วง min และ max
  2. เหตุผลที่ฟังก์ชั่นrange ()นั้นเร็วมากใน Python3 คือที่นี่เราใช้การให้เหตุผลเชิงคณิตศาสตร์สำหรับขอบเขตมากกว่าการวนซ้ำโดยตรงของวัตถุช่วง
  3. ดังนั้นเพื่ออธิบายเหตุผลที่นี่:
    • ตรวจสอบว่าหมายเลขอยู่ระหว่างเริ่มต้นและหยุด
    • ตรวจสอบว่าค่าความแม่นยำขั้นตอนไม่เกินจำนวนของเรา
  4. ยกตัวอย่าง997 อยู่ในช่วง (4, 1,000, 3)เพราะ:

    4 <= 997 < 1000, and (997 - 4) % 3 == 0.


1
คุณสามารถแบ่งปันแหล่งข้อมูลนั้นได้หรือไม่ แม้ว่าจะฟังดูเป็นเรื่องดี แต่ก็เป็นการดีที่จะคืนการเรียกร้องเหล่านี้ด้วยรหัสจริง
Nico Haase

ฉันคิดว่านี่เป็นตัวอย่างของการใช้งานได้ ไม่ใช่วิธีการที่แน่นอน แม้ว่าจะไม่มีการอ้างอิงใด ๆ ก็ตามมันเป็นคำแนะนำที่ดีดีพอที่จะเข้าใจว่าทำไมการตรวจสอบช่วงรวมสามารถทำได้เร็วกว่ารายการหรือสิ่งอันดับมาก
Mohammed Shareef C

0

ลองใช้ค่าx-1 in (i for i in range(x))ขนาดใหญ่xซึ่งใช้ตัวสร้างความเข้าใจเพื่อหลีกเลี่ยงการเรียกใช้การปรับให้range.__contains__เหมาะสม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.