Tikitu
Tikitu

Reputation: 699

What is the pythonic way to add type information to an object's attributes?

I'm building classes where I know the intended types of the attributes, but Python of course doesn't. While it's un-pythonic to want to tell it, supposing I do want to, is there an idiomatic way to do so?

Why: I'm reading in foreign-format data (without type information) involving objects-nested-inside-objects. It's easy to put it into nested dictionaries, but I want it in objects of my class-types, to get the right behaviours as well as the data. For instance: suppose my class Book has an attribute isbn which I will fill with an ISBNumber object. My data gives me the isbn as a string; I would like to be able to look at Book and say "That field should be filled by ISBNumber(theString)."

Bonus glee for me if the solution can be applied to classes I get from someone else without editing their code.

(I'm restricted to 2.6, although interested in solutions for 3.x if they exist.)

Upvotes: 1

Views: 1756

Answers (5)

frmdstryr
frmdstryr

Reputation: 21382

There's two very nice libraries for this:

  1. atom
  2. traitlets

They both allow you to restrict attributes to a specific type and provide means of notification when an attribute changes.

from atom.api import Atom, Unicode, Range, Bool, observe
class Person(Atom):
    """ A simple class representing a person object.

    """
    last_name = Unicode()

    first_name = Unicode()

    age = Range(low=0)

    debug = Bool(False)

Upvotes: 0

Ants Aasma
Ants Aasma

Reputation: 54935

There are many ways to achieve something like this. If coupling the input format to your object model is acceptable then you could use descriptors to create type adaptors:

class TypeAdaptingProperty(object):
    def __init__(self, key, type_, factory=None):
        self.key = key
        self.type_ = type_
        if factory is None:
            self.factory = type_

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.key)

    def __set__(self, instance, value):
        if not isinstance(value, self.type_):
            value = self.factory(value)
        setattr(instance, self.key, value)

    def __delete__(self, instance):
        delattr(instance, self.key)

class Book(object):
    isbn = TypeAdaptingProperty('isbn_', ISBNNumber)

b = Book()
b.isbn = 123 # Does the equivalent of b.isbn = ISBNNumber(123)

However if you don't fully control the message structure, such coupling isn't a good idea. In such cases I like to use the interpreter pattern to adapt input messages to output types. I create a small framework that enables me to build declarative object structures to process the input data.

The framework may look something like this:

class Adaptor(object):
    """Any callable can be an adaptor. This base class just proxies calls
    to an appropriately named method."""
    def __call__(self, input):
        return self.adapt(input)

class ObjectAdaptor(Adaptor):
    """Adaptor to create objects adapting the input value to the
    factory function/constructor arguments, and optionally setting
    fields after construction."""
    def __init__(self, factory, args=(), kwargs={}, fields={}):
        self.factory = factory
        self.arg_adaptors = args
        self.kwarg_adaptors = kwargs
        self.field_adaptors = fields

    def adapt(self, input):
        args = (adaptor(input) for adaptor in self.arg_adaptors)
        kwargs = dict((key, adaptor(input)) for key,adaptor in self.kwarg_adaptors.items())
        obj = self.factory(*args, **kwargs)
        for key, adaptor in self.field_adaptors.items():
            setattr(obj, key, adaptor(input))
        return obj

def TypeWrapper(type_):
    """Converts the input to the specified type."""
    return ObjectAdaptor(type_, args=[lambda input:input])

class ListAdaptor(Adaptor):
    """Converts a list of objects to a single type."""
    def __init__(self, item_adaptor):
        self.item_adaptor = item_adaptor
    def adapt(self, input):
        return map(self.item_adaptor, input)

class Pick(Adaptor):
    """Picks a key from an input dictionary."""
    def __init__(self, key, inner_adaptor):
        self.key = key
        self.inner_adaptor = inner_adaptor
    def adapt(self, input):
        return self.inner_adaptor(input[self.key])

The message adaptors look something like this:

book_message_adaptor = ObjectAdaptor(Book, kwargs={
    'isbn': Pick('isbn_number', TypeWrapper(ISBNNumber)),
    'authors': Pick('authorlist', ListAdaptor(TypeWrapper(Author)))
})

Notice that the message structure names might not be the same as the object model.

Message processing itself looks like this:

message = {'isbn_number': 123, 'authorlist': ['foo', 'bar', 'baz']}
book = book_message_adaptor(message)
# Does the equivalent of:
# Book(isbn=ISBNNumber(message['isbn_number']),
#      authors=map(Author, message['author_list']))

Upvotes: 2

Noufal Ibrahim
Noufal Ibrahim

Reputation: 72855

It's perhaps not exactly what you want but the Enthought traits API does give you an ability to add explicit typing to Python class attributes. It might work for you.

Upvotes: 1

msw
msw

Reputation: 43527

That's - in principle, at least - what an object's __repr__ function is supposed to enable: the unambiguous reconstruction of an object.

If your class is defined like:

class ISBN(object):
    def __init__(self, isbn):
        self.isbn = isbn

    def __repr__(self):
        return 'ISBN(%r)' % self.isbn

Then you are one eval away from reconstituting your original object. See also the pickle module.

Riffing on the ISBN class then gives you a book class

class Book(object):
    def __init__(self, title, author, isbn):
        self.title = title
        self.author = author
        self.isbn = isbn

    def __repr__(self):
        return 'Book(%r, %r, %r)' % (self.title, self.author, self.isbn)

good_book = Book("Cat's Cradle", 'Kurt Vonnegut, Jr.', ISBN('038533348X'))
bad_book = Book('The Carpetbaggers', 'Harold Robbins', ISBN('0765351463'))
library = [good_book, bad_book]
print library
# => [Book("Cat's Cradle", 'Kurt Vonnegut, Jr.', ISBN('038533348X')), 
      Book('The Carpetbaggers', 'Harold Robbins', ISBN('0765351463'))]
reconstruct = eval(str(library))
print reconstruct
# => [Book("Cat's Cradle", 'Kurt Vonnegut, Jr.', ISBN('038533348X')),
      Book('The Carpetbaggers', 'Harold Robbins', ISBN('0765351463'))]

Of course your objects might do more than just construct and reconstruct themselves... and the caveat about eval that Tamás noted.

Upvotes: 1

Tamás
Tamás

Reputation: 48101

I'm assuming that you have already considered the pickle module and it is not suitable for your purposes. In that case, you can attach an attribute to your class that specifies the type for each attribute:

class MyClass(object):
    _types = {"isbn": ISBNNumber}

Upon reconstruction, you iterate over _types and try to enforce the type:

for name, type_name in MyClass._types.iteritems():
    if hasattr(obj, name):
        value = getattr(obj, name)
        if not isinstance(value, type_name):
            setattr(obj, name, type_name(value))

In the code sample above, obj is the object being reconstructed and I assume that the attributes are already assigned in a string format (or whatever you get from unserialisation).

If you want this to work with third-party classes where you cannot alter the source code, you can attach the _types attribute to the class at run-time after having imported it from somewhere else. For instance:

>>> from ConfigParser import ConfigParser
>>> cp = ConfigParser()
>>> cp._types
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ConfigParser instance has no attribute '_types'
>>> ConfigParser._types = {"name": "whatever"}
>>> cp._types
{"name": "whatever"}

I also agree with using __repr__ and eval if you have full control over what you will get from your input files; however, if any user input is involved, using eval leads to the possibility of arbitrary code execution, which is quite dangerous.

Upvotes: 1

Related Questions