Точечная комбинация функций в Python - PullRequest
8 голосов
/ 08 февраля 2012

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

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))

Вопрос теперь: можно ли написать такую ​​комбинацию в стиле pointfree , например:

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 и без использования (лямбда) функций?

Ответы [ 5 ]

8 голосов
/ 08 февраля 2012

Вы можете переопределить оператор & (побитовое И) в 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]
3 голосов
/ 08 февраля 2012

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

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

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

2 голосов
/ 08 февраля 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
1 голос
/ 20 июля 2013

В 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)))
1 голос
/ 08 февраля 2012

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

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
...