2

What's the most efficient way (where "efficient" doesn't necessarily mean fast, but "elegant", or "maintainable") to do type check when setting attributes in an object?

I can use __slots__ to define the allowed attributes, but how should I constrain the types?

Surely I can write "setter" methods for each attribute, but I find it a bit cumbersome to maintain since my type checks are usually simple.

So I'm doing something like this:

import datetime

# ------------------------------------------------------------------------------
# MyCustomObject
# ------------------------------------------------------------------------------
class MyCustomObject(object):
    pass

# ------------------------------------------------------------------------------
# MyTypedObject
# ------------------------------------------------------------------------------
class MyTypedObject(object):     
    attr_types = {'id'         : int,
                  'start_time' : datetime.time,
                  'duration'   : float,
                  'reference'  : MyCustomObject,
                  'result'     : bool,
                  'details'    : str}

    __slots__ = attr_types.keys()

    # --------------------------------------------------------------------------
    # __setattr__
    # --------------------------------------------------------------------------
    def __setattr__(self, name, value):
        if name not in self.__slots__:
            raise AttributeError(
                "'%s' object has no attribute '%s'" 
                % (self.__class__.__name__, name))
        if type(value) is not self.attr_types[name]:
                raise TypeError(
                    "'%s' object attribute '%s' must be of type '%s'" 
                    % (self.__class__.__name__, name, 
                       self.attr_types[name].__name__))
        # call __setattr__ on parent class
        super(MyTypedObject, self).__setattr__(name, value)

Which works fine for my purpose:

>>> my_typed_object            = MyTypedObject()
>>> my_typed_object.id         = "XYZ"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 28, in __setattr__
TypeError: 'MyTypedObject' object attribute 'id' must be of type 'int'
>>> my_typed_object.id         = 123
>>> my_typed_object.reference  = []
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 28, in __setattr__
TypeError: 'MyTypedObject' object attribute 'reference' must be of type 'MyCustomObject'
>>> my_typed_object.reference  = MyCustomObject()
>>> my_typed_object.start_time = "13:45"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 28, in __setattr__
TypeError: 'MyTypedObject' object attribute 'start_time' must be of type 'time'
>>> my_typed_object.start_time = datetime.time(13, 45)

Is there a better way to do this? Having worked with Python for a while now, I feel like I'm reinventing the wheel.

E.Z.
  • 6,393
  • 11
  • 42
  • 69
  • 1
    You could use [Enthought Traits](http://code.enthought.com/projects/traits/). – millimoose Nov 19 '12 at 15:00
  • 3
    Don't. Python is duck typed for a reason. – Katriel Nov 19 '12 at 15:03
  • @millimoose: could you post it as an answer? That's what I was looking for :) – E.Z. Nov 21 '12 at 09:02
  • @katrielalex The reason isn't "do not ever restrict or validate inputs to your code". Defensive programming and fail-fast behaviour is desirable in any programming language. Doing so without explicit type checks is only reasonably easy with the simple contracts of the built-in types, like "iterable", "coercible to a number", "coercible to a truth value". There's certainly a time and a place for duck typing – the internals of a module. But when doing a domain model, or at an API surface, it's mostly a bad idea. – millimoose Nov 21 '12 at 12:53
  • @katrielalex Consider the situation where you create an object `Foo` that expects an object of type `Bar` but doesn't really do anything with it in the constructor. Someone calling your code makes a mistake and passes the wrong type in. Now when you actually call the method of `Foo` that uses a method of `Bar`, the caller of your code gets an `AttributeError`, reporting a missing method that he probably doesn't even recognize (if it's internal to your module), on a line of code that might be completely unrelated to where the error originated. This would make for "fun" debugging. – millimoose Nov 21 '12 at 13:00
  • @millimoose: right, that is a standard argument for strict typechecking. But consider the converse case that you have written a `DatabaseLoggedInt` that looks a bit like an int but is really backed by a database and logging information. If you strictly require `int`s then `Foo` won't work, even if it would be perfectly happy with a `DatabaseLoggedInt`. A good compromise is often to implement a `collections.Number` or similar. – Katriel Nov 21 '12 at 13:13
  • BTW I think your claim about duck typing is precisely backwards. It's not particularly helpful within a module, because you have written both the producer and the consumer of data anyway. It's useful when _other people_ want to use _your API_, because you only require the minimum necessary from them. – Katriel Nov 21 '12 at 13:14
  • @katrielalex It's really a judgement call. The tradeoff is early error detection vs. allowing flexibility to API clients vs. the effort required to do both (by doing fine-grained validation of the input parameters). I don't think preferring either is the one right answer. – millimoose Nov 21 '12 at 13:16
  • @katrielalex Also, if I expect an `int`, I'd still do a type check (of a sort) by calling `int(x)`. If the object passed defines an `__int__()`, it's safe to assume that whatever that returns is sufficiently `int`-like. Now, you could apply this pattern of typecheck-via-coercion consistently throughout a codebase, and it's certainly more flexible than a flat out `isinstance`. It's also fairly laborious, and less conceptually familiar than, say, Python's ABC concept. (In my opinion a decent compromise when it comes to documenting a protocol – which you have to do anyway – and checking for it.) – millimoose Nov 21 '12 at 13:42

2 Answers2

1

You should ask yourself why you feel the need to do this. It's certainly not very Pythonic. Normally in Python we don't demand that attributes have specific types: instead, we document the expected types, and assume that any actual parameters conform. Note that this can mean a completely unrelated type that implements the same method: for example, we might expect that a parameter is iterable, without specifically demanding that it inherits from list or tuple.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • +1, I certainly agree with you on that. This came up as a solution for a very particular situation, sorry I can't be much more specific :/ – E.Z. Nov 19 '12 at 15:13
1

A library that already implements what you're looking for (and provides a bunch of other features) is Enthought Traits.

millimoose
  • 39,073
  • 9
  • 82
  • 134