0

I'm trying to create a wrapper that blocks the execution of some methods. The classic solution is to use this pattern:

class RestrictingWrapper(object):
    def __init__(self, w, block):
        self._w = w
        self._block = block
    def __getattr__(self, n):
        if n in self._block:
            raise AttributeError, n
        return getattr(self._w, n)

The problem with this solution is the overhead that introduces in every call, so I am trying to use a MetaClass to accomplish the same task. Here is my solution:

class RestrictingMetaWrapper(type):
    def __new__(cls, name, bases, dic):
        wrapped = dic['_w']
        block = dic.get('_block', [])

        new_class_dict = {}
        new_class_dict.update(wrapped.__dict__)
        for attr_to_block in block:
            del new_class_dict[attr_to_block]
        new_class_dict.update(dic)

        return type.__new__(cls, name, bases, new_class_dict)

Works perfectly with simple classes:

class A(object):
    def __init__(self, i):
        self.i = i
    def blocked(self):
        return 'BAD: executed'
    def no_blocked(self):
        return 'OK: executed'
class B(object):
    __metaclass__ = RestrictingMetaWrapper
    _w = A
    _block = ['blocked']

b= B('something')
b.no_blocked  # 'OK: executed'
b.blocked     # OK: AttributeError: 'B' object has no attribute 'blocked'

The problem comes with 'more complex' classes like ndarray from numpy:

class NArray(object):
    __metaclass__ = RestrictingMetaWrapper
    _w = np.ndarray
    _block = ['max']

na = NArray()        # OK
na.max()             # OK: AttributeError: 'NArray' object has no attribute 'max'
na = NArray([3,3])   # TypeError: object.__new__() takes no parameters
na.min()             # TypeError: descriptor 'min' for 'numpy.ndarray' objects doesn't apply to 'NArray' object

I assume that my metaclass is not well defined because other classes (ex: pandas.Series) suffer weird errors, like not blocking the indicated methods.

Could you find where the error is? Any other idea to solve this problem?

UPDATE: The nneonneo's solution works great, but seems like wrapped classes can break the blocker with some black magic inside the class definition.

Using the nneonneo's solution:

import pandas

@restrict_methods('max')
class Row(pandas.Series):
    pass

r = Row([1,2,3])
r.max()        # BAD: 3      AttributeError expected  
crispamares
  • 530
  • 4
  • 9
  • This is a fairly dodgy use of a metaclass. In terms of functionality, it's not a metaclass at all, it's a function which takes a class (along with some others stuff) and returns a new class. – Ben Sep 25 '12 at 05:26
  • Actually this is my first meta-programming, ¿Do you know any other way to implement this patter without overhead? – crispamares Sep 25 '12 at 05:29
  • 1
    Just a warning: subclassing `ndarray` is a lot of fun, but takes longer than expected. Here's a [tutorial](http://docs.scipy.org/doc/numpy/user/basics.subclassing.html) to get you started... – Pierre GM Sep 25 '12 at 08:26
  • @crispamares I wouldn't worry about overhead overly much. (If I ever had the need to do this I would probably just go with your original `RestrictingWrapper`). But if I were determined to do this by hacking the classes, I would use a function that transforms classes (which can be used as a class deocorator, but can also be invoked after class definition time if it's someone else's class you're modifying). – Ben Sep 25 '12 at 11:26

1 Answers1

1

As it says in the TypeError, min (and related functions) will only work on instances of np.ndarray; thus, the new subclass must inherit from the class you are trying to wrap.

Then, since you extend the base class, you have to replace the methods with a suitable descriptor:

class RestrictedMethod(object):
    def __get__(self, obj, objtype):
        raise AttributeError("Access denied.")

class RestrictingMetaWrapper(type):
    def __new__(cls, name, bases, dic):
        block = dic.get('_block', [])

        for attr in block:
            dic[attr] = RestrictedMethod()

        return type.__new__(cls, name, bases, dic) # note we inject the base class here

class NArray(np.ndarray):
    __metaclass__ = RestrictingMetaWrapper
    _block = ['max']

Note: enterprising applications can still access "restricted" functionality through the base class methods (e.g. np.ndarray.max(na)).

EDIT: Simplified the wrapper and made it transparently subclassable.

Note that this can all be done in a simpler way using a class decorator:

class RestrictedMethod(object):
    def __get__(self, obj, objtype):
        raise AttributeError("Access denied.")

def restrict_methods(*args):
    def wrap(cls):
        for attr in args:
            setattr(cls, attr, RestrictedMethod())
        return cls
    return wrap

@restrict_methods('max', 'abs')
class NArray(np.ndarray):
    pass
nneonneo
  • 171,345
  • 36
  • 312
  • 383
  • Instead of editing my answer, please reply as a comment. That way I get notified about it. – nneonneo Sep 25 '12 at 05:37
  • Wow, really neat! Thanks a lot! I just need to figure out what kind of black magic is using pandas.Series. See the update my initial question. – crispamares Sep 25 '12 at 06:20