Reputation: 699
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
Reputation: 21382
There's two very nice libraries for this:
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
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
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
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
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