Python只读属性

时间:2013-01-29 23:37:13

标签: python properties python-2.7 private readonly

我不知道什么时候属性应该是私有的,如果我应该使用属性。

我最近读到,setter和getter不是pythonic,我应该使用属性装饰器。 没关系。

但是,如果我有属性,那么不能从类外部设置但可以读取(只读属性)。这个属性应该是私有的吗?私有的我应该用下划线表示self._x? 如果是,那么如何在不使用getter的情况下阅读它? 我现在只知道的方法是写

@property
def x(self):
    return self._x

这样我可以通过obj.x读取属性,但我无法设置它obj.x = 1,所以没关系。

但是我真的应该关心设置不能设置的对象吗?也许我应该离开它。但是我又不能使用下划线,因为阅读obj._x对于用户来说是奇怪的,所以我应该使用obj.x然后再次用户不知道他不能设置这个属性。

您的意见和实践是什么?

10 个答案:

答案 0 :(得分:49)

一般来说,Python程序应该假设所有用户都同意成人,因此负责自己正确使用事物。但是,在极少数情况下,对于可设置的属性(例如派生值或从某些静态数据源读取的值)没有意义,getter-only属性通常是首选模式。

答案 1 :(得分:46)

仅仅是我的两分钱,Silas Ray是在正确的轨道上,但我想添加一个例子。 ; - )

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

答案 2 :(得分:38)

这是一种避免假设

的方法
  

所有用户都同意成年人,因此负责使用   事情本身就是正确的。

请参阅下面的更新

使用@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's 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

你问:

  

read_only_properties来自哪里?

很高兴你问,这是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

更新

我从没想过这个答案会受到如此多的关注。令人惊讶的是。这鼓励我创建一个可以使用的包。

$ pip install read-only-properties
你的python shell中的

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

答案 3 :(得分:3)

这是一种略有不同的只读属性方法,可能应该称为一次写入属性,因为它们必须初始化,不是吗?对于我们中担心能够通过直接访问对象字典来修改属性的偏执狂,我引入了“极端”名称修改:

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)

答案 4 :(得分:0)

请注意,实例方法也是(类的)属性,如果你真的想成为一个坏蛋,你可以在类或实例级别设置它们。或者您可以设置一个类变量(也是该类的一个属性),其中方便的只读属性不会立即开箱即用。我想说的是&#34; readonly属性&#34;问题实际上比通常认为的更为普遍。幸运的是,在工作中存在传统的期望,这些期望是如此强大,以至于使这些其他情况失明(毕竟,几乎所有东西都是python中的某种属性)。

基于这些期望,我认为最通用和最轻量级的方法是采用公共&#34;公共&#34; (没有前导下划线)属性是只读的,除非明确记录为可写。这包含了通常的期望,即方法不会被修补,而表示实例默认值的类变量更不用说了。如果您对某些特殊属性感到非常偏执,请使用只读描述符作为最后一个资源度量。

答案 5 :(得分:0)

虽然我喜欢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()

答案 6 :(得分:0)

这是我的解决方法。

@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

答案 7 :(得分:0)

我对创建只读属性的前两个答案不满意,因为第一种解决方案允许删除readonly属性,然后进行设置,并且不会阻止__dict__。第二种解决方案可以与测试一起解决-找到等于您将其设置为2的值,并最终对其进行更改。

现在,输入代码。

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()

除非您正在编写库代码代码,这些代码将被分发给其他人以用于增强其程序的代码,而不是出于任何其他目的的代码,否则没有必要设置只读属性,例如应用程序开发。解决了__dict__问题,因为__dict__现在具有不可变的类型。MappingProxyType,因此无法通过__dict__更改属性。设置或删除__dict__也被阻止。更改只读属性的唯一方法是更改​​类本身的方法。

尽管我认为我的解决方案比前两个解决方案要好,但是可以改进。这些是此代码的弱点:

a)不允许在子类中添加设置或删除只读属性的方法。即使调用了超类的方法,也会自动禁止子类中定义的方法访问只读属性。

b)可以更改类的只读方法以克服只读限制。

但是,如果不编辑类来设置或删除只读属性,就无法实现。这不依赖于命名约定,这很好,因为Python与命名约定不太一致。这提供了一种方法,使只读属性无法通过隐藏的漏洞进行更改,而无需编辑类本身。只需在将装饰器作为参数调用时列出要只读的属性即可,它们将变为只读。

How to get the caller class name inside a function of another class in python?中使用Brice的答案来获得调用者的类和方法。

答案 8 :(得分:0)

有人提到使用代理对象,但我没有看到这样的例子,所以我最终尝试了一下,[可怜]。

/!\请尽可能使用类定义和类构造函数

此代码有效地重写了class.__new__(类构造函数),但在各个方面都更糟。减轻痛苦,如果可以的话,不要使用此模式。

def attr_proxy(obj):
    """ Use dynamic class definition to bind obj and proxy_attrs.
        If you can extend the target class constructor that is 
        cleaner, but its not always trivial to do so.
    """
    proxy_attrs = dict()

    class MyObjAttrProxy():
        def __getattr__(self, name):
            if name in proxy_attrs:
                return proxy_attrs[name]  # overloaded

            return getattr(obj, name)  # proxy

        def __setattr__(self, name, value):
            """ note, self is not bound when overloading methods
            """
            proxy_attrs[name] = value

    return MyObjAttrProxy()


myobj = attr_proxy(Object())
setattr(myobj, 'foo_str', 'foo')

def func_bind_obj_as_self(func, self):
    def _method(*args, **kwargs):
        return func(self, *args, **kwargs)
    return _method

def mymethod(self, foo_ct):
    """ self is not bound because we aren't using object __new__
        you can write the __setattr__ method to bind a self 
        argument, or declare your functions dynamically to bind in 
        a static object reference.
    """
    return self.foo_str + foo_ct

setattr(myobj, 'foo', func_bind_obj_as_self(mymethod, myobj))

答案 9 :(得分:-1)

我知道我正在从死里复活这个帖子,但是我正在研究如何使一个属性只读,在找到这个主题后,我对已经分享的解决方案不满意。

所以,回到最初的问题,如果你从这个代码开始:

@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"