Gibt es einen Vorteil bei der Verwendung von Kompilieren für reguläre Ausdrücke in Python?
h = re.compile('hello')
h.match('hello world')
gegen
re.match('hello', 'hello world')
Gibt es einen Vorteil bei der Verwendung von Kompilieren für reguläre Ausdrücke in Python?
h = re.compile('hello')
h.match('hello world')
gegen
re.match('hello', 'hello world')
Hier ist ein Beispiel für die Verwendung von re.compile
ist über 50 Mal schneller, als angefordert .
Der Punkt ist genau derselbe wie in meinem obigen Kommentar, nämlich die Verwendung von re.compile
kann ein erheblicher Vorteil sein, wenn Ihre Nutzung so beschaffen ist, dass der Kompilierungscache nicht viel bringt. Dies ist zumindest in einem bestimmten Fall der Fall (den ich in der Praxis erlebt habe), nämlich wenn alle der folgenden Punkte zutreffen:
re._MAXCACHE
, dessen Standard ist derzeit 512), undSie aufeinanderfolgende Verwendungen desselben Musters mit einem Abstand von mehr als re._MAXCACHE
andere Regexe dazwischen, so dass jede Regex zwischen aufeinanderfolgenden Verwendungen aus dem Cache gespült wird.
import re import time
def setup(N=1000):
patterns = [chr(i) + '.*' + chr(j)
for i in range(ord('a'), ord('z') + 1)
for j in range(ord('a'), ord('z') + 1)]
# If this assertion below fails, just add more (distinct) patterns.
# assert(re._MAXCACHE < len(patterns))
# N strings. Increase N for larger effect.
strings = ['abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'] * N
return (patterns, strings)
def without_compile(): print('Without re.compile:') patterns, strings = setup() print('searching') count = 0 for s in strings: for pat in patterns: count += bool(re.search(pat, s)) return count
def without_compile_cache_friendly(): print('Without re.compile, cache-friendly order:') patterns, strings = setup() print('searching') count = 0 for pat in patterns: for s in strings: count += bool(re.search(pat, s)) return count
def with_compile(): print('With re.compile:') patterns, strings = setup() print('compiling') compiled = [re.compile(pattern) for pattern in patterns] print('searching') count = 0 for s in strings: for regex in compiled: count += bool(regex.search(s)) return count
start = time.time() print(with_compile()) d1 = time.time() - start print(f'-- That took {d1:.2f} seconds.\n')
start = time.time() print(without_compile_cache_friendly()) d2 = time.time() - start print(f'-- That took {d2:.2f} seconds.\n')
start = time.time() print(without_compile()) d3 = time.time() - start print(f'-- That took {d3:.2f} seconds.\n')
print(f'Ratio: {d3/d1:.2f}')
Beispielausgabe auf meinem Laptop (Python 3.7.7):
With re.compile:
compiling
searching
676000
-- That took 0.33 seconds.
Without re.compile, cache-friendly order:
searching
676000
-- That took 0.67 seconds.
Without re.compile:
searching
676000
-- That took 23.54 seconds.
Ratio: 70.89
Ich habe mich nicht um die timeit
weil der Unterschied so groß ist, aber ich erhalte jedes Mal qualitativ ähnliche Zahlen. Beachten Sie, dass auch ohne re.compile
war es gar nicht so schlimm, dieselbe Regex mehrfach zu verwenden und zur nächsten zu wechseln (nur etwa 2-mal so langsam wie mit re.compile
), aber in der anderen Reihenfolge (Schleifenbildung durch viele Regexe) ist es erwartungsgemäß deutlich schlechter. Auch das Erhöhen der Cache-Größe funktioniert: Setzen Sie einfach re._MAXCACHE = len(patterns)
en setup()
oben (natürlich empfehle ich nicht, so etwas in der Produktion zu tun, da Namen mit Unterstrichen üblicherweise "privat" sind) senkt die ~23 Sekunden wieder auf ~0,7 Sekunden, was auch unserem Verständnis entspricht.
Ich stimme mit Honest Abe überein, dass die match(...)
in den genannten Beispielen sind unterschiedlich. Sie sind keine Eins-zu-eins-Vergleiche und die Ergebnisse sind daher unterschiedlich. Um meine Antwort zu vereinfachen, verwende ich A, B, C, D für die fraglichen Funktionen. Oh ja, wir haben es mit 4 Funktionen in re.py
anstelle von 3.
Ausführen dieses Code-Stücks:
h = re.compile('hello') # (A)
h.match('hello world') # (B)
ist dasselbe wie die Ausführung dieses Codes:
re.match('hello', 'hello world') # (C)
Denn, wenn man in die Quelle schaut re.py
, (A + B) bedeutet:
h = re._compile('hello') # (D)
h.match('hello world')
und (C) ist tatsächlich:
re._compile('hello').match('hello world')
Also ist (C) nicht dasselbe wie (B). Vielmehr ruft (C) (B) auf, nachdem es (D) aufgerufen hat, das wiederum von (A) aufgerufen wird. Mit anderen Worten, (C) = (A) + (B)
. Daher hat der Vergleich von (A + B) innerhalb einer Schleife das gleiche Ergebnis wie (C) innerhalb einer Schleife.
Georges regexTest.py
hat dies für uns bewiesen.
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
Jeder ist daran interessiert, wie man das Ergebnis von 2,323 Sekunden erreichen kann. Um sicherzustellen, dass compile(...)
nur einmal aufgerufen wird, müssen wir das kompilierte Regex-Objekt im Speicher ablegen. Wenn wir eine Klasse verwenden, können wir das Objekt speichern und jedes Mal wiederverwenden, wenn unsere Funktion aufgerufen wird.
class Foo:
regex = re.compile('hello')
def my_function(text)
return regex.match(text)
Wenn wir die Klasse nicht benutzen (was ich heute beantrage), habe ich keinen Kommentar. Ich lerne immer noch, wie man eine globale Variable in Python verwendet, und ich weiß, dass eine globale Variable eine schlechte Sache ist.
Ein weiterer Punkt: Ich glaube, dass die Verwendung von (A) + (B)
Ansatz die Oberhand hat. Hier sind einige Fakten, die ich beobachtet habe (bitte korrigieren Sie mich, wenn ich falsch liege):
Ruft A einmal auf, so wird eine Suche in der _cache
gefolgt von einem sre_compile.compile()
um ein Regex-Objekt zu erstellen. Ruft man A zweimal auf, werden zwei Suchen und eine Kompilierung durchgeführt (da das Regex-Objekt zwischengespeichert wird).
Wenn die _cache
dazwischen gespült wird, dann wird das Regex-Objekt aus dem Speicher freigegeben und Python muss erneut kompilieren. (Jemand behauptet, dass Python nicht neu kompiliert.)
Wenn wir das Regex-Objekt mit (A) beibehalten, wird das Regex-Objekt trotzdem in den _cache gelangen und irgendwie gespült werden. Aber unser Code behält eine Referenz darauf und das Regex-Objekt wird nicht aus dem Speicher freigegeben. Das bedeutet, dass Python nicht neu kompiliert werden muss.
Der Unterschied von 2 Sekunden zwischen der kompilierten Schleife in Georges Test und der kompilierten Schleife ist hauptsächlich die Zeit, die zum Erstellen des Schlüssels und zum Durchsuchen des _cache erforderlich ist. Es bedeutet nicht die Kompilierzeit der Regex.
Georges reallycompile-Test zeigt, was passiert, wenn die Kompilierung wirklich jedes Mal wiederholt wird: Sie wird 100x langsamer (er hat die Schleife von 1.000.000 auf 10.000 reduziert).
Hier sind die einzigen Fälle, in denen (A + B) besser ist als (C):
Fall, dass (C) gut genug ist:
Nur eine Zusammenfassung, hier sind die A B C:
h = re.compile('hello') # (A)
h.match('hello world') # (B)
re.match('hello', 'hello world') # (C)
Vielen Dank für die Lektüre.
Meistens gibt es kaum einen Unterschied, ob Sie die re.kompilieren oder nicht. Intern sind alle Funktionen in Form eines Kompilierschritts implementiert:
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)
Darüber hinaus umgeht re.compile() die zusätzliche Umleitung und Zwischenspeicherlogik:
_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
Zusätzlich zu dem kleinen Geschwindigkeitsvorteil durch die Verwendung von re.kompilieren Auch die Lesbarkeit, die sich aus der Benennung potenziell komplexer Musterspezifikationen und ihrer Trennung von der angewandten Geschäftslogik ergibt, wird geschätzt:
#### 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()
Ein weiterer Befragter glaubte fälschlicherweise, dass pyc Dateien direkt kompilierte Muster gespeichert; in Wirklichkeit werden sie jedoch jedes Mal neu erstellt, wenn der PYC geladen wird:
>>> 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
Die obige Disassemblierung stammt aus der PYC-Datei für eine tmp.py
enthalten:
import re
lc_vowels = re.compile(r'[aeiou]{2,5}')
Ein weiterer Vorteil der Verwendung von re.compile() besteht darin, dass ich mit re.VERBOSE Kommentare zu meinen Regex-Mustern hinzufügen kann
pattern = '''
hello[ ]world # Some info on my pattern logic. [ ] to recognize space
'''
re.search(pattern, 'hello world', re.VERBOSE)
Obwohl dies keinen Einfluss auf die Geschwindigkeit der Ausführung Ihres Codes hat, mache ich es gerne so, da es Teil meiner Gewohnheit ist, Kommentare zu schreiben. Ich verbringe nur ungern Zeit damit, mich an die Logik meines Codes zu erinnern, wenn ich 2 Monate später Änderungen vornehmen möchte.
Gemäß der Python Dokumentation :
Die Reihenfolge
prog = re.compile(pattern)
result = prog.match(string)
ist gleichbedeutend mit
result = re.match(pattern, string)
sondern mit re.compile()
und das Speichern des resultierenden regulären Ausdrucks zur Wiederverwendung ist effizienter, wenn der Ausdruck mehrmals in einem einzigen Programm verwendet wird.
Daraus schließe ich: Wenn Sie dasselbe Muster für viele verschiedene Texte verwenden wollen, sollten Sie es besser vorkompilieren.
CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.
9 Stimmen
Mit Ausnahme der Tatsache, dass in 2.6
re.sub
lässt sich nicht auf ein Fahnenargument ein...84 Stimmen
Ich bin gerade auf einen Fall gestoßen, bei dem die Verwendung von
re.compile
führte zu einer 10-50fachen Verbesserung. Die Moral ist, dass wenn Sie haben sehr viele Regexe (mehr als MAXCACHE = 100) y Sie verwenden sie jeweils sehr oft (und zwar mit mehr als MAXCACHE Regexen dazwischen, so dass jede einzelne aus dem Cache gelöscht wird: wenn Sie also dieselbe Regex sehr oft verwenden und dann zur nächsten übergehen, zählt das nicht), dann wäre es sicherlich hilfreich, sie zusammenzustellen. Ansonsten macht es keinen Unterschied.19 Stimmen
Eine kleine Anmerkung ist, dass für Strings, die keinen Regex benötigen, die
in
string substring test ist VIEL schneller:>python -m timeit -s "import re" "re.match('hello', 'hello world')" 1000000 loops, best of 3: 1.41 usec per loop
>python -m timeit "x = 'hello' in 'hello world'" 10000000 loops, best of 3: 0.0513 usec per loop
0 Stimmen
@ShreevatsaR Interessant! Können Sie eine Antwort mit einem Beispiel posten, das eine 10-50-fache Verbesserung zeigt? Die meisten Antworten, die hier gegeben werden, zeigen in einigen konkreten Fällen eine 3-fache Verbesserung und in anderen Fällen fast keine Verbesserung.
4 Stimmen
@Basj Done, gepostet eine Antwort . Ich habe mir nicht die Mühe gemacht, herauszufinden, wofür ich Python im Dezember 2013 verwendet habe, aber die erste einfache Sache, die ich ausprobiert habe, zeigt das gleiche Verhalten.