Visitar URL original
gh-91524: Speed up the regular expression substitution by serhiy-storchaka · Pull Request #91525 · python/cpython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-91524: Speed up the regular expression substitution
Functions re.sub() and re.subn() and corresponding re.Pattern methods
are now 2-3 times faster for replacement strings containing group references.
  • Loading branch information
serhiy-storchaka committed Apr 14, 2022
commit c213b3756764f2a094f523b327ae00badd326a2a
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,11 @@ Optimizations
becomes 272 bytes from 352 bytes on 64bit platform.
(Contributed by Inada Naoki in :issue:`46845`.)

* Speed up the regular expression substitution (functions :func:`re.sub` and
:func:`re.subn` and corresponding :class:`re.Pattern` methods) for
replacement strings containing group references by 2--3 times.
(Contributed by Serhiy Storchaka in :issue:`91524`.)


Faster CPython
==============
Expand Down
22 changes: 4 additions & 18 deletions Lib/re/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
import enum
from . import _compiler, _parser
import functools
import _sre


# public symbols
Expand Down Expand Up @@ -227,7 +228,7 @@ def compile(pattern, flags=0):
def purge():
"Clear the regular expression caches"
_cache.clear()
_compile_repl.cache_clear()
_compile_template.cache_clear()

# SPECIAL_CHARS
# closing ')', '}' and ']'
Expand Down Expand Up @@ -282,24 +283,9 @@ def _compile(pattern, flags):
return p

@functools.lru_cache(_MAXCACHE)
def _compile_repl(repl, pattern):
def _compile_template(pattern, repl):
Comment thread
gpshead marked this conversation as resolved.
# internal: compile replacement pattern
return _parser.parse_template(repl, pattern)

def _expand(pattern, match, template):
# internal: Match.expand implementation hook
template = _parser.parse_template(template, pattern)
return _parser.expand_template(template, match)

def _subx(pattern, template):
# internal: Pattern.sub/subn implementation helper
template = _compile_repl(template, pattern)
if not template[0] and len(template[1]) == 1:
# literal replacement
return template[1][0]
def filter(match, template=template):
return _parser.expand_template(template, match)
return filter
return _sre.template(pattern, _parser.parse_template(repl, pattern))

# register myself for pickling

Expand Down
2 changes: 1 addition & 1 deletion Lib/re/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

# update when constants are added or removed

MAGIC = 20220402
MAGIC = 20220409
Comment thread
gpshead marked this conversation as resolved.
Outdated

from _sre import MAXREPEAT, MAXGROUPS

Expand Down
46 changes: 16 additions & 30 deletions Lib/re/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,24 +980,28 @@ def parse(str, flags=0, state=None):

return p

def parse_template(source, state):
def parse_template(source, pattern):
Comment thread
gpshead marked this conversation as resolved.
# parse 're' replacement string into list of literals and
# group references
s = Tokenizer(source)
sget = s.get
groups = []
literals = []
result = []
literal = []
lappend = literal.append
def addliteral():
if s.istext:
result.append(''.join(literal))
else:
# The tokenizer implicitly decodes bytes objects as latin-1, we must
# therefore re-encode the final representation.
result.append(''.join(literal).encode('latin-1'))
del literal[:]
def addgroup(index, pos):
if index > state.groups:
if index > pattern.groups:
raise s.error("invalid group reference %d" % index, pos)
if literal:
literals.append(''.join(literal))
del literal[:]
groups.append((len(literals), index))
literals.append(None)
groupindex = state.groupindex
addliteral()
result.append(index)
groupindex = pattern.groupindex
while True:
this = sget()
if this is None:
Expand All @@ -1006,7 +1010,6 @@ def addgroup(index, pos):
# group
c = this[1]
if c == "g":
name = ""
if not s.match("<"):
raise s.error("missing <")
name = s.getuntil(">", "group name")
Expand Down Expand Up @@ -1057,22 +1060,5 @@ def addgroup(index, pos):
lappend(this)
else:
lappend(this)
if literal:
literals.append(''.join(literal))
if not isinstance(source, str):
# The tokenizer implicitly decodes bytes objects as latin-1, we must
# therefore re-encode the final representation.
literals = [None if s is None else s.encode('latin-1') for s in literals]
return groups, literals

def expand_template(template, match):
g = match.group
empty = match.string[:0]
groups, literals = template
literals = literals[:]
try:
for index, group in groups:
literals[index] = g(group) or empty
except IndexError:
raise error("invalid group reference %d" % index) from None
return empty.join(literals)
addliteral()
return result
35 changes: 34 additions & 1 deletion Modules/_sre/clinic/sre.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading