Getting captured group in one line
Asked Answered
P

10

13

There is a known "pattern" to get the captured group value or an empty string if no match:

match = re.search('regex', 'text')
if match:
    value = match.group(1)
else:
    value = ""

or:

match = re.search('regex', 'text')
value = match.group(1) if match else ''

Is there a simple and pythonic way to do this in one line?

In other words, can I provide a default for a capturing group in case it's not found?


For example, I need to extract all alphanumeric characters (and _) from the text after the key= string:

>>> import re
>>> PATTERN = re.compile('key=(\w+)')
>>> def find_text(text):
...     match = PATTERN.search(text)
...     return match.group(1) if match else ''
... 
>>> find_text('foo=bar,key=value,beer=pub')
'value'
>>> find_text('no match here')
''

Is it possible for find_text() to be a one-liner?

It is just an example, I'm looking for a generic approach.

Pitchblack answered 29/4, 2014 at 13:59 Comment(13)
Is there a reason that you cannot use re.findall()?Bazar
@Bazar Won't that be inefficient, if he just wants to check if the string matches or not?Facilitation
@Bazar yeah, besides, I would get a list as a result, so I would need one more line to check if the list is empty or not. Right?Pitchblack
Why would you need to check anything? You could join. If it's empty you'd get an empty string back.Bazar
Maybe ''.join(re.findall(r'key=(\w+)', text))?Bazar
@Bazar exactly, thanks. This is an option then. You can post it as an answer.Pitchblack
@Pitchblack But, if your RegEx matches multiple times in the string, then the result maynot be what you expected.Facilitation
@Facilitation Don't worry so much. I'm not too keen to post an answer anyways.Bazar
@Facilitation yeah, it depends on the input. Strictly speaking the result would be different of what I would have with search, but anyway it's an option too. Thank you.Pitchblack
@Bazar No, it is not about that. Even I wondered the same always. So, I also want a solid solution to this. Sorry. I didn't mean to interrupt you from posting an answer.Facilitation
Re: "Is there a simple and pythonic way to do this in one line?" The answer is no. Any means to get this to work in one line (without defining your own wrapper), is going to be too ugly to read. But defining your own wrapper is perfectly Pythonic, as is using two quite readable lines instead of a single difficult-to-read line.Pulsatory
@JohnY this is a good point. I wasn't attentive to the word "pythonic" I've used. "Flat is better than nested.", but "Simple is better than complex." - it's difficult to follow all of the statements :)Pitchblack
@JohnY I would really appreciate if you put this as an answer. Strictly speaking, leaving it as is or making a wrapper is a way to go too. Thank you.Pitchblack
F
10

Quoting from the MatchObjects docs,

Match objects always have a boolean value of True. Since match() and search() return None when there is no match, you can test whether there was a match with a simple if statement:

match = re.search(pattern, string)
if match:
   process(match)

Since there is no other option, and as you use a function, I would like to present this alternative

def find_text(text, matches = lambda x: x.group(1) if x else ''):
    return matches(PATTERN.search(text))

assert find_text('foo=bar,key=value,beer=pub') == 'value'
assert find_text('no match here') == ''

It is the same exact thing, but only the check which you need to do has been default parameterized.

Thinking of @Kevin's solution and @devnull's suggestions in the comments, you can do something like this

def find_text(text):
    return next((item.group(1) for item in PATTERN.finditer(text)), "")

This takes advantage of the fact that, next accepts the default to be returned as an argument. But this has the overhead of creating a generator expression on every iteration. So, I would stick to the first version.

Facilitation answered 29/4, 2014 at 14:12 Comment(2)
Good option in case of using a function, thank you! I think Kevin's answer is the best option here for now. What do you think?Pitchblack
@Pitchblack That really is pretty, but if you are going to do this check often, creating a lambda function could be pretty heavy, but the function arguments are evaluated only once. So, this might be a little light weight.Facilitation
B
6

You can play with the pattern, using an empty alternative at the end of the string in the capture group:

>>> re.search(r'((?<=key=)\w+|$)', 'foo=bar,key=value').group(1)
'value'
>>> re.search(r'((?<=key=)\w+|$)', 'no match here').group(1)
''
Blond answered 29/4, 2014 at 16:43 Comment(0)
C
3

It's possible to refer to the result of a function call twice in a single one-liner: create a lambda expression and call the function in the arguments.

value = (lambda match: match.group(1) if match else '')(re.search(regex,text))

However, I don't consider this especially readable. Code responsibly - if you're going to write tricky code, leave a descriptive comment!

Capsaicin answered 29/4, 2014 at 14:23 Comment(1)
I haven't decided yet whether I like this answer enough to upvote it. I will say this: If you absolutely must get it done in one line, I'm confident this is the best there is. And I appreciate that you've at least put in the disclaimer about readability.Pulsatory
P
2

Re: "Is there a simple and pythonic way to do this in one line?" The answer is no. Any means to get this to work in one line (without defining your own wrapper), is going to be uglier to read than the ways you've already presented. But defining your own wrapper is perfectly Pythonic, as is using two quite readable lines instead of a single difficult-to-read line.

Update for Python 3.8+: The new "walrus operator" introduced with PEP 572 does allow this to be a one-liner without convoluted tricks:

value = match.group(1) if (match := re.search('regex', 'text')) else ''

Many would consider this Pythonic, particularly those who supported the PEP. However, it should be noted that there was fierce opposition to it as well. The conflict was so intense that Guido van Rossum stepped down from his role as Python's BDFL the day after announcing his acceptance of the PEP.

Pulsatory answered 29/4, 2014 at 14:46 Comment(1)
Haven't assignment expressions (PEP 572) changed The Way and now doing it on one line with := operator is Pythonic?Hassock
W
2

One-line version:

if re.findall(pattern,string): pass

The issue here is that you want to prepare for multiple matches or ensure that your pattern only hits once. Expanded version:

# matches is a list
matches = re.findall(pattern,string)

# condition on the list fails when list is empty
if matches:
    pass

So for your example "extract all alphanumeric characters (and _) from the text after the key= string":

# Returns 
def find_text(text):
    return re.findall("(?<=key=)[a-zA-Z0-9_]*",text)[0]
Western answered 8/5, 2014 at 15:14 Comment(0)
M
2

One line for you, although not quite Pythonic.

find_text = lambda text: (lambda m: m and m.group(1) or '')(PATTERN.search(text))

Indeed, in Scheme programming language, all local variable constructs can be derived from lambda function applications.

Marileemarilin answered 27/7, 2014 at 15:23 Comment(0)
W
1

You can do it as:

value = re.search('regex', 'text').group(1) if re.search('regex', 'text') else ''

Although it's not terribly efficient considering the fact that you run the regex twice.

Or to run it only once as @Kevin suggested:

value = (lambda match: match.group(1) if match else '')(re.search(regex,text))

Wickedness answered 29/4, 2014 at 14:3 Comment(9)
@Downvoter, Please let me know my mistake. I just tested it and it works.Wickedness
I didn't downvote, but this is somewhat inefficient since it calls re.search twice. You know what they say: Don't Repeat Yourself.Capsaicin
You could use lambda trickery to only call it once: value = (lambda match: match.group(1) if match else '')(re.search(regex,text)). But that is not terribly readable IMOCapsaicin
@Kevin, I admitted that it is. I was simply providing one solution that works.Wickedness
Oops, I didn't notice your admission. Now it's me that's being repetitive :pCapsaicin
@Capsaicin I think your option deserves to be a separate answer.Pitchblack
@alecxe, I agree. I will remove my answer now.Wickedness
@Wickedness I didn't downvote since I usually appreciate everyone for the help and time, trying to comment instead of downvoting. Thank you for the option.Pitchblack
@Kevin, I agree with alecxe, I will remove my answer. Please post your suggestion as a separate questionWickedness
R
1

One liners, one liners... Why can't you write it on 2 lines?

getattr(re.search('regex', 'text'), 'group', lambda x: '')(1)

Your second solution if fine. Make a function from it if you wish. My solution is for demonstrational purposes and it's in no way pythonic.

Recluse answered 24/7, 2014 at 18:6 Comment(0)
A
1

Starting Python 3.8, and the introduction of assignment expressions (PEP 572) (:= operator), we can name the regex search expression pattern.search(text) in order to both check if there is a match (as pattern.search(text) returns either None or a re.Match object) and use it to extract the matching group:

# pattern = re.compile(r'key=(\w+)')
match.group(1) if (match := pattern.search('foo=bar,key=value,beer=pub')) else ''
# 'value'
match.group(1) if (match := pattern.search('no match here')) else ''
# ''
Amnion answered 27/4, 2019 at 15:44 Comment(0)
F
1

I like John Y's answer to use the walrus operator, and I frequently work with comprehensions so I tested it with lists and it works there too:

>>> lines = ['line one', 'line two', 'line three']
>>> [m.group() for ln in lines if (m := re.search(r'ne t.', ln))]
['ne tw', 'ne th']

But I sometimes have to use Python 3.6, and I didn't see this variant provided as an answer yet...

And for those stuck with it, it also works with 2.x ...

You can use filter with:

>>> [m.group() for m in filter(None, [re.search(r'ne t.', ln) for ln in lines])]
['ne tw', 'ne th']
Falito answered 15/5 at 22:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.