Python 3에서 "100000000000000000000001"이 왜 그렇게 빠른가?
이 나의 다.range()
Python 3에서 실제로 개체 유형인 함수는 생성기와 유사하게 콘텐츠를 즉석에서 생성한다.
이 경우, 나는 다음 라인이 1,000조원이 범위 내에 있는지 여부를 판단하기 위해서는 1,000조개의 값이 생성되어야 하기 때문에 과도한 시간이 소요될 것으로 예상했을 것이다.
1_000_000_000_000_000 in range(1_000_000_000_000_001)
더욱이, 내가 아무리 0을 더해도, 계산은 거의 같은 시간(기본적으로 즉각적)을 필요로 하는 것 같다.
나 또한 이런 것들을 시도해 보았지만, 계산은 여전히 거의 즉각적이다.
# count by tens
1_000_000_000_000_000_000_000 in range(0,1_000_000_000_000_000_000_001,10)
나만의 레인지 기능을 구현하려고 하면 결과가 별로 좋지 않아!
def my_crappy_range(N):
i = 0
while i < N:
yield i
i += 1
return
무엇 때문에 그러십니까?range()
후드 아래에서 일을 하는 걸 그렇게 빨리 하는 걸 반대한다고?
마티진 피에테르의 대답은 그 완전성 때문에 선택되었지만, 또한 그것이 무엇을 위한 것인지에 대한 좋은 논의를 위한 첫 번째 대답을 보게 된다.range
Python 3의 전체 시퀀스와 에 대한 잠재적 불일치에 대한 일부 정보/경고__contains__
Python 구현 전반에 걸친 기능 최적화. 에바너트의 다른 답변은 좀 더 상세하게 설명되며 Python 3의 최적화 이면에 있는 이력에 관심이 있는 사람들을 위한 링크를 제공한다(그리고 최적화 부족).xrange
Python 2).쿡쿡 찌르고 윔으로 대답하면 관심 있는 사람에게 관련 C 소스 코드와 설명이 제공된다.
파이톤 3range()
개체는 즉시 숫자를 생산하지 않는다; 그것은 요구 시 숫자를 생산하는 스마트 시퀀스 개체다.여기에 포함된 모든 것은 시작, 중지 및 단계 값이며, 그 다음 정수를 개체 위에 반복할 때 각 반복이 계산된다.
물체는 또한 후크를 구현하고, 당신의 번호가 그 범위의 일부인지 계산한다.계산은 일정한 시간 연산이다범위 내에서 가능한 모든 정수를 스캔할 필요는 없다.
의
range
규칙적으로 타이핑하다.list
또는tuple
범위 객체는 (그것이 단지 저장하기 때문에) 나타내는 범위의 크기와 상관없이 항상 같은 (작은) 메모리 양을 차지한다는 것이다.start
stop
그리고step
값, 필요에 따라 개별 항목 및 하위 범위 계산).
그러니까 최소한 당신의.range()
객체에서 수행할 수 있는 작업:
class my_range:
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('my_range object index out of range')
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()
방법, 해싱, 평등 테스트 또는 슬라이싱) 그러나 아이디어를 제공해야 한다.
나는 또한 단순화했다.__contains__
정수 테스트에만 집중하기 위한 구현; 실제를 제공하는 경우range()
비반복적 값(하위 클래스 포함)을 대상으로 한다.int
() 포함된 모든 값 목록에 대해 격납용기 테스트를 사용하는 것처럼, 일치하는 것이 있는지 확인하기 위해 저속 스캔을 시작한다.이는 정수로 동등성 시험을 지원할 때 발생하지만 정수 산술도 지원할 것으로 예상되지 않는 다른 숫자 유형을 계속 지원하기 위해 수행되었다.격납 테스트를 구현한 원래 Python 문제를 참조하십시오.
* Python 정수는 한이 없기 때문에 거의 일정한 시간이고, 따라서 수학 연산 또한 N이 증가함에 따라 시간에 따라 증가하므로 O(log N) 연산이다.최적화된 C 코드로 모두 실행되며 파이썬은 정수 값을 30비트 청크로 저장하기 때문에 여기에 포함된 정수의 크기로 인해 성능에 미치는 영향을 보기 전에 메모리가 부족할 수 있다.
여기서 근본적인 오해는 다음과 같은 생각에서 비롯된다.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
의 .range
그리고 alist
저것은 a이다range
게으르거나 역동적인 연속이다; 그것은 그것의 모든 가치를 기억하지 못하고 단지 그것의 가치를 기억한다.start
stop
그리고step
, 그리고 온 디맨드 가치 창출__getitem__
.
(부기사항으로, 만약 당신이 한다면.print(iter(a))
에 주목하게 될 것이다.range
같은 것을 사용하다listiterator
로 타이핑하다.list
어떻게 되는가?listiterator
에 대해 특별한 것을 사용하지 않다list
는 C로 시행하고 있다.__getitem__
, 그래서 그것은 잘 작동한다.range
역시)
이제, 그 때, 그 라고 하는 Sequence.__contains__
항상 일정한 시간이어야 한다. 사실 다음과 같은 명확한 시퀀스의 예에 대해서는list
절대 아닙니다.그러나 있을 수 없는 일이라고 하는 것은 아무것도 없다.그리고 구현이 더 용이하다.range.__contains__
그냥 수학적으로 확인하다 ((val - start) % step
, 그러나 부정적인 단계를 다루기 위한 약간의 추가적인 복잡성으로) 모든 값을 실제로 생성하고 테스트하는 것보다 더 나은 방법으로 하지 않는 이유는 무엇인가?
그러나 언어에는 이런 일이 일어날 것이라고 보장해 주는 것은 아무것도 없는 것 같다.아슈히니 차우드하리가 지적하듯이 정수로 변환하여 수학적 테스트를 하는 대신 비통합적 값을 부여하면 모든 값을 반복하여 하나하나 비교하는 것으로 되돌아간다.그리고 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
)?
소스를 사용해, 루크!
CPython은range(...).__contains__
(method wrapper)는 결국 값이 범위 내에 있을 수 있는지 확인하는 간단한 계산을 위임한다.여기서 속도를 내는 이유는 우리가 범위 객체를 직접 반복하기 보다는 한계에 대한 수학적 추론을 사용하고 있기 때문이다.사용된 논리를 설명하려면:
- 번호가 다음 사이에 있는지 확인하십시오.
start
그리고stop
그리고 - 보폭 값이 우리 번호를 "넘어가지" 않는지 확인하십시오.
예를 들어,994
에 있다range(4, 1000, 2)
이유:
4 <= 994 < 1000
그리고(994 - 4) % 2 == 0
.
전체 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);
}
아이디어의 "고기"는 다음 줄에서 언급된다.
/* result = ((int(ob) - start) % step) == 0 */
마지막 메모로 - 다음 항목을 보십시오.range_contains
코드 조각의 하단에서 기능한다.정확한 유형 확인이 실패할 경우, 우리는 설명한 영리한 알고리즘을 사용하지 않고 대신 범위를 다시 단순 반복 검색으로 되돌린다._PySequence_IterSearch
에서 이 할 수 .5있다 통역기에서 이 동작을 확인할 수 있다(여기서 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)
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
3에서는 Python 3)을 한다.range_contains_long
결과를 결정하는 기능.그리고 그 함수는 본질적으로ob
지정된 범위 안에 있다(C에서 좀 더 복잡해 보이지만).
만약 그게 아니라면.int
값을 찾을 때까지(또는 찾을 수 없을 때까지) 반복으로 되돌아간다.
이 모든 논리는 다음과 같이 사이비 피톤으로 번역될 수 있다.
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
이 최적화가 왜 에 추가되었는지 궁금하다면range.__contains__
, 그리고 왜 그것이 추가되지 않았는지.xrange.__contains__
2.:
첫째, 애쉬위니 차우드하리가 발견한 바와 같이, 1766304호를 명시적으로 개방하여 최적화하였다.[x]range.__contains__
. 3.2에 대한 패치가 승인되어 체크인되었지만, "이 때문에 2.7로 백포팅되지 않았다.xrange
오랫동안 이렇게 행동해 왔기 때문에 이렇게 늦게 패치를 저지르면 우리가 무엇을 얻을 수 있는지 모르겠다."(2.7은 거의그 시점에 나왔다.)오랫동안 이렇게 행동해 왔기 때문에 이렇게 늦게 패치를 저지르면 우리가 무엇을 얻을 수 있는지 모르겠다."(2.7은 거의그시점에 나왔다.)
한편:
원raxrange
눈에 띄지 않는 물건이었어3.1 문서에 따르면:
범위 객체는 동작이 거의 없다. 즉 인덱싱, 반복 및 데이터만 지원한다.
len
기능을 발휘하다
이것은 그다지 사실이 아니었다.xrange
개체는 실제로 인덱싱과 함께 자동으로 제공되는 몇 가지 다른 것들을 지원했다.len
,* 포함__contains__
(선형 검색을 통해).하지만 아무도 그 당시에는 그것들을 완전한 순서에 맞게 만들 가치가 있다고 생각하지 않았다.
그 후, 추상 베이스 클래스 PEP의 이행의 일환으로, 어떤 빌트인 타입이 어떤 ABC를 구현하는 것으로 표시되어야 하는가를 파악하는 것이 중요했다.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
객체에 사용자 정의 반복기가 있음.
** 첫 번째 버전은 실제로 이를 재조명하고 세부사항을 잘못 이해함(예: 다음과 같은 경우)MyIntSubclass(2) in range(5) == False
그러나 Daniel Stutzbach의 업데이트 버전 패치는 일반적이고 느린 패치를 포함하여 이전 코드 대부분을 복구했다._PySequence_IterSearch
저 3.2 이전range.__contains__
최적화가 적용되지 않을 때 암묵적으로 사용하고 있었다.
다른 답은 이미 잘 설명했지만, 범위 객체의 특성을 보여주는 또 다른 실험을 제안하고자 한다.
>>> 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]
a는range
객체는 그 범위를 기억하는 물체로, 일회성 발전기만이 아니라 여러 번(반복하는 동안에도) 사용할 수 있다.
그것은 모두 평가에 대한 게으른 접근과 약간의 추가적인 최적화에 관한 것이다.range
될 더 계산할가 없다 범위의 값은 실제 사용이 될 때까지 또는 추가 최적화로 인해 더 이상 계산할 필요가 없다.
그런데, 당신의 정수는 그렇게 크지 않다, 고려해봐.sys.maxsize
sys.maxsize in range(sys.maxsize)
꽤 빠르다
최적화 때문에 주어진 정수를 최소 및 최대 범위의 정수와 비교하기가 쉽다.
그러나:
Decimal(sys.maxsize) in range(sys.maxsize)
꽤 느리다.
(이 에, 에 가가 없이 다다)에는.range
, 그래서 만약 python이 예기치 않은 소수점을 받는다면 python은 모든 숫자를 비교할 것이다.)
구현 세부사항을 알고 있어야 하지만 나중에 변경될 수 있기 때문에 신뢰할 수 없다.
TL;DR
반환된에 range()
사실은 a이다.range
③의 ③기하다 이 그 할 수 이 개체는 발생기, 목록 또는 튜플처럼 순차적으로 값을 반복할 수 있도록 반복기 인터페이스를 구현한다.
그러나 그것은 또한 실제로 물체가 오른쪽에 나타날 때 호출되는 인터페이스도 구현한다.in
교환원의그__contains__()
방법은 a를 반환한다.bool
이 의의의 의 여부.in
목적지에 있다.이후range
물체는 그 한계와 속도를 알고 있다. 이것은 O(1)에서 구현하기 매우 쉽다.
- 최적화 때문에 주어진 정수를 최소 및 최대 범위만으로 비교하는 것은 매우 쉽다.
- Python3에서 레인지() 함수가 이렇게 빠른 이유는 여기서 레인지 객체의 직접 반복이 아닌 경계에 대한 수학적 추론을 사용하기 때문이다.
- 여기서 논리를 설명하려면:
- 번호가 시작과 중지 사이에 있는지 확인하십시오.
- 스텝 정밀도가 우리 번호를 넘지 않는지 확인해봐.
예를 들어, 997은 다음과 같은 이유로 범위(4, 1000, 3)에 있다.
4 <= 997 < 1000, and (997 - 4) % 3 == 0.
해보다x-1 in (i for i in range(x))
대체로x
제너레이터의 이해를 사용하여 호출되지 않는 값range.__contains__
최적화
TLDR;range
그 물체가 거기에 있는지 매우 쉽게 계산할 수 있는 산술 시리즈다.만약 그것이 정말 빨리 목록화된다면 그것은 그것의 지수까지 얻을 수 있을 것이다.
'programing' 카테고리의 다른 글
경로에 @를 추가하는 방법 nuxtjs.예: example.com/@properties (0) | 2022.03.27 |
---|---|
기본 리액션에서 텍스트에 스트라이크를 추가하는 방법? (0) | 2022.03.27 |
VS Code를 사용하여 .vue 파일의 Typecript를 사용하는 방법 (0) | 2022.03.27 |
Vue.js - 알 수 없는 사용자 지정 요소: (0) | 2022.03.27 |
파이톤 3의 로_input()과 인풋()의 차이점은 무엇일까. (0) | 2022.03.27 |