8

This code:

import inspect

class Obj():

    def c(self):
        return 1

    def b(self):
        return 2

    def a(self):
        return 3

o = Obj()

for name, value in inspect.getmembers(o, inspect.ismethod):
    print str(value())+" "+name

print:

3 a
2 b
1 c

Because of inspect.getmembers return all the members of an object in a list of (name, value) pairs sorted by name, as you can read in https://docs.python.org/2/library/inspect.html#inspect.getmembers

But I want to get that list in the same order that the members was written in the code, in other words, the output would be:

1 c
2 b
3 a

Is any way to do that?

Thanks

santos82h
  • 311
  • 2
  • 9
  • 1
    What about inheritance? If `o`'s methods are defined in two different classes, what order should they come in? – user2357112 supports Monica Apr 06 '17 at 17:28
  • 2
    Possible duplicate of [How to read class attributes in the same order as declared?](http://stackoverflow.com/questions/4459531/how-to-read-class-attributes-in-the-same-order-as-declared) – pvg Apr 06 '17 at 17:31
  • Which Python version is this? Python 2 and 3 have very different solutions (and even minor versions have differences). – Markus Meskanen Apr 06 '17 at 17:43

4 Answers4

3

No. Class members are not ordered. They are gathered into a dictionary, immediately losing order. You can resort to tricks like parsing the source, but it will break easily. For starters, the source could not be available.

[edit: it seems python3 allows more flexibility in class creation, making it possible to customize the way class members are gathered, if you are on python3 only, that's probably a better approach]

If changing the code is not a problem, you can use a decorator though:

import inspect

def track_order(fn):
    fn.order = track_order.idx
    track_order.idx += 1
    return fn
track_order.idx = 0

class Obj(object):
    @track_order
    def c(self):
        return 1

    @track_order
    def b(self):
        return 2

    @track_order
    def a(self):
        return 3

o = Obj()

methods = sorted((item
                  for item in inspect.getmembers(o, inspect.ismethod)),
                 key=lambda item: item[1].order)

for name, value in methods:
    print str(value())+" "+name

The decorator adds an idx attribute to all methods that pass through it. This makes use of the fact that python has first-class functions.

$ python test.py
1 c
2 b
3 a

Note: this is the method used by Django to keep track of form and model fields order. Only, they don't need a decorator because fields' classes have the instanciation order attribute built-in (it is named creation_counter).

Community
  • 1
  • 1
spectras
  • 10,850
  • 2
  • 24
  • 42
  • There is actually quite a [nice solution](http://stackoverflow.com/a/36060212/5014455) in the dupe target using meta-classes and `__prepare__` available for Python 3. And in 3.6, members will be ordered automatically! – juanpa.arrivillaga Apr 06 '17 at 17:39
  • @juanpa.arrivillaga> nice solution indeed. Probably much better for python3-only code. I added a comment at the top, but I'll let my answer around for those of us who still have to maintain compatibility with legacy code,though :) – spectras Apr 06 '17 at 17:42
2

When creating an object, all of its attributes are contained in another specialized attribute in the object called __dict__, which as the name suggests is just a normal Python non-ordered dictionary, hence they are not guaranteed to be stored in the same fashion they were added in. When retrieving the values in __dict__ using getmembers(), Python automatically reorganizes the dictionary when printing it in order to make some logical sense.

To combat this, something must be done to turn the regular Python dictionary __dict__ into some sort of ordered one.

This can be done a number of ways, for simplicity's sake, I will assume you are using Python 3.

Using the collections package, you can obtain an OrderedDict, which is exactly the technology we require for such an issue. Prepare this ordered dictionary for use in a metaclass for the class which needs ordered members to be stored, copy over the members, and finally access this new OrderedDict when wanting to print out said members.

This can be seen in action in this Stack Overflow answer.

Community
  • 1
  • 1
Ziyad Edher
  • 2,002
  • 15
  • 30
1

In cpython the code is compiled down to bytecode for the VM. And the functions have a __code__ attribute, which is a code object. The code object has a co_firstlineno attribute, which is the first line in Python source code. (Detailed in the inspect module.)

If you know your methods are all in source code, and you know you are using cpython, you could use this as a sort key. But it seems awful shaky if you don't know these things.

members = [ (name,meth) for name, meth in inspect.getmembers(o, inspect.ismethod)]

members = sorted(members, key=lambda t: t[1].__code__.co_firstlineno)

print '\n'.join(m[0] for m in members)
aghast
  • 13,113
  • 1
  • 16
  • 44
0

Hm, this is very hacky, but basically I inspect the source directly and use re to find method names. This solution is pretty brittle, though, and it doesn't deal with inheritance, but maybe it works for you. Assuming I've saved your class definition in a file named test.py:

>>> import test
>>> import re
>>> findmethods = re.compile(r"    def (.+)\(")
>>> findmethods.findall(inspect.getsource(test.Obj))
['c', 'b', 'a']
>>>
juanpa.arrivillaga
  • 65,257
  • 7
  • 88
  • 122
  • If you're worried about the regex failing, you could `import ast` and do `names = [node.name for node in ast.parse(inspect.getsource(test.Obj)).body[0].body if isinstance(node, ast.FunctionDef)]`. Still depends on the class having accessible source code, though, which isn't a sure thing. as spectras indicates. – Kevin Apr 06 '17 at 17:38
  • @Kevin Yeah, definitely any solution that requires inspecting the source code directly is bound to be hacky as hell and full of potential pitfalls. No doubt about that! But this is a great use of `ast` to make it a little less brittle! – juanpa.arrivillaga Apr 06 '17 at 17:40