5

I need to create an object that would raise a custom exception, UnusableObjectError, when it is used in any way (creating it should not create an exception though).

a = UnusableClass()     # No error
b = UnusableClass()     # No error
a == 4                  # Raises UnusableObjectError
'x' in a                # Raises UnusableObjectError
for i in a:             # Raises UnusableObjectError
    print(i)

# ..and so on

I came up with the code below which seems to behave as expected.

class UnusableObjectError(Exception):
    pass

CLASSES_WITH_MAGIC_METHODS = (str(), object, float(), dict())

# Combines all magic methods I can think of.
MAGIC_METHODS_TO_CHANGE = set()
for i in CLASSES_WITH_MAGIC_METHODS:
    MAGIC_METHODS_TO_CHANGE |= set(dir(i))
MAGIC_METHODS_TO_CHANGE.add('__call__')
# __init__ and __new__ must not raise an UnusableObjectError
# otherwise it would raise error even on creation of objects.
MAGIC_METHODS_TO_CHANGE -= {'__class__', '__init__', '__new__'}


def error_func(*args, **kwargs):
    """(nearly) all magic methods will be set to this function."""
    raise UnusableObjectError


class UnusableClass(object):
    pass

for i in MAGIC_METHODS_TO_CHANGE:
    setattr(UnusableClass, i, error_func)

(some improvements made, as suggested by Duncan in comments)


Questions:
Is there an already existing class that behaves as described?

If not, is there any flaw in my UnusableClass() (e.g., situations when using the instances of the class wouldn't raise an error) and if so, how can I fix those flaws?

user
  • 5,370
  • 8
  • 47
  • 75
  • 3
    Setting `__` methods on an instance won't generally work as you expect. You should define them directly on the class instead. Redefining `__getattribute__` to block access to any attribute other than the ones you want to permit would probably be a simpler way to do what you're attempting. – Duncan Sep 10 '15 at 12:12
  • 1
    One example that won't work as you expect, try calling an instance: `a()` gives `TypeError: 'UnusableClass' object is not callable` because setting `__call__` on the instance is ignored. Likewise `a[1]=0`. Of course since these aren't implemented they will still throw an error just not your custom error. Define on the class could be with `def` or just use `setattr` on the class rather than on the instance. – Duncan Sep 10 '15 at 12:57
  • This also seems like a perfect use-case for metaclasses... – thebjorn Sep 10 '15 at 13:02
  • @thebjorn: Not really. This seems like a one-off thing, and creating a separate metaclass for a *single* class with a *single* object seems kinda excessive. –  Sep 10 '15 at 14:25

2 Answers2

2

Turns out metaclasses and dunder (double underscore) methods don't go well together (which is unfortunate, since that would have been a more streamlined way to implement this).

I couldn't find any importable listing of magic method names, so I created one and put it on PyPi (https://pypi.python.org/pypi/magicmethods/0.1.1). With it, the implementation of UnusableClass can be written as a simple class decorator:

import magicmethods

class UnusableObjectError(Exception): 
    pass

def unusable(cls):
    def _unusable(*args, **kwargs):
        raise UnusableObjectError()

    for name in set(magicmethods.all) - set(magicmethods.lifecycle):
        setattr(cls, name, _unusable)
    return cls

@unusable
class UnusableClass(object):
    pass

magicmethods.lifecycle contains __new__, __init__, and __del__. You might want to adjust this..

This implementation also handles:

a = UnusableClass()
with a:
    print 'oops'
thebjorn
  • 26,297
  • 11
  • 96
  • 138
1

You can use __getattribute__ to block all access to attributes, except special __ attributes like __contains__ or __eq__ which are not catched by __getattribute__, and use a whitelist to allow access to some methods:

class UnuseableClass(object):

    whitelist = ('alpha', 'echo',)

    def __init__(self):
        self.alpha = 42

    def echo(self, text):
        print text

    def not_callable(self):
        return 113

    def __getattribute__(self, name):
        if name in type(self).whitelist:
            return super(UnuseableClass, self).__getattribute__(name)
        else:
            raise Exception('Attribute is not useable: %s' % name)


unuseable_object = UnuseableClass()
print(unuseable_object.alpha)
unuseable_object.echo('calling echo')

try:
    unuseable_object.not_callable()
except Exception as exc:
    print(exc.message)

If you really need to catch even special method calls, you can use How to catch any method called on an object in python?.

Community
  • 1
  • 1
amirouche
  • 7,682
  • 6
  • 40
  • 94