什么时候应该将属性设置为私有并创建只读属性?

127

我不知道属性何时应该是私有的,以及是否应该使用property

我最近读到说,使用setter和getter不符合pythonic风格,但使用property装饰器是可以的。

但是如果我有一个必须不能从类外部设置但可以被读取的属性(只读属性),这个属性应该是私有的吗?而且我指的是带下划线的私有属性,就像这样:self._x? 如果是的话,那么我怎么能在没有使用getter的情况下读取它呢? 目前我知道的唯一方法是写成:

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

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

但是我真的需要在不应该被设置的对象上进行设置吗?也许我应该放弃。但是再说一遍,我不能使用下划线,因为读取obj._x对用户来说很奇怪,所以我应该使用obj.x,但是用户又不知道他不应该设置这个属性。

你有什么看法和做法吗?


1
属性的概念是它的行为类似于一个属性,但可以有额外的代码。如果你只想获取一个值,我甚至不会费心:只需使用 self.x 并相信没有人会更改 x。如果确保 x 不能被更改很重要,那么请使用属性。 - li.davidm
此外,_x 根本不是奇数:按照惯例,它表示某些“私有”的东西。 - li.davidm
1
我的意思是从 _x 读取是奇怪的,不是 _x 名称本身。如果用户直接从 _x 读取,那么他是不负责任的。 - Rafał Łużyński
3
重要提示!为了确保无法设置obj.x,您的类必须是新式类,也就是继承自object。在旧式类中,您实际上仍然可以设置obj.x,但会得到一些意外的结果。 - Ian H
有几个合理的理由使用只读属性。其中之一是当您有一个值,该值由合并两个其他(读/写)值组成时。您可以在方法中执行此操作,但也可以在只读属性中执行。 - philologon
如果你独自在项目上工作,而该项目无法生存1年,那么关注访问控制就没有意义。然而,在这些边界之外,花费一些时间来加强安全性确实是有意义的。 - uuu777
10个回答

99

只是我的个人看法,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

20
Python不是类型不安全的,它是动态类型的。名称改编不是为了使欺骗更加困难,而是为了避免在继承可能有问题的情况下出现名称冲突的情况(如果你不是在大型项目中编程,你甚至不用关心这个)。 - memeplex
3
但是你仍然要记住,使用这种方法可以改变可变对象。例如,如果self.__a = [],你仍然可以执行a.a.append('anything'),它会起作用。 - Igor
4
我不清楚“合理(明智)的人”对这个答案有什么影响。您能否更明确地说明您认为合理的人会做和不会做哪些事情? - winni2k
3
这个回答的重点是“如果要使用@property修饰符,就需要在继承时从object类继承”。谢谢。 - akki
2
@kkm,永远不让漏洞潜入代码的唯一方法就是永远不写代码。 - Alechan
显示剩余3条评论

88

通常情况下,Python程序应该假设所有用户都是有责任正确使用事物的成年人。然而,在极少数情况下,如果某个属性无法被设置(例如派生值或从某些静态数据源读取的值),则只允许getter的属性通常是首选模式。


38
看起来你的回答有矛盾之处。你说用户应该对正确使用东西负责,然后又说有时设置属性可能没有意义,最好使用getter属性。在我看来,你可以或者不能设置属性。唯一的问题是我是否应该保护这个属性或者保持不变。中间不应该有其他答案。 - Rafał Łużyński
23
不,我说的是如果您真的无法设置一个值,那么拥有setter就没有意义。例如,如果您有一个圆对象,它有一个半径成员和一个从半径推导出来的周长属性,或者您有一个包装某些只读实时API的对象,它具有许多只读属性。这不会与任何事情相矛盾。 - Silas Ray
10
负责任的用户不会尝试设置实际上不能设置的属性。而不负责任的用户则会设置实际上可以设置但会在代码其他地方由于他的设置而引发错误的属性。因此,最终两个属性都不能被设置。我应该在两个属性上都使用property还是不使用任何属性? - Rafał Łużyński
8
负责任的用户不应尝试设置确实不能设置的属性。在编程中,如果某个值严格来说是不可设置的,明智的做法就是确保它无法被设置。这些微小的细节都有助于编写可靠的程序。 - Robin Smith
6
很多人和语言都持有这种立场。如果你认为这是不可妥协的立场,那么你可能不应该使用Python。 - Silas Ray
显示剩余7条评论

70

这里有一种方法可以避免假设所有用户都是成年人,并因此对自己正确使用事物负责的假设。

请注意下面我的更新

使用@property非常冗长,例如:

请看下面的更新

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

1
这非常有帮助,完全符合我想要做的事情。谢谢。不过,这是给已经安装了Python 3的人用的。我正在使用Python 2.7.8,所以我必须对你的解决方案进行两个小调整:"class NewClass(cls, <b>object<\b>):" … "<b>super(NewClass, self)<\b>.setattr(name, value)"。 - Ying Zhang
1
此外,应该注意类成员变量是列表和字典的情况。你不能真正地防止它们被更新。 - Ying Zhang
2
这里有一个改进和三个问题。 改进:if..elif..else块可以只是if name in attrs and name in self.__dict__: raise Attr...,不需要pass。 问题1:所有这样装饰的类最终都具有相同的__name__,它们的类型的字符串表示也被同化了。 问题2:此装饰会覆盖任何自定义的__setattr__。 问题3:用户可以通过del MyClass.__setattr__来击败它。 - TigerhawkT3
只是一个语言问题。“Alas…”的意思是“遗憾地说……”,我认为这不是你想要的。 - Thomas Andrews
1
我喜欢装饰器的方法,但它会丢失原始类名和类文档,这可能对“官方、有文档记录”的类不利。难道不应该有一个修饰器来修改原始类而不是创建一个新类吗?只需定义一个内部函数fixSetAttr,它与被覆盖的__setattr__本质上相同;它需要在内部调用super(cls, self)而不是super(),然后使用setattr(cls, '__setattr__', fixSetAttr)来修改原始类而不是创建一个新类。 - Georg Sander
显示剩余2条评论

6
这里提供了一种略有不同的只读属性访问方式,也许应该称之为“只写一次”属性,因为它们必须得到初始化。对于我们中的偏执狂者而言,他们担心通过直接访问对象的字典来修改属性,我引入了“极端”的名称混淆:
from uuid import uuid4

class ReadOnlyProperty:
    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 = ReadOnlyProperty('x')
    y = ReadOnlyProperty('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)

不错。如果你将 dict_name 搞混了,例如 dict_name = "_spam_" + name,它将消除对 uuid4 的依赖,并使调试变得容易得多。 - c z
但是我可以说 p.__dict__['_spam_x'] = 5 来改变 p.x 的值,因此这并不提供足够的名称混淆。 - Booboo

4

我对之前两个创建只读属性的答案不满意,因为第一个解决方案允许删除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__
        #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()
        

除非您在编写库代码(即分发给他人以增强其程序而不是为任何其他目的(如应用程序开发)而编写的代码),否则制作只读属性没有意义。通过使用不可变的types.MappingProxyType解决了__dict__问题,因此无法通过__dict__更改属性。设置或删除__dict__也被阻止。改变只读属性的唯一方法是通过更改类本身的方法。
虽然我认为我的解决方案比之前的两个都要好,但仍有改进余地。这些是代码的缺点:
1. 不允许在子类中添加设置或删除只读属性的方法。在子类中定义的方法将自动被禁止访问只读属性,即使通过调用超类版本的方法也不行。 2. 类的只读方法可以更改以打破只读限制。
然而,没有一种方法可以在不编辑类的情况下设置或删除只读属性。这不依赖于命名约定,这很好,因为Python在命名约定上并不那么一致。通过将要作为参数传递给修饰符的属性列出来,它们将变为只读属性。
感谢Brice's answer提供了获取调用者类和方法的方法。

object.__setattr__(pfi, 'readonly', 'foobar') 破坏了这个解决方案,而不需要编辑类本身。 - L3viathan

3
那是我的解决方法。
@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

1

有人提到使用代理对象,但我没有看到示例,所以最终尝试了一下,效果很差。

/!\ 如果可能,请优先使用类定义和类构造函数

这段代码实际上是在重新编写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))

1
请注意,实例方法也是属性(类的属性),如果您真的想成为一个牛逼的人,您可以在类或实例级别上设置它们。或者您可以设置一个类变量(也是类的属性),其中方便的只读属性不会很容易地工作。我想说的是,“只读属性”问题实际上比通常认为的更普遍。幸运的是,有一些传统的期望正在起作用,以至于我们对这些其他情况视而不见(毕竟,在Python中几乎所有东西都是某种属性)。
基于这些期望,我认为最通用和轻量级的方法是采用公共属性(没有前导下划线)只读,除非明确记录为可写入。这包括了通常的期望,即方法不会被修补程序,指示实例默认值的类变量更好地放置在原处。如果您对某个特殊属性感到非常偏执,可以将只读描述符用作最后的资源措施。

1

虽然我喜欢Oz123的类修饰符,但您也可以使用以下方法,它使用显式类包装器和__new__及闭包中返回类的类工厂方法:

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

你应该添加一些测试,展示它如何在多个属性下工作。 - oz123

-2

我知道我正在重拾这个帖子,但我正在研究如何使属性只读,并在找到这个主题后,我对已经分享的解决方案不满意。

因此,回到最初的问题,如果您从以下代码开始:

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

5
如果你没有编写 setter 方法,试图进行赋值操作同样会引发错误(一个 AttributeError('can't set attribute'))。 - Artyer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接