如何以Pythonic方式为对象属性添加类型信息?
我正在创建一些类,虽然我知道这些属性应该是什么类型,但Python并不知道。虽然想要告诉Python这些信息有点不符合Python的风格,但如果我真的想这么做,有没有简单的方法可以实现呢?
原因是:我在处理一些外部格式的数据(没有类型信息),这些数据涉及到对象嵌套对象。把这些数据放进嵌套的字典里很简单,但我希望能把它们放进我自己定义的类对象里,这样不仅能得到数据,还能获得正确的行为。举个例子:假设我的类叫做 Book
,它有一个属性 isbn
,我会用一个 ISBNumber
对象来填充这个属性。我的数据给我的是一个字符串形式的isbn;我希望能查看 Book
类,并说“这个字段应该用 ISBNumber(theString)
来填充。”
如果这个解决方案还能应用到我从别人那里得到的类上,而不需要修改他们的代码,那就更好了。
(我只能使用Python 2.6,不过如果有适用于3.x的解决方案我也很感兴趣。)
5 个回答
我假设你已经考虑过 pickle
模块,但觉得它不适合你的需求。在这种情况下,你可以给你的类添加一个属性,用来指定每个属性的类型:
class MyClass(object):
_types = {"isbn": ISBNNumber}
在重建对象时,你需要遍历 _types
,并尝试强制执行类型:
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))
在上面的代码示例中,obj
是正在重建的对象,我假设这些属性已经以字符串格式(或者你从反序列化中得到的其他格式)被赋值。
如果你想让这个方法适用于你无法修改源代码的第三方类,你可以在运行时将 _types
属性附加到类上,前提是你已经从其他地方导入了这个类。例如:
>>> 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"}
我也同意在你对输入文件的内容有完全控制的情况下使用 __repr__
和 eval
;但是,如果涉及到任何用户输入,使用 eval
就有可能导致任意代码执行,这非常危险。
原则上,一个对象的 __repr__
函数就是用来帮助我们清晰地重建这个对象的。
如果你的类是这样定义的:
class ISBN(object):
def __init__(self, isbn):
self.isbn = isbn
def __repr__(self):
return 'ISBN(%r)' % self.isbn
那么你只需要用一次 eval
就能把原来的对象重新创建出来。你也可以看看 pickle模块。
基于ISBN类,你可以进一步创建一个书籍类:
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'))]
当然,你的对象可能不仅仅是自己构建和重建……还有Tamás提到的关于eval的注意事项。
有很多方法可以实现类似的功能。如果你可以接受将输入格式和你的对象模型绑定在一起,那么你可以使用描述符来创建类型适配器:
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)
但是,如果你不能完全控制消息的结构,那么这种绑定就不太合适。在这种情况下,我喜欢使用解释器模式来将输入消息适配到输出类型。我会创建一个小框架,让我能够构建声明式的对象结构来处理输入数据。
这个框架可能看起来像这样:
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])
消息适配器看起来像这样:
book_message_adaptor = ObjectAdaptor(Book, kwargs={
'isbn': Pick('isbn_number', TypeWrapper(ISBNNumber)),
'authors': Pick('authorlist', ListAdaptor(TypeWrapper(Author)))
})
注意,消息结构的名称可能和对象模型的名称不一样。
消息处理本身看起来像这样:
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']))