Get method of a class in the order that it was in the code
Asked Answered
F

5

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

Fussy answered 6/4, 2017 at 17:21 Comment(3)
What about inheritance? If o's methods are defined in two different classes, what order should they come in?Venusian
Possible duplicate of How to read class attributes in the same order as declared?Killian
Which Python version is this? Python 2 and 3 have very different solutions (and even minor versions have differences).Salverform
U
4

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).

Undercut answered 6/4, 2017 at 17:34 Comment(2)
There is actually quite a nice solution in the dupe target using meta-classes and __prepare__ available for Python 3. And in 3.6, members will be ordered automatically!Luckily
@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 :)Undercut
G
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.

Germicide answered 6/4, 2017 at 17:37 Comment(2)
Python 2.7 doesn't have __prepare__.Venusian
Woops meant to type out 3. Thanks for the catch.Germicide
P
2

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)
Phenylalanine answered 6/4, 2017 at 17:48 Comment(2)
This almost works, until you introduce decorators. Then the co_firstlineno comes from somewhere completely unconnected to the location of the decorated function's definition.Venusian
True. @Wintro's is probably a better answer if you have control of the sources.Phenylalanine
L
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']
>>>
Luckily answered 6/4, 2017 at 17:28 Comment(2)
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.Huygens
@Huygens 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!Luckily
S
0
def print_class_methods_by_order(class_object):
    for attr in class_object.__dict__:
        if callable(getattr(class_object, attr)):
            print(attr)


class MyObject:
    def c(self):
        pass

    def a(self):
        pass

    def b(self):
        pass

output:

>>> print_class_methods_by_order(MyObject)
c
a
b

Also works with decorators:

def print_class_methods_by_order(class_object):
    for attr in class_object.__dict__:
        if callable(getattr(class_object, attr)):
            print(attr)

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("my_decorator")
        func(*args, **kwargs)
    return wrapper


class MyObject:
    @my_decorator
    def c(self):
        pass

    @my_decorator
    def a(self):
        pass

    @my_decorator
    def b(self):
        pass

output:

>>> print_class_methods_by_order(MyObject)
c
a
b

And also works with "class" decorator (not sure there is any difference compared to the previous case):

def print_class_methods_by_order(class_object):
    for attr in class_object.__dict__:
        if callable(getattr(class_object, attr)):
            print(attr)

def decorate_all(decorator):
    def decorate(cls):
        for attr in cls.__dict__:
            if callable(getattr(cls, attr)):
                setattr(cls, attr, decorator(getattr(cls, attr), cls))
        return cls
    return decorate

def my_decorator(func, cls):
    def wrapper(*args, **kwargs):
        print("my_decorator")
        fn = func(*args, **kwargs)
        return fn
    return wrapper

@decorate_all(my_decorator)
class MyObject:
    def c(self):
        pass

    def a(self):
        pass

    def b(self):
        pass

output:

>>> print_class_methods_by_order(MyObject)
c
a
b
Smack answered 22/12, 2022 at 15:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.