Python只读属性


95

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

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

但是,如果我有属性,该属性不能从类外部设置,而是可以读取的(只读属性)。这个属性应该是私有的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 2013年

同样,_x一点也不奇怪:按照惯例,这意味着“私有”。
li.davidm 2013年

1
我的意思是从_x读取是奇怪的。不是_x名称本身。如果用户直接从_x读取,则他是不负责任的。
拉法尔Łużyński

3
重要!您的类必须是新类,即从继承object,这样才能真正停止设置obj.x。在老式类上,您实际上仍然可以设置obj.x,但结果出乎意料。
伊恩H

具有只读属性有几个有效的原因。一种是当您拥有一个由两个其他(读/写)值合并而成的值时。您可以在方法中执行此操作,但也可以在只读属性中执行此操作。
philologon

Answers:


68

通常,在编写Python程序时应假定所有用户都同意成年人,因此他们有责任自己正确使用事物。但是,在极少数情况下,无法设置属性(例如派生值或从某个静态数据源读取的值)就没有意义,仅使用吸气剂的属性通常是首选模式。


26
您的答案似乎与自己矛盾。您说用户应该负责并正确使用事物,然后说有时可设置属性是没有意义的,而getter属性是一种首选方法。我认为您可以或不能设置属性。唯一的问题是我应该保护还是保留它。两者之间应该没有答案。
2013年

19
不,我说过,如果您真的不能设置一个值,那么拥有一个setter就没有意义了。例如,如果您有一个带半径成员的圆对象和一个从半径派生的圆周属性,或者您有一个对象,该对象包装了带有一些仅具有吸气剂属性的只读实时api。没有什么矛盾的。
西拉斯·雷

9
但是负责任的用户不会尝试设置实际上无法设置的attr。再一次,不负责任的用户将设置可以实际设置的attr,并且由于其设置而在代码中的其他位置引发错误。因此,最后两个attr都无法设置。我应该同时使用财产还是不使用财产?
2013年

8
但是负责任的用户不应尝试设置实际上无法设置的attr。在编程中,如果某些东西严格来说是不可设置的值,则负责任的或明智的做法是确保它不能设置。这些小东西都有助于可靠的程序。
罗宾·史密斯

6
这是很多人和语言所采取的立场。如果您觉得这个职位不可转让,那么您可能不应该使用Python。
Silas Ray

72

西拉斯·雷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

9
Python不是不安全类型,而是动态类型。名称修改并不是为了使作弊变得更加困难,而是为了防止在可能存在继承问题的情况下发生名称冲突(如果您不是在编写大型程序,则甚至不必在意)。
memeplex

3
但是您仍然应该记住,无论如何都可以使用此方法更改可变对象。例如,如果self.__a = []您仍然可以执行此操作a.a.append('anything'),那么它将起作用。
伊戈尔(Igor)

3
我不清楚这个答案对“一个合理的(明智的)人”有什么影响。您能否更清楚地说明一个合理的人会做和不会做的事情?
winni2k

3
对我来说,利用@property装饰,当您这样做时,您需要从object继承。谢谢。
akki

2
@kkm永远不允许错误潜入代码的唯一方法是永远不要编写代码。
Alechan

55

这是一种避免假设的方法

所有使用者都是成年人,因此有责任自行正确使用事物。

请在下面查看我的更新

使用@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 __(名称,值)”。
张颖

1
另外,应该注意类成员变量是列表和字典。您无法真正“锁定它们”以这种方式进行更新。
张颖

1
这里有一个改进和三个问题。改进:该if..elif..else块可能只是if name in attrs and name in self.__dict__: raise Attr...没有pass要求。问题1:这样装饰的类最终都具有相同的__name__,并且其类型的字符串表示也被均化。问题2:这种修饰会覆盖任何风俗习惯__setattr__。问题3:用户可以使用击败它del MyClass.__setattr__
TigerhawkT3'3

只是一种语言问题。“唉......”的意思是“不幸的是,......”,这是不是你想要的,我想。
托马斯·安德鲁斯'18

没有什么可以阻止我做的object.__setattr__(f, 'forbidden', 42)。我看不到read_only_properties双下划线名称改写无法处理的新增内容。
L3viathan

4

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

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)

真好 如果您改头换面dict_name,例如,dict_name = "_spam_" + name它消除了对它的依赖uuid4,并使调试变得更加容易。
cz

但是然后我可以说p.__dict__['_spam_x'] = 5要更改的值p.x,因此这不能提供足够的名称修饰。
Booboo '18

1

我对创建只读属性的前两个答案不满意,因为第一个解决方案允许删除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与命名约定不太一致。这提供了一种方法,使只读属性无法通过隐藏的漏洞进行更改,而无需编辑类本身。只需在将装饰器作为参数调用时列出要只读的属性即可,它们将变为只读。

归功于Brice的回答:如何在python中另一个类的函数中获取调用方类名称?获取调用方的类和方法。


object.__setattr__(pfi, 'readonly', 'foobar')无需编辑类本身即可中断此解决方案。
L3viathan

0

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

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


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

您应该添加一些测试来显示其如何与多种属性一起工作
Oz123'4

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

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

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

3
但是,如果您只是不做二传手,尝试分配也会引起错误(An AttributeError('can't set attribute')
Artyer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.