Python setters do not apply to instantiation?

Issue

I am trying to make a programme that allows you to perfrom various forms of chemical analysis on some user defined molecule. So far, I have defined a Molecule object with a coordinate property associated with it. I set this to generate an Atom object of some sort on when the user sets the coordinate property to some list that descibes an molecule. However, the coordinate property only corrosponds to a list of atom objects when I reset it following instantiation of the molecule object. When I pass a list of coordinates on instantiation of the object, the property remains as the list of strings (same as input), and does not corrospond to a list of the corrosponding atom objects. The corrosponding code is,

periodic_table = {
    'H': {'Z': 1.008, 'nelec': 1},
    'C': {'Z': 12.011, 'nelec': 6},
    'N': {'Z': 14.007, 'nelec': 7},
    'O': {'Z': 15.999, 'nelec': 8},
    'S': {'Z': 32.06, 'nelec': 16},
    'Br': {'Z': 79.904, 'nelec': 35},
    'I': {'Z': 126.90, 'nelec': 53}
}




class Atom:

    def __init__(self, label: str, vector: list):
        self.__label = label
        self.vector = vector
        self.__lookup_info()


    def __lookup_info(self):
        atom_info = periodic_table[self.__label]
        self.__Z = atom_info['Z']
        self.__nelec = atom_info['nelec']

    @property
    def label(self):
        return self.__label

    @label.setter
    def label(self, value):
        if type(value) != str:
            raise Exception("Atomic labels must be a string")
        else:
            value = value.upper()
            self.__label = value
            self.__lookup_info()
            # lookup info and set z/nelec

    @property
    def Z(self):
        return self.__Z

    @property
    def nelec(self):
        return self.__nelec


class Molecule(object):

    def __init__(self, coordinates=None):
        self.__coordinates = coordinates
        self.nelec = None
        self.Z = None
        self.natom = None

    @property
    def coordinates(self):
        return self.__coordinates

    @coordinates.setter
    def coordinates(self, coords: list):
        depth = lambda l: isinstance(l, list) and max(map(depth, l)) + 1
        if depth(coords) != 2:
            raise Exception("Coordinates must be provided as a nested list of depth 2")
        else:
            atom_list = []
            for atom_coord in coords:
                print("Test")
                atom_coord = self.__check_coord(atom_coord)
                atom_label, atom_vec = atom_coord[0], atom_coord[1:]
                atom_list.append(Atom(atom_label, atom_vec))
            self.__coordinates = atom_list

    @staticmethod
    def __check_coord(coord):
        isnumber = lambda x: (type(x) == float or type(x) == int)
        if type(coord[0]) != str:
            raise Exception("First element must a string for an atomic label")
        if len(list(filter(isnumber, coord))) != 3 and len(coord) != 4:
            raise Exception("Each coordinate must be a list of length 4 with 1 string and 3 numbers")
        coord[0] = coord[0].upper()
        return coord


which yields the following outputs,

>>> mol = Molecule([['H', 0, 1, 1], ['O', -1, 0, 0], ['H', -1, 0, 1]])
>>> mol.coordinates
    [['H', 0, 1, 1], ['O', -1, 0, 0], ['H', -1, 0, 1]]
>>> mol2 = Molecule()
>>> mol2.coordinates =[['H', 0, 1, 1], ['O', -1, 0, 0], ['H', -1, 0, 1]]
    Test
>>> mol2.coordinates
    [<__main__.Atom object at 0x10c62c850>, <__main__.Atom object at 0x10c62c640>, <__main__.Atom object at 0x10c62c910>]

How can I achieve the behaviour where in both the case of the list being passed on init of the object or when the coordinated property is set after, the coordinates are set to a list of atom objects?

Solution

When you do self.__coordinates = coordinates you set directly __coordinates, you don’t call the setter

The Molecule.__init__() should look something like

def __init__(self, coordinates=None):
    if coordinates is not None:
        self.coordinates = coordinates
    self.nelec = None
    self.Z = None
    self.natom = None

Same apply for any other case where you set the "internal" attribute directly

As a side note, probably you want to use single leading underscore, meaning "internal use", not double leading underscore. There are also other things I would refactor, e.g. the use of lambda functions, the general Exception you raise, etc.

Answered By – buran

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published