Как динамически добавить свойство в класс? - PullRequest
182 голосов
/ 25 августа 2009

Цель состоит в том, чтобы создать фиктивный класс, который будет вести себя как набор результатов db.

Так, например, если запрос к базе данных возвращает, используя выражение dict, {'ab':100, 'cd':200}, то я хотел бы видеть:

>>> dummy.ab
100

Сначала я подумал, может быть, я мог бы сделать это так:

ks = ['ab', 'cd']
vs = [12, 34]
class C(dict):
    def __init__(self, ks, vs):
        for i, k in enumerate(ks):
            self[k] = vs[i]
            setattr(self, k, property(lambda x: vs[i], self.fn_readyonly))

    def fn_readonly(self, v)
        raise "It is ready only"

if __name__ == "__main__":
    c = C(ks, vs)
    print c.ab

но c.ab возвращает объект свойства.

Замена строки setattr на k = property(lambda x: vs[i]) совершенно бесполезна.

Итак, как правильно создать свойство экземпляра во время выполнения?

P.S. Мне известна альтернатива, представленная в Как используется метод __getattribute__?

Ответы [ 21 ]

290 голосов
/ 31 августа 2009

Полагаю, мне следует расширить этот ответ, теперь, когда я стал старше и мудрее и знаю, что происходит. Лучше поздно, чем никогда.

Вы можете динамически добавлять свойство в класс. Но в этом-то и суть: вы должны добавить его к классу .

>>> class Foo(object):
...     pass
... 
>>> foo = Foo()
>>> foo.a = 3
>>> Foo.b = property(lambda self: self.a + 1)
>>> foo.b
4

A property на самом деле является простой реализацией вещи, называемой дескриптором . Это объект, который обеспечивает пользовательскую обработку для данного атрибута, для данного класса . Вроде как способ выделить огромное if дерево из __getattribute__.

Когда я запрашиваю foo.b в приведенном выше примере, Python видит, что b, определенный в классе, реализует протокол дескриптора - что просто означает, что это объект с __get__, __set__ или __delete__ метод. Дескриптор несет ответственность за обработку этого атрибута, поэтому Python вызывает Foo.b.__get__(foo, Foo), и возвращаемое значение передается вам в качестве значения атрибута. В случае property каждый из этих методов просто вызывает fget, fset или fdel, которые вы передали конструктору property.

Дескрипторы - это действительно способ Python раскрыть всю реализацию OO. На самом деле, существует другой тип дескриптора, даже более распространенный, чем property.

>>> class Foo(object):
...     def bar(self):
...         pass
... 
>>> Foo().bar
<bound method Foo.bar of <__main__.Foo object at 0x7f2a439d5dd0>>
>>> Foo().bar.__get__
<method-wrapper '__get__' of instancemethod object at 0x7f2a43a8a5a0>

Скромный метод - это просто другой вид дескриптора. __get__ указывает на вызывающий экземпляр в качестве первого аргумента; по сути, он делает это:

def __get__(self, instance, owner):
    return functools.partial(self.function, instance)

В любом случае, я подозреваю, что именно поэтому дескрипторы работают только с классами: они являются формализацией того, что обеспечивает классы в первую очередь. Они даже являются исключением из правила: очевидно, вы можете назначать дескрипторы классу, а сами классы являются экземплярами type! На самом деле, попытка прочитать Foo.b все еще вызывает property.__get__; для дескрипторов идиоматично возвращать себя, когда к ним обращаются как к атрибутам класса.

Я думаю, это круто, что практически вся ОО-система Python может быть выражена в Python. :)

О, и я написал многословное сообщение в блоге о дескрипторах некоторое время назад, если вам интересно.

52 голосов
/ 25 августа 2009

Цель состоит в том, чтобы создать фиктивный класс, который будет вести себя как набор результатов db.

Итак, вам нужен словарь, где вы можете написать a ['b'] как a.b?

Это просто:

class atdict(dict):
    __getattr__= dict.__getitem__
    __setattr__= dict.__setitem__
    __delattr__= dict.__delitem__
33 голосов
/ 25 августа 2009

Кажется, вы могли бы решить эту проблему намного проще с помощью namedtuple, поскольку вы заранее знаете весь список полей.

from collections import namedtuple

Foo = namedtuple('Foo', ['bar', 'quux'])

foo = Foo(bar=13, quux=74)
print foo.bar, foo.quux

foo2 = Foo()  # error

Если вам абсолютно необходимо написать собственный установщик, вам придется выполнять метапрограммирование на уровне класса; property() не работает на экземплярах.

29 голосов
/ 25 августа 2009

Вам не нужно использовать свойство для этого. Просто переопределите __setattr__, чтобы они были доступны только для чтения.

class C(object):
    def __init__(self, keys, values):
        for (key, value) in zip(keys, values):
            self.__dict__[key] = value

    def __setattr__(self, name, value):
        raise Exception("It is read only!")

Тад.

>>> c = C('abc', [1,2,3])
>>> c.a
1
>>> c.b
2
>>> c.c
3
>>> c.d
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'C' object has no attribute 'd'
>>> c.d = 42
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __setattr__
Exception: It is read only!
>>> c.a = 'blah'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __setattr__
Exception: It is read only!
6 голосов
/ 06 февраля 2015

Как динамически добавить свойство в класс Python?

Скажем, у вас есть объект, к которому вы хотите добавить свойство. Как правило, я хочу использовать свойства, когда мне нужно начать управлять доступом к атрибуту в коде, который используется в нисходящем направлении, чтобы я мог поддерживать согласованный API. Теперь я обычно добавляю их в исходный код, в котором определен объект, но давайте предположим, что у вас нет такого доступа, или вам нужно действительно динамически выбирать свои функции программно.

Создать класс

Используя пример, основанный на документации для property, давайте создадим класс объекта со скрытым атрибутом и создадим его экземпляр:

class C(object):
    '''basic class'''
    _x = None

o = C()

В Python мы ожидаем, что будет один очевидный способ сделать что-то. Однако в этом случае я собираюсь показать два способа: с обозначением декоратора и без. Во-первых, без обозначения декоратора. Это может быть более полезно для динамического назначения методов получения, установки или удаления.

Динамический (например, исправление обезьян)

Давайте создадим некоторые для нашего класса:

def getx(self):
    return self._x

def setx(self, value):
    self._x = value

def delx(self):
    del self._x

А теперь мы присваиваем это свойство. Обратите внимание, что мы могли бы выбрать наши функции программно здесь, отвечая на динамический вопрос:

C.x = property(getx, setx, delx, "I'm the 'x' property.")

И использование:

>>> o.x = 'foo'
>>> o.x
'foo'
>>> del o.x
>>> print(o.x)
None
>>> help(C.x)
Help on property:

    I'm the 'x' property.

Декораторы

Мы могли бы сделать то же самое, что мы делали выше с нотацией декоратора, но в этом случае мы должны назвать методы одинаковыми именами (и я бы рекомендовал оставить его таким же, как атрибут) , поэтому программное назначение не так тривиально, как при использовании вышеуказанного метода:

@property
def x(self):
    '''I'm the 'x' property.'''
    return self._x

@x.setter
def x(self, value):
    self._x = value

@x.deleter
def x(self):
    del self._x

И присвоить объекту свойства с предоставленными ему установщиками и удалителями класс:

C.x = x

И использование:

>>> help(C.x)
Help on property:

    I'm the 'x' property.

>>> o.x
>>> o.x = 'foo'
>>> o.x
'foo'
>>> del o.x
>>> print(o.x)
None
5 голосов
/ 25 августа 2009

Вы не можете добавить новый property() к экземпляру во время выполнения, потому что свойства являются дескрипторами данных. Вместо этого вы должны динамически создать новый класс или перегрузить __getattribute__ для обработки дескрипторов данных в экземплярах.

5 голосов
/ 26 августа 2009

Я задал похожий вопрос на этом посте переполнения стека , чтобы создать фабрику классов, которая создавала простые типы. Результатом был этот ответ , у которого была рабочая версия фабрики классов. Вот фрагмент ответа:

def Struct(*args, **kwargs):
    def init(self, *iargs, **ikwargs):
        for k,v in kwargs.items():
            setattr(self, k, v)
        for i in range(len(iargs)):
            setattr(self, args[i], iargs[i])
        for k,v in ikwargs.items():
            setattr(self, k, v)

    name = kwargs.pop("name", "MyStruct")
    kwargs.update(dict((k, None) for k in args))
    return type(name, (object,), {'__init__': init, '__slots__': kwargs.keys()})

>>> Person = Struct('fname', 'age')
>>> person1 = Person('Kevin', 25)
>>> person2 = Person(age=42, fname='Terry')
>>> person1.age += 10
>>> person2.age -= 10
>>> person1.fname, person1.age, person2.fname, person2.age
('Kevin', 35, 'Terry', 32)
>>>

Вы можете использовать некоторые варианты этого для создания значений по умолчанию, которые являются вашей целью (в этом вопросе также есть ответ, который имеет к этому отношение).

3 голосов
/ 25 августа 2009

Не уверен, что я полностью понимаю вопрос, но вы можете изменить свойства экземпляра во время выполнения с помощью встроенного __dict__ вашего класса:

class C(object):
    def __init__(self, ks, vs):
        self.__dict__ = dict(zip(ks, vs))


if __name__ == "__main__":
    ks = ['ab', 'cd']
    vs = [12, 34]
    c = C(ks, vs)
    print(c.ab) # 12
3 голосов
/ 10 августа 2016

Для тех, кто прибывает из поисковых систем, вот две вещи, которые я искал, говоря о динамических свойствах:

class Foo:
    def __init__(self):
        # we can dynamically have access to the properties dict using __dict__
        self.__dict__['foo'] = 'bar'

assert Foo().foo == 'bar'


# or we can use __getattr__ and __setattr__ to execute code on set/get
class Bar:
    def __init__(self):
        self._data = {}
    def __getattr__(self, key):
        return self._data[key]
    def __setattr__(self, key, value):
        self._data[key] = value

bar = Bar()
bar.foo = 'bar'
assert bar.foo == 'bar'

__dict__ хорошо, если вы хотите поместить динамически созданные свойства. __getattr__ хорошо делать что-то только тогда, когда нужно значение, например, запрашивать базу данных. Комбинация set / get полезна для упрощения доступа к данным, хранящимся в классе (как в примере выше).

Если вам нужно только одно динамическое свойство, взгляните на встроенную функцию property () .

2 голосов
/ 10 мая 2014

Еще один пример того, как добиться желаемого эффекта

class Foo(object):

    _bar = None

    @property
    def bar(self):
        return self._bar

    @bar.setter
    def bar(self, value):
        self._bar = value

    def __init__(self, dyn_property_name):
        setattr(Foo, dyn_property_name, Foo.bar)

Так что теперь мы можем делать такие вещи, как:

>>> foo = Foo('baz')
>>> foo.baz = 5
>>> foo.bar
5
>>> foo.baz
5
...