파이썬에서 정규 표현식으로 컴파일을 사용하면 어떤 이점이 있습니까?
h = re.compile('hello')
h.match('hello world')
vs
re.match('hello', 'hello world')
답변
컴파일 된 정규 표현식을 1000 번 실행하는 것과 비교하여 많은 경험을 쌓았으며 즉각적인 컴파일과는 차이가 없었습니다. 분명히, 이것은 일화, 그리고 확실히 좋은 인수 에 대한 컴파일,하지만 난 무시할 수의 차이를 발견했습니다.
편집 : 실제 Python 2.5 라이브러리 코드를 간략히 살펴보면 파이썬이 내부적으로 re.match()
정규 표현식을 컴파일하고 캐쉬 할 때마다 (콜을 포함하여 ) 정규 표현식을 컴파일하므로 정규 표현식이 컴파일 될 때만 변경됩니다. 캐시를 확인하는 데 걸리는 시간 (내부 dict
유형 의 키 조회)만으로도 많은 시간을 절약 할 수 있습니다 .
모듈 re.py에서 (의견은 내 것입니다) :
def match(pattern, string, flags=0):
return _compile(pattern, flags).match(string)
def _compile(*key):
# Does cache check at top of function
cachekey = (type(key[0]),) + key
p = _cache.get(cachekey)
if p is not None: return p
# ...
# Does actual compilation on cache miss
# ...
# Caches compiled regex
if len(_cache) >= _MAXCACHE:
_cache.clear()
_cache[cachekey] = p
return p
나는 종종 정규 표현식을 미리 컴파일하지만 예상되는 성능 향상이 아니라 재사용 가능한 멋진 이름에만 바인딩합니다.
답변
저에게있어 가장 큰 이점 re.compile
은 정규식 정의를 사용과 분리 할 수 있다는 것입니다.
0|[1-9][0-9]*
(제로 0이없는 10 진 정수) 와 같은 간단한 표현조차도 다시 입력하지 않고 오타가 있는지 확인한 다음 나중에 디버깅을 시작할 때 오타가 있는지 다시 확인해야 할 정도로 복잡 할 수 있습니다 . 또한 num 또는 num_b10과 같은 변수 이름을 사용하는 것이 좋습니다 0|[1-9][0-9]*
.
문자열을 저장하고 다시 일치시킬 수 있습니다. 그러나 읽기 쉽지 않습니다 .
num = "..."
# then, much later:
m = re.match(num, input)
컴파일 대 :
num = re.compile("...")
# then, much later:
m = num.match(input)
꽤 가깝지만 두 번째 줄의 마지막 줄은 반복해서 사용할 때 더 자연스럽고 단순합니다.
답변
FWIW :
$ python -m timeit -s "import re" "re.match('hello', 'hello world')"
100000 loops, best of 3: 3.82 usec per loop
$ python -m timeit -s "import re; h=re.compile('hello')" "h.match('hello world')"
1000000 loops, best of 3: 1.26 usec per loop
따라서 동일한 정규 표현식을 많이 사용 하려는 경우 re.compile
(특히 복잡한 정규 표현식) 할 가치가 있습니다 .
조기 최적화에 대한 표준 주장이 적용되지만 정규 re.compile
표현식이 성능 병목 현상이 될 것으로 의심되는 경우 사용하여 명확성 / 직선 성이 크게 손실되지는 않습니다 .
최신 정보:
Python 3.6 (위의 타이밍이 Python 2.x를 사용하여 수행되었다고 생각합니다) 및 2018 하드웨어 (MacBook Pro)에서 이제 다음 타이밍을 얻습니다.
% python -m timeit -s "import re" "re.match('hello', 'hello world')"
1000000 loops, best of 3: 0.661 usec per loop
% python -m timeit -s "import re; h=re.compile('hello')" "h.match('hello world')"
1000000 loops, best of 3: 0.285 usec per loop
% python -m timeit -s "import re" "h=re.compile('hello'); h.match('hello world')"
1000000 loops, best of 3: 0.65 usec per loop
% python --version
Python 3.6.5 :: Anaconda, Inc.
나는 또한 re.match(x, ...)
문자 그대로 [거의] 동등한 re.compile(x).match(...)
, 즉 컴파일 된 표현의 비하인드 캐싱이 발생하지 않는 것으로 보이는 사례 (마지막 두 실행 사이의 인용 부호 차이에 주목)를 추가했습니다 .
답변
간단한 테스트 사례는 다음과 같습니다.
~$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 're.match("[0-9]{3}-[0-9]{3}-[0-9]{4}", "123-123-1234")'; done
1 loops, best of 3: 3.1 usec per loop
10 loops, best of 3: 2.41 usec per loop
100 loops, best of 3: 2.24 usec per loop
1000 loops, best of 3: 2.21 usec per loop
10000 loops, best of 3: 2.23 usec per loop
100000 loops, best of 3: 2.24 usec per loop
1000000 loops, best of 3: 2.31 usec per loop
re.compile로 :
~$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 'r = re.compile("[0-9]{3}-[0-9]{3}-[0-9]{4}")' 'r.match("123-123-1234")'; done
1 loops, best of 3: 1.91 usec per loop
10 loops, best of 3: 0.691 usec per loop
100 loops, best of 3: 0.701 usec per loop
1000 loops, best of 3: 0.684 usec per loop
10000 loops, best of 3: 0.682 usec per loop
100000 loops, best of 3: 0.694 usec per loop
1000000 loops, best of 3: 0.702 usec per loop
따라서 한 번만 일치 하더라도이 간단한 경우 컴파일이 더 빠릅니다 .
답변
방금 직접 시도했습니다. 문자열에서 숫자를 파싱하고 합산하는 간단한 경우 컴파일 된 정규식 객체를 사용하는 것이 re
메서드 를 사용하는 것보다 약 2 배 빠릅니다 .
다른 사람들이 지적했듯이을 re
포함한 메소드 re.compile
는 이전에 컴파일 된 표현식의 캐시에서 정규 표현식 문자열을 찾습니다. 따라서 일반적인 경우, re
메소드 사용에 따른 추가 비용 은 단순히 캐시 조회 비용입니다.
그러나 코드를 검사 하면 캐시가 100 식으로 제한됩니다. 캐시를 오버플로하는 것이 얼마나 고통 스럽습니까? 코드에는 정규식 컴파일러에 대한 내부 인터페이스가 포함되어 있습니다 re.sre_compile.compile
. 호출하면 캐시를 무시합니다. 와 같은 기본 정규 표현식의 경우 약 2 배 느리게 나타납니다 r'\w+\s+([0-9_]+)\s+\w*'
.
내 테스트는 다음과 같습니다.
#!/usr/bin/env python
import re
import time
def timed(func):
def wrapper(*args):
t = time.time()
result = func(*args)
t = time.time() - t
print '%s took %.3f seconds.' % (func.func_name, t)
return result
return wrapper
regularExpression = r'\w+\s+([0-9_]+)\s+\w*'
testString = "average 2 never"
@timed
def noncompiled():
a = 0
for x in xrange(1000000):
m = re.match(regularExpression, testString)
a += int(m.group(1))
return a
@timed
def compiled():
a = 0
rgx = re.compile(regularExpression)
for x in xrange(1000000):
m = rgx.match(testString)
a += int(m.group(1))
return a
@timed
def reallyCompiled():
a = 0
rgx = re.sre_compile.compile(regularExpression)
for x in xrange(1000000):
m = rgx.match(testString)
a += int(m.group(1))
return a
@timed
def compiledInLoop():
a = 0
for x in xrange(1000000):
rgx = re.compile(regularExpression)
m = rgx.match(testString)
a += int(m.group(1))
return a
@timed
def reallyCompiledInLoop():
a = 0
for x in xrange(10000):
rgx = re.sre_compile.compile(regularExpression)
m = rgx.match(testString)
a += int(m.group(1))
return a
r1 = noncompiled()
r2 = compiled()
r3 = reallyCompiled()
r4 = compiledInLoop()
r5 = reallyCompiledInLoop()
print "r1 = ", r1
print "r2 = ", r2
print "r3 = ", r3
print "r4 = ", r4
print "r5 = ", r5
</pre>
And here is the output on my machine:
<pre>
$ regexTest.py
noncompiled took 4.555 seconds.
compiled took 2.323 seconds.
reallyCompiled took 2.325 seconds.
compiledInLoop took 4.620 seconds.
reallyCompiledInLoop took 4.074 seconds.
r1 = 2000000
r2 = 2000000
r3 = 2000000
r4 = 2000000
r5 = 20000
‘reallyCompiled’메소드는 캐시를 우회하는 내부 인터페이스를 사용합니다. 각 루프 반복에서 컴파일되는 것은 백만이 아니라 10,000 회만 반복됩니다.
답변
나는 match(...)
주어진 예에서 서로 다른 Honest Abe에 동의합니다 . 그것들은 일대일 비교가 아니므로 결과는 다양합니다. 답장을 단순화하기 위해 해당 기능에 A, B, C, D를 사용합니다. 예, 우리는 re.py
3 대신 4 개의 기능을 다루고 있습니다.
이 코드를 실행 :
h = re.compile('hello') # (A)
h.match('hello world') # (B)
이 코드를 실행하는 것과 같습니다.
re.match('hello', 'hello world') # (C)
소스를 살펴볼 때 re.py
(A + B)는 다음을 의미합니다.
h = re._compile('hello') # (D)
h.match('hello world')
그리고 (C)는 실제로 다음과 같습니다.
re._compile('hello').match('hello world')
따라서 (C)는 (B)와 같지 않습니다. 실제로 (C)는 (D)를 호출 한 후 (B)를 호출합니다. 즉, (C) = (A) + (B)
. 따라서 루프 내부의 (A + B)를 비교하면 루프 내부의 (C)와 결과가 같습니다.
조지 regexTest.py
는 우리를 위해 이것을 증명했습니다.
noncompiled took 4.555 seconds. # (C) in a loop
compiledInLoop took 4.620 seconds. # (A + B) in a loop
compiled took 2.323 seconds. # (A) once + (B) in a loop
모든 사람의 관심은 2.323 초의 결과를 얻는 방법입니다. 확인하기 위해 compile(...)
한 번만 호출되는, 우리는 메모리에 컴파일 된 정규식 개체를 저장해야합니다. 클래스를 사용하는 경우, 함수를 호출 할 때마다 객체를 저장하고 재사용 할 수 있습니다.
class Foo:
regex = re.compile('hello')
def my_function(text)
return regex.match(text)
우리가 수업을 사용하지 않는다면 (오늘 나의 요청입니다), 나는 의견이 없습니다. 나는 여전히 파이썬에서 전역 변수를 사용하는 법을 배우고 있으며 전역 변수가 나쁜 것임을 알고 있습니다.
한 가지 더 요점 (A) + (B)
은 접근 방식 을 사용 하는 것이 우위에 있다고 생각합니다 . 내가 관찰 한 사실은 다음과 같습니다 (잘못되면 수정하십시오).
-
A를 한 번 호출 하면 정규식 객체를 만들기 위해
_cache
한 번의 검색이 수행됩니다sre_compile.compile()
. A를 두 번 호출하면 정규식 객체가 캐시되기 때문에 두 번의 검색과 한 번의 컴파일이 수행됩니다. -
경우
_cache
GET 사이에 플러시, 다음 정규식 개체는 메모리와 다시 컴파일 파이썬 필요에서 해제됩니다. (누군가 파이썬이 다시 컴파일하지 않을 것을 제안합니다.) -
(A)를 사용하여 정규식 객체를 유지하면 정규식 객체는 여전히 _cache로 들어가서 어떻게 든 지워집니다. 그러나 우리 코드는 그것에 대한 참조를 유지하며 정규식 객체는 메모리에서 해제되지 않습니다. 그것들은 파이썬이 다시 컴파일 할 필요가 없습니다.
-
George의 테스트 compileInLoop와 컴파일 된 것의 2 초 차이는 주로 키를 빌드하고 _cache를 검색하는 데 필요한 시간입니다. 정규 표현식의 컴파일 시간을 의미하지는 않습니다.
-
George의 실제로 컴파일 테스트는 매번 컴파일을 실제로 다시 수행하면 어떻게되는지 보여줍니다. 100 배 느려질 것입니다 (루프를 1,000,000에서 10,000으로 줄였습니다).
(A + B)가 (C)보다 나은 유일한 경우는 다음과 같습니다.
- 클래스 내에서 정규 표현식 객체의 참조를 캐시 할 수 있다면.
- 루프 내부 또는 여러 번 반복적으로 (B)를 호출해야하는 경우 루프 외부의 정규식 객체에 대한 참조를 캐시해야합니다.
(C)가 충분한 경우 :
- 참조를 캐시 할 수 없습니다.
- 우리는 가끔 한 번만 사용합니다.
- 전반적으로, 우리는 너무 많은 정규 표현식을 가지고 있지 않습니다 (컴파일 된 정규 표현식이 결코 플러시되지 않는다고 가정)
요약하면 ABC는 다음과 같습니다.
h = re.compile('hello') # (A)
h.match('hello world') # (B)
re.match('hello', 'hello world') # (C)
읽어 주셔서 감사합니다.
답변
대부분 re.compile 사용 여부에 차이가 거의 없습니다. 내부적으로 모든 함수는 컴파일 단계 측면에서 구현됩니다.
def match(pattern, string, flags=0):
return _compile(pattern, flags).match(string)
def fullmatch(pattern, string, flags=0):
return _compile(pattern, flags).fullmatch(string)
def search(pattern, string, flags=0):
return _compile(pattern, flags).search(string)
def sub(pattern, repl, string, count=0, flags=0):
return _compile(pattern, flags).sub(repl, string, count)
def subn(pattern, repl, string, count=0, flags=0):
return _compile(pattern, flags).subn(repl, string, count)
def split(pattern, string, maxsplit=0, flags=0):
return _compile(pattern, flags).split(string, maxsplit)
def findall(pattern, string, flags=0):
return _compile(pattern, flags).findall(string)
def finditer(pattern, string, flags=0):
return _compile(pattern, flags).finditer(string)
또한 re.compile ()은 추가 간접 처리 및 캐싱 로직을 무시합니다.
_cache = {}
_pattern_type = type(sre_compile.compile("", 0))
_MAXCACHE = 512
def _compile(pattern, flags):
# internal: compile pattern
try:
p, loc = _cache[type(pattern), pattern, flags]
if loc is None or loc == _locale.setlocale(_locale.LC_CTYPE):
return p
except KeyError:
pass
if isinstance(pattern, _pattern_type):
if flags:
raise ValueError(
"cannot process flags argument with a compiled pattern")
return pattern
if not sre_compile.isstring(pattern):
raise TypeError("first argument must be string or compiled pattern")
p = sre_compile.compile(pattern, flags)
if not (flags & DEBUG):
if len(_cache) >= _MAXCACHE:
_cache.clear()
if p.flags & LOCALE:
if not _locale:
return p
loc = _locale.setlocale(_locale.LC_CTYPE)
else:
loc = None
_cache[type(pattern), pattern, flags] = p, loc
return p
re.compile 사용의 작은 속도 이점 외에도 사람들은 잠재적으로 복잡한 패턴 사양의 이름을 지정하고 적용되는 비즈니스 로직과 분리 하여 얻을 수있는 가독성을 좋아합니다.
#### Patterns ############################################################
number_pattern = re.compile(r'\d+(\.\d*)?') # Integer or decimal number
assign_pattern = re.compile(r':=') # Assignment operator
identifier_pattern = re.compile(r'[A-Za-z]+') # Identifiers
whitespace_pattern = re.compile(r'[\t ]+') # Spaces and tabs
#### Applications ########################################################
if whitespace_pattern.match(s): business_logic_rule_1()
if assign_pattern.match(s): business_logic_rule_2()
다른 응답자는 pyc 파일이 컴파일 된 패턴을 직접 저장 했다고 잘못 생각했습니다 . 그러나 실제로는 PYC가로드 될 때마다 다시 작성됩니다.
>>> from dis import dis
>>> with open('tmp.pyc', 'rb') as f:
f.read(8)
dis(marshal.load(f))
1 0 LOAD_CONST 0 (-1)
3 LOAD_CONST 1 (None)
6 IMPORT_NAME 0 (re)
9 STORE_NAME 0 (re)
3 12 LOAD_NAME 0 (re)
15 LOAD_ATTR 1 (compile)
18 LOAD_CONST 2 ('[aeiou]{2,5}')
21 CALL_FUNCTION 1
24 STORE_NAME 2 (lc_vowels)
27 LOAD_CONST 1 (None)
30 RETURN_VALUE
위의 분해는 다음을 포함하는 PYC 파일에서 비롯된 것입니다 tmp.py
.
import re
lc_vowels = re.compile(r'[aeiou]{2,5}')