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?
CodePudding user response:
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.