简体   繁体   中英

What is the most pythonic way to re-order a class objects' attributes?

I'm in the process of creating a set of Python 2.7/3+ tools to programmatically modify the contents of XML files my team uses constantly. So far, I'm parsing the XML into meaningful class object attributes (via **kwargs and .__dict__.update() or setattr() ), to which I've associated some fairly complex methods. I really love being able to define my automation in terms of method calls, eg foo.do_complex_modification(x) .

I am now at the point where I would like to write my modified data back into an XML file, to be used by the rest of our software. The contents of the file are just fine, but unfortunately our legacy tools only accepts the XML with the order preserved , something I can't count on Python object dictionaries to provide. I can read in the "proper" order when parsing the XML without a problem and store that somehow, but modifying the legacy systems is not an option.

Possibly related, there is also an XSD schema for the XML.

Question : What is the most pythonic or elegant way to serialize my class attributes so that they preserve their original order ? If appropriate, how would I go about writing, say, .sort(key=ordering_function) when reading back from the object's __dict__ ?

class Foo(object):
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)
    def really_complex_method(self):
        pass


def attrs(cls):   
    return [(k, v) for k, v in cls.__dict__.items() if k[:1] != '_']

d = dict(bar=1, baz=2, quux=3)  # Need them back in this particular order
print(attrs(Foo(**d)))

Returns

[('quux', 3), ('bar', 1), ('baz', 2)]

And is un-ordered on any version prior to Python 3.6.

The biggest problem is probably because you keep passing keyword arguments around, which doesn't preserve the order they in which they were defined. The other issue is that a class.__dict__ is unordered. Here's a way to work around both these things (which doesn't assume you want the attributes alphabetically ordered by their name). The technique of replacing the class' special __dict__ attribute is allowable because, as the documentation says, it can be a "dictionary or other mapping object " (emphasis mine). This true in both Python 2 and 3.

from collections import OrderedDict
from operator import itemgetter

class Foo(object):
    def __init__(self, *keyvalues):
        self.__dict__ = OrderedDict(keyvalues)

    def really_complex_method(self):
        pass

def attrs(instance):
    """ Return a list of instance attributes sorted by their value. """
    return sorted(instance.__dict__.items(), key=itemgetter(1))

print(attrs(Foo(('bar', 1), ('baz', 2), ('quux', 3), ('question', -42))))

Output:

[('question', -42), ('bar', 1), ('baz', 2), ('quux', 3)]

An alternative to using OrderedDict would be to create your own mapping class. Here's an example takien from PEP 3115 (which is about metaclasses in Python 3, but the subject's not relevant). It also works in both Python 2 and 3):

from operator import itemgetter

class MemberTable(dict):
    """ Custom dictionary that keeps track of the order members (keys) are added. """
    def __init__(self, *args, **kwargs):
        super(MemberTable, self).__init__(*args, **kwargs)
        self.member_names = []

    def __setitem__(self, key, value):
        # if the key is not already defined, add to the list of keys.
        if key not in self:
            self.member_names.append(key)
        super(MemberTable, self).__setitem__(self, key, value)

class Foo(object):
    def __init__(self, *keyvalues):
        self.__dict__ = MemberTable(keyvalues)

    def really_complex_method(self):
        pass

def attrs(instance):
    """ Return a list of instance attributes sorted their value. """
    return sorted(instance.__dict__.items(), key=itemgetter(1))

print(attrs(Foo(('bar', 1), ('baz', 2), ('quux', 3), ('question', -42))))

Yet another way to do it, that doesn't involve changing the instance's __dict__ , would be to make the class keep track of the order attributes were added to it and then iterate them in that order:

from operator import itemgetter

class Foo(object):
    def __init__(self, *keyvalues):
        self._sequence = []
        for (key, value) in keyvalues:
            setattr(self, key, value)
            self._sequence.append(key)  # keep track of order added

    def __iter__(self):
        for key in self._sequence:
            yield key, getattr(self, key)

    def really_complex_method(self):
        pass

def attrs(instance):
    """ Return a list of instance attributes sorted their value. """
    return sorted((item for item in instance), key=itemgetter(1))

print(attrs(Foo(('bar', 1), ('baz', 2), ('quux', 3), ('question', -42))))

Note that in all of these implementations, if sorted() wasn't used the attrs() function the attributes would be accessed in the order they were added (which was the only thing you originally seemed to want before revising your question).

Even if you sort the class attributes, as soon as they are stored in a dict the order will change. The best way is to use an OrderedDict which will preserve the order from the sorted() method.

from collections import OrderedDict

class Foo(object):
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)
    def really_complex_method(self):
        pass


def attrs(cls):
    return OrderedDict(sorted(cls.__dict__.items()))

d = dict(bar=1, baz=2, quux=3)
print(attrs(Foo(**d)))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM