Tại sao có 1000000000000000 trong phạm vi (1000000000000001), nhanh như vậy trong Python 3?


2116

Theo hiểu biết của tôi, range()hàm, thực sự là một loại đối tượng trong Python 3 , tạo ra nội dung của nó một cách nhanh chóng, tương tự như một trình tạo.

Đây là trường hợp, tôi đã dự kiến ​​dòng sau sẽ mất một lượng thời gian không phù hợp, bởi vì để xác định xem 1 triệu có trong phạm vi hay không, một giá trị một triệu sẽ được tạo ra:

1000000000000000 in range(1000000000000001)

Hơn nữa: có vẻ như cho dù tôi thêm bao nhiêu số 0, việc tính toán ít nhiều cũng mất cùng một khoảng thời gian (về cơ bản là tức thời).

Tôi cũng đã thử những thứ như thế này, nhưng tính toán vẫn gần như ngay lập tức:

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

Nếu tôi cố gắng thực hiện chức năng phạm vi của riêng mình, kết quả không được tốt lắm !!

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

Các range()đối tượng đang làm gì dưới mui xe làm cho nó rất nhanh?


Câu trả lời của Martijn Pieters đã được chọn vì tính hoàn chỉnh của nó, nhưng cũng thấy câu trả lời đầu tiên của abarnert cho một cuộc thảo luận tốt về ý nghĩa của rangemột chuỗi đầy đủ trong Python 3 và một số thông tin / cảnh báo về sự không nhất quán tiềm năng để __contains__tối ưu hóa chức năng trong quá trình triển khai Python . Câu trả lời khác của abarnert đi sâu vào chi tiết hơn và cung cấp các liên kết cho những người quan tâm đến lịch sử đằng sau việc tối ưu hóa trong Python 3 (và thiếu tối ưu hóa xrangetrong Python 2). Câu trả lời bằng cách chọcbởi wim cung cấp mã nguồn C và giải thích có liên quan cho những người quan tâm.


70
Lưu ý rằng đây chỉ là trường hợp nếu mục chúng tôi đang kiểm tra là một boolhoặc longloại, với các loại đối tượng khác, nó sẽ phát điên. Thử với:100000000000000.0 in range(1000000000000001)
Ashwini Chaudhary

10
Ai nói với bạn rằng đó rangelà một máy phát điện?
abarnert

7
@abarnert Tôi nghĩ rằng bản chỉnh sửa tôi thực hiện vẫn còn nguyên sự nhầm lẫn.
Rick hỗ trợ Monica

5
@AshwiniChaudhary không phải là Python2 xrangegiống với Python3range ?
Tuyệt vời nhất

28
Các xrange()đối tượng @Superbest không có __contains__phương thức, vì vậy việc kiểm tra mục phải lặp qua tất cả các mục. Thêm vào đó, có một vài thay đổi khác range(), như nó hỗ trợ cắt (một lần nữa trả về một rangeđối tượng) và bây giờ cũng có countindexcác phương thức để làm cho nó tương thích với collections.SequenceABC.
Ashwini Chaudhary

Câu trả lời:


2171

range()Đối tượng Python 3 không tạo ra số ngay lập tức; nó là một đối tượng trình tự thông minh tạo ra các số theo yêu cầu . Tất cả những gì nó chứa là các giá trị bắt đầu, dừng và bước của bạn, sau đó khi bạn lặp lại đối tượng, số nguyên tiếp theo được tính cho mỗi lần lặp.

Đối tượng cũng thực hiện object.__contains__hooktính toán nếu số của bạn là một phần trong phạm vi của nó. Tính toán là một hoạt động thời gian không đổi (gần) * . Không bao giờ cần phải quét qua tất cả các số nguyên có thể có trong phạm vi.

Từ range()tài liệu đối tượng :

Ưu điểm của rangeloại so với thông thường listhoặc tuplelà một đối tượng phạm vi sẽ luôn chiếm cùng một lượng bộ nhớ (nhỏ), bất kể kích thước của phạm vi mà nó đại diện (vì nó chỉ lưu trữ start, stopstepcác giá trị, tính toán các mục riêng lẻ và phụ khi cần thiết).

Vì vậy, ở mức tối thiểu, range()đối tượng của bạn sẽ làm:

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

Điều này vẫn còn thiếu một số điều mà một range()hỗ trợ thực sự (chẳng hạn như .index()hoặc .count()phương pháp, băm, kiểm tra đẳng thức hoặc cắt), nhưng sẽ cho bạn một ý tưởng.

Tôi cũng đơn giản hóa việc __contains__thực hiện để chỉ tập trung vào các bài kiểm tra số nguyên; nếu bạn cung cấp cho một range()đối tượng thực một giá trị không nguyên (bao gồm các lớp con int), việc quét chậm được bắt đầu để xem có khớp hay không, giống như khi bạn sử dụng kiểm tra ngăn chặn đối với danh sách tất cả các giá trị được chứa. Điều này được thực hiện để tiếp tục hỗ trợ các loại số khác chỉ xảy ra để hỗ trợ kiểm tra đẳng thức với các số nguyên nhưng dự kiến ​​cũng không hỗ trợ số học số nguyên. Xem vấn đề Python ban đầu đã thực hiện kiểm tra ngăn chặn.


* Thời gian gần như không đổi vì số nguyên Python không bị ràng buộc và do đó các phép toán cũng phát triển theo thời gian khi N phát triển, biến đây thành phép toán O (log N). Vì tất cả được thực thi trong mã C được tối ưu hóa và Python lưu trữ các giá trị nguyên trong các khối 30 bit, bạn sẽ hết bộ nhớ trước khi bạn thấy bất kỳ tác động hiệu suất nào do kích thước của các số nguyên liên quan ở đây.


58
Thực tế thú vị: bởi vì bạn có một triển khai làm việc __getitem____len__, việc __iter__thực hiện là không cần thiết.
Lucretiel

2
@Lucretiel: Trong Python 2.3 , một đặc biệt xrangeiteratorđã được thêm vào vì nó không đủ nhanh. Và sau đó ở đâu đó trong 3.x (Tôi không chắc là 3.0 hay 3.2) nó đã bị ném và họ sử dụng cùng listiteratorloại listsử dụng.
abarnert

1
Tôi sẽ định nghĩa hàm tạo def __init__(self, *start_stop_step)và phân tích cú pháp từ đó; cách các đối số được dán nhãn bây giờ là loại khó hiểu. Tuy nhiên, +1; bạn vẫn chắc chắn giải thích hành vi.
Cody Piersall

1
@CodyPiersall: Thật không may, đó là chữ ký của trình khởi tạo của lớp thực. rangecũ hơn *args(ít hơn nhiều so với argclinicAPI cho phép các hàm C-API có chữ ký Python hoàn chỉnh). Một vài chức năng cũ khác (và một vài chức năng mới hơn, như xrange, sliceitertools.islice, để thống nhất) hoạt động theo cùng một cách, nhưng đối với hầu hết các phần, Guido và phần còn lại của các nhà phát triển cốt lõi dường như đồng ý với bạn. Các tài liệu 2.0+ thậm chí mô tả rangevà bạn bè như thể chúng là quá tải kiểu C ++ thay vì hiển thị chữ ký khó hiểu thực tế.
abarnert

2
@CodyPiersall: Trên thực tế, đây là một trích dẫn từ argcliniccuộc thảo luận của Guido , khi Nick Coghlan đưa ra một cách để cho phép xác định rangerõ ràng: "Xin đừng để mọi người dễ dàng sao chép quyết định thiết kế tồi tệ nhất của tôi." Vì vậy, tôi khá chắc chắn rằng anh ấy đồng ý rằng rangenó khó hiểu như được viết.
abarnert

845

Sự hiểu lầm cơ bản ở đây là trong suy nghĩ đó rangelà một máy phát điện. Nó không thể. Trong thực tế, nó không phải là bất kỳ loại lặp.

Bạn có thể nói điều này khá dễ dàng:

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

Nếu nó là một máy phát điện, lặp đi lặp lại một lần sẽ làm cạn kiệt nó:

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

Những gì rangethực sự là, là một chuỗi, giống như một danh sách. Bạn thậm chí có thể kiểm tra điều này:

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

Điều này có nghĩa là nó phải tuân theo tất cả các quy tắc của một chuỗi:

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

Sự khác biệt giữa a rangevà a listlà a rangelà một chuỗi lười hoặc động ; nó không nhớ tất cả các giá trị của nó, nó chỉ nhớ nó start, stopstep, và tạo ra các giá trị theo yêu cầu trên __getitem__.

(Như một mặt lưu ý, nếu bạn print(iter(a)), bạn sẽ nhận thấy rằng rangesử dụng cùng listiteratorloại như list. Làm thế nào mà làm việc? Một listiteratorkhông sử dụng bất cứ điều gì đặc biệt về listngoại trừ một thực tế mà nó cung cấp một thực hiện C __getitem__, vì vậy nó hoạt động tốt cho rangequá.)


Bây giờ, không có gì nói rằng đó Sequence.__contains__phải là thời gian không đổi trong thực tế, đối với các ví dụ rõ ràng về các chuỗi như list, thì không. Nhưng không có gì nói rằng nó không thể. Và thực hiện dễ dàng hơn khi range.__contains__chỉ kiểm tra nó bằng toán học ( (val - start) % stepnhưng với một số phức tạp hơn để xử lý các bước tiêu cực) hơn là thực sự tạo và kiểm tra tất cả các giá trị, vậy tại sao không nên làm theo cách tốt hơn?

Nhưng dường như không có bất cứ điều gì trong ngôn ngữ đảm bảo điều này sẽ xảy ra. Như Ashwini Chaudhari chỉ ra, nếu bạn cho nó một giá trị không tách rời, thay vì chuyển đổi thành số nguyên và thực hiện kiểm tra toán học, nó sẽ quay trở lại lặp lại tất cả các giá trị và so sánh từng giá trị một. Và chỉ vì các phiên bản CPython 3.2+ và PyPy 3.x có chứa tối ưu hóa này, và đó là một ý tưởng tốt rõ ràng và dễ thực hiện, không có lý do gì mà IronPython hoặc NewKickAssPython 3.x không thể bỏ qua. (Và trên thực tế CPython 3.0-3.1 không bao gồm nó.)


Nếu rangethực sự là một máy phát điện, my_crappy_rangethì sẽ không có ý nghĩa gì khi thử nghiệm __contains__theo cách này, hoặc ít nhất là cách nó có ý nghĩa sẽ không rõ ràng. Nếu bạn đã lặp lại 3 giá trị đầu tiên, 1vẫn là intrình tạo? Có nên kiểm tra 1nguyên nhân để lặp lại và tiêu thụ tất cả các giá trị lên đến 1(hoặc lên đến giá trị đầu tiên >= 1) không?


10
Đây là một điều khá quan trọng để có được thẳng. Tôi cho rằng sự khác biệt giữa Python 2 và 3 có thể dẫn đến sự nhầm lẫn của tôi về điểm này. Trong mọi trường hợp, tôi nên nhận ra rangeđược liệt kê (cùng với listtuple) dưới dạng một chuỗi .
Rick hỗ trợ Monica

4
@RickTeachey: Thật ra, trong 2.6+ (tôi nghĩ; có thể là 2,5+), xrangecũng là một chuỗi. Xem 2.7 tài liệu . Trong thực tế, nó luôn luôn là một chuỗi gần như.
abarnert

5
@RickTeachey: Thật ra, tôi đã sai; trong 2.6-2.7 (và 3.0-3.1), nó tuyên bố là một chuỗi, nhưng nó vẫn chỉ là một chuỗi gần như. Xem câu trả lời khác của tôi.
abarnert

2
Nó không phải là một trình vòng lặp, đó là một trình tự (Có thể lặp theo thuật ngữ Java, IEnumerable của C #) - một cái gì đó với một .__iter__()phương thức sẽ trả về một trình vòng lặp. Đến lượt nó chỉ có thể được sử dụng một lần.
Smit Johnth

4
@ThomasAhle: Bởi vì rangekhông kiểm tra các loại khi nó không phải là số nguyên, vì luôn có khả năng một loại có __eq__tương thích với int. Chắc chắn, strrõ ràng sẽ không hoạt động, nhưng họ không muốn làm chậm mọi thứ bằng cách kiểm tra rõ ràng tất cả các loại không thể có trong đó (và sau tất cả, một strlớp con có thể ghi đè __eq__và được chứa trong range).
ShadowRanger

377

Sử dụng nguồn , Luke!

Trong CPython, range(...).__contains__(trình bao bọc phương thức) cuối cùng sẽ ủy quyền cho một phép tính đơn giản để kiểm tra xem giá trị có thể nằm trong phạm vi hay không. Lý do cho tốc độ ở đây là chúng tôi sử dụng lý luận toán học về giới hạn, thay vì lặp lại trực tiếp của đối tượng phạm vi . Để giải thích logic được sử dụng:

  1. Kiểm tra xem số nằm giữa startstop
  2. Kiểm tra xem giá trị sải chân không "bước qua" số của chúng tôi.

Ví dụ: 994range(4, 1000, 2)vì:

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

Mã C đầy đủ được bao gồm bên dưới, dài hơn một chút vì quản lý bộ nhớ và chi tiết đếm tham chiếu, nhưng ý tưởng cơ bản là có:

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

"Thịt" của ý tưởng được đề cập trong dòng :

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

Như một lưu ý cuối cùng - hãy xem range_containschức năng ở dưới cùng của đoạn mã. Nếu kiểm tra loại chính xác thất bại thì chúng ta không sử dụng thuật toán thông minh được mô tả, thay vào đó, quay lại tìm kiếm lặp lại ngu ngốc của phạm vi bằng cách sử dụng _PySequence_IterSearch! Bạn có thể kiểm tra hành vi này trong trình thông dịch (Tôi đang sử dụng v3.5.0 tại đây):

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

Để thêm vào câu trả lời của Martijn, đây là phần có liên quan của nguồn (bằng C, vì đối tượng phạm vi được viết bằng mã gố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);
}

Vì vậy, đối với PyLongcác đối tượng ( inttrong Python 3), nó sẽ sử dụng range_contains_longhàm để xác định kết quả. Và chức năng đó về cơ bản sẽ kiểm tra xem obcó nằm trong phạm vi được chỉ định không (mặc dù nó có vẻ phức tạp hơn một chút trong C).

Nếu nó không phải là một intđối tượng, nó sẽ quay trở lại lặp đi lặp lại cho đến khi tìm thấy giá trị (hoặc không).

Toàn bộ logic có thể được dịch sang giả Python như thế này:

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: Tôi nghĩ rằng đây là thông tin đủ khác nhau (và đủ thông tin) rằng việc chỉnh sửa câu trả lời của Martijn sẽ không phù hợp ở đây. Đó là một lời kêu gọi phán xét, nhưng mọi người thường sai lầm khi không thực hiện những thay đổi mạnh mẽ đối với câu trả lời của người khác.
abarnert

105

Nếu bạn tự hỏi tại sao tối ưu hóa này được thêm vào range.__contains__và tại sao nó không được thêm xrange.__contains__vào 2.7:

Đầu tiên, như Ashwini Chaudhary phát hiện ra, vấn đề 1766304 đã được mở một cách rõ ràng để tối ưu hóa [x]range.__contains__. Một bản vá cho điều này đã được chấp nhận và kiểm tra trong 3.2 , nhưng không được đưa vào 2.7 vì "xrange đã hành xử như vậy trong một thời gian dài đến nỗi tôi không thấy những gì nó mua cho chúng tôi để cam kết bản vá này muộn." (2.7 đã gần hết thời điểm đó.)

Trong khi đó:

Ban đầu, xrangelà một đối tượng không hoàn toàn trình tự. Như các tài liệu 3.1 nói:

Các đối tượng phạm vi có rất ít hành vi: chúng chỉ hỗ trợ lập chỉ mục, lặp và lenhàm.

Điều này không hoàn toàn đúng; một xrangeđối tượng thực sự được hỗ trợ một vài điều khác đi kèm với tự động lập chỉ mục và len, * bao gồm __contains__(thông qua tìm kiếm tuyến tính). Nhưng không ai nghĩ rằng nó là giá trị làm cho họ đầy đủ trình tự tại thời điểm đó.

Sau đó, như là một phần của việc triển khai PEP của các lớp cơ sở trừu tượng , điều quan trọng là tìm ra loại dựng sẵn nào sẽ được đánh dấu là triển khai ABC nào và xrange/ rangetuyên bố thực hiện collections.Sequence, mặc dù nó vẫn chỉ xử lý cùng một "hành vi rất ít". Không ai nhận thấy vấn đề đó cho đến khi vấn đề 9213 . Bản vá cho vấn đề đó không chỉ được thêm vào indexcountđến 3,2 range, nó còn hoạt động lại được tối ưu hóa __contains__(chia sẻ cùng một phép toán indexvà được sử dụng trực tiếp bởi count). ** Thay đổi này cũng được áp dụng cho 3.2 và không được đưa vào 2.x, vì "đó là một lỗi bổ sung các phương thức mới". (Tại thời điểm này, 2.7 đã qua trạng thái RC.)

Vì vậy, có hai cơ hội để đưa tối ưu hóa này trở thành 2.7, nhưng cả hai đều bị từ chối.


* Trong thực tế, bạn thậm chí có được phép lặp miễn phí khi lập chỉ mục một mình, nhưng trong 2,3 xrange đối tượng có một trình vòng lặp tùy chỉnh.

** Phiên bản đầu tiên thực sự thực hiện lại nó, và có các chi tiết sai, ví dụ, nó sẽ cung cấp cho bạn MyIntSubclass(2) in range(5) == False. Nhưng phiên bản cập nhật của bản vá của Daniel Stutzbach đã khôi phục hầu hết các mã trước đó, bao gồm cả dự phòng cho bản chung, chậm _PySequence_IterSearchmà trước 3.2 range.__contains__được sử dụng khi tối ưu hóa không áp dụng.


4
Từ các nhận xét ở đây: cải thiệnxrange.__contains__ , có vẻ như họ đã không đưa nó vào Python 2 chỉ để lại một yếu tố gây bất ngờ cho người dùng và đã quá muộn o_O. Các bản vácount và đã được thêm vào sau này. Tập tin tại thời điểm đó: hg.python.org/cpython/file/d599a3f2e72d/Objects/rangeobject.cindex
Ashwini Chaudhary

12
Tôi có một nghi ngờ nham hiểm mà một số nhà phát triển cốt lõi python là một phần để "tình yêu khó khăn" cho python 2.x vì họ muốn khuyến khích mọi người chuyển sang python3 xa vượt trội :)
Wim

4
Ngoài ra tôi cá rằng đó là một gánh nặng lớn khi phải thêm các tính năng mới cho các phiên bản cũ. Hãy tưởng tượng nếu bạn đã đến Oracle và nói: "Hãy nhìn xem, tôi đang dùng Java 1.4 và tôi xứng đáng với các biểu thức lambda! Backport chúng chẳng vì gì cả."
Rob Grant

2
@RickTeachey vâng đó chỉ là một ví dụ. Nếu tôi nói 1.7 nó vẫn sẽ được áp dụng. Đó là một sự khác biệt định lượng không định tính. Về cơ bản, các nhà phát triển (chưa thanh toán) không thể mãi mãi tạo ra những thứ mới tuyệt vời trong 3.x và chuyển nó thành 2.x cho những ai không muốn nâng cấp. Đó là một gánh nặng to lớn và lố bịch. Bạn có nghĩ rằng vẫn còn điều gì đó sai với lý luận của tôi?
Rob Grant

3
@RickTeachey: 2.7 nằm trong khoảng từ 3.1 đến 3.2, không phải khoảng 3.3. Và điều đó có nghĩa là 2.7 đã có trong RC khi những thay đổi cuối cùng thành 3.2 xuất hiện, điều này làm cho các bình luận lỗi dễ hiểu hơn. Dù sao, tôi nghĩ rằng họ đã mắc một vài lỗi khi nhìn lại (đặc biệt là giả sử mọi người sẽ di chuyển qua 2to3thay vì thông qua mã phiên bản kép với sự trợ giúp của các thư viện như six, đó là lý do tại sao chúng tôi có những thứ dict.viewkeysmà không ai sẽ sử dụng), và đã có một vài thay đổi xuất hiện quá muộn trong 3.2, nhưng đối với hầu hết phần 2.7 là một bản phát hành "lần cuối 2.x" khá ấn tượng.
abarnert

47

Các câu trả lời khác đã giải thích điều đó rất tốt, nhưng tôi muốn đưa ra một thử nghiệm khác minh họa bản chất của các đối tượng phạm vi:

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

Như bạn có thể thấy, một đối tượng phạm vi là một đối tượng ghi nhớ phạm vi của nó và có thể được sử dụng nhiều lần (ngay cả khi lặp qua nó), không chỉ là trình tạo một lần.


27

Đó là tất cả về một cách tiếp cận lười biếng để đánh giá và một số phụ tối ưu hóa của range. Các giá trị trong phạm vi không cần phải được tính toán cho đến khi sử dụng thực sự, hoặc thậm chí xa hơn do tối ưu hóa thêm.

Nhân tiện, số nguyên của bạn không lớn như vậy, hãy xem xét sys.maxsize

sys.maxsize in range(sys.maxsize) khá nhanh

do tối ưu hóa - thật dễ dàng để so sánh số nguyên đã cho chỉ với tối thiểu và tối đa của phạm vi.

nhưng:

Decimal(sys.maxsize) in range(sys.maxsize) là khá chậm .

(trong trường hợp này, không có tối ưu hóa range, vì vậy nếu python nhận được số thập phân bất ngờ, python sẽ so sánh tất cả các số)

Bạn nên biết về một chi tiết thực hiện nhưng không nên dựa vào, vì điều này có thể thay đổi trong tương lai.


4
Cẩn thận nổi số nguyên lớn. Trên hầu hết các máy, float(sys.maxsize) != sys.maxsize)mặc dù sys.maxsize-float(sys.maxsize) == 0.
Holdenweb

18

TL; DR

Đối tượng được trả về range()thực sự là một rangeđối tượng. Đối tượng này thực hiện giao diện iterator để bạn có thể lặp lại các giá trị của nó một cách tuần tự, giống như một trình tạo, danh sách hoặc tuple.

Nhưng nó cũng thực hiện __contains__giao diện thực sự là cái được gọi khi một đối tượng xuất hiện ở phía bên phải của intoán tử. Các __contains__()trở về phương pháp một boolhay không các mục trên trái tay bên inlà trong đối tượng. Vì rangecác đối tượng biết giới hạn và sải chân của chúng, điều này rất dễ thực hiện trong O (1).


0
  1. Do tối ưu hóa, rất dễ dàng để so sánh các số nguyên đã cho chỉ với phạm vi tối thiểu và tối đa.
  2. Lý do hàm phạm vi () quá nhanh trong Python3 là ở đây chúng ta sử dụng lý luận toán học cho các giới hạn, thay vì lặp lại trực tiếp của đối tượng phạm vi.
  3. Vì vậy, để giải thích logic ở đây:
    • Kiểm tra xem số nằm giữa điểm bắt đầu và điểm dừng.
    • Kiểm tra xem giá trị chính xác của bước không vượt quá số của chúng tôi.
  4. Lấy một ví dụ, 997 nằm trong phạm vi (4, 1000, 3) vì:

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


1
Bạn có thể chia sẻ nguồn cho điều đó? Ngay cả khi điều đó nghe có vẻ hợp pháp, thì vẫn nên ủng hộ những tuyên bố này bằng mã thực tế
Nico Haase

Tôi nghĩ rằng đây là một ví dụ về nó có thể được thực hiện. Không phải là cách chính xác nó được thực hiện. Mặc dù không có tài liệu tham khảo nào cung cấp nhưng đó là gợi ý tốt đủ tốt để hiểu tại sao việc kiểm tra phạm vi có thể nhanh hơn nhiều so với danh sách hoặc bộ dữ liệu
Mohammed Shareef C

0

Hãy thử x-1 in (i for i in range(x))các xgiá trị lớn , sử dụng mức độ hiểu của trình tạo để tránh yêu cầu range.__contains__tối ưu hóa.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.