ewok
ewok

Reputation: 21503

Python: find classes in module before class is defined

I have a python class in a module, and I have a few methods within it that need to have a list of certain other classes within the same module. Here is how I'm doing it right now:

module.py

class Main:
  @staticmethod
  def meth1():
    for c in classes:
      #do something

  @staticmethod
  def meth2():
    for c in classes:
      #do something

class Class1:
  pass

class Class2:
  pass

class Class3:
  pass

classes = [Class1, Class3]

A few things I would like to improve:

  1. I'd like to put the classes list somewhere more prevalent. Ideally, either outside all classes, but at the top of the module file, or as a class attribute of Main, but outside of either meth1 or meth2. The purpose of that is to make it easier to find, if someone needs to add another class definition.
  2. If possible, I'd like to do this programmatically so I don't need to explicitly define the list to begin with. This eliminates the need for #1 (though I'd still like it to be prevalent). To do this, I need a way to list all classes defined within the same module. The closest I've been able to come is dir() or locals(), but they also list imported classes, methods, and modules. Also, I would need some way to identify the classes I want. I can do that just with an attribute in the classes, but if there's some more elegant way, that would be nice.

Is what I'm trying to do even possible?

Upvotes: 4

Views: 1304

Answers (6)

TheRadianer
TheRadianer

Reputation: 57

In current versions of Python you can use:

from __future__ import annotations

Upvotes: 0

Don Kirkby
Don Kirkby

Reputation: 56230

Personally, I would use a decorator to mark the classes that are important. You can place the list that will hold them at the top of the file where it will be noticable.

Here's a simple example:

# Classes are added here if they are important, because...
important_classes = []


def important(cls):
    important_classes.append(cls)
    return cls


@important
class ClassA(object):
    pass


class ClassB(object):
    pass


@important
class ClassC(object):
    pass

# Now you can use the important_classes list however you like.
print(important_classes)
# => [<class '__main__.ClassA'>, <class '__main__.ClassC'>]

Upvotes: 4

Alex Huszagh
Alex Huszagh

Reputation: 14644

You can use inspect.

First, get the list of local variables:

local_vars = locals().values()

Then we need to inspect each one:

import inspect
local_vars = [i for i in local_vars if inspect.isclass(i)]

To get only classes locally defined, check if cls.__module__ == __name__ as follows:

def get_classes():
    global_vars = list(globals().values())
    classes = [i for i in global_vars if inspect.isclass(i)]
    return [i for i in classes if i.__module__ == __name__]

The overall idea is this: inspect allows you to inspect live objects, and iterating over all local variables allows you to inspect everything within your current namespace. The final part, which classes are defined locally, can be done by checking if the module name is the same as the current namespace, or cls.__module__ == __name__.

Finally, for Python3 compatibility, I've added list(globals().values(), since the dictionary size will change during list comprehension. For Python2, since dict.values() returns a list, this can be omitted.

EDIT:

For further filtering, you can also use specific class attributes or other, as was mentioned in the comments. This is great if you are worried about restructuring your module into a package later.

def get_classes(name='target'):
    global_vars = list(globals().values())
    classes = [i for i in global_vars if inspect.isclass(i)]
    return [i for i in classes if hasattr(i, name)]

Upvotes: 2

7stud
7stud

Reputation: 48649

Your list seems to be targeting a subset of the available classes in the module, so at some point you will have to specify the classes you are targeting.

import sys

target_classes = ["Class1", "Class3"]

class Main:
    def __init__(self, classes):
        self.target_classes = classes

    def meth1(self):
        for s in self.target_classes:
            C = getattr(sys.modules[__name__], s)
            C().speak()

    def meth2(self):
        for c in classes:
            print c
            #do something

class Class1:
    def speak(self):
        print "woof"

class Class2:
    def speak(self):
        print "squeak"

class Class3:
    def speak(self):
        print "meow"


Main(target_classes).meth1()

--output:--
woof
meow

Upvotes: 2

Spade
Spade

Reputation: 2280

I am not sure if this is the best practice, but this will do what you need:

class Main:
    def __init__(self, locals):
        self.classes = []
        for (c, val) in locals.iteritems():
            try:
                if c[:5] == 'Class':
                    self.classes.append(val)
            except:
                pass

    def meth1(self):
        for c in self.classes:
            pass

    def meth2(self):
        for c in self.classes:
            pass

class Class1:
    pass

class Class2:
    pass

class Class3:
    pass

main = Main(locals())
print main.classes

Upvotes: 1

MrAlexBailey
MrAlexBailey

Reputation: 5289

There may be better ways to achieve this, but I would make all of these subclasses of a holder, then use __subclasses__() to pull them all:

class Main:
    def meth1(self):
        for c in Holder._subclasses__():
            #do something

    def meth2(self):
        for c in Holder._subclasses__():
            #do something

class Holder(object):
    pass

class Class1(Holder):
    pass

class Class2(Holder):
    pass

class Class3(Holder):
    pass

You could even make them subclasses of Main if you wanted to, and then pull them with a classmethod:

class Main(object):
    @classmethod
    def meth1(cls):
        for c in cls._subclasses__():
            #do something

class Class1(Main): pass

You do need to inherit from object with Python 2 for this to work.

Upvotes: 2

Related Questions