Проверить атрибуты класса Python

Мне нужен способ проверки класса, чтобы я мог безопасно определить, какие атрибуты являются определяемыми пользователем атрибутами класса. Проблема в том, что такие функции, как dir (), inspect.getmembers () и друзья, возвращают все атрибуты класса, включая предопределенные, такие как: __class__, __doc__, __dict__, __hash__. Это, конечно, понятно, и можно было бы возразить, что я мог бы просто составить список именованных членов, которые нужно игнорировать, но, к сожалению, эти предопределенные атрибуты обязательно изменятся с разными версиями Python, поэтому мой проект может быть изменен в проекте python. - и мне это не нравится.

пример:

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

В приведенном выше примере мне нужен безопасный способ извлечения только определенных пользователем атрибутов класса ['a', 'b'], а не 'c', поскольку это атрибут экземпляра. Итак, мой вопрос ... Может ли кто-нибудь помочь мне с описанной выше фиктивной функцией get_user_attributes(cls)?

P.S. Я потратил некоторое время, пытаясь решить проблему, проанализировав класс на уровне AST, что было бы очень просто. Но я не могу найти способ преобразовать уже проанализированные объекты в дерево узлов AST. Я предполагаю, что вся информация AST отбрасывается после того, как класс был скомпилирован в байт-код.

С уважением, Якоб


person Jakob Simon-Gaarde    schedule 22.11.2010    source источник
comment
Вы упомянули, что пытались сделать это в AST. Означает ли это, что вам нужны только атрибуты, которые определены непосредственно в классе, а не в его суперклассах? Я понимаю, что вам не нужны «встроенные», но меня это смущает.   -  person aaronasterling    schedule 22.11.2010


Ответы (6)


Ниже приведен трудный путь. Вот простой способ. Не знаю, почему это не пришло мне в голову раньше.

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]

Вот начало

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

Это должно быть довольно надежно. По сути, он работает, заставляя игнорировать атрибуты подкласса по умолчанию object. Затем он получает MRO класса, который ему передан, и просматривает его в обратном порядке, чтобы ключи подкласса могли перезаписывать ключи суперкласса. Он возвращает словарь пар ключ-значение. Если вам нужен список кортежей ключей и значений, как в inspect.getmembers, тогда просто верните либо attrs.items(), либо list(attrs.items()) в Python 3.

Если вы на самом деле не хотите проходить mro и просто хотите, чтобы атрибуты определялись непосредственно в подклассе, тогда это проще:

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
person aaronasterling    schedule 22.11.2010
comment
не думаю. Тогда я просто играл в адвоката дьявола, потому что в то время я просто думал о __slots__ (пытался понять кое-что с PyPy) - person Chris Morgan; 22.11.2010
comment
Это было то, что мне было нужно :-) спасибо aaronasterling: boring = dir (type ('dummy', (object,), {})) - person Jakob Simon-Gaarde; 22.11.2010
comment
@Jakob Simon-Gaarde Спасибо за комментарий. Это дало мне одну из тех вспышек, которые я идиот. Смотрите мое обновление. - person aaronasterling; 22.11.2010
comment
Мне нравится идея dir: в отличие от __dict__, ее можно переопределить в __dir()__, чтобы отображать атрибуты, сгенерированные с помощью getattr. Меня беспокоит то, что dir описан в документации как удобство для использования в интерактивном запросе, а не в строгом или последовательно определенном наборе имен, и его подробное поведение может меняться в зависимости от реализации. Но я полагаю, что мы мало что можем с этим поделать. - person max; 21.09.2012
comment
boring может быть глобальной константой, не так ли? - person jpmc26; 20.10.2015
comment
А как насчет встроенной функции vars? - person w_jay; 28.01.2020

Двойное подчеркивание на обоих концах «специальных атрибутов» было частью Python до 2.0. Маловероятно, что они изменят это когда-либо в ближайшем будущем.

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

person nate c    schedule 22.11.2010
comment
как насчет определенных пользователем __add__, __mul__, __iter__ и т. д.? - person aaronasterling; 22.11.2010
comment
Если они определены пользователем, я тоже хочу их. Можно ли получить дерево AST для класса, который уже проанализирован и скомпилирован в байтах? - person Jakob Simon-Gaarde; 22.11.2010
comment
@jakob, нет, невозможно получить AST для живого кода, так как исходный код больше не сохраняется в памяти после синтаксического анализа, и бывают случаи, когда у Python заканчивается байт-код даже без источника, поэтому AST не может быть. - person toriningen; 07.06.2011
comment
Маленькая ошибка: Foo.__dict__.keys() должно быть klass.__dict__.keys(). - person smci; 13.08.2011

Спасибо, aaronasterling, вы дали мне нужное выражение :-) Моя последняя функция инспектора атрибутов класса выглядит так:

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

Либо верните только переменные атрибутов класса (exclude_methods = True), либо также получите методы. Мои начальные тесты вышеупомянутой функции поддерживают классы Python как в старом, так и в новом стиле.

/ Якоб

person Jakob Simon-Gaarde    schedule 22.11.2010
comment
Ницца. Одно из улучшений, которое можно было бы сделать, - это переключить проверку для exclude_methods и callable(getattr(...)), чтобы callable запускался только в случае сбоя простой логической проверки. - person aaronasterling; 22.11.2010

Если вы используете новые классы стилей, не могли бы вы просто вычесть атрибуты родительского класса?

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

Изменить: Не совсем. _2 _, _ 3_ и __weakref__ появляются при наследовании от объекта, но отсутствуют в самом объекте. Вы можете выделить их в особом случае - я сомневаюсь, что они будут меняться очень часто.

person Thomas K    schedule 22.11.2010

Прошу прощения за некроз нити. Я удивлен, что по состоянию на 2019 год до сих пор нет простой функции (или библиотеки) для обработки такого распространенного использования.

Хочу поблагодарить aaronasterling за идею. На самом деле контейнер set предоставляет более простой способ выразить это:

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

Исходное решение, использующее понимание списка, на самом деле представляет собой двухуровневый цикл, потому что в нем объединены два in ключевых слова, несмотря на то, что наличие только одного ключевого слова for делало его менее трудоемким, чем есть на самом деле.

person Hoi Wong    schedule 28.04.2019

Это помогло мне включить определенные пользователем атрибуты с __, которые могут быть найдены в cls.__dict__

import inspect

class A:
    __a = True
    
    def __init__(self, _a, b, c):
        self._a = _a
        self.b = b
        self.c = c 

    def test(self):
        return False

cls = A(1, 2, 3)

members = inspect.getmembers(cls, predicate=lambda x: not inspect.ismethod(x))
attrs = set(dict(members).keys()).intersection(set(cls.__dict__.keys()))
__attrs = {m[0] for m in members if m[0].startswith(f'_{cls.__class__.__name__}')}
attrs.update(__attrs)

Это правильно даст: {'_A__a', '_a', 'b', 'c'}

Вы можете обновить, чтобы очистить cls.__class__.__name__, если хотите

person Alexander McFarlane    schedule 23.01.2021