1

I have a class with an attribute. Originally it held a single string of an error message. I thought I would get clever and turn it into a list so every time I 'set' the attribute, rather than replacing the string it would append the string to a list.

Now, however, the err attribute is shared amongst the objects rather than being unique for each one.

Here is my code (test.py, trimmed to just the minimum for this example)

#!/usr/bin/python3
# pylint: disable=C0301

'''Testing MyHost class'''

class MyHostGood:           # pylint: disable=R0903
    '''Host class'''
    err = ''
    name = ''

    def __init__(self, name=''):
        '''Constructor'''
        self.name = name

    def __str__(self):
        '''Return a printable user representation of the object - print(x)'''
        info = f'{self.name}:'
        # These are error/warning messages
        if self.err:
            info += f'\n└ ERROR: {self.err}'
        return info

class MyHostBad:           # pylint: disable=R0903
    '''Host class'''
    _err = []
    name = ''

    def __init__(self, name=''):
        '''Constructor'''
        self.name = name

    def __str__(self):
        '''Return a printable user representation of the object - print(x)'''
        info = f'{self.name}:'
        # These are error/warning messages
        if self._err:
            for errMsg in self._err:
                info += f'\n└ ERROR: {errMsg}'
        return info

    @property
    def err(self):
        '''Return the list of error messages'''
        return self._err

    @err.setter
    def err(self, arg):
        '''Add another string to the error messages'''
        if isinstance(arg, str):
            self._err.append(arg)
        if isinstance(arg, list):
            self._err.extend(arg)

Here is the good class where the err attribute is unique:

>>> import test
>>> a = test.MyHostGood('foo')
>>> a.err = 'Here is one error string'
>>> print(a)
foo:
└ ERROR: Here is one error string
>>> b = test.MyHostGood('bar')
>>> print(b)
bar:
>>> b.err = 'a new error'
>>> print(a)
foo:
└ ERROR: Here is one error string
>>> print(b)
bar:
└ ERROR: a new error
>>>

And here the err attribute is now shared between the objects:

>>> import test
>>> c = test.MyHostBad('wiz')
>>> c.err = 'Here is one error string'
>>> print(c)
wiz:
└ ERROR: Here is one error string
>>> d = test.MyHostBad('baz')
>>> print(d)
baz:
└ ERROR: Here is one error string
>>> d.err = 'a new error'
>>> print(c)
wiz:
└ ERROR: Here is one error string
└ ERROR: a new error
>>> print(d)
baz:
└ ERROR: Here is one error string
└ ERROR: a new error
>>>

How do I fix the getter/setter so the objects have their own error strings again?

shepster
  • 339
  • 3
  • 13
  • 1
    It isn't inappropriately sharing anything. **You created a class variable**, which is shared by all instances of the class. If y ou don't want a class variable, use an instance variable. "Now, however, the err attribute is shared amongst the objects rather than being unique for each one." *of course*. You really should read the [official tutorial on class definitions](https://docs.python.org/3/tutorial/classes.html) this should really be a first step before asking a question on stack overflow – juanpa.arrivillaga Dec 15 '21 at 00:04
  • Thank you for the reference @juanpa-arrivillaga. I'll look into it. – shepster Dec 15 '21 at 00:14
  • If I've been creating class variables, I'm not sure why `err` wasn't shared in the `MyHostGood` example. Nonetheless, it looks like I need to adjust my coding. – shepster Dec 15 '21 at 00:36
  • 1
    It *was* shared, but doing `self.error += whatever` **shadowed** the class variable with an instance variable, and created a new object since `str` objects are immutable. If you did this with a list, `self.error += [1,2,]` it woudl do the same thing, but now the instance variable and class variable are referring to the same object. If you did `self.error = self.error + [1,2,3]`, it would *shadow* the class variable with an instance variable, but it would be two different lists. – juanpa.arrivillaga Dec 15 '21 at 00:48
  • You basically should stop using class variables like this, they serve no purpose, you should be using instance variables. – juanpa.arrivillaga Dec 15 '21 at 00:48
  • Agreed. Thank you for your insight (and your documentation recommendation). I'm learning how classes work and need to correct my coding. Pylint doesn't catch everything! ;-) – shepster Dec 15 '21 at 15:27

1 Answers1

0

_err is a class variable, shared by all objects. Since the setter method modifies it in place, changes are visible to all objects. To fix this, make _err into an object attribute by modifying the constructor of MyHostBad:

def __init__(self, name=''):
    '''Constructor'''
    self.name = name
    self._err = []
bb1
  • 7,174
  • 2
  • 8
  • 23
  • Thank you @bb1! While that works, I don't quite understand what is going on. In this example, the variables outside of the constructor aren't shared: ``` >>> class myClass: ... a = '1' ... def __init__(self): ... print(self.a) ... >>> x = myClass() 1 >>> y = myClass() 1 >>> >>> x.a = 'xyzzy' >>> print(x.a) xyzzy >>> print(y.a) 1 >>> ``` – shepster Dec 15 '21 at 00:08
  • @shepster because `x.a = 'xyxxy'` **creates an instance variable on that instance**. The class variable is still there – juanpa.arrivillaga Dec 15 '21 at 00:49
  • @shepster The difference is that lists are mutable. When you append to a list, you modify the list in place without making a copy. For this reason, all instances of MyHostBad were using the same list. On the other hand, strings are not mutable, so when an instance of myClass modifies the variable `a`, it creates a new value that is visible to that instance only, while the class variable stays unchanged. – bb1 Dec 15 '21 at 04:59
  • Thanks everyone for all your explanation. I had poor programming practices that were hidden due to the immutability of strings. I know better how to code my class objects! – shepster Dec 16 '21 at 21:48