2010-11-22 19 views
22

Necesito una forma de inspeccionar una clase para poder identificar con seguridad qué atributos son atributos de clase definidos por el usuario. El problema es que las funciones como dir(), inspect.getmembers() y amigos devuelven todos los atributos de clase incluidos los predefinidos como: __class__, __doc__, __dict__, __hash__. Esto, por supuesto, es comprensible, y se podría argumentar que podría hacer una lista de miembros nombrados para ignorar, pero desafortunadamente estos atributos predefinidos están destinados a cambiar con diferentes versiones de Python, lo que hace que mi proyecto sea modificable en el proyecto python. - y no me gusta eso.Inspeccionar los atributos de la clase python

ejemplo:

>>> class A: 
... a=10 
... b=20 
... def __init__(self): 
...  self.c=30 
>>> dir(A) 
['__doc__', '__init__', '__module__', 'a', 'b'] 
>>> get_user_attributes(A) 
['a','b'] 

En el ejemplo anterior quiero una manera segura para recuperar sólo la clase definida por el usuario atributos [ 'a', 'b'] no 'c', ya que es un atributo de instancia . Entonces mi pregunta es ... ¿Alguien me puede ayudar con la función ficticia anterior get_user_attributes(cls)?

P.S. He dedicado un tiempo a tratar de resolver el problema analizando la clase en AST, lo que sería muy fácil. Pero no puedo encontrar una forma de convertir objetos ya analizados a un árbol de nodos AST. Supongo que toda la información AST se descarta una vez que una clase ha sido compilada en bytecode.

Saludos Jakob

+1

Menciona que ha estado tratando de hacerlo en la AST. ¿Eso significa que solo quieres atributos que se definan inmediatamente en la clase y no en sus superclases también? Reconozco que no quieres los 'integrados', pero estoy confundido sobre este tema. – aaronasterling

Respuesta

27

A continuación se muestra la manera difícil. Aquí está la manera fácil. No sé por qué no se me ocurrió antes.

import inspect 

def get_user_attributes(cls): 
    boring = dir(type('dummy', (object,), {})) 
    return [item 
      for item in inspect.getmembers(cls) 
      if item[0] not in boring] 

Aquí es un comienzo

def get_user_attributes(cls): 
    boring = dir(type('dummy', (object,), {})) 
    attrs = {} 
    bases = reversed(inspect.getmro(cls)) 
    for base in bases: 
     if hasattr(base, '__dict__'): 
      attrs.update(base.__dict__) 
     elif hasattr(base, '__slots__'): 
      if hasattr(base, base.__slots__[0]): 
       # We're dealing with a non-string sequence or one char string 
       for item in base.__slots__: 
        attrs[item] = getattr(base, item) 
      else: 
       # We're dealing with a single identifier as a string 
       attrs[base.__slots__] = getattr(base, base.__slots__) 
    for key in boring: 
     del attrs['key'] # we can be sure it will be present so no need to guard this 
    return attrs 

Esto debe ser bastante robusto. Esencialmente, funciona al ignorar los atributos que se encuentran en una subclase predeterminada de object. A continuación, obtiene el mro de la clase que se le pasa y lo cruza en orden inverso para que las claves de subclase puedan sobrescribir las claves de superclase. Devuelve un diccionario de pares clave-valor. Si desea una lista de claves, tuplas valor como en inspect.getmembers a continuación, sólo devuelven attrs.items() o list(attrs.items()) en Python 3.

si en realidad no quiere atravesar el MRO y sólo quiere atributos definidos directamente en la subclase entonces es más fácil:

def get_user_attributes(cls): 
    boring = dir(type('dummy', (object,), {})) 
    if hasattr(cls, '__dict__'): 
     attrs = cls.__dict__.copy() 
    elif hasattr(cls, '__slots__'): 
     if hasattr(base, base.__slots__[0]): 
      # We're dealing with a non-string sequence or one char string 
      for item in base.__slots__: 
       attrs[item] = getattr(base, item) 
      else: 
       # We're dealing with a single identifier as a string 
       attrs[base.__slots__] = getattr(base, base.__slots__) 
    for key in boring: 
     del attrs['key'] # we can be sure it will be present so no need to guard this 
    return attrs 
+3

'__slots__'? ;-) –

+1

@Chris Morgan. Buen ojo. ¿Algo más está mal? – aaronasterling

+0

no lo creo. Estaba jugando al defensor del diablo porque estaba pensando en '__slots__' en ese momento (tratando de descifrar algunas cosas con PyPy) –

6

doble de relieve en ambos extremos de atributos especiales '' han sido una parte de pitón antes de 2.0. Sería muy poco probable que cambien eso en cualquier momento en el futuro cercano.

class Foo(object): 
    a = 1 
    b = 2 

def get_attrs(klass): 
    return [k for k in klass.__dict__.keys() 
      if not k.startswith('__') 
      and not k.endswith('__')] 

print get_attrs(Foo) 

[ 'a', 'b']

+2

¿Qué hay de definido por el usuario '__add__',' __mul__', '__iter__', etc.? – aaronasterling

+0

Si están definidos por el usuario, entonces yo también los quiero. ¿Es posible adquirir el árbol AST para una clase que ya está analizada y compilada por bytes? –

+0

@jakob, no, es imposible obtener AST para el código "vivo", ya que la fuente ya no se conserva en la memoria después de ser analizada, y hay casos en que Python se queda sin bytecode incluso sin fuente, por lo que no puede haber AST. – toriningen

2

Si usa nuevas clases de estilo, ¿podría simplemente restar los atributos de la clase padre?

class A(object): 
    a = 10 
    b = 20 
    #... 

def get_attrs(Foo): 
    return [k for k in dir(Foo) if k not in dir(super(Foo))] 

Editar: No del todo. __dict__, __module__ y __weakref__ aparecen cuando se heredan de un objeto, pero no están en el mismo objeto.Podrían tratarse casos especiales, dudo que cambien muy a menudo.

3

Gracias aaronasterling, que me dieron la expresión que necesitaba :-) Mi función final atributo de clase inspector se ve así: atributo de clase

def get_user_attributes(cls,exclude_methods=True): 
    base_attrs = dir(type('dummy', (object,), {})) 
    this_cls_attrs = dir(cls) 
    res = [] 
    for attr in this_cls_attrs: 
    if base_attrs.count(attr) or (callable(getattr(cls,attr)) and exclude_methods): 
     continue 
    res += [attr] 
    return res 

De cualquier retorno variabels solamente (exclude_methods = True) o también recuperar el métodos. Mis pruebas iniciales de la función anterior son compatibles tanto con las clases python antiguas como con las nuevas.

/Jakob

+0

agradable. Una mejora que se puede realizar es cambiar el control de 'exclude_methods' y' invocable (getattr (...)) 'de modo que' callable' solo se ejecute si falla la verificación booleana simple. – aaronasterling

+0

Tienes toda la razón :-) –

Cuestiones relacionadas