Комбинация бесточечных функций в Python

У меня есть некоторые предикаты, например:

is_divisible_by_13 = lambda i: i % 13 == 0
is_palindrome = lambda x: str(x) == str(x)[::-1]

и хотите логически объединить их, как в:

filter(lambda x: is_divisible_by_13(x) and is_palindrome(x), range(1000,10000))

Теперь возникает вопрос: может ли такая комбинация быть написана в стиле без точек, например:

filter(is_divisible_by_13 and is_palindrome, range(1000,10000))

Это, конечно, не имеет желаемого эффекта, поскольку истинное значение лямбда-функций равно True, а and и or являются операторами короткого замыкания. Самое близкое, что я придумал, это определить класс P, который представляет собой простой контейнер предикатов, который реализует __call__() и имеет методы and_() и or_() для объединения предикатов. Определение P выглядит следующим образом:

import copy

class P(object):
    def __init__(self, predicate):
        self.pred = predicate

    def __call__(self, obj):
        return self.pred(obj)

    def __copy_pred(self):
        return copy.copy(self.pred)

    def and_(self, predicate):
        pred = self.__copy_pred()
        self.pred = lambda x: pred(x) and predicate(x)
        return self

    def or_(self, predicate):
        pred = self.__copy_pred()
        self.pred = lambda x: pred(x) or predicate(x)
        return self

Теперь с помощью P я могу создать новый предикат, представляющий собой комбинацию таких предикатов:

P(is_divisible_by_13).and_(is_palindrome)

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


person Frank S. Thomas    schedule 07.02.2012    source источник
comment
Похоже, вы ужасно стараетесь заставить нефункциональный язык вести себя как функциональный язык. Вы привязаны к Python?   -  person Eric Andres    schedule 08.02.2012
comment
@ Эрик: Да, вроде. Мой код Python встроен в проект C++, и я не могу просто переключиться на другой язык.   -  person Frank S. Thomas    schedule 08.02.2012


Ответы (5)


Вы можете переопределить оператор & (побитовое И) в Python, добавив метод __and__ в класс P. Затем вы можете написать что-то вроде:

P(is_divisible_by_13) & P(is_palindrome)

или даже

P(is_divisible_by_13) & is_palindrome

Точно так же вы можете переопределить оператор | (побитовое ИЛИ), добавив метод __or__, и оператор ~ (побитовое отрицание), добавив метод __not__. Обратите внимание, что вы не можете переопределить встроенный оператор and, or и not, так что это, вероятно, максимально близко к вашей цели. Вам все еще нужно иметь экземпляр P в качестве крайнего левого аргумента.

Для полноты вы также можете переопределить варианты на месте (__iand__, __ior__) и правосторонние варианты (__rand__, __ror__) этих операторов.

Пример кода (непроверенный, не стесняйтесь исправлять):

class P(object):
    def __init__(self, predicate):
        self.pred = predicate

    def __call__(self, obj):
        return self.pred(obj)

    def __copy_pred(self):
        return copy.copy(self.pred)

    def __and__(self, predicate):
        def func(obj):
            return self.pred(obj) and predicate(obj)
        return P(func)

    def __or__(self, predicate):
        def func(obj):
            return self.pred(obj) or predicate(obj)
        return P(func)

Еще один трюк, который приблизит вас к безточечной нирване, — это следующий декоратор:

from functools import update_wrapper

def predicate(func):
    """Decorator that constructs a predicate (``P``) instance from
    the given function."""
    result = P(func)
    update_wrapper(result, func)
    return result

Затем вы можете пометить свои предикаты декоратором predicate, чтобы автоматически сделать их экземпляром P:

@predicate
def is_divisible_by_13(number):
    return number % 13 == 0

@predicate
def is_palindrome(number):
    return str(number) == str(number)[::-1]

>>> pred = (is_divisible_by_13 & is_palindrome)
>>> print [x for x in xrange(1, 1000) if pred(x)]
[494, 585, 676, 767, 858, 949]
person Tamás    schedule 07.02.2012
comment
Я не уверен, что хочу перегрузить операторы bitwise другим значением, но я нахожу идею декоратора очень интересной. Спасибо! - person Frank S. Thomas; 08.02.2012
comment
@FrankS.Thomas: Имеют ли смысл побитовые операции с вашими предикатами? Если нет, то ничего страшного в их перегрузке. - person Ethan Furman; 08.02.2012

По сути, ваш подход кажется единственно возможным в Python. На github есть модуль python, использующий примерно тот же механизм для реализации бесточечной композиции функций.

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

Но в остальном это технически не бесточечный код, а просто «точечный скрытый», если хотите. Которого вам может и не хватить.

person rwos    schedule 07.02.2012

Вы можете использовать рецепт оператора Infix:

AND = Infix(lambda f, g: (lambda x: f(x) and g(x)))
for n in filter(is_divisible_by_13 |AND| is_palindrome, range(1000,10000)):
    print(n)

урожаи

1001
2002
3003
4004
5005
6006
7007
8008
9009
person Reinstate Monica    schedule 07.02.2012
comment
Вероятно, это Python Hack of the Year в 2012 году. По крайней мере, для меня. - person Tamás; 08.02.2012
comment
@Tamás: Рецепт уже является лучшим лайфхаком 2005 года :-) Кроме того, это Фердинанд Ямицкий, не я. Я только (абу?) использовал его для своего ответа. - person Reinstate Monica; 08.02.2012

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

compose2 = lambda f,g: lambda x: f(g(x))
compose = lambda *ff: reduce(ff,compose2)

filter(compose(is_divisible_by_13, is_palindrome, xrange(1000)))
person Marcin    schedule 20.07.2013
comment
должно быть reduce(compose2, ff) или просто reduce(lambda f, g: lambda x: f(g(x)), ff) - person modular; 10.03.2017

Это было бы моим решением:

class Chainable(object):

    def __init__(self, function):
        self.function = function

    def __call__(self, *args, **kwargs):
        return self.function(*args, **kwargs)

    def __and__(self, other):
        return Chainable( lambda *args, **kwargs:
                                 self.function(*args, **kwargs)
                                 and other(*args, **kwargs) )

    def __or__(self, other):
        return Chainable( lambda *args, **kwargs:
                                 self.function(*args, **kwargs)
                                 or other(*args, **kwargs) )

def is_divisible_by_13(x):
    return x % 13 == 0

def is_palindrome(x):
    return str(x) == str(x)[::-1]

filtered = filter( Chainable(is_divisible_by_13) & is_palindrome,
                   range(0, 100000) )

i = 0
for e in filtered:
    print str(e).rjust(7),
    if i % 10 == 9:
        print
    i += 1

И это мой результат:

    0     494     585     676     767     858     949    1001    2002    3003
 4004    5005    6006    7007    8008    9009   10101   11011   15951   16861
17771   18681   19591   20202   21112   22022   26962   27872   28782   29692
30303   31213   32123   33033   37973   38883   39793   40404   41314   42224
43134   44044   48984   49894   50505   51415   52325   53235   54145   55055
59995   60606   61516   62426   63336   64246   65156   66066   70707   71617
72527   73437   74347   75257   76167   77077   80808   81718   82628   83538
84448   85358   86268   87178   88088   90909   91819   92729   93639   94549
95459   96369   97279   98189   99099
person Niklas R    schedule 08.02.2012