确定类属性是否为只读数据描述符。

4

只读数据描述符是一种同时定义了__get____set__方法的描述符,但是当调用__set__方法时会引发AttributeError异常。

一个简单的只读属性示例:

class Test():

    _i = 1

    @property
    def i(self):
        return self._i

assert hasattr(Test.i, '__get__')
assert hasattr(Test.i, '__set__')
t = Test()
t.i # 1
t.i = 2 # ERROR

如果我有一个类的实例,可以通过以下方式确定实例属性是否为只读数据描述符(尽管我非常不喜欢这样做):

def is_ro_data_descriptor_from_instance(instance, attr):
    temp = getattr(instance, attr)
    try:
        setattr(instance, attr, None)
    except AttributeError:
        return True
    else:
        setattr(instance, attr, temp)
        return False

如果我知道该类不需要任何参数进行实例化,我可以确定它的类属性是否类似于上面的只读数据描述符:

def is_ro_data_descriptor_from_klass(klass, attr):
    try:
        setattr(klass(), attr, None)
    except AttributeError:
        return True
    else:
        return False

然而,如果我事先不知道类的签名,并尝试以这种方式实例化临时对象,可能会出现错误:

class MyClass():
    i = 1
    def __init__(self, a, b, c):
        '''a, b, and c are required!'''
        pass

def is_ro_data_descriptor_from_klass(MyClass, 'i') # Error

怎样确定一个类属性是只读数据描述符?

编辑:添加更多信息。

以下是我正在尝试让其工作的代码:

class StaticVarsMeta(type):
    '''A metaclass that will emulate the "static variable" behavior of
    other languages. For example: 

        class Test(metaclass = StaticVarsMeta):
            _i = 1
            @property
            def i(self):
                return self._i
        t = Test()
        assert t.i == Test.i'''
    statics = {}
    def __new__(meta, name, bases, dct):
        klass = super().__new__(meta, name, bases, dct)
        meta.statics[klass] = {}
        for key, value in dct.items():
            if "_" + key in dct:
                meta.statics[klass][key] = set()
                if hasattr(value, '__get__'):
                    meta.statics[klass][key].add('__get__')
                if hasattr(value, '__set__'):
                    try:
                        value.__set__(None, None)
                    except AttributeError:
                        continue
                    else:
                        meta.statics[klass][key].add('__set__')
        return klass
    def __getattribute__(klass, attr):
        if attr not in StaticVarsMeta.statics[klass]:
            return super().__getattribute__(attr)
        elif '__get__' not in StaticVarsMeta.statics[klass][attr]:
            return super().__getattribute__(attr)
        else:
            return getattr(klass, '_' + attr)
    def __setattr__(klass, attr, value):
        if attr not in StaticVarsMeta.statics[klass]:
            super().__setattr__(attr, value)
        elif '__set__' not in StaticVarsMeta.statics[klass][attr]:
            super().__setattr__(attr, value)
        else:
            setattr(klass, '_' + attr, value)

class Test(metaclass = StaticVarsMeta):
    _i = 1
    def get_i(self):
        return self._i
    i = property(get_i)

请注意以下内容:
type(Test.i) # int
type(Test.__dict__['i']) # property
Test().i = 2 # ERROR, as expected
Test.i = 2 # NO ERROR - should produce an error

1
我重新阅读了问题并更好地理解了它。我已经删除了与其他类型描述符相关的两个实现,因为我认为它们只会让事情变得更加混乱。 - jonrsharpe
1
出于好奇,你为什么需要知道它是否是只读的?这里有什么用例吗? - mgilson
这有点复杂。我正在扩展我的回答。我正在编写一个元类StaticVarsMeta,它将模拟其他语言的静态变量行为。这些类将允许您通过类本身或实例获取和设置“静态变量”。我已经完成了大部分工作,但意识到当描述符是只读数据描述符时,我必须在StaticVarsMeta.__setattr__中引发AttributeError - Rick
@RickTeachey -- 也许我没有跟上,但看起来引发AttributeError是只读描述符的特征,您正在使用它来将其标识为只读--那么为什么不直接传递给描述符并让它引发AttributeError呢? - mgilson
@mgilson 不行。如果你在元类中通过 super().__setattr__(attr, value) 进行传递,它会覆盖描述符对象。它不会引发错误。 - Rick
显示剩余5条评论
2个回答

4

看起来非常尴尬,但是根据我的评论,以下是如何实现它:

class StaticVarsMeta(type):

    statics = {}

    def __new__(meta, name, bases, dct):
        cls = super().__new__(meta, name, bases, dct)
        meta.statics[cls] = {}
        for key, val in dct.items():
            if hasattr(val, '__get__') and hasattr(val, '__set__'):
                meta.statics[cls][key] = {'__get__'}
                try:
                    val.__set__(None, None)
                except AttributeError as err:
                    if "can't set attribute" in err.args:
                        continue
                meta.statics[cls][key].add('__set__')
        return cls

使用中:

>>> class ReadOnly(metaclass=StaticVarsMeta):
    @property
    def foo(self):
        return None


>>> class ReadWrite(metaclass=StaticVarsMeta):
    @property
    def bar(self):
        return None
    @bar.setter
    def bar(self, val):
        pass


>>> StaticVarsMeta.statics
{<class '__main__.ReadOnly'>: {'foo': {'__get__'}}, 
 <class '__main__.ReadWrite'>: {'bar': {'__get__', '__set__'}}}

这更像是一个“初学者入门”的内容,肯定有更好的方法来实现它...

太棒了。这比我能想出的任何想法都要好。我认为没有一种方法可以做到这一点而不会变得非常尴尬。 - Rick
1
@RickTeachey 或许不是,但我不喜欢依赖于这样的具体错误信息。无论如何,希望有人能出现并告诉我为什么我错了! - jonrsharpe
@RickTeachey 嗯,我展示的例子对我来说是有效的。你使用的是哪个版本? - jonrsharpe
顺便提一下,我上面写的元类仍然是有问题的,但现在它有了一个不同的问题。 - Rick

3

你的第一个解决方案可以更简单、稍微更健壮,只需尝试分配它已经拥有的值。这样,就不需要撤销了(但是,这仍然不能保证线程安全)。

def is_ro_data_descriptor_from_instance(instance, attr):
    temp = getattr(instance, attr)
    try:
        setattr(instance, attr, temp)
    except AttributeError:
        return True
    else:
        return False

虽然这是一个好的观点,但我不确定它是否回答了问题,因为它没有告诉OP如何通过对象找出属性是否是只读的。 - jonrsharpe
@jonrsharpe,虽然不是对问题的回答,但仍然是一个有用的想法。谢谢。 - Rick

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