Как найти первый индекс любого из набора символов в строке

Я хотел бы найти индекс первого вхождения любого "специального" символа в строке, например:

>>> "Hello world!".index([' ', '!'])
5

... за исключением того, что недействителен синтаксис Python. Конечно, я могу написать функцию, которая эмулирует это поведение:

def first_index(s, characters):
    i = []
    for c in characters:
        try:
            i.append(s.index(c))
        except ValueError:
            pass
    if not i:
        raise ValueError
    return min(i)

Я мог бы также использовать регулярные выражения, но оба решения кажутся немного переборщиками. Есть ли какой-либо "нормальный" способ сделать это в Python?

Ответы

Ответ 1

Вы можете использовать enumerate и next с помощью выражение генератора, получение первого совпадения или возврат Нет, если в s не отображается символ:

s = "Hello world!"

st = {"!"," "}
ind = next((i for i, ch  in enumerate(s) if ch in st),None)
print(ind)

Вы можете передать любое значение, которое вы хотите использовать в качестве возвращаемого значения по умолчанию, если нет совпадения.

Если вы хотите использовать функцию и поднять значение ValueError:

def first_index(s, characters):
    st = set(characters)
    ind = next((i for i, ch in enumerate(s) if ch in st), None)
    if ind is not None:
        return ind
    raise ValueError

Для небольших входов с использованием набора не будет много, если будет какая-либо разница, но для больших строк это будет более эффективно.

Некоторые тайминги:

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

In [40]: s = "Hello world!" * 100    
In [41]: string = s    
In [42]: %%timeit
st = {"x","y","!"}
next((i for i, ch in enumerate(s) if ch in st), None)
   ....: 
1000000 loops, best of 3: 1.71 µs per loop    
In [43]: %%timeit
specials = ['x', 'y', '!']
min(map(lambda x: (string.index(x) if (x in string) else len(string)), specials))
   ....: 
100000 loops, best of 3: 2.64 µs per loop

Не в строке, большой набор символов:

In [44]: %%timeit
st = {"u","v","w","x","y","z"}
next((i for i, ch in enumerate(s) if ch in st), None)
   ....: 
1000000 loops, best of 3: 1.49 µs per loop

In [45]: %%timeit
specials = ["u","v","w","x","y","z"]
min(map(lambda x: (string.index(x) if (x in string) else len(string)), specials))
   ....: 
100000 loops, best of 3: 5.48 µs per loop

В строке находится самый первый символ набора символов:

In [47]: %%timeit
specials = ['H', 'y', '!']
min(map(lambda x: (string.index(x) if (x in string) else len(string)), specials))
   ....: 
100000 loops, best of 3: 2.02 µs per loop

In [48]: %%timeit
st = {"H","y","!"}
next((i for i, ch in enumerate(s) if ch in st), None)
   ....: 
1000000 loops, best of 3: 903 ns per loop

Ответ 2

Используйте метод gen-exp и find.

>>> a = [' ', '!']
>>> s = "Hello World!"
>>> min(s.find(i) for i in a)
5

Чтобы удалить -1, если они произойдут, вы можете иметь фильтр в списке comp

>>> a = [' ', '!','$']
>>> s = "Hello World!"
>>> min(s.find(i) for i in a if i in s)
5

или вы можете заменить None

>>> min(s.find(i) if i in s else None for i in a)
5

Добавление результатов timeit

$ python -m timeit "a = [' ', '\!'];s = 'Hello World\!';min(s.find(i) for i in a if i in s)"
1000000 loops, best of 3: 0.902 usec per loop
$ python -m timeit "a = [' ', '\!'];s = 'Hello World\!';next((i for i, ch  in enumerate(s) if ch in a),None)"
1000000 loops, best of 3: 1.25 usec per loop
$ python -m timeit "a = [' ', '\!'];s = 'Hello World\!';min(map(lambda x: (s.index(x) if (x in s) else len(s)), a))"
1000000 loops, best of 3: 1.12 usec per loop

В примере вашего примера красивое решение Padraic немного медленное. Однако в больших тестовых случаях это определенно победитель. (Это немного удивительно, что альфасин "Не так оптимизирован" также здесь быстрее)

Добавлены сведения о реализации

>>> def take1(s,a):
...     min(s.find(i) for i in a if i in s)
... 
>>> import dis
>>> dis.dis(take1)
  2           0 LOAD_GLOBAL              0 (min)
              3 LOAD_CLOSURE             0 (s)
              6 BUILD_TUPLE              1
              9 LOAD_CONST               1 (<code object <genexpr> at 0x7fa622e961b0, file "<stdin>", line 2>)
             12 MAKE_CLOSURE             0
             15 LOAD_FAST                1 (a)
             18 GET_ITER            
             19 CALL_FUNCTION            1
             22 CALL_FUNCTION            1
             25 POP_TOP             
             26 LOAD_CONST               0 (None)
             29 RETURN_VALUE        
>>> def take2(s,a):
...     next((i for i, ch  in enumerate(s) if ch in a),None)
... 
>>> dis.dis(take2)
  2           0 LOAD_GLOBAL              0 (next)
              3 LOAD_CLOSURE             0 (a)
              6 BUILD_TUPLE              1
              9 LOAD_CONST               1 (<code object <genexpr> at 0x7fa622e96e30, file "<stdin>", line 2>)
             12 MAKE_CLOSURE             0
             15 LOAD_GLOBAL              1 (enumerate)
             18 LOAD_FAST                0 (s)
             21 CALL_FUNCTION            1
             24 GET_ITER            
             25 CALL_FUNCTION            1
             28 LOAD_CONST               0 (None)
             31 CALL_FUNCTION            2
             34 POP_TOP             
             35 LOAD_CONST               0 (None)
             38 RETURN_VALUE        
>>> def take3(s,a):
...     min(map(lambda x: (s.index(x) if (x in s) else len(s)), a))
... 
>>> dis.dis(take3)
  2           0 LOAD_GLOBAL              0 (min)
              3 LOAD_GLOBAL              1 (map)
              6 LOAD_CLOSURE             0 (s)
              9 BUILD_TUPLE              1
             12 LOAD_CONST               1 (<code object <lambda> at 0x7fa622e44eb0, file "<stdin>", line 2>)
             15 MAKE_CLOSURE             0
             18 LOAD_FAST                1 (a)
             21 CALL_FUNCTION            2
             24 CALL_FUNCTION            1
             27 POP_TOP             
             28 LOAD_CONST               0 (None)
             31 RETURN_VALUE        

Как вы можете ясно видеть в случае Padraic, загрузка глобальных функций next и enumerate - это те, которые убивают время вместе с None в конце. В решении альфазина основным замедлением является функция lambda.

Ответ 3

Не так оптимизирован, как решение Padraic Cunningham, но еще один лайнер:

string = "Hello world!"
specials = [' ', '!', 'x']
min(map(lambda x: (string.index(x) if (x in string) else len(string)), specials))

Ответ 4

Я бы поддержал модуль re, поскольку он был встроен и уже протестирован. Он также оптимизирован для такого рода вещей.

>>> import re
>>> re.search(r'[ !]', 'Hello World!').start()
5

Вероятно, вы захотите проверить, что совпадение было найдено или исключить исключение, если это не так.

Есть причины не использовать re, но я бы хотел увидеть хороший комментарий, оправдывающий рациональность. Думать, что вы можете "сделать это лучше", как правило, не нужно, затрудняет чтение другими людьми кода и менее удобен для обслуживания.