简体   繁体   中英

How to use default property descriptors and successfully assign from __init__()?

What's the correct idiom for this please?

I want to define an object containing properties which can (optionally) be initialized from a dict (the dict comes from JSON; it may be incomplete). Later on I may modify the properties via setters. There are actually 13+ properties, and I want to be able to use default getters and setters , but that doesn't seem to work for this case:

But I don't want to have to write explicit descriptors for all of prop1... propn Also, I'd like to move the default assignments out of __init__() and into the accessors... but then I'd need expicit descriptors.

What's the most elegant solution? (other than move all the setter calls out of __init__() and into a method/classmethod _make() ?)

[DELETED COMMENT The code for badprop using default descriptor was due to comment by a previous SO user, who gave the impression it gives you a default setter. But it doesn't - the setter is undefined and it necessarily throws AttributeError .]

class DubiousPropertyExample(object):
    def __init__(self,dct=None):
        self.prop1 = 'some default'
        self.prop2 = 'other default'
        #self.badprop = 'This throws AttributeError: can\'t set attribute'
        if dct is None: dct = dict() # or use defaultdict 
        for prop,val in dct.items():
            self.__setattr__(prop,val)

    # How do I do default property descriptors? this is wrong
    #@property
    #def badprop(self): pass

    # Explicit descriptors for all properties - yukk
    @property
    def prop1(self): return self._prop1
    @prop1.setter
    def prop1(self,value): self._prop1 = value

    @property
    def prop2(self): return self._prop2
    @prop2.setter
    def prop2(self,value): self._prop2 = value

dub = DubiousPropertyExample({'prop2':'crashandburn'})

print dub.__dict__
# {'_prop2': 'crashandburn', '_prop1': 'some default'}

If you run this with line 5 self.badprop = ... uncommented, it fails:

self.badprop = 'This throws AttributeError: can\'t set attribute'

AttributeError: can't set attribute

[As ever, I read the SO posts on descriptors, implicit descriptors, calling them from init ]

I think you're slightly misunderstanding how properties work. There is no "default setter". It throws an AttributeError on setting badprop not because it doesn't yet know that badprop is a property rather than a normal attribute (if that were the case it would just set the attribute with no error, because that's now normal attributes behave), but because you haven't provided a setter for badprop , only a getter.

Have a look at this:

>>> class Foo(object):
    @property
    def foo(self):
        return self._foo
    def __init__(self):
        self._foo = 1


>>> f = Foo()
>>> f.foo = 2

Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    f.foo = 2
AttributeError: can't set attribute

You can't set such an attribute even from outside of __init__ , after the instance is constructed. If you just use @property , then what you have is a read-only property (effectively a method call that looks like an attribute read).

If all you're doing in your getters and setters is redirecting read/write access to an attribute of the same name but with an underscore prepended, then by far the simplest thing to do is get rid of the properties altogether and just use normal attributes. Python isn't Java (and even in Java I'm not convinced of the virtue of private fields with the obvious public getter/setter anyway). An attribute that is directly accessible to the outside world is a perfectly reasonable part of your "public" interface. If you later discover that you need to run some code whenever an attribute is read/written you can make it a property then without changing your interface (this is actually what descriptors were originally intended for, not so that we could start writing Java style getters/setters for every single attribute).

If you're actually doing something in the properties other than changing the name of the attribute, and you do want your attributes to be readonly, then your best bet is probably to treat the initialisation in __init__ as directly setting the underlying data attributes with the underscore prepended. Then your class can be straightforwardly initialised without AttributeErrors , and thereafter the properties will do their thing as the attributes are read.

If you're actually doing something in the properties other than changing the name of the attribute, and you want your attributes to be readable and writable, then you'll need to actually specify what happens when you get/set them. If each attribute has independent custom behaviour, then there is no more efficient way to do this than explicitly providing a getter and a setter for each attribute.

If you're running exactly the same (or very similar) code in every single getter/setter (and it's not just adding an underscore to the real attribute name), and that's why you object to writing them all out (rightly so!), then you may be better served by implementing some of __getattr__ , __getattribute__ , and __setattr__ . These allow you to redirect attribute reading/writing to the same code each time (with the name of the attribute as a parameter), rather than to two functions for each attribute (getting/setting).

It seems like the easiest way to go about this is to just implement __getattr__ and __setattr__ such that they will access any key in your parsed JSON dict, which you should set as an instance member. Alternatively, you could call update() on self.__dict__ with your parsed JSON, but that's not really the best way to go about things, as it means your input dict could potentially trample members of your instance.

As to your setters and getters, you should only be creating them if they actually do something special other than directly set or retrieve the value in question. Python isn't Java (or C++ or anything else), you shouldn't try to mimic the private/set/get paradigm that is common in those languages.

I simply put the dict in the local scope and get/set there my properties.

class test(object):
    def __init__(self,**kwargs):
        self.kwargs = kwargs
        #self.value = 20 asign from init is possible
    @property
    def value(self):
        if self.kwargs.get('value') == None:
            self.kwargs.update(value=0)#default
        return self.kwargs.get('value')
    @value.setter
    def value(self,v):
        print(v) #do something with v
        self.kwargs.update(value=v)

x = test()
print(x.value)
x.value = 10
x.value = 5

Output

0
10
5

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM