No estoy satisfecho con las dos respuestas anteriores para crear propiedades de solo lectura porque la primera solución permite eliminar el atributo de solo lectura y luego configurarlo y no bloquea el __dict__. La segunda solución podría resolverse con pruebas: encontrar el valor que sea igual al que estableció dos y cambiarlo eventualmente.
Ahora, por el código.
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__
#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()
No tiene sentido hacer atributos de solo lectura, excepto cuando escribe el código de la biblioteca, código que se distribuye a otros como código para usar con el fin de mejorar sus programas, no código para ningún otro propósito, como el desarrollo de aplicaciones. El problema de __dict__ está resuelto, porque el __dict__ ahora es de los tipos inmutables.MappingProxyType , por lo que los atributos no se pueden cambiar a través de __dict__. También se bloquea la configuración o eliminación de __dict__. La única forma de cambiar las propiedades de solo lectura es cambiando los métodos de la propia clase.
Aunque creo que mi solución es mejor que las dos anteriores, podría mejorarse. Estas son las debilidades de este código:
a) No permite agregar a un método en una subclase que establece o elimina un atributo de solo lectura. A un método definido en una subclase se le prohíbe automáticamente el acceso a un atributo de solo lectura, incluso al llamar a la versión del método de la superclase.
b) Los métodos de solo lectura de la clase se pueden cambiar para anular las restricciones de solo lectura.
Sin embargo, no hay forma sin editar la clase para establecer o eliminar un atributo de solo lectura. Esto no depende de las convenciones de nomenclatura, lo cual es bueno porque Python no es tan consistente con las convenciones de nomenclatura. Esto proporciona una forma de hacer atributos de solo lectura que no se pueden cambiar con lagunas ocultas sin editar la clase en sí. Simplemente enumere los atributos que se leerán solo cuando llame al decorador como argumentos y se convertirán en solo lectura.
Crédito a la respuesta de Brice en ¿Cómo obtener el nombre de la clase de la persona que llama dentro de una función de otra clase en Python? para obtener las clases y métodos de la persona que llama.
self.x
y confía en que nadie cambiaráx
. Six
es importante asegurarse de que no se pueda cambiar, utilice una propiedad.