Sobrecarga de Operdadores em Python

By Paulo Scardine - http://goo.gl/Ke1P0p

O problema

No site StackOverflow um usuário de R perguntou como implementar o pipe-operator do pacote dplyr (%>%), onde x %>% f(y) é equivalente a f(x, y). Adicionalmente, ele gostaria de usar uma sintaxe parecida com o pacote Pipe do cheese shop:

df = df | select('one') | rename(one='new_one')

No pacote Pipe esta sintaxe é chamada de "infix notation", e é equivalente a:

df = rename(select(df, 'one'), one='new_one')

In [3]:
import pandas as pd

df = pd.DataFrame({'one' : [1., 2., 3., 4., 4.],
                   'two' : [4., 3., 2., 1., 3.]})

def select(df, *args):
    return df[list(args)]


def rename(df, **kwargs):
    for name, value in kwargs.items():
        df = df.rename(columns={'%s' % name: '%s' % value})
    return df

In [4]:
df


Out[4]:
one two
0 1 4
1 2 3
2 3 2
3 4 1
4 4 3

In [5]:
select(df, 'one')


Out[5]:
one
0 1
1 2
2 3
3 4
4 4

In [6]:
rename(select(df, 'one'), one='other')


Out[6]:
other
0 1
1 2
2 3
3 4
4 4

Como sobrecarregar operadores em Python

Para cada operador em Python existe um ou mais métodos mágicos __dunder__, um para a operação normal e um para a operação "à direita". Por exemplo, para implementar o operador +, você precisa sobrecarregar o método __add__.


In [7]:
class Idem(object):
    def __add__(self, other):
        return other * 2

    
idem = Idem()

In [8]:
idem + 5


Out[8]:
10

In [9]:
5 + idem


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-e85f72d6ce93> in <module>()
----> 1 5 + idem

TypeError: unsupported operand type(s) for +: 'int' and 'Idem'

In [24]:
class Idem(object):
    def __add__(self, other):
        return other * 2
    
    def __radd__(self, other):
        return self.__add__(other)
    
idem = Idem()

In [25]:
5 + idem


Out[25]:
10

Um exemplo polêmico

Somar datetime.date com datetime.time:


In [11]:
import datetime

data = datetime.date.today()
hora = datetime.time(19)

In [21]:
data


Out[21]:
SmartDate(2015, 11, 23)

In [13]:
hora


Out[13]:
datetime.time(19, 0)

In [14]:
datetime.datetime.now()


Out[14]:
datetime.datetime(2015, 11, 23, 20, 24, 15, 858188)

In [15]:
data + hora


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-8c7c29d2106d> in <module>()
----> 1 data + hora

TypeError: unsupported operand type(s) for +: 'datetime.date' and 'datetime.time'

In [16]:
class SmartDate(datetime.date):
    def __add__(self, other):
        if isinstance(other, datetime.time):
            return datetime.datetime.combine(self, other)
        return super(SmartDate, self).__add__(other)

In [17]:
data = SmartDate(*data.timetuple()[:3])

In [18]:
data + hora


Out[18]:
datetime.datetime(2015, 11, 23, 19, 0)

Princípio da Menor Surpresa

Finalmente


In [19]:
def pipe(original):
    class PipeInto(object):
        data = {'function': original}

        def __init__(self, *args, **kwargs):
            self.data['args'] = args
            self.data['kwargs'] = kwargs

        def __rrshift__(self, other):
            return self.data['function'](
                other, 
                *self.data['args'], 
                **self.data['kwargs']
            )

    return PipeInto


@pipe
def select(df, *args):
    return df[list(cols)]


@pipe
def rename(df, **kwargs):
    for name, value in kwargs.items():
        df = df.rename(columns={'%s' % name: '%s' % value})
    return df

In [28]:
df >> select('two', 'one')


Out[28]:
two one
0 4 1
1 3 2
2 2 3
3 1 4
4 3 4

In [29]:
df


Out[29]:
one two
0 1 4
1 2 3
2 3 2
3 4 1
4 4 3

In [32]:
df >> select('one') >> rename(one='first')


Out[32]:
first
0 1
1 2
2 3
3 4
4 4

In [37]:
16 << 1


Out[37]:
32

Takeaways

  • Python is awesome
  • Grupy is awesome
  • VivaReal is awesome
  • Tenham juízo, crianças!

http://goo.gl/Ke1P0p

Perguntas ???


In [ ]: