Refined implementation
Getter and setter functions
Given the way we have implemented the classes, we can directly access and manipulate
the class attributes. For example, if we have a structure object we can get the attribute
values:
name = structure.name
conf = structure.conformation
We can also change these values:
structure.name = 'new name'
structure.conformation = 235
Although the former seems harmless enough, the latter might be a bad idea. It’s
possible the application might rely on these two attributes not changing. Or even if the
information is allowed to change, it’s possible that the values being set might be illegal in
some way. For example, in the constructor we checked that name was not an empty string
and that is not done here, so it is perfectly possible that we could do:
structure.name = ''
The standard way to deal with situations like this in most modern computer languages
(e.g. Java) is to design the class so that attributes are private and thus not accessible in this
way. Instead we would have getters and possibly setters, which are functions that allow
access to query and redefine the relevant information, but in a guarded way. For example,
here we could have:
class Structure:
# …
def getName(self):
return self.name
def setName(self, name):
if not name:
raise Exception('name must be set to non-empty string')
self.name = name
We see that the setter function setName() has some validity checking in it. Access then
becomes:
name = structure.getName()
structure.setName('new name')
If for some reason we thought that the name should not be changeable, we would
simply not provide the setter function, so only the getter function would exist. This
approach still has a couple of problems. First of all, this access is rather verbose to use.
For example, instead of:
residue.chain.structure.name = 'new name'
we would have to do:
residue.getChain().getStructure().setName('new name')
Secondly, as it stands, the attribute name is still accessible, so someone could still use
direct access, albeit deliberately or by mistake. In Python, nothing can be totally hidden
from the user in the implementation of a class. However, there are ways to signal the clear
intent to disallow access. Instead of using name for the attribute we could use _name, so
starting with an underscore. Then the getter and setter become:
class Structure:
# …
def getName(self):
return self._name
def setName(self, name):
if not name:
raise Exception('name must be set to non-empty string')
self._name = name
With this change structure.name no longer works, but structure._name does, although
the underscore in front is a warning that this is not intended to be used. Another alternative
is to use __name, starting with two underscores. In this case structure.__name does not
work, and trying to use it results in an AttributeError exception. This is a bit of Python
‘magic’ to make the attribute somewhat private. However, it turns out that a determined
person could still get at the attribute, just using another bit of Python magic: an underscore
and the class name have to be joined to the beginning of the attribute name. Thus, here the
access would be via structure._Structure__name. There is no real privacy in Python.
Setter functions often have validity checking in them, so they seem to serve a useful
purpose. On the other hand getter functions are often very boring, and just give back an
attribute, so they often seem to serve little purpose. Nevertheless, there are a few clear
situations when they actually do something useful. The first case is when the high
cardinality of the attribute is greater than 1; we have a collection of items. Returning to an
example of this from earlier in the chapter, we could have decided that the Structure class
has pdbIds, instead of just one pdbId. The naïve getter function would simply be:
class Structure:
# …
def getPdbIds(self):
return self.pdbIds
Unlike strings and integers, collections are modifiable; their contents can be changed.
Thus, returning the value in this way would allow the person using the function to directly
manipulate the collection, which may cause problems for the objects:
pdbIds = structure.getPdbIds()
del pdbIds[0] # delete first one; changes structure.pdbIds
An alternative implementation could return a copy of the collection instead of the
internal attribute:
class Structure:
# …
def getPdbIds(self):
return list(self.pdbIds)
Here the list that is returned from the function can be manipulated without any harm:
pdbIds = structure.getPdbIds()
del pdbIds[0] # does not change structure.pdbIds
Another case when a getter function is useful is when the associated value is not
directly stored in the object, but instead is calculated on-the-fly. For example, suppose we
wanted to have a function, structure.getMass(), which returns the molecular mass of the
structure. We might choose not to store the mass at all, but instead do something like:
class Structure:
# …
def getMass(self):
mass = 0
for chain in self.chains:
mass += chain.getMass()
return mass
and in turn the chain.getMass() could use residue.getMass() and that could use
atom.getMass(). Even the Atom class might not store the mass but instead calculate it on-
the-fly using a dictionary based on the element.
Our final example of useful getter functions illustrates that the underlying
implementation of a class can change, but any code that uses a getter function instead of
direct attribute access does not itself have to change. Here, we might decide that
calculating the mass on-the-fly is a slow procedure, so we want to calculate it once and
then cache the result:
9
class Structure:
# …
def getMass(self):
if hasattr(self, 'mass'):
# already been calculated so just return value
return self.mass
# not been calculated yet so do it
mass = 0
for chain in self.chains:
mass += chain.getMass()
self.mass = mass # cache for next time
return mass
This has changed how the class is implemented, but all code that uses the getMass()
function is unaffected.
Do'stlaringiz bilan baham: |