web-dev-qa-db-fra.com

Inspecter les attributs de classe python

J'ai besoin d'un moyen d'inspecter une classe afin de pouvoir identifier en toute sécurité les attributs qui sont des attributs de classe définis par l'utilisateur. Le problème est que des fonctions comme dir (), inspect.getmembers () et friends renvoient tous les attributs de classe, y compris ceux prédéfinis comme: __class__, __doc__, __dict__, __hash__. Ceci est bien sûr compréhensible, et on pourrait dire que je pourrais simplement faire une liste de membres nommés à ignorer, mais malheureusement, ces attributs prédéfinis sont appelés à changer avec différentes versions de Python donc faire mon projet susceptible de changer dans le projet python - et je n'aime pas ça.

exemple:

>>> 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']

Dans l'exemple ci-dessus, je veux un moyen sûr de récupérer uniquement les attributs de classe définis par l'utilisateur ['a', 'b'] et non 'c' car il s'agit d'un attribut d'instance. Donc ma question est ... Quelqu'un peut-il m'aider avec la fonction fictive ci-dessus get_user_attributes(cls)?

P.S. J'ai passé du temps à essayer de résoudre le problème en analysant la classe au niveau AST ce qui serait très facile. Mais je ne trouve pas de moyen de convertir des objets déjà analysés en un AST arborescence de nœuds. Je suppose que toutes les informations AST sont supprimées une fois qu'une classe a été compilée en bytecode).

Cordialement Jakob

33
Jakob Simon-Gaarde

Voici la voie difficile. Voici la manière la plus simple. Je ne sais pas pourquoi cela ne m'est pas venu à l'esprit plus tôt.

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]

Voici un début

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

Cela devrait être assez robuste. Essentiellement, cela fonctionne en obtenant les attributs qui se trouvent sur une sous-classe par défaut de object à ignorer. Il obtient ensuite le mro de la classe qui lui est transmise et le traverse dans l'ordre inverse afin que les clés de sous-classe puissent remplacer les clés de super-classe. Il renvoie un dictionnaire de paires clé-valeur. Si vous voulez une liste de clés, valorisez les tuples comme dans inspect.getmembers, Puis renvoyez simplement attrs.items() ou list(attrs.items()) dans Python 3.

Si vous ne voulez pas réellement traverser la mro et que vous voulez juste que les attributs soient définis directement sur la sous-classe, c'est plus simple:

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
29
aaronasterling

Les doubles soulignements aux deux extrémités des "attributs spéciaux" ont fait partie de python avant 2.0. Il serait très peu probable qu'ils changeraient cela à tout moment dans un avenir proche.

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)

['un B']

6
nate c

Merci aaronasterling, vous m'avez donné l'expression dont j'avais besoin :-) Ma fonction d'inspecteur d'attribut de classe finale ressemble à ceci:

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

Soit renvoyer uniquement les variables d'attribut de classe (exclude_methods = True), soit récupérer les méthodes. Mes tests initiaux et la fonction ci-dessus prennent en charge les classes anciennes et nouvelles python.

/ Jakob

3
Jakob Simon-Gaarde

Si vous utilisez de nouvelles classes de style, pourriez-vous simplement soustraire les attributs de la classe parente?

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

Edit: Pas tout à fait. __dict__, __module__ et __weakref__ apparaît lors de l'héritage de l'objet, mais n'est pas présent dans l'objet lui-même. Vous pourriez cas particulier ceux-ci - je doute qu'ils changeraient très souvent.

2
Thomas K

Désolé d'avoir nécro-heurté le fil. Je suis surpris qu'il n'y ait toujours pas de fonction simple (ou de bibliothèque) pour gérer une telle utilisation courante à partir de 2019.

Je voudrais remercier aaronasterling pour cette idée. En fait, set container fournit un moyen plus simple de l'exprimer:

class dummy:    pass

def abridged_set_of_user_attributes(obj):
    return set(dir(obj))-set(dir(dummy))

def abridged_list_of_user_attributes(obj):
    return list(abridged_set_of_user_attributes(obj))

La solution originale utilisant la compréhension de liste est en fait deux niveaux de boucles car il y a deux mots clés in composés, bien qu'un seul mot clé for le fasse ressembler à moins de travail qu'il ne l'est.

1
Hoi Wong