如何以Pythonic方式为对象属性添加类型信息?

1 投票
5 回答
1365 浏览
提问于 2025-04-15 22:58

我正在创建一些类,虽然我知道这些属性应该是什么类型,但Python并不知道。虽然想要告诉Python这些信息有点不符合Python的风格,但如果我真的想这么做,有没有简单的方法可以实现呢?

原因是:我在处理一些外部格式的数据(没有类型信息),这些数据涉及到对象嵌套对象。把这些数据放进嵌套的字典里很简单,但我希望能把它们放进我自己定义的类对象里,这样不仅能得到数据,还能获得正确的行为。举个例子:假设我的类叫做 Book,它有一个属性 isbn,我会用一个 ISBNumber 对象来填充这个属性。我的数据给我的是一个字符串形式的isbn;我希望能查看 Book 类,并说“这个字段应该用 ISBNumber(theString) 来填充。”

如果这个解决方案还能应用到我从别人那里得到的类上,而不需要修改他们的代码,那就更好了。

(我只能使用Python 2.6,不过如果有适用于3.x的解决方案我也很感兴趣。)

5 个回答

1

我假设你已经考虑过 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 就有可能导致任意代码执行,这非常危险。

1

原则上,一个对象的 __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的注意事项。

2

有很多方法可以实现类似的功能。如果你可以接受将输入格式和你的对象模型绑定在一起,那么你可以使用描述符来创建类型适配器:

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

撰写回答