10

I'm relatively new to Python so I hope I haven't missed something, but here goes...

I'm trying to write a Python module, and I'd like to create a class with a "private" attribute that can (or maybe 'should') only be modified through one or more functions within the module. This is in an effort to make the module more robust, since setting of this attribute outside of these functions could lead to unwanted behaviour. For example, I might have:

  1. A class that stores x and y values for a scatter plot, Data
  2. A function to read x and y values from a file and store them in the class, read()
  3. A function to plot them, plot()

In this case, I would prefer if the user wasn't able to do something like this:

data = Data()
read("file.csv", data)
data.x = [0, 3, 2, 6, 1]
plot(data)

I realise that adding a single leading underscore to the name indicates to the user that the attribute should not be changed, i.e. rename to _x and add a property decorator so that the user can access the value without feeling guilty. However, what if I wanted to add a setter property as well:

class Data(object):
    _x = []
    _y = []
    @property
    def x(self):
        return self._x
    @x.setter
    def x(self, value):
        # Do something with value
        self._x = value

I'm now in the same position as I was before - the user can no longer directly access the attribute _x, but they can still set it using:

data.x = [0, 3, 2, 6, 1]

Ideally I'd rename the property function definitions to _x(), but this leads to confusion about what self._x actually means (depending on the order in which they are declared, this seems to result in either the setter being called recursively or the setter being ignored in favour of the attribute).

A couple of solutions I can think of:

  1. Add a double leading underscore to the attribute, __x, so that the name becomes mangled and does not get confused with the setter function. As I understand it, this should be reserved for attributes that a class does not wish to share with possible subclasses, so I'm not sure if this is a legitimate use.
  2. Rename the attribute, e.g. _x_stored. While this solves the problem completely, it makes the code harder to read and introduces naming convention issues - which attributes do I rename? just the ones that are relevant? just the ones that have properties? just the ones within this class?

Are either of the above solutions applicable? And if not, is there a better way to solve this problem?

A few points brought up in the comments:

  1. I want to retain the extra logic that the setter property gives me - the # Do something with value section in the above example - so internally setting the attribute through direct access of self._x doesn't solve the problem.
  2. Removing the setter property and creating a separate function _set_x() does solve the problem, but is not a very neat solution since it allows setting of _x in two different ways - either by calling that function or through direct access of self._x. I'd then have to keep track of which attributes should be set by their own (non-property) setter function and which should be modified through direct access. I'd probably rather use one of the solutions I suggested above, because even though they make a mess of the naming conventions within the class they are at least consistent in their use outside of the class, i.e. they all use the syntactical sugar of properties. If there's no way of doing this in a neater way then I guess I just have to choose the one that causes the least disruption.
4
  • I am confused about why you want to define a setter that you don't want people to use. Commented May 2, 2014 at 16:07
  • @cmd - I still want to be able to use the setter in other functions within the module. For example, I might have an interpolate() function that needs to be able to set the x and y values. Commented May 2, 2014 at 16:13
  • But those can just set _x and _y. Commented May 2, 2014 at 16:18
  • I would make a function _setx() that is used internally if you want to capture your extra logic when setting. I dont think the other option complexities are worth the limited syntactical sugar. Commented May 2, 2014 at 16:59

2 Answers 2

6

If you want to discourage users from changing a property, but want it to be clear that they can read it, I'd use @property without providing a setter, similar to what you described earlier:

class Data(object):
    def __init__(self):
       self._x = []
       self._y = []

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

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

I know you mention "What if I wanted to add a setter to the property?", but I guess I would counter that with: Why add the setter if you don't want your clients to be able to set the property? Internally, you can access self._x directly.

As for a client directly accessing _x or _y, any variable with an '_' prefix is understood to be "private" in Python, so you should trust your clients to obey that. If they don't obey that, and end up screwing things up, that's their own fault. This kind of mindset is counter to a many other languages (C++, Java, etc.) where keeping data private is considered very important, but Python's culture is just different in this regard.

One other note, since your private properties in this particular case are lists, which are mutable (unlike strings or ints, which are immutable), a client could end up changing them somewhat accidentally:

>>> d = Data()
>>> print d.x
['1', '2']
>>> l = d.x
>>> print l
['1', '2']
>>> l.append("3")
>>> print d.x
['1', '2', '3']  # Oops!

If you want to avoid this, you'd need your property to return a copy of the list:

@property
def x(self):
    return list(self._x)
Sign up to request clarification or add additional context in comments.

3 Comments

I understand that it's easy enough to internally set the value using direct access of self._x, but that removes the advantage that having a setter gives me, i.e. the # Do something with value bit.
Ah, you mean having the setter do more than a simple self._x = blah? I guess in that case, you either have to add @property to the private property as well (which I think you mentioned you didn't like), or just always remember to call some internal helper function when you want to assign to the private property.
Is there a more elegant way to do this that doesn't introduce an inconsistent naming convention? Please see original post edit. (@cmd, @kindall)
0

If you want less convoluted properties, that manage their own storage without leaving it open to "under the hood" alteration, you can define a class (similar to property) and use it to declare your class member:

I called mine 'Field':

class Field:    
    def __init__(self,default=None):    
        self.valueName = None               # actual attribute name
        self.default   = default            # type or value or lambda
        if not callable(default): self.default = lambda:default
        self._didSet   = None               # observers
        self._willSet  = None

    def findName(self,owner):                     # find name of field
        if self.valueName: return                 # once per field for class
        for name,attr in owner.__dict__.items():
            if attr is self: 
                self.valueName = f"<{name}>"      # actual attribute name
                break

    def __get__(self,obj,owner=None):             # generic getter
        if not obj: return self
        self.findName(owner or type(obj))
        value = getattr(obj,self.valueName,self)  # attribute from instance
        if value is self:                 
            value = self.default()                # default value
            setattr(obj,self.valueName,value)     # set at 1st reference
        return value

    def __set__(self,obj,value):                  # generic setter
        self.findName(type(obj))
        if self._willSet: value    = self._willSet(obj,value)
        if self._didSet:  oldValue = self.__get__(obj)
        setattr(obj,self.valueName,value)         # attribute in instance
        if self._didSet: self._didSet(obj,oldValue)

    def willSet(self,f): self._willSet = f
    def didSet(self,f):  self._didSet  = f

usage:

class myClass:

    lastName  = Field("Doe")
    firstName = Field("")
    age       = Field(int)
    gender    = Field("M")
    relatives = Field(list)

    @lastName.willSet
    def _(self,newValue):              # no function name needed
        return newValue.capitalize()

    @lastName.didSet
    def _(self,oldValue):              # no function name needed
        print('last name changed from',oldValue,'to',self.lastName)

c           = myClass()

c.firstName = "John"
c.lastName  = "Smith"
# last name changed from Doe to Smith

c.relatives.extend(['Lucy','Frank'])

print(c.gender)
# M

print(c.__dict__)
# {'<lastName>': 'Smith', '<firstName>': 'John', 
   '<relatives>': ['Lucy', 'Frank'], '<gender>': 'M'}

Attributes added to the instance are not accessible from Python because they use identifiers that would not be valid in code.
Because you define default values at the class level, there is no need to set the field values in the constructor (although you could still do it as needed)

Field values are only added as instance attributes when they are referenced making the instance creation process more efficient.

Note that my actual Field class is a lot more sophisticated and supports change tracking, more observer functions, type checking, and read-only/calculated fields. I boiled it down to essentials for this response

For Private/Public method protection, you may want to look at this answer

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.