5

Let's say I have the following class:

class Foo(object):
    def __init__(self, value):
        self.d = value
        self.a = value
        self.s = value
        self.k = value

I want to retrieve the instance variables in the order of declaration.

I tried with vars() without any success:

list(vars(Foo('value')).keys())
# ['a', 'k', 's', 'd']

What I would like:

list(magic_method(Foo('value')).keys())
# ['d', 'a', 's', 'k']

Edit:

Obviously, there would be a different value for each field.

My goal is to generate XML thanks to the object variables. To be valid, the XML tags has to be in the correct order.

This, combined with __iter__ override would allow me to only have to manage dictionaries of object to generate my XML.

Let's take a library as example. Imagine you have a class Book, Date, Person, Author and Borrower:

class Book(object):
    def self.__init__()
        self._borrower = Borrower()
        self._author = Author()

class Date(object)
    def __init__(self, date):
        self._date = date

class Person(object):
    def __init__(self, name):
        self._name = name

class Author(object):
    def __init__(self):
        self._person = Person("Daniel")

class Borrower(object):
    def __init__(self):
        self._person = Person("Jack")
        self._date = Date("2016-06-02")

I would like to create the following XML:

<Book>
    <Borrower>
        <Person>Jack</Person>
        <Date>2016-06-02</Date>
    </Borrower>
    <Author>
        <Person>Daniel</Person>
    </Author>
</Book>

I know the classes might look weird (like Date here), but I wanted to make the problem as simple as possible (and there are fields that make perfect sense). In practice, I would query a database and probably pass an record identifier in initializers. The point is that there are some data that respects the same syntax (i.e. Person here).

To summarize, I would like to create such an XML using Python objects. Order matters. That's why I wanted to retrieve variables in order for that purpose: I could then extract the class and generate the XML tag.

Community
  • 1
  • 1
maddersky
  • 107
  • 1
  • 8
  • I don't believe this is possible, because dictionaries do not maintain order. – Jonathon Reinhart Jun 02 '16 at 11:58
  • @JonathonReinhart: it's possible with meta classes. – Daniel Jun 02 '16 at 11:59
  • ...`keys().sorted(key=lambda x: mapdict[x])`? – tripleee Jun 02 '16 at 12:17
  • @AlexHall: see my edit. :) – maddersky Jun 02 '16 at 12:41
  • @tripleee: Unless I mistaken, `list(vars(Foo('value')).keys().sorted(key=lambda x: mapdict[x]))` produce the following error: `AttributeError: 'list' object has no attribute 'sorted'`. Could you be more explicit ? – maddersky Jun 02 '16 at 12:43
  • Are you really assigning `value` to all four fields, or is there a parameter in `__init__` for each field? – Alex Hall Jun 02 '16 at 12:46
  • @AlexHall There would be a different value to each field. That was just a quicker way to fill fields. – maddersky Jun 02 '16 at 12:49
  • That makes a HUGE difference because you can inspect the `__init__` method signature and get all the info you need from there. But there is an even simpler possibility: use a `collections.namedtuple`. See if that suits your needs. – Alex Hall Jun 02 '16 at 12:52
  • I'm fairly certain that there is a much simpler solution to your problem. If you elaborate on your usage, we could probably help you much better. There's really no good use-case I can think of for maintaining the order of instance variables. – pzp Jun 02 '16 at 13:49
  • @pzp Special edit for you then. I hope it is clear. :) – maddersky Jun 02 '16 at 14:51

4 Answers4

4

If you want ordering of object variables you can use something like that:

from collections import OrderedDict

class FooModel(object):
    def __new__(cls, *args, **kwargs):
        instance = object.__new__(cls)
        instance.__odict__ = OrderedDict()
        return instance

    def __setattr__(self, key, value):
        if key != '__odict__':
            self.__odict__[key] = value
        object.__setattr__(self, key, value)

    def keys(self):
        return self.__odict__.keys()

    def iteritems(self):
        return self.__odict__.iteritems()


class Foo(FooModel):
    def __init__(self, value):
        self.d = value
        self.a = value
        self.s = value
        self.k = value

Output:

>>> f = Foo('value')
>>> f.x = 5
>>> f.y = 10
>>> f.a = 15
>>> f2 = Foo('value')
>>> print "f.keys()", f.keys()
f.keys() ['d', 'a', 's', 'k', 'x', 'y']
>>> print "f2.keys()", f2.keys()
f2.keys() ['d', 'a', 's', 'k']
print list(f.iteritems())
[('d', 'value'), ('a', 15), ('s', 'value'), ('k', 'value'), ('x', 5), ('y', 10)]
Schore
  • 885
  • 6
  • 16
  • 1
    That's interesting and seems to work, but I get a `DeprecationWarning: object() takes no parameters` message. I'm using python2.7. – maddersky Jun 02 '16 at 13:09
1

You can implement the __setattr__ method to keep track of these things for you:

#!python3
class OrderedAttrs:
    def __init__(self, d, a, s, k):
        self._order = []
        self.d = d
        self.a = a
        self.s = s
        self.k = k


    def __setattr__(self, name, value):
        super().__setattr__(name, value)

        if not name in self._order:
            self._order.append(name)

        return value

    def ordered_attrs(self, with_order=False):
        return [(k,getattr(self, k)) for k in self._order if k != '_order' or with_order]

oa = OrderedAttrs('dee', 'eh', 'ess', 'kay')
oa.foo = 'bar'
oa.baz = 'moo'

print("Default:",oa.ordered_attrs())
print("With _order:", oa.ordered_attrs(with_order=True))
aghast
  • 14,785
  • 3
  • 24
  • 56
0

You can't easily do this without using metaclasses (as @Daniel said). But if you want a workaround that produces a similar effect, you can store all of your "instance variables" in a collections.OrderedDict:

from collections import OrderedDict


class Foo(object):
    def __init__(self, value):
        self.instvars = OrderedDict()
        self.instvars['d'] = value
        self.instvars['a'] = value
        self.instvars['s'] = value
        self.instvars['k'] = value


print(list(Foo('value').instvars.keys()))
# OUT: ['d', 'a', 's', 'k']
pzp
  • 6,249
  • 1
  • 26
  • 38
  • But then these aren't accessible as regular instance variables. – Jonathon Reinhart Jun 02 '16 at 12:19
  • Of course they aren't, but it does "produce a similar effect". And that's why I wrote "'instance variables'" (note the quotation marks). If you don't want to write your own metaclass, this is about as close as you'll get. – pzp Jun 02 '16 at 12:21
-1

You can give your class a method that is similar to __dict__, but which returns an OrderedDict instead.

from collections import OrderedDict

class Foo(object):
    def __init__(self, d, a, s, k):
        self.d = d
        self.a = a
        self.s = s
        self.k = k

    def ordered_dict(self):
        return OrderedDict(
            (key, getattr(self, key)) for key in ('d', 'a', 's', 'k')
        )

This works like so:

>>> myfoo = Foo('bar', 1, 7, 'foo')
>>> myfoo.ordered_dict()
OrderedDict([('d', 'bar'),('a', 1), ('s', 7), ('k', 'foo')])

>>> list(myfoo.ordered_dict())
['d', 'a', 's', 'k']
Håken Lid
  • 22,318
  • 9
  • 52
  • 67
  • Unfortunately, this is the reason I wanted to get the variable instance thanks to `vars()`. I would have to type a lot of variable names. :/ – maddersky Jun 02 '16 at 13:13
  • If you want to get the attributes in a specific correct order, you have to declare that order somewhere. – Håken Lid Jun 02 '16 at 13:27