Search code examples
pythonpython-3.xintrospectionmetaclass

Generate a list of class members that preserves their definition order


I am trying to automatically create some SQL tables from the definition of some Python classes, I tried using dir() but since it returns a Python Dictionary, it's not ordered so the definition order of the class members is lost.

Reading on the internet I found the following here

class OrderedClass(type):

     @classmethod
     def __prepare__(metacls, name, bases, **kwds):
        return collections.OrderedDict()

     def __new__(cls, name, bases, namespace, **kwds):
        result = type.__new__(cls, name, bases, dict(namespace))
        result.members = tuple(namespace)
        return result

class A(metaclass=OrderedClass):
    def one(self): pass
    def two(self): pass
    def three(self): pass
    def four(self): pass

>>> A.members
('__module__', 'one', 'two', 'three', 'four')

I successfuly implemented a copy of it, and it appears to be doing what it should except that it's only saving the methods in the members variable, and I need to have also the class member variables.

Question:

How could I get a list of the member variables preserving their definition order?, I don't care about class methods, and I am actually ignoring them.

Note: The reason why the order is important is because the tables will have constraints that reference some of the table columns, and they must go after defining the column, but they are appearing before.

Edit: This is a sample class in my real program

class SQLTable(type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwds):
        return OrderedDict()

    def __new__(cls, name, bases, namespace, **kwds):
        result = type.__new__(cls, name, bases, dict(namespace))
        result.members = tuple(namespace)
        return result

class AreaFisicoAmbiental(metaclass = SQLTable):
    def __init__(self, persona, datos):
        # edificacion
        self.persona = persona
        self.tipoEdificacion = datos[0]
        self.tipoDeParedes = datos[1]
        self.detallesTipoDeParedes = datos[2]
        self.tipoDeTecho = datos[3]
        self.detallesTipoDeTecho = datos[4]
        self.tipoDePiso = datos[5]
        self.detallesTipoDePiso = datos[6]
        # ambientes
        self.problemaDeInfraestructura = datos[7]
        self.detallesProblemaDeInfraestructura = datos[9]
        self.condicionDeTenencia = datos[10]
        self.detallesCondicionDeTenencia = datos[11]
        self.sala = toBool(datos[12])
        self.comedor = toBool(datos[13])
        self.baño = toBool(datos[14])
        self.porche = toBool(datos[15])
        self.patio = toBool(datos[16])
        self.lavandero = toBool(datos[17])
        self.habitaciones = toInt(datos[19])
        # servicios básicos
        self.aguasServidas = toBool(datos[21])
        self.aguaPotable = toBool(datos[22])
        self.luz = toBool(datos[23])
        self.gas = datos[24]
        self.internet = toBool(datos[25])

Doing

print(AreaFisicoAmbiental.members)

Outputs:

('__module__', '__qualname__', '__init__')

Variable names are in spanish because their names will be used as the table column names, and also as the labels for a web application that will be generated from the database structure.

I know that Django does something like this, but I already have my database inspector which does the opposite thing, so know I need a Django like functionality to use my generator.


Solution

  • Updated

    As I commented, I think you're probably confusing instance attributes with class attributes and really want to keep track of the latter. Instance attributes are dynamic and can be added, changed, or removed at any time, so trying to do this with a metaclass like shown in your question won't work (and different instances may have a different group of them defined).

    You may be able to keep track of their creation and deletion by overloading a couple of the class's special methods, namely __setattr__() and __delattr__() and storing their effects in a private data member which is an OrderedSet. Do so will keep track of what they are and preserve the order in which they were created.

    Both of these methods will need to be careful not to operate upon the private data member itself.

    That said, here's something illustrating such an implementation:

    # -*- coding: iso-8859-1 -*-
    # from http://code.activestate.com/recipes/576694
    from orderedset import OrderedSet
    
    class AreaFisicoAmbiental(object):
        def __init__(self, persona, datos):
            self._members = OrderedSet()
            self.persona = persona
            self.tipoEdificacion = datos[0]
            self.tipoDeParedes = datos[1]
    
        def __setattr__(self, name, value):
            object.__setattr__(self, name, value)
            if name != '_members':
                self._members.add(name)
    
        def __delattr__(self, name):
            if name != '_members':
                object.__delattr__(self, name)
                self._members.discard(name)
    
        def methodA(self, value1, value2):  # add some members
            self.attribute1 = value1
            self.attribute2 = value2
    
        def methodB(self):
            del self.attribute1  # remove a member
    
    if __name__ == '__main__':
        a = AreaFisicoAmbiental('Martineau', ['de albañilería', 'vinilo'])
        a.methodA('attribute1 will be deleted', 'but this one will be retained')
        a.methodB()  # deletes a.attribute1
        a.attribute3 = 42  # add an attribute outside the class
    
        print('current members of "a":')
        for name in a._members:
            print('  {}'.format(name))
    

    Output:

    current members of "a":
      persona
      tipoEdificacion
      tipoDeParedes
      attribute2
      attribute3
    

    A final note: It would be possible to create a metaclass that added these two methods automatically to client classes, which would make it easier to modify existing classes.