Використання властивості () на classmethods


173

У мене є клас з двома методами класу (використовуючи функцію classmethod ()) для отримання та встановлення того, що по суті є статичною змінною. Я намагався використати функцію property () з цими, але це призводить до помилки. Мені вдалося відтворити помилку за допомогою наступного в перекладачі:

class Foo(object):
    _var = 5
    @classmethod
    def getvar(cls):
        return cls._var
    @classmethod
    def setvar(cls, value):
        cls._var = value
    var = property(getvar, setvar)

Я можу продемонструвати методи класу, але вони не працюють як властивості:

>>> f = Foo()
>>> f.getvar()
5
>>> f.setvar(4)
>>> f.getvar()
4
>>> f.var
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'classmethod' object is not callable
>>> f.var=5
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'classmethod' object is not callable

Чи можливо використовувати функцію властивості () за допомогою функцій, прикрашених класним методом?

Відповіді:


90

Властивість створюється в класі, але впливає на екземпляр. Отже, якщо ви хочете властивість classmethod, створіть властивість у метакласі.

>>> class foo(object):
...     _var = 5
...     class __metaclass__(type):  # Python 2 syntax for metaclasses
...         pass
...     @classmethod
...     def getvar(cls):
...         return cls._var
...     @classmethod
...     def setvar(cls, value):
...         cls._var = value
...     
>>> foo.__metaclass__.var = property(foo.getvar.im_func, foo.setvar.im_func)
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

Але оскільки ви все одно використовуєте метаклас, він буде читати краще, якщо ви просто перемістите туди класові методи.

>>> class foo(object):
...     _var = 5
...     class __metaclass__(type):  # Python 2 syntax for metaclasses
...         @property
...         def var(cls):
...             return cls._var
...         @var.setter
...         def var(cls, value):
...             cls._var = value
... 
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

або, використовуючи metaclass=...синтаксис Python 3 , і метаклас, визначений поза fooтілом класу, і метаклас, відповідальний за встановлення початкового значення _var:

>>> class foo_meta(type):
...     def __init__(cls, *args, **kwargs):
...         cls._var = 5
...     @property
...     def var(cls):
...         return cls._var
...     @var.setter
...     def var(cls, value):
...         cls._var = value
...
>>> class foo(metaclass=foo_meta):
...     pass
...
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

1
Здається, це не працює для мене в Python 3.2. Якщо я зміню foo .__ metaclass __. Var = властивість (foo.getvar.im_func, foo.setvar.im_func) на foo .__ metaclass __. Var = властивість (foo.getvar .__ func__, foo.setvar .__ func__) я отримую "AttributeError: type об'єкт 'foo' не має атрибута 'var' "під час виконання" foo.var ".
Майкл Келлі

Подвійна корекція SIGH : це працює в Python 2.7, але не в Python 3.2.
Майкл Келлі

@MichaelKelley - Це тому, що синтаксис метакласів змінився в Python 3.x
mac

1
Я не зовсім впевнений, що розумію, яким був би спосіб Python 3.x написати це?
SylvainD

8
@Josay: Вам потрібно спочатку визначити метаклас, а потім визначити клас за допомогою нового class Foo(metaclass=...)синтаксису.
Кевін

69

Читаючи примітки до випуску Python 2.2 , я знаходжу наступне.

Метод get [властивості] не буде викликатися, коли доступ до властивості є атрибутом класу (Cx), а не як атрибут екземпляра (C (). X). Якщо ви хочете змінити операцію __get__ для властивостей, коли вони використовуються як атрибут класу, ви можете підкласирувати властивість - це сам тип нового стилю - розширити свій метод __get__, або ви можете визначити тип дескриптора з нуля, створивши новий -стильний клас, який визначає методи __get__, __set__ та __delete__.

ПРИМІТКА. Наведений нижче метод насправді не працює для сеттерів, а лише гетерів.

Тому я вважаю, що встановленим рішенням є створення ClassProperty як підкласу властивості.

class ClassProperty(property):
    def __get__(self, cls, owner):
        return self.fget.__get__(None, owner)()

class foo(object):
    _var=5
    def getvar(cls):
        return cls._var
    getvar=classmethod(getvar)
    def setvar(cls,value):
        cls._var=value
    setvar=classmethod(setvar)
    var=ClassProperty(getvar,setvar)

assert foo.getvar() == 5
foo.setvar(4)
assert foo.getvar() == 4
assert foo.var == 4
foo.var = 3
assert foo.var == 3

Однак сетери насправді не працюють:

foo.var = 4
assert foo.var == foo._var # raises AssertionError

foo._var не змінюється, ви просто перезаписали властивість на нове значення.

Ви також можете використовувати ClassPropertyяк декоратор:

class foo(object):
    _var = 5

    @ClassProperty
    @classmethod
    def var(cls):
        return cls._var

    @var.setter
    @classmethod
    def var(cls, value):
        cls._var = value

assert foo.var == 5

20
Я не думаю, що частина сеттера ClassProperty насправді працює так, як описано: хоча всі твердження прикладу проходять, в кінці foo._var == 4 (не 3, як мається на увазі). Налаштування властивості клобує саму властивість. Коли властивості класу обговорювались на python-dev, було зазначено, що, хоча getters тривіальні, сеттерам важко (неможливо?) Без метакласу
Габріель Грант

4
@Gabriel Повністю правильно. Я не можу повірити, що ніхто не вказував на це два роки.
agf

Я також не впевнений, чому ви не просто використовуєте self.fget(owner)та не знімаєте необхідність тут взагалі використовувати @classmethod? (ось що classmethod робить , перекладайте .__get__(instance, owner)(*args, **kwargs)на function(owner, *args, **kwargs)дзвінки через посередника; властивостям посередник не потрібен).
Martijn Pieters

У вашій демонстрації не вистачає фактичної трансформації ні в геттер, ні в сеттер, який би чітко продемонстрував, що ваше foo.var = 3завдання насправді не проходить через властивість , а натомість просто замінив об'єкт властивості на fooціле число. Якщо ви додали assert isinstance(foo.__dict__['var'], ClassProperty)дзвінки між вашими твердженнями, ви побачите, що після foo.var = 3цього виконується помилка .
Martijn Pieters

1
Класи Python не підтримує дескриптор прив'язки по налаштуванню на самому класі , тільки на отримання (так instance.attr, instance.attr = valueі del instance.attrвсе буде пов'язувати дескриптор знайдені на type(instance), але в той час як classobj.attrприв'язки, classobj.attr = valueі del classobj.attrнічого НЕ і замість того, щоб замінити або видалити сам об'єкт дескриптора). Вам потрібен метаклас для підтримки налаштування та видалення (роблячи об'єкт класу екземпляром, а метаклас - типом).
Martijn Pieters

56

Я сподіваюся, що цей мертвий простий @classpropertyдекоратор, призначений лише для читання , допоможе комусь, хто шукає властивості класу.

class classproperty(object):

    def __init__(self, fget):
        self.fget = fget

    def __get__(self, owner_self, owner_cls):
        return self.fget(owner_cls)

class C(object):

    @classproperty
    def x(cls):
        return 1

assert C.x == 1
assert C().x == 1

2
Чи працює це з підкласами? (чи може підклас заміняти властивості класу?)
zakdances

1
Ум так? class D(C): x = 2; assert D.x == 2
dtheodor

Я б хотів, щоб це спрацювало, коли я використовую його .formatяк "{x}".format(**dict(self.__class__.__dict__, **self.__dict__)):(
2rs2ts

@Nathan Не тільки ... коли ви встановите це, ви перекриєте весь xдоступ 10. Мені подобається такий підхід, оскільки акуратний і простий, але звучить як антипатерн
Michele d'Amico

Легко фіксується: додайте значення, __set__яке підвищує значення, ValueErrorщоб запобігти переопрацюванню.
Кіран Джонналагада

30

Чи можливо використовувати функцію властивості () за допомогою функцій, прикрашених класним методом?

Немає.

Однак класний метод - це просто зв'язаний метод (часткова функція) для класу, доступний з екземплярів цього класу.

Оскільки екземпляр є функцією класу, і ви можете отримати клас з екземпляра, ви можете отримати будь-яку бажану поведінку, яка вам потрібна, з властивістю класу за допомогою property:

class Example(object):
    _class_property = None
    @property
    def class_property(self):
        return self._class_property
    @class_property.setter
    def class_property(self, value):
        type(self)._class_property = value
    @class_property.deleter
    def class_property(self):
        del type(self)._class_property

Цей код можна використовувати для тестування - він повинен пройти, не викликаючи помилок:

ex1 = Example()
ex2 = Example()
ex1.class_property = None
ex2.class_property = 'Example'
assert ex1.class_property is ex2.class_property
del ex2.class_property
assert not hasattr(ex1, 'class_property')

І зауважте, що нам взагалі не потрібні метакласи - і ви жодним чином не маєте доступу до метакласу через екземпляри його класів.

написання @classpropertyдекоратора

Ви можете створити classpropertyдекоратор лише за декількома рядками коду шляхом підкласифікації property(це реалізовано в C, але еквівалентний Python ви можете побачити тут ):

class classproperty(property):
    def __get__(self, obj, objtype=None):
        return super(classproperty, self).__get__(objtype)
    def __set__(self, obj, value):
        super(classproperty, self).__set__(type(obj), value)
    def __delete__(self, obj):
        super(classproperty, self).__delete__(type(obj))

Тоді поводьтеся з декоратором так, ніби це метод класу, поєднаний із властивістю:

class Foo(object):
    _bar = 5
    @classproperty
    def bar(cls):
        """this is the bar attribute - each subclass of Foo gets its own.
        Lookups should follow the method resolution order.
        """
        return cls._bar
    @bar.setter
    def bar(cls, value):
        cls._bar = value
    @bar.deleter
    def bar(cls):
        del cls._bar

І цей код повинен працювати без помилок:

def main():
    f = Foo()
    print(f.bar)
    f.bar = 4
    print(f.bar)
    del f.bar
    try:
        f.bar
    except AttributeError:
        pass
    else:
        raise RuntimeError('f.bar must have worked - inconceivable!')
    help(f)  # includes the Foo.bar help.
    f.bar = 5

    class Bar(Foo):
        "a subclass of Foo, nothing more"
    help(Bar) # includes the Foo.bar help!
    b = Bar()
    b.bar = 'baz'
    print(b.bar) # prints baz
    del b.bar
    print(b.bar) # prints 5 - looked up from Foo!

    
if __name__ == '__main__':
    main()

Але я не впевнений, наскільки це було б радимо. Старі статті зі списку розсилки припускають, що вона не повинна працювати.

Початок власності працювати над класом:

Недоліком вищесказаного є те, що "властивість класу" недоступна для класу, оскільки вона просто перезапише дескриптор даних із класу __dict__.

Однак ми можемо замінити це властивістю, визначеним у метакласі __dict__. Наприклад:

class MetaWithFooClassProperty(type):
    @property
    def foo(cls):
        """The foo property is a function of the class -
        in this case, the trivial case of the identity function.
        """
        return cls

І тоді екземпляр класу метакласу може мати властивість, яка отримує доступ до властивості класу, використовуючи принцип, вже продемонстрований у попередніх розділах:

class FooClassProperty(metaclass=MetaWithFooClassProperty):
    @property
    def foo(self):
        """access the class's property"""
        return type(self).foo

І тепер ми бачимо обидва екземпляри

>>> FooClassProperty().foo
<class '__main__.FooClassProperty'>

і клас

>>> FooClassProperty.foo
<class '__main__.FooClassProperty'>

мати доступ до властивості класу.


3
Чітка, лаконічна, ретельна: це має бути прийнята відповідь.
pfabri

25

Пітон 3!

Старе питання, багато поглядів, дуже потребує єдиного вірного способу Python 3.

На щастя, з metaclassкваргом це легко :

class FooProperties(type):

    @property
    def var(cls):
        return cls._var

class Foo(object, metaclass=FooProperties):
    _var = 'FOO!'

Тоді, >>> Foo.var

"FOO!"


1
це означає, що немає простого виходу з коробки
mehmet

@mehmet Це не просто? Fooє екземпляром його метакласу, і @propertyйого можна використовувати для своїх методів так само, як це можливо для тих примірників Foo.
OJFord

2
вам довелося визначити інший клас для класу, тобто подвійну складність, припускаючи, що метаклас не може бути використаний повторно.
mehmet

Метод class працює як від класу, так і з екземпляра. Ця властивість працює лише з класу. Я не думаю, що саме цього просять.
Зал Аарона

1
@AaronHall Якщо це важливо, він легко додається Foo.__new__. Хоча в цей момент, можливо, варто застосувати замість gettttribute, або поставити під сумнів, чи існує вигляд, що існує мовна функція, це дійсно той підхід, який ви хочете взагалі взяти.
OJFord

16

Немає розумного способу змусити цю систему "властивості класу" працювати в Python.

Ось один необґрунтований спосіб змусити його працювати. Ви, звичайно, можете зробити його більш безшовним із збільшенням кількості магії метакласу.

class ClassProperty(object):
    def __init__(self, getter, setter):
        self.getter = getter
        self.setter = setter
    def __get__(self, cls, owner):
        return getattr(cls, self.getter)()
    def __set__(self, cls, value):
        getattr(cls, self.setter)(value)

class MetaFoo(type):
    var = ClassProperty('getvar', 'setvar')

class Foo(object):
    __metaclass__ = MetaFoo
    _var = 5
    @classmethod
    def getvar(cls):
        print "Getting var =", cls._var
        return cls._var
    @classmethod
    def setvar(cls, value):
        print "Setting var =", value
        cls._var = value

x = Foo.var
print "Foo.var = ", x
Foo.var = 42
x = Foo.var
print "Foo.var = ", x

Вузол проблеми полягає в тому, що властивості - це те, що Python називає "дескрипторами". Немає короткого і простого способу пояснити, як працює такий вид метапрограмування, тому я повинен вказати вам на те, як описано дескриптор .

Вам потрібно коли-небудь розуміти подібні речі, лише якщо ви реалізуєте досить просунуті рамки. Як прозора система збереження об'єкта або система RPC, або якась доменна мова.

Однак у коментарі до попередньої відповіді ти кажеш, що ти

потрібно змінити атрибут, який таким чином, який бачать усі екземпляри класу, і в області, з якої ці методи класу викликаються, не має посилань на всі екземпляри класу.

Мені здається, те, що ви насправді хочете, - це модель дизайну Observer .


Мені подобається ідея прикладу коду, але, схоже, це було б трохи незграбно на практиці.
Марк Родді

Те, що я намагаюся досягти, - це досить просте встановлення вперед і отримання єдиного атрибута, який використовується як прапор для зміни поведінки всіх екземплярів, тому я думаю, що спостерігач буде перекритий тим, що я намагаюся зробити. Якби було декілька атрибутів, про які йдеться, тоді я був би більш схильний.
Марк Родді

Здається, що просто оприлюднення функцій та виклик їх безпосередньо було рішенням спрощеного списку. Мені було цікаво, чи роблю я щось не так або якщо намагаюся зробити це неможливо. Вибачте за багато коментарів, до речі. Ліміт 300 символів відстій.
Марк Родді,

Чудова річ у прикладі коду полягає в тому, що ви можете реалізувати всі незграбні біти один раз, а потім успадкувати їх. Просто перемістіть _var у похідний клас. клас D1 (Foo): _var = 21 клас D2 (Foo) _var = "Привіт" D1.var 21 D2.var Привіт
Thomas L Holaday

6

Якщо встановити його лише для мета-класу, це не допоможе, якщо ви хочете отримати доступ до властивості класу через створений об'єкт, і в цьому випадку вам потрібно встановити нормальне властивість і на об'єкт (який пересилає до властивості класу). Я думаю, що наступне трохи зрозуміліше:

#!/usr/bin/python

class classproperty(property):
    def __get__(self, obj, type_):
        return self.fget.__get__(None, type_)()

    def __set__(self, obj, value):
        cls = type(obj)
        return self.fset.__get__(None, cls)(value)

class A (object):

    _foo = 1

    @classproperty
    @classmethod
    def foo(cls):
        return cls._foo

    @foo.setter
    @classmethod
    def foo(cls, value):
        cls.foo = value

a = A()

print a.foo

b = A()

print b.foo

b.foo = 5

print a.foo

A.foo = 10

print b.foo

print A.foo

3

Половина рішення, __set__ на класі не працює, досі. Рішення - це власний клас властивостей, що реалізує як властивість, так і статичний метод

class ClassProperty(object):
    def __init__(self, fget, fset):
        self.fget = fget
        self.fset = fset

    def __get__(self, instance, owner):
        return self.fget()

    def __set__(self, instance, value):
        self.fset(value)

class Foo(object):
    _bar = 1
    def get_bar():
        print 'getting'
        return Foo._bar

    def set_bar(value):
        print 'setting'
        Foo._bar = value

    bar = ClassProperty(get_bar, set_bar)

f = Foo()
#__get__ works
f.bar
Foo.bar

f.bar = 2
Foo.bar = 3 #__set__ does not

3

Тому що мені потрібно змінити атрибут, який відображається всіма екземплярами класу, і в області, з якої ці методи класу викликаються, немає посилань на всі екземпляри класу.

Чи маєте ви доступ принаймні до одного примірника класу? Я можу придумати спосіб, як це зробити:

class MyClass (object):
    __var = None

    def _set_var (self, value):
        type (self).__var = value

    def _get_var (self):
        return self.__var

    var = property (_get_var, _set_var)

a = MyClass ()
b = MyClass ()
a.var = "foo"
print b.var

2

Спробуйте це, він виконує роботу, не змінюючи / додаючи багато існуючого коду.

>>> class foo(object):
...     _var = 5
...     def getvar(cls):
...         return cls._var
...     getvar = classmethod(getvar)
...     def setvar(cls, value):
...         cls._var = value
...     setvar = classmethod(setvar)
...     var = property(lambda self: self.getvar(), lambda self, val: self.setvar(val))
...
>>> f = foo()
>>> f.var
5
>>> f.var = 3
>>> f.var
3

propertyФункції необхідні два callableаргументу. дайте їм лямбда-обгортки (що це передає екземпляр як перший аргумент) і все добре.


Як зазначає Флоріан Бьош, потрібний синтаксис (сторонні бібліотеки чи застарілий код) - foo.var.
Томас Л Холадей

2

Ось рішення, яке повинно працювати як для доступу через клас, так і для доступу через екземпляр, який використовує метаклас.

In [1]: class ClassPropertyMeta(type):
   ...:     @property
   ...:     def prop(cls):
   ...:         return cls._prop
   ...:     def __new__(cls, name, parents, dct):
   ...:         # This makes overriding __getattr__ and __setattr__ in the class impossible, but should be fixable
   ...:         dct['__getattr__'] = classmethod(lambda cls, attr: getattr(cls, attr))
   ...:         dct['__setattr__'] = classmethod(lambda cls, attr, val: setattr(cls, attr, val))
   ...:         return super(ClassPropertyMeta, cls).__new__(cls, name, parents, dct)
   ...:

In [2]: class ClassProperty(object):
   ...:     __metaclass__ = ClassPropertyMeta
   ...:     _prop = 42
   ...:     def __getattr__(self, attr):
   ...:         raise Exception('Never gets called')
   ...:

In [3]: ClassProperty.prop
Out[3]: 42

In [4]: ClassProperty.prop = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-e2e8b423818a> in <module>()
----> 1 ClassProperty.prop = 1

AttributeError: can't set attribute

In [5]: cp = ClassProperty()

In [6]: cp.prop
Out[6]: 42

In [7]: cp.prop = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-e8284a3ee950> in <module>()
----> 1 cp.prop = 1

<ipython-input-1-16b7c320d521> in <lambda>(cls, attr, val)
      6         # This makes overriding __getattr__ and __setattr__ in the class impossible, but should be fixable
      7         dct['__getattr__'] = classmethod(lambda cls, attr: getattr(cls, attr))
----> 8         dct['__setattr__'] = classmethod(lambda cls, attr, val: setattr(cls, attr, val))
      9         return super(ClassPropertyMeta, cls).__new__(cls, name, parents, dct)

AttributeError: can't set attribute

Це також працює із сетером, визначеним у метакласі.


1

Шукаючи різні місця, я знайшов метод визначення властивості класу, дійсного для Python 2 та 3.

from future.utils import with_metaclass

class BuilderMetaClass(type):
    @property
    def load_namespaces(self):
        return (self.__sourcepath__)

class BuilderMixin(with_metaclass(BuilderMetaClass, object)):
    __sourcepath__ = 'sp'        

print(BuilderMixin.load_namespaces)

Сподіваюся, це може комусь допомогти :)


1
Якщо цей метод десь ви знайшли, було б добре надати посилання (див. Як посилатися на матеріали, написані іншими )
Ендрю Майєрс

-27

Ось моя пропозиція. Не використовуйте методи класу.

Серйозно.

У чому причина використання методів класу в цьому випадку? Чому б не мати звичайний предмет звичайного класу?


Якщо ви просто хочете змінити значення, властивість насправді не дуже корисна? Просто встановіть значення атрибута і виконайте його.

Властивість слід використовувати лише у тому випадку, якщо є що приховати - щось, що може змінитися в майбутньому впровадженні.

Можливо, ваш приклад позбавлений шляху, і є пекельний розрахунок, який ви закінчили. Але це не схоже на те, що властивість додає значної цінності.

Методи "конфіденційності", що впливають на Java (в Python, імена атрибутів, які починаються з _) насправді не дуже корисні. Приватний від кого? Суть приватного є дещо туманною, коли у вас є джерело (як це робиться в Python.)

Інструменти стилів EJB, що впливають на Яву (часто це робиться як властивості в Python), є для того, щоб полегшити примітивну самоаналізацію Java, а також передати збирач з компілятором статичної мови. Усі ці геттери і сетери не так корисні в Python.


14
Тому що мені потрібно змінити атрибут, який відображається всіма екземплярами класу, і в області, з якої ці методи класу викликаються, немає посилань на всі екземпляри класу.
Марк Родді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.