2012-06-25 7 views
22

Простой вопрос, на который я не могу найти «хороший» ответ сам:Есть ли лучший способ писать последовательные «или» утверждения в Python?

Скажем, у меня есть следующее условие:

if 'foo' in mystring or 'bar' in mystring or 'hello' in mystring: 
    # Do something 
    pass 

Когда число or заявления может быть довольно дольше, в зависимости от ситуация.

Есть ли способ «лучше» (более Pythonic) писать это, не жертвуя успехом?

Если вы считаете, что используете any(), но он принимает список элементов типа boolean, поэтому мне нужно будет сначала создать этот список (отказ от короткого замыкания в процессе), поэтому я думаю, что он менее эффективен.

спасибо.

ответ

30

Способ может быть

if any(s in mystring for s in ('foo', 'bar', 'hello')): 
    pass 

Что вам перебрать является кортеж, который построен на компиляции функции, поэтому она не должна быть ниже исходной версии.

Если вы боитесь, что кортеж будет слишком долго, вы могли бы сделать

def mystringlist(): 
    yield 'foo' 
    yield 'bar' 
    yield 'hello' 
if any(s in mystring for s in mystringlist()): 
    pass 
+1

Спасибо. Но разве эта техника не предотвращает оптимизацию короткого замыкания? – ereOn

+2

Это генератор, а не список. – johv

+9

Нет. '(s в mystring для s в 'foo', 'bar', 'hello')' является выражением генератора, что означает, что он не вычисляется мгновенно в целом только по требованию. 'any()' останавливает итерацию, видя первое истинное значение, поэтому остальные никогда не будут проверяться. Прочитайте выражения генератора. – Kos

7

Это звучит как работа для регулярных выражений.

import re 

if re.search("(foo|bar|hello)", mystring): 
    # Do something 
    pass 

Должно быть и быстрее. Особенно если вы компилируете регулярное выражение раньше времени.

Если вы генерируете регулярное выражение автоматически, вы можете использовать re.escape(), чтобы убедиться, что никакие специальные символы не разорвут ваше регулярное выражение. Например, если words список строк, которые вы хотите найти, вы можете создать свой шаблон, как это:

pattern = "(%s)" % ("|".join(re.escape(word) for word in words),) 

Следует также отметить, что если у вас есть m слова и ваша строка имеет n символы, оригинал код имеет O(n*m) сложность, в то время как регулярное выражение имеет сложность O(n). Несмотря на то, что регулярные выражения Python на самом деле не являются теоретическими регулярными выражениями comp-sci, а не всегдаO(n) сложность, в этом простом случае они есть.

+4

Но вы должны быть осторожны, если какое-либо из «слов», которое вы ищете, содержит специальные регулярные выражения. –

+0

@gnibbler: True. И наоборот, вы можете написать меньше кода, используя сопоставление шаблонов. Если вы делаете что-то вроде автоматического генерации регулярного выражения, вы можете использовать 're.escape()'. – cha0site

+0

Действительно, вы можете добавить это к своему ответу –

2

Поскольку вы обрабатываете слово за словом по сравнению с mystring, наверняка мистик может использоваться как набор. Тогда просто взять пересечение между множеством, содержащим слова в mystring и целевые группы слов:

In [370]: mystring=set(['foobar','barfoo','foo']) 

In [371]: mystring.intersection(set(['foo', 'bar', 'hello'])) 
Out[371]: set(['foo']) 

логический «или» есть члены пересечения двух множеств.

Использование набора также выполняется быстрее. Вот относительно времени по сравнению с генератором и регулярным выражением:

f1: generator to test against large string 
f2: re to test against large string 
f3: set intersection of two sets of words 

    rate/sec  f2  f1  f3 
f2 101,333  -- -95.0% -95.5% 
f1 2,026,329 1899.7%  -- -10.1% 
f3 2,253,539 2123.9% 11.2%  -- 

Таким образом, генератор и операция in является 19x быстрее, чем регулярное выражение и множество пересечений 21x быстрее, чем регулярное выражение и 11% быстрее, чем генератор.

Вот код, который сгенерировал времени:

import re 

with open('/usr/share/dict/words','r') as fin: 
    set_words={word.strip() for word in fin} 

s_words=' '.join(set_words) 
target=set(['bar','foo','hello']) 
target_re = re.compile("(%s)" % ("|".join(re.escape(word) for word in target),)) 

gen_target=(word for word in ('bar','foo','hello')) 

def f1(): 
    """ generator to test against large string """   
    if any(s in s_words for s in gen_target): 
     return True 

def f2(): 
    """ re to test against large string """ 
    if re.search(target_re, s_words): 
     return True 

def f3(): 
    """ set intersection of two sets of words """ 
    if target.intersection(set_words): 
     return True 

funcs=[f1,f2,f3] 
legend(funcs) 
cmpthese(funcs)   
+0

Как это отличается от принятого ответа? Единственное различие заключается в том, что вместо этого вы используете 'set' «tuple», в противном случае это точно то же самое. – cha0site

+0

@ cha0site: принятый ответ также предлагает функцию для большого списка. Я думаю, что набор - лучший способ так же.Это также предлагает два набора - без использования 'any' –

2

Если у вас есть список известных элементов, чтобы проверить против, вы можете также записать его как

if mystring in ['foo', 'bar', 'hello']: 

Вы не можете получить преимущества обеспечения порядка сравнения (я не думаю, что Python должен проверять элементы списка слева направо), но это только проблема, если вы знаете, что «foo» более вероятен, чем «bar».

+0

Это не совсем то же самое. Условие в вопросе также будет верно, когда 'mystring = 'hello world''. Этого не было бы. – Izkata

+0

Хорошая точка, спасибо - как вы говорите, не совсем то же самое, и, возможно, не очень подходит для конкретной проблемы. – kimvanwyk

Смежные вопросы