Я не удовлетворен двумя предыдущими ответами на создание свойств только для чтения, потому что первое решение позволяет удалить атрибут 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's __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__
cls = final(cls)
return cls
return classrebuilder
def setreadonlyattributes(cls, *readonlyattrs):
return readonlyattributes(*readonlyattrs)(cls)
if __name__ == '__main__':
@readonlyattributes('readonlyfield')
class ReadonlyFieldClass(object):
def __init__(self, a, b):
self.readonlyfield = a
self.publicfield = b
attr = None
def main():
global attr
pfi = ReadonlyFieldClass('forbidden', 'changable')
try:
print(pfi.publicfield)
print('__getattribute__ works')
pfi.publicfield = 'mutable'
print('__setattr__ seems to work')
print(pfi.publicfield)
print('__setattr__ definitely works')
del pfi.publicfield
print('__delattr__ seems to work')
print(pfi.publlicfield)
raise RuntimeError('__delattr__ doesn\'t work')
except(AttributeError):
print('__delattr__ works')
try:
print(pfi.readonlyfield)
print('__getattribute__ works')
pfi.readonlyfield = 'readonly'
raise RuntimeError('__setattr__ doesn\'t work')
except(AttributeError):
print('__setattr__ seems to work')
try:
print(pfi.readonlyfield)
print('__setattr__ works')
del pfi.readonlyfield
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__ также заблокировано. Единственный способ изменить свойства, доступные только для чтения, - это изменить методы самого класса.
Хотя я считаю, что мое решение лучше, чем из двух предыдущих, его можно улучшить. Вот слабые стороны этого кода:
a) Не позволяет добавлять в метод в подклассе, который устанавливает или удаляет атрибут только для чтения. Методу, определенному в подклассе, автоматически запрещается доступ к атрибуту только для чтения, даже при вызове версии метода суперкласса.
б) Методы класса только для чтения можно изменить, чтобы обойти ограничения только для чтения.
Однако без редактирования класса невозможно установить или удалить атрибут только для чтения. Это не зависит от соглашений об именах, и это хорошо, потому что Python не очень согласуется с соглашениями об именах. Это дает возможность сделать атрибуты только для чтения, которые нельзя изменить с помощью скрытых лазеек без редактирования самого класса. Просто укажите атрибуты, которые будут доступны только для чтения при вызове декоратора в качестве аргументов, и они станут доступны только для чтения.
Кредит на ответ Брайса в разделе Как получить имя класса вызывающего абонента внутри функции другого класса в Python? для получения классов и методов вызывающего объекта.
self.x
и верьте, что никто не изменитсяx
. Еслиx
важно убедиться, что это нельзя изменить, используйте свойство.