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']))