Свойство Python для чтения
Я не знаю, когда атрибут должен быть закрытым, и если я должен использовать свойство.
Недавно я читал, что сеттеры и геттеры не являются питоническими, и я должен использовать декоратор свойств.
Все в порядке.
Но что, если у меня есть атрибут, который не должен быть установлен извне класса, но может быть прочитан (атрибут только для чтения). Должен ли этот атрибут быть закрытым, а частным - под знаком подчеркивания, например self._x
?
Если да, то как я могу прочитать его без использования getter?
Только метод, который я знаю прямо сейчас, это написать
@property
def x(self):
return self._x
Таким образом, я могу читать атрибут obj.x
, но я не могу установить его obj.x = 1
, так что это нормально.
Но должен ли я действительно заботиться об установке объекта, который не должен быть установлен? Может быть, я просто оставлю это. Но опять-таки я не могу использовать подчеркивание, потому что чтение obj._x
нечетно для пользователя, поэтому я должен использовать obj.x
, а затем снова пользователь не знает, что он не должен устанавливать этот атрибут.
Какое ваше мнение и практика?
Ответы
Ответ 1
Как правило, программы Python должны быть написаны с предположением, что все пользователи согласны с взрослыми и, следовательно, несут ответственность за правильное использование вещей. Однако в редком случае, когда атрибут, который может быть установлен (например, производное значение или значение, считываемое из некоторого статического источника данных), просто не имеет смысла, свойство getter-only обычно является предпочтительным.
Ответ 2
Просто мои два цента, Сайлас Рэй находится на правильном пути, однако я хотел добавить пример. ;-)
Python - это небезопасный язык, и поэтому вам всегда нужно будет доверять пользователям вашего кода, чтобы использовать код как разумный (разумный) человек.
Согласно PEP 8:
Используйте одно начальное подчеркивание только для закрытых методов и переменных экземпляра.
Чтобы иметь свойство "только для чтения" в классе, вы можете использовать декорацию @property
, вам нужно наследовать от object
когда вы делаете это, чтобы использовать классы нового стиля.
Пример:
>>> class A(object):
... def __init__(self, a):
... self._a = a
...
... @property
... def a(self):
... return self._a
...
>>> a = A('test')
>>> a.a
'test'
>>> a.a = 'pleh'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
Ответ 3
Вот способ избежать предположения о том, что
все пользователи являются взрослыми по согласию и, следовательно, несут ответственность за все правильно сами.
пожалуйста, смотрите мое обновление ниже
Использование @property
очень многословно, например:
class AClassWithManyAttributes:
'''refactored to properties'''
def __init__(a, b, c, d, e ...)
self._a = a
self._b = b
self._c = c
self.d = d
self.e = e
@property
def a(self):
return self._a
@property
def b(self):
return self._b
@property
def c(self):
return self._c
# you get this ... it long
Использование
Без подчеркивания: это публичная переменная.
Одно подчеркивание: это защищенная переменная.
Два подчеркивания: это приватная переменная.
Кроме последнего, это соглашение. Тем не менее, если вы действительно стараетесь, вы можете получить доступ к переменным с двойным подчеркиванием.
Так что же нам делать? Мы отказываемся от того, что в Python есть свойства только для чтения?
Вот! read_only_properties
декоратор на помощь!
@read_only_properties('readonly', 'forbidden')
class MyClass(object):
def __init__(self, a, b, c):
self.readonly = a
self.forbidden = b
self.ok = c
m = MyClass(1, 2, 3)
m.ok = 4
# we can re-assign a value to m.ok
# read only access to m.readonly is OK
print(m.ok, m.readonly)
print("This worked...")
# this will explode, and raise AttributeError
m.forbidden = 4
Вы спрашиваете:
Where is read_only_properties
coming from?
Рад, что вы спросили, вот источник для read_only_properties:
def read_only_properties(*attrs):
def class_rebuilder(cls):
"The class decorator"
class NewClass(cls):
"This is the overwritten class"
def __setattr__(self, name, value):
if name not in attrs:
pass
elif name not in self.__dict__:
pass
else:
raise AttributeError("Can't modify {}".format(name))
super().__setattr__(name, value)
return NewClass
return class_rebuilder
update
обновлениеЯ никогда не ожидал, что этот ответ получит столько внимания. Удивительно, но это так. Это подтолкнуло меня к созданию пакета, который вы можете использовать.
$ pip install read-only-properties
в вашей оболочке Python:
In [1]: from rop import read_only_properties
In [2]: @read_only_properties('a')
...: class Foo:
...: def __init__(self, a, b):
...: self.a = a
...: self.b = b
...:
In [3]: f=Foo('explodes', 'ok-to-overwrite')
In [4]: f.b = 5
In [5]: f.a = 'boom'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-5-a5226072b3b4> in <module>()
----> 1 f.a = 'boom'
/home/oznt/.virtualenvs/tracker/lib/python3.5/site-packages/rop.py in __setattr__(self, name, value)
116 pass
117 else:
--> 118 raise AttributeError("Can't touch {}".format(name))
119
120 super().__setattr__(name, value)
AttributeError: Can't touch a
Ответ 4
Вот немного другой подход к свойствам только для чтения, которые, возможно, следует называть свойствами с однократной записью, поскольку они должны инициализироваться, не так ли? Для параноиков среди нас, которые беспокоятся о возможности изменить свойства путем прямого доступа к словарю объектов, я ввел "экстремальное" искажение имен:
from uuid import uuid4
class Read_Only_Property:
def __init__(self, name):
self.name = name
self.dict_name = uuid4().hex
self.initialized = False
def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.dict_name]
def __set__(self, instance, value):
if self.initialized:
raise AttributeError("Attempt to modify read-only property '%s'." % self.name)
instance.__dict__[self.dict_name] = value
self.initialized = True
class Point:
x = Read_Only_Property('x')
y = Read_Only_Property('y')
def __init__(self, x, y):
self.x = x
self.y = y
if __name__ == '__main__':
try:
p = Point(2, 3)
print(p.x, p.y)
p.x = 9
except Exception as e:
print(e)
Ответ 5
Обратите внимание, что методы экземпляра также являются атрибутами (класса) и что вы можете установить их на уровне класса или экземпляра, если вы действительно хотите быть задиром. Или вы можете установить переменную класса (которая также является атрибутом класса), где удобные свойства readonly не будут работать аккуратно из коробки. Я пытаюсь сказать, что проблема "readonly attribute" на самом деле более общая, чем обычно воспринимается. К счастью, есть обычные ожидания на работе, которые настолько сильны, что мы следим за этими другими случаями (в конце концов, почти все является атрибутом какого-то типа на питоне).
Основываясь на этих ожиданиях, я думаю, что самым общим и легким подходом является принятие соглашения о том, что атрибуты "общедоступные" (без указания подчеркивания) читаются только за исключением случаев, когда они явно документируются как пригодные для записи. Это предполагает обычное ожидание того, что методы не будут исправлены, а переменные класса, указывающие значения по умолчанию, лучше не учитываются. Если вы чувствуете себя параноидальным по поводу какого-то специального атрибута, используйте дескриптор readonly в качестве последней меры ресурса.
Ответ 6
В то время как мне нравится декоратор класса от Oz123, вы также можете сделать следующее, которое использует явную оболочку класса и __new__ с методом класса Factory, возвращающим класс в закрытие:
class B(object):
def __new__(cls, val):
return cls.factory(val)
@classmethod
def factory(cls, val):
private = {'var': 'test'}
class InnerB(object):
def __init__(self):
self.variable = val
pass
@property
def var(self):
return private['var']
return InnerB()
Ответ 7
Это мой обходной путь.
@property
def language(self):
return self._language
@language.setter
def language(self, value):
# WORKAROUND to get a "getter-only" behavior
# set the value only if the attribute does not exist
try:
if self.language == value:
pass
print("WARNING: Cannot set attribute \'language\'.")
except AttributeError:
self._language = value
Ответ 8
Я недоволен предыдущими двумя ответами на создание свойств только для чтения, поскольку первое решение позволяет удалить атрибут readonly, а затем установить его и не блокировать __dict__. Второе решение можно обойти с помощью тестирования - найти значение, равное тому, которое вы установили два, и в конечном итоге изменить его.
Теперь для кода.
def final(cls):
clss = cls
@classmethod
def __init_subclass__(cls, **kwargs):
raise TypeError("type '{}' is not an acceptable base type".format(clss.__name__))
cls.__init_subclass__ = __init_subclass__
return cls
def methoddefiner(cls, method_name):
for clss in cls.mro():
try:
getattr(clss, method_name)
return clss
except(AttributeError):
pass
return None
def readonlyattributes(*attrs):
"""Method to create readonly attributes in a class
Use as a decorator for a class. This function takes in unlimited
string arguments for names of readonly attributes and returns a
function to make the readonly attributes readonly.
The original class __getattribute__, __setattr__, and __delattr__ methods
are redefined so avoid defining those methods in the decorated class
You may create setters and deleters for readonly attributes, however
if they are overwritten by the subclass, they lose access to the readonly
attributes.
Any method which sets or deletes a readonly attribute within
the class loses access if overwritten by the subclass besides the __new__
or __init__ constructors.
This decorator doesn't support subclassing of these classes
"""
def classrebuilder(cls):
def __getattribute__(self, name):
if name == '__dict__':
from types import MappingProxyType
return MappingProxyType(super(cls, self).__getattribute__('__dict__'))
return super(cls, self).__getattribute__(name)
def __setattr__(self, name, value):
if name == '__dict__' or name in attrs:
import inspect
stack = inspect.stack()
try:
the_class = stack[1][0].f_locals['self'].__class__
except(KeyError):
the_class = None
the_method = stack[1][0].f_code.co_name
if the_class != cls:
if methoddefiner(type(self), the_method) != cls:
raise AttributeError("Cannot set readonly attribute '{}'".format(name))
return super(cls, self).__setattr__(name, value)
def __delattr__(self, name):
if name == '__dict__' or name in attrs:
import inspect
stack = inspect.stack()
try:
the_class = stack[1][0].f_locals['self'].__class__
except(KeyError):
the_class = None
the_method = stack[1][0].f_code.co_name
if the_class != cls:
if methoddefiner(type(self), the_method) != cls:
raise AttributeError("Cannot delete readonly attribute '{}'".format(name))
return super(cls, self).__delattr__(name)
clss = cls
cls.__getattribute__ = __getattribute__
cls.__setattr__ = __setattr__
cls.__delattr__ = __delattr__
#This line will be moved when this algorithm will be compatible with inheritance
cls = final(cls)
return cls
return classrebuilder
def setreadonlyattributes(cls, *readonlyattrs):
return readonlyattributes(*readonlyattrs)(cls)
if __name__ == '__main__':
#test readonlyattributes only as an indpendent module
@readonlyattributes('readonlyfield')
class ReadonlyFieldClass(object):
def __init__(self, a, b):
#Prevent initalization of the internal, unmodified PrivateFieldClass
#External PrivateFieldClass can be initalized
self.readonlyfield = a
self.publicfield = b
attr = None
def main():
global attr
pfi = ReadonlyFieldClass('forbidden', 'changable')
###---test publicfield, ensure its mutable---###
try:
#get publicfield
print(pfi.publicfield)
print('__getattribute__ works')
#set publicfield
pfi.publicfield = 'mutable'
print('__setattr__ seems to work')
#get previously set publicfield
print(pfi.publicfield)
print('__setattr__ definitely works')
#delete publicfield
del pfi.publicfield
print('__delattr__ seems to work')
#get publicfield which was supposed to be deleted therefore should raise AttributeError
print(pfi.publlicfield)
#publicfield wasn't deleted, raise RuntimeError
raise RuntimeError('__delattr__ doesn\'t work')
except(AttributeError):
print('__delattr__ works')
try:
###---test readonly, make sure its readonly---###
#get readonlyfield
print(pfi.readonlyfield)
print('__getattribute__ works')
#set readonlyfield, should raise AttributeError
pfi.readonlyfield = 'readonly'
#apparently readonlyfield was set, notify user
raise RuntimeError('__setattr__ doesn\'t work')
except(AttributeError):
print('__setattr__ seems to work')
try:
#ensure readonlyfield wasn't set
print(pfi.readonlyfield)
print('__setattr__ works')
#delete readonlyfield
del pfi.readonlyfield
#readonlyfield was deleted, raise RuntimeError
raise RuntimeError('__delattr__ doesn\'t work')
except(AttributeError):
print('__delattr__ works')
try:
print("Dict testing")
print(pfi.__dict__, type(pfi.__dict__))
attr = pfi.readonlyfield
print(attr)
print("__getattribute__ works")
if pfi.readonlyfield != 'forbidden':
print(pfi.readonlyfield)
raise RuntimeError("__getattr__ doesn't work")
try:
pfi.__dict__ = {}
raise RuntimeError("__setattr__ doesn't work")
except(AttributeError):
print("__setattr__ works")
del pfi.__dict__
raise RuntimeError("__delattr__ doesn't work")
except(AttributeError):
print(pfi.__dict__)
print("__delattr__ works")
print("Basic things work")
main()
Нет смысла создавать атрибуты только для чтения, за исключением случаев, когда вы пишете код библиотеки, код, который распространяется другим пользователям в качестве кода, используемого для улучшения их программ, а не кода для каких-либо других целей, таких как разработка приложений. Проблема __dict__ решена, потому что __dict__ теперь имеет неизменяемые типы .MappingProxyType, поэтому атрибуты нельзя изменить с помощью __dict__. Установка или удаление __dict__ также блокируется. Единственный способ изменить свойства только для чтения - это изменить методы самого класса.
Хотя я считаю, что мое решение лучше, чем в предыдущих двух, оно может быть улучшено. Вот эти недостатки кода:
а) Не позволяет добавлять метод в подкласс, который устанавливает или удаляет атрибут "только для чтения". Методу, определенному в подклассе, автоматически запрещается доступ к атрибуту только для чтения, даже при вызове версии метода для суперкласса.
b) Методы readonly класса могут быть изменены, чтобы обойти ограничения только для чтения.
Однако без редактирования класса невозможно установить или удалить атрибут только для чтения. Это не зависит от соглашений об именах, что хорошо, потому что Python не так согласуется с соглашениями об именах. Это позволяет создавать атрибуты только для чтения, которые нельзя изменить скрытыми лазейками без редактирования самого класса. Просто перечислите атрибуты для чтения только при вызове декоратора в качестве аргументов, и они станут только для чтения.
Ответ благодарности Брайсу в Как получить имя класса вызывающей стороны внутри функции другого класса в python? для получения классов вызывающих и методов.
Ответ 9
Я знаю, что возвращаю из мертвых эту тему, но я смотрел, как сделать свойство только для чтения, и после того, как я нашел эту тему, меня не устраивали уже имеющиеся решения.
Итак, вернемся к первоначальному вопросу, если вы начнете с этого кода:
@property
def x(self):
return self._x
И вы хотите сделать X только для чтения, вы можете просто добавить:
@x.setter
def x(self, value):
raise Exception("Member readonly")
Затем, если вы запустите следующее:
print (x) # Will print whatever X value is
x = 3 # Will raise exception "Member readonly"