Evil Instance Method Alternatives in Python

An evil way to exploit and abuse Python for fun and profit. I have no idea if this belongs in any real code, but if nothing else, call it an introduction to such neat Python features as metaclasses and decorators.

The idea is to create a class which allows specification of an instance-specific bound method (that is, a method that works like a normal method, with implicit self) in the constructor, from an internal set of alternatives, which are added externally using a decorator. The availability of the method should be enforced, but its implementation (and possibly signature) can vary. It should also be easily subclassed, and the set of options should follow the intuitive inheritance rules: subclasses inherit the base class’ alternatives, and can define their own new ones.

First, we will need to import MethodType, to allow dynamic creation of bound methods in instances.

1
from types import MethodType

Then, we define a metaclass that adds a class attribute called _options, which is an empty dict, to the class. The purpose of this dict (and of adding it through a metaclass, rather than directly in the class) will be seen shortly.

3
4
5
6
class EvilType(type):
    def __new__(cls, name, bases, attr):
        attr['_options'] = dict()
        return super(EvilType, cls).__new__(cls, name, bases, attr)

Now, we define the actual Evil base class. This is the root of the evil.

9
10
11
12
13
14
15
class Evil(object, metaclass=EvilType):
    def __init__(self, option):
        try:
            self.option = MethodType(self._options[option], self)
        except KeyError:
            raise KeyError('invalid option method %s' % option)
        self.integer = 0

First, notice the metaclass keyword in the class definition arguments: this tells Python to build the class through EvilType, which gives the class the _options dict. (In Python 2, this would be done instead using the __metaclass__ attribute.)

The __init__ constructor takes a positional argument, a string specifying the method alternative to use in the instance. This is the key to _options, paired with a callable taking one argument (trust me for a moment, it is). A KeyError will prevent the object being created, so every instance of Evil will have an option() method.

The integer instance member is just for demonstration purposes, and is not part of the evil.

So how does _options get populated with callables that can become bound methods? This is where the optionmethod decorator comes in.

17
18
19
20
21
22
23
24
25
26
27
28
    @classmethod
    def _add_to_options(cls, name, func):
        cls._options[name] = func
        for subcls in cls.__subclasses__():
            subcls._add_to_options(name, func)
 
    @classmethod
    def optionmethod(cls, func):
        def wrapped(self, *args, **kwargs):
            return func(self, *args, **kwargs)
        cls._add_to_options(func.__name__, wrapped)
        return wrapped

optionmethod() is a class method, so it has access to the current class’ _options dict, when called as @Evil.optionmethod, for example. The wrapper itself is thin, but before the decorator method exits, it calls _add_to_options(), which adds the wrapped callable to _options of the current class and (importantly) all of its subclasses, recursively. Note that the signature of option() could be enforced by replacing *args and **kwargs in the definition of wrapped() with an explicit signature.

We also provide a convenient public way to get a list of available alternatives for option(). This could, for example, be passed as the choices parameter to an argparse argument definition.

30
31
32
    @classmethod
    def options(cls):
        return cls._options.keys()

Now, we can define some subclasses, without doing anything special. The additional members here, again, are just for demonstration purposes and not part of the evil.

35
36
37
38
class EvilChild(Evil):
    def __init__(self, option):
        self.boolean = False
        super(EvilChild, self).__init__(option)
41
42
43
44
class EvilGrandChild(EvilChild):
    def __init__(self, option):
        self.string = 'foo'
        super(EvilChild, self).__init__(option)

And finally, we define a few method alternatives.

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Evil.optionmethod
def option_one(self):
    self.integer = 1
 
@Evil.optionmethod
def option_any(self, integer):
    self.integer = integer
 
@EvilChild.optionmethod
def option_two(self):
    self.integer = 2
    self.boolean = True
 
@EvilGrandChild.optionmethod
def option_three(self):
    self.integer = 3
    self.boolean = False
    self.string = 'bar'

Now, off to a live Python interpreter!

>>> from evil import *

We can see that Evil indeed has the first two options.

>>> Evil.options()
dict_keys(['option_one', 'option_any'])

Let’s try them out.

>>> asmodeus = Evil('option_one')
>>> asmodeus.integer
0
>>> asmodeus.option()
>>> asmodeus.integer
1
>>> bile = Evil('option_any')
>>> bile.option(13)
>>> bile.integer
13

Cool! How about our subclass?

>>> EvilChild.options()
dict_keys(['option_two', 'option_one', 'option_any'])
>>> cimeries = EvilChild('option_any')
>>> cimeries.option(5)
>>> cimeries.integer, cimeries.boolean
(5, False)
>>> damian = EvilChild('option_two')
>>> damian.boolean
False
>>> damian.option()
>>> damian.integer, damian.boolean
(2, True)

And our sub-subclass?

>>> EvilGrandChild.options()
dict_keys(['option_two', 'option_one', 'option_three', 'option_any'])
>>> eblis = EvilGrandChild('option_three')
>>> eblis.string
'foo'
>>> eblis.option()
>>> eblis.integer, eblis.boolean, eblis.string
(3, False, 'bar')

Now, let’s be really evil and monkey-patch in a new option alternative.

>>> @EvilGrandChild.optionmethod
... def option_baz(self, integer, string='baz'):
...     self.integer = integer
...     self.string = string
... 
>>> EvilGrandChild.options()
dict_keys(['option_two', 'option_baz', 'option_one', 'option_three', 'option_any'])
>>> furfur = EvilGrandChild('option_baz')
>>> furfur.option(9)
>>> furfur.integer, furfur.string
(9, 'baz')

Totally evil!

Feb 20th, 2013
Tags: ,
Comments are closed.