何时应将属性设为私有并设为只读属性? [关闭]

Posted

技术标签:

【中文标题】何时应将属性设为私有并设为只读属性? [关闭]【英文标题】:When should an attribute be private and made a read-only property? [closed] 【发布时间】:2013-01-13 16:27:00 【问题描述】:

我不知道什么时候应该是私有属性以及是否应该使用property

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

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

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

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

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

您的意见和做法是什么?

【问题讨论】:

属性的想法是它的行为类似于属性,但可以有额外的代码。如果您只想获得一个价值,我什至不会打扰:只需使用 self.x 并相信没有人会更改 x。如果确保无法更改 x 很重要,请使用属性。 另外,_x 一点也不奇怪:按照惯例,它的意思是“私人”。 我的意思是从 _x 读取是奇怪的。不是 _x 名称本身。如果用户直接从 _x 读取,那么他是不负责任的。 重要!你的类必须是new-style class,即继承自object,这样才能真正阻止你设置obj.x。在一个老式的类上,你实际上仍然可以设置obj.x,结果非常出乎意料。 拥有只读属性有几个正当的理由。一种是当您有一个由合并其他两个(读/写)值组成的值时。您可以在方法中执行此操作,但您也可以在只读属性中执行此操作。 【参考方案1】:

就我的两分钱,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

【讨论】:

Python 不是类型不安全的,它是动态类型的。并且名称修改不是为了让作弊变得更加困难,而是为了防止在继承可能有问题的情况下发生名称冲突(如果你不是在大编程,你甚至不应该关心)。 但是你仍然应该记住,可变对象无论如何都可以使用这个方法来改变。例如如果self.__a = [],你仍然可以这样做a.a.append('anything'),它会工作。 我不清楚“一个合理(明智)的人”对这个答案有什么影响。您能否更明确地说明您认为一个通情达理的人会做和不会做的事情? 对我来说使用@property 装饰,这样做时您需要从对象继承 是这个答案的重点。谢谢。 @kkm 永远不会让错误潜入代码的唯一方法是永远不要编写代码。【参考方案2】:

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

【讨论】:

看来您的回答自相矛盾。您说用户应该负责并正确使用事物,然后您说有时设置属性是没有意义的,而 getter 属性是一种首选方式。在我看来,您可以或不能设置属性。唯一的问题是我应该保护这个属性还是离开它。中间应该没有答案。 不,我说如果你真的不能设置一个值,那么拥有一个 setter 是没有意义的。例如,如果您有一个带有半径成员和从半径派生的圆周属性的圆形对象,或者您有一个对象,该对象包装了一些具有许多 getter-only 属性的只读实时 api。没有什么矛盾的。 但负责任的用户不会尝试设置字面上无法设置的 attr。再次,不负责任的用户会设置 attr 字面上可以设置,并且由于他的设置会在代码中的其他地方引发错误。所以最后两个 attr 都不能设置。我应该同时使用属性还是不使用任何属性? 但负责任的用户不应该尝试设置无法设置的 attr。在编程中,如果某些东西严格来说是一个不可设置的值,那么负责任或明智的事情就是确保它不能是。这些小事都有助于建立可靠的程序。 这是许多人和语言的立场。如果这是一个你认为不可协商的职位,你可能不应该使用 Python。【参考方案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'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

【讨论】:

这真的很有帮助,而且正是我想做的。谢谢你。但是,它适用于安装了 Python 3 的用户。我使用的是 Python 2.7.8,因此我必须对您的解决方案进行两个小调整:“class NewClass(cls, object):”... "super(NewClass, self) .__setattr__(name, value)". 另外,应该注意类成员变量是列表和字典。您不能真正“锁定”它们以防止以这种方式更新。 这里一个改进三个问题。改进:if..elif..else 块可以只是if name in attrs and name in self.__dict__: raise Attr...,不需要pass。问题 1:这样装饰的类都以相同的__name__ 结束,并且它们类型的字符串表示也是同质化的。问题 2:此装饰会覆盖任何自定义 __setattr__。问题 3:用户可以通过 del MyClass.__setattr__ 解决这个问题。 只是语言问题。 “唉……”的意思是“说起来很难过……”我认为这不是你想要的。 我喜欢装饰器的方法,但是它丢失了原始的类名和类文档,这可能对“官方,文档化”的类不利。拥有一个修改原始类而不是创建新类的装饰器不是更好吗?只需定义一个内部函数fixSetAttr,它与覆盖的__setattr__基本相同;它需要在里面调用super(cls, self)而不是super(),然后用setattr(cls, '__setattr__', fixSetAttr)修改原来的类而不是新建一个类。【参考方案4】:

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

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 的依赖,并使调试变得更加容易。 但是我可以说p.__dict__['_spam_x'] = 5 来更改p.x 的值,所以这不能提供足够的名称修饰。【参考方案5】:

我对创建只读属性的前两个答案不满意,因为第一个解决方案允许删除然后设置只读属性并且不会阻止__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 clas-s-rebuilder(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 clas-s-rebuilder

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__现在是不可变的types.MappingProxyType,所以属性不能通过__dict__改变。设置或删除__dict__ 也被阻止。更改只读属性的唯一方法是更改​​类本身的方法。

虽然我相信我的解决方案比前两个更好,但它还可以改进。这些是这段代码的弱点:

    不允许添加到子类中设置或删除只读属性的方法。子类中定义的方法被自动禁止访问只读属性,即使调用超类的方法版本也是如此。

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

但是,没有办法不编辑类来设置或删除只读属性。这不依赖于命名约定,这很好,因为 Python 与命名约定不太一致。这提供了一种方法,可以在不编辑类本身的情况下使用隐藏的漏洞来更改无法更改的只读属性。只需列出作为参数调用装饰器时要只读的属性,它们就会变成只读的。

感谢Brice's answer 获取调用者类和方法。

【讨论】:

object.__setattr__(pfi, 'readonly', 'foobar') 打破了这个解决方案,没有编辑类本身。【参考方案6】:

这是我的解决方法。

@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】:

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

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

【讨论】:

【参考方案8】:

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

【讨论】:

你应该添加一些测试来展示它如何与多个属性一起工作【参考方案9】:

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

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

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

【讨论】:

【参考方案10】:

我知道我要从死里复活这个线程,但我正在研究如何将属性设置为只读,在找到这个主题后,我对已经共享的解决方案不满意。

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

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

【讨论】:

但是如果你只是不做一个setter,那么尝试分配也会引发错误(An AttributeError('can't set attribute')

以上是关于何时应将属性设为私有并设为只读属性? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

如何使用属性将私有数组公开为只读? [关闭]

如何将 UserControl 中的控件设为私有?

在派生类中将属性设为只读

在 Python 中将类实例属性设为只读

Objective-C:(私有/公共属性)为外部类调用设置只读属性,为自调用设置只读属性

在 PHP5 类中,何时调用私有构造函数?