[python] 파이썬의 re.compile을 사용할 가치가 있습니까?

파이썬에서 정규 표현식으로 컴파일을 사용하면 어떤 이점이 있습니까?

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.py3 대신 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)은 접근 방식 을 사용 하는 것이 우위에 있다고 생각합니다 . 내가 관찰 한 사실은 다음과 같습니다 (잘못되면 수정하십시오).

  1. A를 한 번 호출 하면 정규식 객체를 만들기 위해 _cache한 번의 검색이 수행됩니다 sre_compile.compile(). A를 두 번 호출하면 정규식 객체가 캐시되기 때문에 두 번의 검색과 한 번의 컴파일이 수행됩니다.

  2. 경우 _cacheGET 사이에 플러시, 다음 정규식 개체는 메모리와 다시 컴파일 파이썬 필요에서 해제됩니다. (누군가 파이썬이 다시 컴파일하지 않을 것을 제안합니다.)

  3. (A)를 사용하여 정규식 객체를 유지하면 정규식 객체는 여전히 _cache로 들어가서 어떻게 든 지워집니다. 그러나 우리 코드는 그것에 대한 참조를 유지하며 정규식 객체는 메모리에서 해제되지 않습니다. 그것들은 파이썬이 다시 컴파일 할 필요가 없습니다.

  4. George의 테스트 compileInLoop와 컴파일 된 것의 2 초 차이는 주로 키를 빌드하고 _cache를 검색하는 데 필요한 시간입니다. 정규 표현식의 컴파일 시간을 의미하지는 않습니다.

  5. George의 실제로 컴파일 테스트는 매번 컴파일을 실제로 다시 수행하면 어떻게되는지 보여줍니다. 100 배 느려질 것입니다 (루프를 1,000,000에서 10,000으로 줄였습니다).

(A + B)가 (C)보다 나은 유일한 경우는 다음과 같습니다.

  1. 클래스 내에서 정규 표현식 객체의 참조를 캐시 할 수 있다면.
  2. 루프 내부 또는 여러 번 반복적으로 (B)를 호출해야하는 경우 루프 외부의 정규식 객체에 대한 참조를 캐시해야합니다.

(C)가 충분한 경우 :

  1. 참조를 캐시 할 수 없습니다.
  2. 우리는 가끔 한 번만 사용합니다.
  3. 전반적으로, 우리는 너무 많은 정규 표현식을 가지고 있지 않습니다 (컴파일 된 정규 표현식이 결코 플러시되지 않는다고 가정)

요약하면 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}')