2.4. Object-oriented programming (OOP): a primer

Giving a complete overview of the concepts of object-oriented programming is difficult to achieve in the given context. Therefore, we will only briefly go through the building blocks (objects and classes) and the central concepts (encapsulation, inheritance, polymorphism, composition). For a more detailed treatment of the topic, which is certainly necessary, the interested reader is referred to the literature. An excellent book introducing the fundamental concepts of object-oriented programming without focussing on a specific programming language is “The Object-Oriented Thought Process” by Matt Weisfeld [Weisfeld, 2013]. The presentation of the basic principles given here partly follows this book.

Admittedly, object-oriented programming is conceptually relatively far away from the usual prior programming experience of the average scientist. Hence, this chapter will initially be somewhat overwhelming for all those who have not been introduced to object-oriented programming before.

2.4.1. Motivation: Why object-oriented programming?

Really understanding object-oriented programming requires a lot of time and initiative - time the average scientist is usually not keen on investing. Hence we need to answer these questions: Why this topic? What are the advantages? and why is it really worth to get familiar with the concepts? Here are four reasons why you might want to program object-oriented:

  • Intellectual manageability of complex issues.

    The motivation behind (almost) all programming concepts since the beginnings of computer science has always been to make the inherently complex issues faced by software developers intellectually manageable. The complexity is mostly an inherent property of the real-world problems we try to tackle with the help of software.

  • Abstraction

    Perhaps the biggest and most challenging intellectual task in programming is finding good generalisations and generalising procedures. This conceptual abstraction not only helps in understanding the problem, but also in developing reusable and modular code.

    Object-oriented programming makes mapping reality to code much easier: In some respect, the inherent concept of the object is quite close to the real world and a very powerful abstraction. In short, we are used to being surrounded by objects (in the broadest sense) that combine properties and behaviour.

  • Modular, robust, reusable code

    The aim of the whole course is to introduce concepts that help you to create modular, robust and reusable code. This is not an end in itself, but in many respects an essential prerequisite for reliable scientific knowledge generation.

    Object-oriented programming helps here in several ways: modularity is an inherent concept of this programming paradigm, and reusability is ensured by central concepts of object-oriented programming (encapsulation, inheritance, polymorphism).

  • Central concepts easy to implement

    Quite independently of the programming paradigm used, software for scientific data analysis should fulfil a number of criteria, among others being robust, reliable, modular, and maintainable in the long run. Some of these aspects can be implemented much more easily (and elegantly) with object-oriented programming than without it. These include automated tests (unit tests) and modularity and reusability.

It should not go unmentioned here that object-oriented programming represents an additional and sometimes quite large hurdle, especially for beginners. On the other hand, programs that attempt to fulfil the criteria mentioned for scientific software are fundamentally of a complexity that requires a certain amount of training and programming experience for their development and mastery.

2.4.2. Paradigm shift: structured to object-oriented

One reason why many people find object-oriented programming difficult (at least initially) is the fundamentally different view of the problem and possible solutions involved compared to structured programming, which is usually taught first (at least outside computer science). Switching between programming paradigms requires considerable intellectual flexibility and a willingness to really engage with the respective concepts.

Important

Object-oriented programming requires a fundamentally different way of thinking than purely structured programming.

A basic understanding of the concepts behind object-oriented programming is necessary to gain some advantage of using it. It is therefore helpful to first understand the basic concepts behind object-oriented programming and only then deal with a concrete programming language. The details of implementation can be quite different, but the basic concepts are (almost) always identical. Therefore, the basic concepts will be presented first independent of the concrete implementation in a programming language and only afterwards the concrete Python details briefly be discussed.

Two aspects of object-oriented programming are central and fundamentally different from the structured programming approach:

  • Objects

    The object is the central (and eponymous) element of object-oriented programming: it combines both properties (data in the form of variables) and behaviour (functions, here called methods).

    This unity of properties and behaviour is familiar to us from the things we encounter in everyday life – and this parallel is one of the great strengths of OOP, because it enables an abstraction of programming that can be applied directly to real problems and issues. (But keep in mind: it is still an abstraction.)

  • Encapsulation

    Only the object itself is allowed to access its properties (the data associated with it). Anyone else should ask the object (call one of its methods) if they want to retrieve or change the property.

    This ensures that the properties (data) of an object are always in a consistent state.

Even though there are possibilities in many languages to access the properties of an object directly, you should be careful to not needlessly break encapsulation – more on this later.

What exactly this means and how these two central aspects, together with the other concepts of object-oriented programming, provide a completely new or different view of programming will be discussed in a little more detail in the next two sections.

2.4.3. The building blocks: objects and classes

There are two basic building blocks in object-oriented programming: objects and classes. Both are closely related and their level of abstraction is above that of the functions or routines of structured programming.

object

(basic) building block of an object-oriented program.

Consists of the data and the associated behaviour

Usually, data of an object (or class) are called “attributes” or “properties” and are variables of arbitrary data type, while the behaviour is provided by “methods”, i.e. the functions/routines that operate on the data.

Objects are created within an application and exist only over the runtime of the application. Closely related to objects, yet clearly distinct from them, are classes:

class

blueprint for the creation of an object.

definition of data (attributes) and behaviour (methods)

Normally, an object must be created in order to access attributes and methods of a class. A fitting image is a metaphor of baking: classes are the cookie cutters, objects the cookies. To make the distinction between classes and objects a bit more practical: What you use in your code are usually objects, but the classes are that part of the code that define the properties and behaviour of these objects you are using.

Direct access and static classes

There are situations where you would like to access the attributes and methods of a class without first creating a corresponding object. This is also made possible by many programming languages that support object orientation, by defining the corresponding class (or optionally also the method or property) as “static”.

A typical example here would be the “singleton”: There should only ever be a maximum of one instantiated object of a class. The “classical” solution from structured programming would be to use a global variable. By using a property defined as “static” that stores a pointer to the existing instance (or NULL), this can be elegantly implemented in an object-oriented way.

Classes extend the data types of a language. Just about every programming language works with different types of data, e.g. character (strings) and numbers. For a certain data type, certain operations are usually defined and allowed in a programming language. Object-oriented programming goes one step further here and allows the programmer to easily and elegantly define their own data types in the form of classes. The methods of the classes are the respective defined operations. Each object belongs to a class (type). In purely object-oriented languages, even primitive data types are always classes that can be used accordingly.

An aspect that needs to briefly be touched on here is scope, i.e. the control of access to properties and methods of objects and classes. In object-oriented programming, we usually distinguish between three scopes:

  • public

    each has read and write access

  • protected

    access only for the current class and classes derived from it

  • private

    only the respective class/object has access

../_images/oop-scope.png

Fig. 2.1 Scope in object-oriented programming. Normally, three levels of control of access to properties and methods of objects (and classes) are distinguished: public, protected and private. The special feature of protected access is that in addition to the class (or object) itself, derived classes/objects may also have access (through inheritance}.

This scope can usually be specified in the class definition for attributes as well as for methods. How this is done depends on the programming language used. A clarification of the meaning of these three possible contexts is provided by Fig. 2.1. The concept of inheritance, which is necessary for understanding protected access, is introduced below.

2.4.4. The basic concepts: encapsulation, inheritance, polymorphism, composition

Three basic concepts are usually considered to constitute object orientation:

  • encapsulation.

  • inheritance

  • polymorphism

A programming language is only considered object-oriented if it implements these three concepts. In addition, however, there is another concept of no less importance:

  • composition

To some extent, inheritance and composition compete with each other in terms of creating objects with new properties. In a similar vein, one could also see a competition between encapsulation and inheritance, since inheritance always weakens encapsulation. But more on this later. First, the individual concepts will be introduced.

2.4.4.1. Encapsulation

Encapsulation massively restricts access to variables and functions, but in doing so it allows a degree of decoupling and modularity that characterises good object-oriented programs.

encapsulation

An object contains data and associated behaviour and can hide both from other objects at will.

Normally, all attributes of an object are private. Only the methods of the object have access. In many ways, this contradicts the intuition of programmers coming from structured programming, and requires some discipline and adjustment. However, it is precisely this discipline and self-restraint that pays off in the end in much more modular code.

Separation of interface and implementation The user of objects/classes only knows the signature (name and parameter) of the methods. The respective concrete implementation is irrelevant and can therefore be changed at will as long as the functionality is maintained. This leads to immense flexibility.

Rule of parsimony A final aspect of encapsulation is the rule of parsimony: there are only as many public methods as necessary. This serves to hide the implementation from the user of the object or class and increases reusability.

Basic paradigm of encapsulation

Each object is responsible for itself. Access is only through the object’s public methods.

Why is encapsulation so important? Firstly, it is the basis of modularity and interchangeability, and secondly, it is the prerequisite for the other OOP concepts (inheritance, polymorphism, composition). The strict separation between interface and implementation is crucial. The rule of parsimony is also correspondingly important, and ultimately it is the responsibility of the programmer to decide in each concrete context what encapsulation means and how it is best implemented.

2.4.4.2. Inheritance

The second central concept of object-oriented programming is inheritance.

Inheritance

A class can inherit from another class and benefit from the attributes and methods of the superclass.

Inheritance is a central concept for code reuse: Superclasses implement only what is necessary (the lowest common denominator). All specific functionality is implemented in the derived subclass. A very simple example of an inheritance relationship is shown in the diagram in Fig. 2.2.

../_images/oop-inheritance-mammals.png

Fig. 2.2 Simple example of inheritance in object-oriented programming. Both a dog and a cat are mammals, and common properties of mammals are defined at the level of the class “Mammal”, while properties and behaviour characterising the respective subclass are defined at the level of the subclass. The open arrow represents an inheritance relation, the + a publicly accessible (public) attribute, the - correspondingly a private attribute accessible only from the object itself.

Class hierarchies What makes for a good class hierarchy is always a question of context as well as of experience and personal preferences. Just note that long and nested hierarchies become confusing. In practice, often the first draft features class hierarchies that are hopelessly too long. Ultimately, the only thing that helps here is trial and error and gaining experience. A value from practice: class hierarchies with more than three “generations” should be avoided unless there are very good reasons.

Multiple inheritance A further aspect is the inheritance from more than one class (multiple inheritance). Not all programming languages support it, but often one can use the concept of interfaces and can then implement (or inherit from) more than one interface. Conceptually, these two approaches can be used in a quasi-identical way, although the explicit implementation of an interface increases readability.

2.4.4.3. Polymorphism

The third constituent concept of object-oriented programming, along with encapsulation and inheritance, is polymorphism.

Polymorphism

Similar objects may respond to the same message in different ways

Polymorphism is closely related to the concept of inheritance and is also of central importance. A class inherits a method from a superclass and implements the functionality accordingly. This makes the resulting code highly modular and extensible:

  • Each class is responsible for itself.

  • Creating a new subclass does not entail any modification of the superclass.

  • The call of the method remains identical for all subclasses of a common superclass.

A simple example contrasting structured and object-oriented programming is given in Fig. 2.3 . Without a lot of effort (and quasi a re-implementation of inheritance), structured programming requires a lot of code duplication and possibly a longer branching structure, whereas in object-oriented programming the interface of the common functionality is defined in a (possibly abstract) superclass and implemented in the derived classes. In addition, further (sub-)types can be implemented in a modular way without having to change in existing source code.

../_images/oop-polymorphism-comparison.png

Fig. 2.3 Clarification of the concept of polymorphism. In a non-object-oriented context, for each shape there would be an explicit routine to represent the shape, which moreover usually has the name of the shape in its name. In an object-oriented context, each concrete shape inherits the method for representation from a (possibly abstract) class, but implements the necessary concrete steps. The advantage: as long as you have shapes in front of you, you can call the draw method for each shape without having to worry about the details or knowing what actual shape it has.

Structured vs. object-oriented programming

Fig. 2.3 graphically illustrates a major advantage of object-oriented programming over structured programming. In the case of structured programming, if you want to be able to draw different shapes, you cannot avoid a case distinction, as shown schematically in the following listing.

shape = 'star'

if shape == 'circle':
    draw_circle()
elif shape == 'star':
    draw_star()
elif shape == 'square':
    draw_square()

As soon as a new shape is added, changes must be made at this point (and at all other points that deal with shapes).

In contrast, object orientation decouples these changes and ensures through polymorphism and encapsulation that only a new class needs to be implemented satisfying (and possibly inheriting from) the (possibly abstract) interface of the superclass Shape. In the concrete example, each object of the (super)type Shape has a method draw(). Code calling this method does not need to know which concrete instance of the class Shape it is, but can simply call its method draw() to render the shape.

shape = Star()
shape.draw()

The implementation of the method for drawing only takes place within the class. Changes beyond that are not necessary. Furthermore, the method is implemented in a local context (of the respective class), which increases clarity.

The above object-oriented example is admittedly pretty incomplete. A somewhat more detailed example, including the definition of a superclass and subclasses, is shown below.

class Shape():
    def draw(self):
        pass

class Circle(Shape):
    def draw(self):
        # actual drawing of circle

class Star(Shape):
    def draw(self):
        # actual drawing of star

class Square(Shape):
    def draw(self):
        # actual drawing of square

# Instantiate object and call draw method
shape = Star()
shape.draw()

In this example, the superclass Shape defines only an empty method draw(), which is overloaded accordingly in the subclasses: This is where the actual implementation of the functionality takes place. The concrete design depends on many factors and can ultimately only be decided upon in the respective context.

Again, the discipline of the programmer is required here: each subclass should implement all methods of the superclass it has inherited in a correspondingly meaningful way. Accordingly, only the absolute minimum of public methods should be defined in a superclass. In practice, this can often only be achieved by trial and error and adapting the code (keyword: refactoring). However, this discipline pays off.

A very important aspect of the correct tailoring of classes and their interfaces as well as of inheritance hierarchies is the Liskov substitution principle. In short, this is about general rules on how subclasses should relate to their superclasses, what assumptions they should be allowed to make (require preconditions), and what post-conditions they should ensure to enable polymorphism.

2.4.4.4. Composition

Even though, in contrast to the three aforementioned aspects of encapsulation, inheritance and polymorphism, it is not regarded as constituting object orientation, composition is of crucial importance in the concrete everyday life of programming. To a certain extent, it competes with inheritance.

Composition

An object is composed of other objects. The objects may be otherwise completely independent.

Just like inheritance, composition is a central concept for code reuse. The big difference to inheritance, however, is that composition is the interaction of independent objects.

../_images/oop-composition-aggregation-association.png

Fig. 2.4 The two types of composition of objects. The “classical” form of composition is aggregation, which defines a clear “has a” relationship. Association, on the other hand, is an interaction of objects/classes at the level that one object/class provides a service to another object/class.

One normally distinguishes between two forms of composition: aggregation and association (cf. Fig. 2.4). The “classical” form of composition is aggregation, which defines a clear “has a” relationship. Association, on the other hand, is an interaction of objects/classes in such a way that one object/class provides a service to another object/class. For association, too, there are rules that have emerged from practice on how it should be sensibly applied in order to maintain modularity and reusability.

In this context, it is important to make clear the difference between composition and inheritance.

Composition is the interaction between independent objects, encapsulation is fully preserved.

Inheritance is the inheritance of all properties (attributes, methods) from a superclass to a subclass. The subclass is of the same type as the superclass, which in turn is the basis of polymorphism. However, changes to the superclass affect the subclass, and the encapsulation is weakened accordingly.

../_images/oop-composition-inheritance.png

Fig. 2.5 Comparing composition and inheritance. While composition (in its classical form) defines a “has a” relationship, inheritance leads to a “is a” relationship. In composition, the objects/classes are completely independent; in inheritance, the subclass inherits all properties from its superclass (thus weakening the encapsulation).

Comparing the two diagrams in Fig. 2.5 illustrates once again the difference between composition and inheritance. Even if it is not true in every case, the difference can be linguistically grasped in terms of the way objects/classes relate to each other: Composition results in a has a relationship, while inheritance results in a is a relationship.

Which of the concepts (composition and inheritance) to use when is a question that is dealt with in detail in object-oriented architecture and design, but clearly beyond the scope of this course. The interested reader is once again referred to the excellent body of literature available.

2.4.5. Object-oriented design and architecture

As mentioned already a few times: knowing the basics of object-oriented programming does not make you write good and particularly modular, robust, and reusable code. A necessary prerequisite (not only) of object-oriented design is to start with getting an overview of the question at hand and to come up with a requirement analysis. For object-oriented design, focus on properties and behaviour of units. Whether you can directly draft classes from this clearly depends on the specific context. Eric Evans has pointed out the value of a joint understanding of the problem domain by users and developers of software. For details, cf. ch. 22 in [Evans, 2004].

Eventually, the proof of the pudding is the eating: simply try out things and be aware that your first draft will never be perfect – regardless whether perfect software can and does exist. You can learn to program only by practicing, and sometimes, progress can feel quite slow and tedious. A few aspects are importand independently of the actual situation and shall hence be briefly mentioned:

Always program against interfaces, not implementations. This is essential for encapsulation and in turn modular code, and it makes it necessary to define interfaces in advance. The big advantage though is independence of concrete implementations. Eventually, this aspect is only the consequence of following the principle of encapsulation. An object is a “black box” that provides only a small number of public methods to manupulate its state.

Furthermore, implementing against interfaces allows to postpone the actual implementation behind an interface to later (“separation of concerns, [Dijkstra, 1982] pp. 60–66). This helps tremendously with focussing on the concrete question. Of course, we need to get used to this approach, but on the positive side, it is quite useful even beyond software development.

Stick with flat hierarchies when using inheritance. Inheritance always breaks (weakens) encapsulation and thus a fundamental concept of object-oriented programming that is essential for modular and reusable code. Thus, use inheritance only deliberately and keep the hierarchies flat. From a practical point of view, hierarchies of more than three levels should only be used in well-justified special cases. Furthermore, take care of the Liskov substitution principle. If you end up with deep hierarchies, often you can use composition and implementation against interfaces instead.

Keep the public interface of an object as small as possible. Encapsulation and hence hiding data and implementations is one key aspect of object-oriented programming. It is our responsibility as programmers to implement these principles, as programming languages do not enforce this aspect.

2.4.6. Object-oriented programming in Python

First of all, Python is fundamentally an object-oriented programming language, meaning that everything in Python is an object, regardless whether you use a structured, object-oriented, or functional programming style. Another thing that should be mentioned – particularly for those with some prior experience with object-oriented languages such as C++ or Java: Python is quite relaxed regarding some of the principles of object-oriented programming described above. There is no such thing as a “protected” property or method, and strictly speaking, it is rather involved to make an attribute or method really private. Furthermore, while nowadays there exist “abstract base classes”, they are only sparingly used. Nevertheless, you can implement all the nice aspects of object-oriented architecture and advanced patterns known from the literature in Python as well. For details, see [Percival and Gregory, 2020].

2.4.6.1. Implementing classes

But now finally a short overview of object-oriented programming in Python. Some basic aspects have been covered already in the “Hello world!” section. A simple class that is capable of printing “Hello world!” may be implemented as follows:

1class Hello:
2
3    def __init__(self):
4        self.greeting = "Hello world!"
5
6    def hello(self):
7        print(self.greeting)

Classes are defined using the keyword class (line 1). Adhering to the Python naming conventions (PEP 8, see https://pep8.org/ for a nicely readable version) class names use CamelCase, while functions and methods (as well as variables) use underscores to separate words in a name and only small caps.

The method __init__() (lines 3–4) is a “constructor”, its name is fixed, and it will be called when instantiating an object of the class, preparing all necessary things for you. Hence, it is an excellent place to define properties of the class, as in our case the property greeting. Make sure to define all your properties of a class in its constructor. While Python does not enforce this, it is a matter of organising your code and not loosing the overview. Good IDEs (such as PyCharm) will warn you if you (accidentally) define a property of a class outside its constructor (in another method).

Why the prefix self? Simply to refer to the class/object itself. Hence, usually every method of a class will have self as a first argument. So what did we do here? We set a class property greeting to the string “Hello world!”, and we can access this property later in other methods of the same class.

Now it should be clear what happens in the method hello(). As it is a regular method of the class Hello, it takes an argument self. And thus it can access the class property greeting, as shown in line 7.

All well and good, you may say, we now have a class. But how to actually use it? The key to using classes is shown below. Actually, it is always (at least) a two-step process: (i) create an object of a class, and (ii) call some method of the object just created:

say = Hello()
say.hello()

With the first line, we create an object of the class Hello and name it say (i.e., assign it to a local variable named say). Sometimes, creating an object of a class is called “instantiating”, as you create an instance of the class. As said above, classes are the blueprints (“cookie cutters”), and objects are the real things (“cookies”) that do the actual work during runtime of your code.

The second line is simply the call of the method hello() of your object say (of class Hello). Now you may understand why we’ve named our object say, as “say hello” is pretty readable code that we can understand without knowing how to program – and this and always was the eventual aim of all serious programmers: writing code that is as readable and expressive as possible.

2.4.6.2. Inheritance

Python allows multiple inheritance, although inheriting from more than two classes us rarely a sensible idea. The general concept of inheritance in Python has been shown above already and gets repeated here:

1class Shape():
2    def draw(self):
3        pass
4
5class Circle(Shape):
6    def draw(self):
7        # actual drawing of circle

The class Shape is the “superclass”, the class Circle a “subclass” of Shape. You inherit by simply adding the name of the parent class in brackets after the class name (line 5).

in this particular case, Shape can be regarded as an abstract base class, as its (only) method draw() purposefully does not contain any implementation (note the use of the keyword pass in line 3). However, as mentioned briefly, abstract base classes are used sparingly in Python.

As soon as your parent class explicitly defines a constructor via the __init__() method, make sure to call the constructor of the superclass in the subclass. Otherwise, the properties of the superclass defined in its __init__() method will not be accessible to the subclass. Good IDEs will inform you about this and even provide context actions to add the call to the superclass. An example of calling the superclass method, extending on the simple example shown above, may look as follows:

 1class Shape():
 2    def __init__(self):
 3        self.color = None
 4
 5    def draw(self):
 6        pass
 7
 8class Circle(Shape):
 9    def __init__(self):
10        super().__init__()
11
12    def draw(self):
13        # actual drawing of circle

Only upon calling the superclass __init__() method (line 10) the class Circle does know about the color property of the Shape class and can make use of it, e.g. in its drawing() method. Usually, the call to the superclass __init__() method is the first statement in the subclass __init__() method, as otherwise, you cannot be sure that calling the superclass method would not override settings you’ve made in the subclass. Note that you cannot generally control nor know in every detail what happens in a superclass you inherit from.

2.4.6.3. Private properties and methods

As mentioned already, Python does not really prevent you from accessing methods and properties of objects, but there is a convention that every programmer should follow: private properties and methods are prefixed by an underscore, and those properties and methods should not be called from outside a class (except of in subclasses of the given class). Nevertheless, Python does make extensive use of “private” properties and methods. The general rule from above, that the public interface onf a class/object should be minimal to keep up with encapsulation (rule of parsimony) still holds and can nicely be implemented in Python.

A simple example of private properties, extending our example of the Shape class from above:

 1class Shape():
 2    def __init__(self):
 3        self.color = None
 4        self._origin_x
 5        self._origin_y
 6
 7    def draw(self):
 8        pass
 9
10class Circle(Shape):
11    def __init__(self):
12        super().__init__()
13
14    def draw(self):
15        # actual drawing of circle
16        self._set_color()
17
18    def _set_color(self):
19        # Set color of circle

Here, first two private (actually: protected) properties _origin_x and _origin_y are created (lines 4 and 5), and the method draw() of the Circle class calls the private method _set_color() that obviously deals with setting the colour of the drawn shape.

In all cases private (or protected, Python does not really make a difference here) properties and methods are prefixed by an underscore and are therefore not part of the public interface of a class.

A quick note on software architecture from the listing shown above: Depending on the situation, context, and further developments of your code, you might want to move the private/protected method _set_color() to the base class Shape (a refactoring called “move method up”). Depending on the IDE you are using, you will get support for those kinds of refactoring. What makes for a good design always depends on a number of factors, such as local context and demands as well as directions of (further) change of the code base.

2.4.6.4. Static methods

Sometimes you have methods in your classes that just don’t use any properties or methods of the class. Good IDEs (e.g., PyCharm) will inform you about this and even suggest refactorings accordingly, i.e. marking the method as static.

An example adapted from real code:

1class DatasetFactory:
2
3    @staticmethod
4    def _create_dataset():
5        return ExperimentalDataset()

Here, we have a (protected) method _create_dataset() that returns a specific dataset object. As this method does not depend on any internal state or method of the class DatasetFactory, it is a static method, as made explicit by the decorator @staticmethod (line 3) and not having the argument self as first argument, as obligatory for all other methods of objects. For obvious reasons, static methods can therefore not access any property or method of the class.

Note that static methods can have arguments, in case they need to operate on some external information. Another example adapted from real code shows this:

1class Projection(SingleProcessingStep):
2
3    @staticmethod
4    def applicable(dataset):
5        return len(dataset.data.axes) > 2

Here, we have a static method applicable that checks whether a processing step (here: projection along one axis) can be performed on a dataset provided as argument. The condition is that the number of axes of the dataset is larger than two. While the class contains a property the dataset it operates on will be (temporarily) stored, we need to check whether we can operate on a dataset before assigning the dataset to the class property. Hence a static method.

As a side effect of making a method static, you can call the method of the class without first instantiating an object of this class. In context of the two classes shown above, this may look something like:

1dataset = ExperimentalDataset()
2Projection.applicable(dataset)

Wile we instantiated an object dataset of class ExperimentalDataset (line 1), we directly accessed the static method applicable of the Projection class without previously instantiating an object of this class (line 2).

2.4.6.5. Getter and setter

In contrast to other programming languages, Python allows to directly access the properties of objects. Hence, it is unusual (and typically a sign of lacking familiarity with the “Pythonic” way) to write explicit getter and setter methods for properties, as is common for other programming languages. There are, however, situations where you would like to restrict access to properties of a class. In this case, you can provide either only a getter (to make the property read-only) or a pair of getters and setters. Typically, this is used in conjunction with a private property of the same name.

An example of a (public) property that should be read-only, adapted from real code:

1class Dataset:
2
3    def __init__(self):
4        self._package_name = ''
5
6    @property
7    def package_name(self):
8        return self._package_name

As usual, this example is stripped down to the absolute necessary to get the point. In real code, there is much more properties set in the __init__() method, and the private property _package_name is set to an actual value (using some fancy method) rather than the empty string. However, you will get the point: the package a class belongs to might be an interesting proprety to know, but nothing that you should be able to set from the outside. Hence, by providing only this getter in form of a method that is decorated with the @property decorator (line 6) adds a public property package_name to the interface of the class Dataset. The actual value is stored in a private property (line 4).

A typical situation for providing an explicit pair of getters and setters: You need to check for consistency of the internal state of your object when setting some property. What follows is another stripped-down example of real code:

 1class Data:
 2
 3    def __init__():
 4        self._data = np.zeros(0)
 5
 6    @property
 7    def data(self):
 8        return self._data
 9
10    @data.setter
11    def data(self, data):
12        # Some internal consistency checks and updates...
13        self._data = data

Again, the actual value is stored in an internal property, here _data. Access is gained via a getter defined as described above (lines 6–8). The setter part is the new thing here. Again, a special decorator is used, consisting of the name of the public property (here: data) and the string setter, separated by a dot: @data.setter (line 10).

Hint: If you use getters (and setters), make sure that they usually do not perform expensive calculations, as particularly in case of getters, users expect the operation of getting the property to be cheap. If you do need to perform extensive calculations, clearly note this in the respective documentation.

2.4.7. Wrap-up

Object-oriented programming is much more about designing your software and a good architecture than it is about syntax. We could only scratch on the surface here, but at least the most important concepts should have been mentioned. Clearly, object-oriented programming is not the first priority of scientists whose approach to programming is rather to get things done now than to develop software that lives up to the high standards of science itself. And this is mostly not a lack of willingness, but rather of sufficient training and insight into the advantages and opportunities of the method. Nevertheless, I’m convinced that developing scientific software for complex tasks, as are typically found in science, and particularly developing such software with usability, maintainability, robustness and reliability in mind, will be much easier using concepts of object orientation.