Идиоматический Python has_one

Я только что изобрел дурацкую вспомогательную функцию:

def has_one(seq, predicate=bool):
    """Return whether there is exactly one item in `seq` that matches
    `predicate`, with a minimum of evaluation (short-circuit).
    """
    iterator = (item for item in seq if predicate(item))
    try:
        iterator.next()
    except StopIteration: # No items match predicate.
        return False
    try:
        iterator.next()
    except StopIteration: # Exactly one item matches predicate.
        return True
    return False # More than one item matches the predicate.

Потому что самая читаемая/идиоматическая встроенная вещь, которую я мог придумать, была:

[predicate(item) for item in seq].count(True) == 1

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

Уточнение

Оглядываясь назад, понимаю, что это был довольно дерьмовый вопрос, хотя мы получили несколько отличных ответов! Я искал либо:

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

@Stephan202 придумал действительно крутую идиому для вспомогательной функции и @Martin v. Löwis придумал более простую встроенную идиому в предположении, что предикат возвращает логическое значение . @Спасибо всем за помощь!


person cdleary    schedule 17.10.2009    source источник
comment
Здесь нет вопросов. Или, если есть вопрос, он хорошо спрятан.   -  person S.Lott    schedule 18.10.2009
comment
@S.Lott: Есть ли идиома, которую я здесь забыл, которая мешает мне вырваться из этого помощника?   -  person Martin v. Löwis    schedule 18.10.2009
comment
В любом случае вы должны посмотреть на каждый элемент, если есть ноль или одно совпадение. Рассмотрите возможность использования чего-то подобного, чтобы увидеть, существует ли ровно одно четное простое число.   -  person John La Rooy    schedule 18.10.2009
comment
Как кому-то могут не нравиться итераторы?   -  person Stephan202    schedule 18.10.2009


Ответы (8)


Однако не уверен, что это лучше, чем версии, которые вы предложили...

Если предикат гарантированно возвращает только True/False, то

sum(map(predicate, seq)) == 1

подойдет (хотя он не остановится на втором элементе)

person Martin v. Löwis    schedule 17.10.2009
comment
я думаю, что остановка выполнения на втором элементе - весь смысл этого упражнения - person SilentGhost; 18.10.2009
comment
@SilentGhost: я не уверен. Версия, которую он опубликовал с пониманием списка, этого не делает; он просто говорит, что это странно (по неустановленным причинам). - person Martin v. Löwis; 18.10.2009
comment
Мне это нравится больше, чем вопрос .count(True) для встроенной идиомы. Соответствующее выражение генератора в этом конкретном случае более уродливо, как sum(1 for item in seq if predicate(item)), поэтому я думаю, что map — это путь. Однако технически genexp не полагается на то, что предикат возвращает логическое значение. - person cdleary; 18.10.2009

Как насчет вызова any дважды на итераторе (Python 2.x и 3.x совместим)?

>>> def has_one(seq, predicate=bool):
...     seq = (predicate(e) for e in seq)
...     return any(seq) and not any(seq)
... 
>>> has_one([])
False
>>> has_one([1])
True
>>> has_one([0])
False
>>> has_one([1, 2])
False

any будет принимать не более один элемент, который оценивается как True от итератора. Если это удается в первый раз и терпит неудачу во второй раз, то только один элемент соответствует предикату.

Изменить: я вижу, что Роберт Россни предлагает обобщенную версию, которая проверяет, точно ли n соответствуют предикату. Позвольте мне присоединиться к веселью, используя all:

>>> def has_n(seq, n, predicate=bool):
...     seq = (predicate(e) for e in seq)
...     return all(any(seq) for _ in range(n)) and not any(seq)
... 
>>> has_n(range(0), 3)
False
>>> has_n(range(3), 3)
False
>>> has_n(range(4), 3)
True
>>> has_n(range(5), 3)
False
person Stephan202    schedule 17.10.2009
comment
Мне это нравится, еще одна умная вещь, связанная с итераторами. - person Jochen Ritzel; 18.10.2009
comment
для python2 просто используйте itertools.imap вместо карты - person John La Rooy; 18.10.2009
comment
даже лучше seq = (x for x in seq if predicate(x)), так как 2.x filter рвется. - person Jochen Ritzel; 18.10.2009
comment
@THC4K: да, но нам нужен map, а не filter. Обновил ответ (не более явный map). - person Stephan202; 18.10.2009
comment
Мне это очень нравится, но было бы неплохо, если бы параметр n имел значение по умолчанию 1 ИМХО. - person Chris Lutz; 18.10.2009

Возможно, что-то подобное вам больше по вкусу?

def has_one(seq,predicate=bool):
    nwanted=1
    n=0
    for item in seq:
        if predicate(item):
            n+=1
            if n>nwanted:
                return False

    return n==nwanted

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

person Community    schedule 17.10.2009

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

def has_one(seq):
    g = (x for x in seq)
    return any(g) and not any(g)

Изменить:

Вот более обобщенная версия, поддерживающая предикат:

def has_exactly(seq, count, predicate = bool):
    g = (predicate(x) for x in seq)
    while(count > 0):
        if not any(g):
            return False
        count -= 1
    if count == 0:
        return not any(g)
person Robert Rossney    schedule 17.10.2009
comment
Это умно, но я думаю, что это, вероятно, плохо. Я бы не стал винить людей за то, что, посмотрев на это, пришли к выводу, что он никогда не возвращает истину. - person cdleary; 18.10.2009
comment
Мне это тоже нравится, но оно не позволяет передавать предикат. В любом случае, учитывая это, как насчет g = iter(seq)? - person Stephan202; 18.10.2009
comment
@cdleary, разве в данном случае это не часть идиомы? - person John La Rooy; 18.10.2009
comment
@gnibbler: Вы, вероятно, могли бы обосновать это, но мне не особенно нравятся идиомы, которые, кажется, нарушают фундаментальные законы логики. (Закон исключенного третьего немного более универсален, чем Python ;-) - person cdleary; 18.10.2009
comment
Я бы, конечно, добавил комментарий, объясняющий, что он делает в производственном коде. - person Robert Rossney; 18.10.2009
comment
Обратите внимание, что этот код не работает, если предикат, например. lambda x: not bool(x), потому что это не результат функции предиката, который передается в any. Другими словами, должно быть g = (predicate(x) for x in seq). - person Stephan202; 18.10.2009
comment
@cdleary, я думаю, вам не нравится zip (g, g) для группировки элементов из итератора вместе, тогда либо - person John La Rooy; 18.10.2009
comment
Ладно, это просто беспокоит. - person Robert Rossney; 18.10.2009

Как насчет ...

import functools
import operator

def exactly_one(seq):
    """
    Handy for ensuring that exactly one of a bunch of options has been set.
    >>> exactly_one((3, None, 'frotz', None))
    False
    >>> exactly_one((None, None, 'frotz', None))
    True
    """
    return 1 == functools.reduce(operator.__add__, [1 for x in seq if x])
person offby1    schedule 22.10.2009
comment
functools.reduce(operator.__add__, ...) вот для чего нужна сумма! - person cdleary; 23.10.2009
comment
+1: для имени exactly_one(). sum(1 for x in seq if x) == 1 - person jfs; 25.11.2009

Смотри, Ма! Никакого rtfm("itertools"), никакой зависимости от predicate(), возвращающего логическое значение, минимальная оценка, просто работает!

Python 1.5.2 (#0, Apr 13 1999, 10:51:12) [MSC 32 bit (Intel)] on win32
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> def count_in_bounds(seq, predicate=lambda x: x, low=1, high=1):
...     count = 0
...     for item in seq:
...         if predicate(item):
...             count = count + 1
...             if count > high:
...                 return 0
...     return count >= low
...
>>> seq1 = [0, 0, 1, 0, 1, 0, 1, 0, 0, 0]
>>> count_in_bounds(seq1)
0
>>> count_in_bounds(seq1, low=3, high=3)
1
>>> count_in_bounds(seq1, low=3, high=4)
1
>>> count_in_bounds(seq1, low=4, high=4)
0
>>> count_in_bounds(seq1, low=0, high=3)
1
>>> count_in_bounds(seq1, low=3, high=3)
1
>>>
person John Machin    schedule 24.11.2009

Вот измененный ответ @Stephan202:

from itertools import imap, repeat

def exactly_n_is_true(iterable, n, predicate=None):
    it = iter(iterable) if predicate is None else imap(predicate, iterable)
    return all(any(it) for _ in repeat(None, n)) and not any(it)

Отличия:

  1. predicate() по умолчанию — Нет. Значение такое же, как и для встроенных функций filter() и itertools.ifilter() из stdlib.

  2. Более явные имена функций и параметров (это субъективно).

  3. repeat() позволяет использовать большие n.

Пример:

if exactly_n_is_true(seq, 1, predicate):
   # predicate() is true for exactly one item from the seq
person jfs    schedule 24.11.2009

Это и это простые решения со счетным циклом, безусловно, самые ясные.

Ради интереса, вот вариация на тему any(g) and not any(g), которая на первый взгляд выглядит менее волшебной, но на самом деле она такая же хрупкая, когда дело доходит до ее отладки/модификации (вы не можете изменить порядок, вы должны понимать, как короткое замыкание and передает один итератор между двумя потребителями короткого замыкания...).

def cumulative_sums(values):
    s = 0
    for v in values:
        s += v
        yield s

def count_in_bounds(iterable, start=1, stop=2):
    counter = cumulative_sums(bool(x) for x in iterable)
    return (start in counter) and (stop not in counter)

Тривиально также взять предикат вместо bool, но я думаю, что лучше следовать any() и all(), оставляя это вызывающей стороне - при необходимости легко передать выражение генератора.

Взятие произвольного [start, stop) — приятный бонус, но он не такой общий, как хотелось бы. Заманчиво передать stop=None для подражания, например. any(), который работает, но всегда потребляет весь ввод; правильная эмуляция довольно неудобна:

def any(iterable):
  return not count_in_bounds(iterable, 0, 1)

def all(iterable):
  return count_in_bounds((not x for x in iterable), 0, 1)

Взять переменное количество границ и указать, какие из них должны возвращать True/False, выйдет из-под контроля.
Возможно, простой счетчик с насыщением — лучший примитив:

def count_true(iterable, stop_at=float('inf')):
    c = 0
    for x in iterable:
        c += bool(x)
        if c >= stop_at:
            break
    return c

def any(iterable):
    return count_true(iterable, 1) >= 1

def exactly_one(iterable):
    return count_true(iterable, 2) == 1

def weird(iterable):
    return count_true(iterable, 10) in {2, 3, 5, 7}

all() по-прежнему требует отрицания входных данных или соответствующего помощника count_false().

person Beni Cherniavsky-Paskin    schedule 18.01.2013