Сопоставление образцов списков в Python - PullRequest
38 голосов
/ 26 октября 2008

Я хочу сделать сопоставление с образцом в списках в Python. Например, в Haskell я могу сделать что-то вроде следующего:

fun (head : rest) = ...

Поэтому, когда я передаю список, head будет первым элементом, а rest будет последними элементами.

Аналогично, в Python я могу автоматически распаковывать кортежи:

(var1, var2) = func_that_returns_a_tuple()

Я хочу сделать что-то подобное со списками в Python. Прямо сейчас у меня есть функция, которая возвращает список, и кусок кода, который делает следующее:

ls = my_func()
(head, rest) = (ls[0], ls[1:])

Мне было интересно, смогу ли я как-нибудь сделать это в одной строке в Python вместо двух.

Ответы [ 10 ]

60 голосов
/ 26 октября 2008

Насколько я знаю, в текущем Python нет способа сделать его однострочным без введения другой функции, например ::

split_list = lambda lst: (lst[0], lst[1:])
head, rest = split_list(my_func())

Однако в Python 3.0 специальный синтаксис, используемый для переменных подписей и распаковки аргументов, станет доступен и для этого типа распаковки общей последовательности, поэтому в 3.0 вы сможете написать:

head, *rest = my_func()

Подробнее см. PEP 3132 .

32 голосов
/ 26 октября 2008

Прежде всего, обратите внимание, что «сопоставление с образцом» функциональных языков и назначение упомянутых вами кортежей на самом деле не очень похоже. В функциональных языках шаблоны используются для частичного определения функции. Таким образом, f (x : s) = e не означает, что нужно взять голову и хвост аргумента f и вернуть e, используя их, но это означает, что , если аргумент f, имеет форму x : s (для некоторых x и s) , тогда f (x : s) равно e.

Назначение python больше похоже на множественное назначение (я подозреваю, что это было его первоначальное намерение). Таким образом, вы пишете, например, x, y = y, x, чтобы поменять местами значения в x и y без необходимости использования временной переменной (как это было бы с простым оператором присваивания). Это не имеет ничего общего с сопоставлением с образцом, так как это в основном сокращение для «одновременного» выполнения x = y и y = x. Хотя python допускает произвольные последовательности вместо разделенных запятыми списков, я бы не советовал называть этот шаблон соответствующим. При сопоставлении с образцом вы проверяете, соответствует ли что-либо шаблону; в назначении Python вы должны убедиться, что последовательности на обеих сторонах одинаковы.

Чтобы делать то, что вам, кажется, нужно, вы обычно (также на функциональных языках) используете либо вспомогательную функцию (как упоминалось другими), либо что-то похожее на конструкции let или where (которые вы можете рассматривать как анонимные функции). Например:

(head, tail) = (x[0], x[1:]) where x = my_func()

Или в реальном питоне:

(head, tail) = (lambda x: (x[0], x[1:]))(my_func())

Обратите внимание, что это, по сути, то же самое, что и решения, предоставленные другими с вспомогательной функцией, за исключением того, что вы хотели одну строку. Это, однако, не обязательно лучше, чем отдельная функция.

(Извините, если мой ответ немного зашкаливает. Я просто думаю, что важно прояснить различие.)

4 голосов
/ 26 октября 2008

Это очень «чисто функциональный» подход, и поэтому он является разумной идиомой в Haskell, но, вероятно, он не очень подходит для Python. Таким образом, Python имеет очень ограниченную концепцию шаблонов - и я подозреваю, что вам может понадобиться более жесткая система типов для реализации такого рода конструкции ( erlang баффы, приглашенные здесь не согласиться) ).

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

Как было заявлено в нескольких случаях до , Python фактически не является функциональным языком. Он просто заимствует идеи из мира FP. По своей природе это не Tail Recursive , как вы ожидаете увидеть встроенным в архитектуру функционального языка, поэтому у вас возникнут некоторые трудности при выполнении такого рода рекурсивных операций с большим набором данных без использования большого количества данных. стекового пространства.

3 голосов
/ 21 июля 2012

Я работаю над pyfpm , библиотекой для сопоставления с образцом в Python с похожим на Scala синтаксисом. Вы можете использовать его для распаковки объектов следующим образом:

from pyfpm import Unpacker

unpacker = Unpacker()

unpacker('head :: tail') << (1, 2, 3)

unpacker.head # 1
unpacker.tail # (2, 3)

Или в списке функций:

from pyfpm import match_args

@match_args('head :: tail')
def f(head, tail):
    return (head, tail)

f(1)          # (1, ())
f(1, 2, 3, 4) # (1, (2, 3, 4))
3 голосов
/ 27 октября 2008

В отличие от Haskell или ML, Python не имеет встроенного сопоставления с образцом структур. Наиболее Pythonic способ сопоставления с шаблоном с блоком try-Кроме:

def recursive_sum(x):
    try:
        head, tail = x[0], x[1:]
        return head + recursive-sum(tail)
    except IndexError:  # empty list: [][0] raises IndexError
        return 0

Обратите внимание, что это работает только с объектами с индексацией фрагментов. Также, если функция усложняется, что-то в теле после строки head, tail может вызвать IndexError, что приведет к незначительным ошибкам. Тем не менее, это позволяет вам делать такие вещи, как:

for frob in eggs.frob_list:
    try:
        frob.spam += 1
    except AttributeError:
        eggs.no_spam_count += 1

В Python хвостовая рекурсия, как правило, лучше реализована в виде цикла с аккумулятором, т. Е .:

def iterative_sum(x):
    ret_val = 0
    for i in x:
        ret_val += i
    return ret_val

Это единственный очевидный, правильный способ сделать это в 99% случаев. Мало того, что он более понятен для чтения, он быстрее и будет работать не только со списками (например, с наборами). Если там произойдет исключение, функция будет успешно завершена и доставит ее по цепочке.

3 голосов
/ 26 октября 2008

расширенная распаковка была введена в 3.0 http://www.python.org/dev/peps/pep-3132/

2 голосов
/ 27 октября 2008

В дополнение к другим ответам, обратите внимание, что эквивалентная операция head / tail в Python, включая расширение синтаксиса * в python3, как правило, будет менее эффективной, чем сопоставление с шаблоном Haskell.

Списки Python реализованы как векторы, поэтому для получения хвоста потребуется копия списка. Это O (n) по отношению к размеру списка, в то время как реализация, использующая связанные списки, такие как Haskell, может просто использовать указатель хвоста, операцию O (1).

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

Например, Подход шифра , если его модифицировать так, чтобы он возвращал итератор, а не преобразовывал его в кортеж, будет иметь такое поведение. В качестве альтернативы более простой двухэлементный метод, не основанный на байт-коде:

def head_tail(lst):
    it = iter(list)
    yield it.next()
    yield it

>>> a, tail = head_tail([1,2,3,4,5])
>>> b, tail = head_tail(tail)
>>> a,b,tail
(1, 2, <listiterator object at 0x2b1c810>)
>>> list(tail)
[3, 4]

Очевидно, что вам все еще нужно обернуться в служебную функцию, а не в хороший синтаксический сахар для нее.

2 голосов
/ 26 октября 2008

Ну, а зачем вам в первую очередь это в 1 строку?

Если вы действительно хотите, вы всегда можете сделать трюк так:

def x(func):
  y = func()
  return y[0], y[1:]

# then, instead of calling my_func() call x(my_func)
(head, rest) = x(my_func) # that's one line :)
1 голос
/ 26 октября 2008

в кулинарной книге питона был рецепт для этого. Я не могу найти его сейчас, но вот код (я слегка его изменил)


def peel(iterable,result=tuple):
    '''Removes the requested items from the iterable and stores the remaining in a tuple
    >>> x,y,z=peel('test')
    >>> print repr(x),repr(y),z
    't' 'e' ('s', 't')
    '''
    def how_many_unpacked():
        import inspect,opcode
        f = inspect.currentframe().f_back.f_back
        if ord(f.f_code.co_code[f.f_lasti])==opcode.opmap['UNPACK_SEQUENCE']:
            return ord(f.f_code.co_code[f.f_lasti+1])
        raise ValueError("Must be a generator on RHS of a multiple assignment!!")
    iterator=iter(iterable)
    hasItems=True
    amountToUnpack=how_many_unpacked()-1
    next=None
    for num in xrange(amountToUnpack):
        if hasItems:        
            try:
                next = iterator.next()
            except StopIteration:
                next = None
                hasItems = False
        yield next
    if hasItems:
        yield result(iterator)
    else:
        yield None

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

0 голосов
/ 29 августа 2018

Для вашего конкретного случая использования - эмуляция Haskell's fun (head : rest) = ..., конечно. Определения функций уже давно поддерживают распаковку параметров:

def my_method(head, *rest):
    # ...

Начиная с Python 3.0, как @bpowah упоминал , Python также поддерживает распаковку при назначении:

my_list = ['alpha', 'bravo', 'charlie', 'delta', 'echo']
head, *rest = my_list
assert head == 'alpha'
assert rest == ['bravo', 'charlie', 'delta', 'echo']

Обратите внимание, что звездочка ("знак") означает "остаток от повторяемого", а не "до конца". Следующее работает отлично:

first, *middle, last = my_list
assert first == 'alpha'
assert last == 'echo'
assert middle == ['bravo', 'charlie', 'delta']

first, *middle, last = ['alpha', 'bravo']
assert first == 'alpha'
assert last == 'bravo'
assert middle == []
...