Path: blob/main/Lesson 6 - Classes.ipynb
968 views
Lesson 6 - Classes
Authors:
Yilber Fabian Bautista
Niklas Becker
Sean Tulin
By the end of this lecture you will be able to:
Understand the concept of namespaces, and differentiate the 4 different namespaces in python
Understand the meaning of Scope of a code
Use classes to efficiently organize a code
Differentiate between class variables and instances variables
Use inheritance to define classes from other existing classes
Namespaces
Formally, a namespace is a mapping from names to objects. When you type the name of a function or variable, the python interpreter has to map that string to the corresponding object. Depending on the namespace, the same name can lead to different objects. For example, there is the namespace of built-in functions, such as abs() and max(). There is also a namespace for included libraries, and one for each function invocation. Some examples of the same name being resolved differently depending on the namespace: For instance, although the abs() and np.abs() functions have the same effect on numbers, they belong to two different namespaces. If we run the lines
we will get the output
That is, the functions do effectively the same, but correspond different implementations in python, as seen from the last line in the output. There is no relation between names in different namespaces! You can have two functions with the same name but in different namespaces do completely different things.
There are 4 types of namespaces:
Built-In
Global
Enclosing
Local
When the execution encounters a name, the namespaces are searched from bottom to top in this list until a name can be resolved.
Built-in
The first type of namespace was already shown in the example above. There are built-in functions that are available at all times. To see them in a list, run the following line
Global
The global namespace is created at the start of any program and contains all names defined at the level of the main program. This is where the user can come in and define variables and functions. For example:
Although the defined function has the same name as the built-in max function, our definition is in the global namespace and as already mentioned, it is found before than the the Built-in max function in the Built-in namespace
The interpreter also creates namespaces for all modules that are imported, such as numpy in the example above. When importing modules, the names can be made available at the global namespace of the program using the syntax
For instance, if we import the max function from numpy as
and run
we will get as output 10, whereas max(5, 10), defined above, will fail. The reason for this is that although the two functions are defined in the global namestace, the interpreter searches the more recent definition of the function.
Local and Enclosing
Every function has its own namespace associated with it. The local namespace is created when the function is executed and 'forgotten' afterwards.
Here, bar is defined inside of foo, so foo is the enclosing namespace for bar. A name lookup will first search the local namespace, then the enclosing namespace, then the global, and finally the built-in one, as mentioned above.
Scope
The scope of a name refers to the region of a code where it has meaning, where it can be associated with the object. Once a variable is out of scope, it is forgotten and no longer accessible.
The above discussion show the importance of to understanding the concepts of namespace and scope, in order to have a full control, and understand the functionality of your code!
Classes
What is a class?
A class is a 'blueprint' for creating objects that binds together data (variables) and manipulation of such data (via methods). Classes are useful ways of organizing the code, in addition to providing objects with code that can be reused.
Class Definition
A class is defined using the following syntax:
A class can have variables and methods (functions) associated to them. These are collectively called attributes of the class. The class definition must be executed before they can be used. The class defines its own namespace. It needs to be accessed with the syntax such as
Let us define an specific example of a class
Class Instances
In order to use a class attributes, one has to specify the entries of the class, and it is cumbersome to do it all the time we want to use the class. For that reason, one can create instances of a class. An instance of a class is created with the following function notation:
This object now has the methods and variables associated with that class
Classes can access their own methods and variables using the self keyword. self represents the instance of the class. By using the self keyword we can access the attributes and methods of the class in python. It binds the attributes with the given arguments. These attributes can also be accessed from the outside
Instance Variables
Every instance of a class can also have their own variables - instance variables - associated to them. They are created similar to local variables and only associated with that instance. Consider the following example of a class
If we want to access classVariable from classInstance, we simply do classInstance.classVariable, which will have as output the string "This is a class variable". We might wonder whether the variables counter and b defined inside the method startCounter() can be accessed in the same way. If we did classInstance.counter we will get the error:
similar if we typed classInstance.b. To access those variables we first need to initialize the startCounter() method.
where now the output will be 0. We might wonder whether the same procedure is true to access the value for the b variable. If we did
the output will be the error message
The reason for this error is that unlike counter, b is not an instance variable. We learned then that instance variables are defined with the self keyword. Here b corresponds to a local variable inside the startCounter method.
Now, if we initialize the second method increaseCounter
we will see that counter is still an instance variable but has increased its value by one.
Of course, these instance variables can differ between instances. We can see this in the following example:
Initialization
The most important instance variables are usually assigned on initialization. For this purpose, there is a special function for classes, the _init_(self,...) function. The _init_(self,...) function is called when the instance is first created. The _init_ function is called the constructor and can take a list of parameters that need to be passed on during initialization.
As an example, let us imagine we want to create a class that has the profile information of a given person. We start by assigning the name of that person to the profile
Class Variable vs Instance Variable
Care must be taken to differentiate between class variables and instance variables. Class variables are copied on each initialization of a class, while instance variables are created from scratch. This can have unintended effects for mutable variables, because their copies are shallow, i.e. they are copied by reference. To see this, let us continue with the class of our previous example, and add a hobbies list to our person profile as a class variable
Now, if the hobbies list was assigned as an instance variable instead
which generates two different outputs. Generally, it is advisable to use class variables only for values that stay constant between all instances and mostly use instance variables instead.
Printing
To simplify output, the classes can define a function that returns a string to represent that object. This is the _str_ function. For instance
Why use classes?
Enables encapsulation: Bind together data and code that manipulates the data in the same place
Brings structure into the code, and allows real-world mapping
Polymorphism -> See below
Exercise 1
Make a class modeling a car with the attributes model, cost, fuel efficiency (km/liter), condition, and mileage. The condition is supposed to model the status of the car in terms of functionality, which of course diminishes over time. Implement a sensible __init__ function and additional functions:
Drive: Takes distance and fuel price as an input. The distance is added to the mileage. It also prints an estimate of the fuel cost and adds it to the total cost. The condition of the car deteriorates at a rate of (1%/1000km). If this drops below 0% the car stops working.Repair: The car is taken to the garage. The condition is set back to 100% at a cost of 100€/1%. The cost is printed and added to the total cost.LifetimeCost: Outputs the mileage and total cost of the car so farHonk: 😉
If you were to buy a new green VMW Imaginaris (cost
20k€) with a fuel efficiency of0.05L/kmwhat would be the approximated cost after driving for10^5 km?And for the newer version VMW Imaginaris^2 for 25k€ with 0.04L/km?
Inheritance
In object-oriented programming, inheritance describes the process of basing a class on another class' implementation, thus inheriting its properties.
The child class inherits all variables and methods from the parent class and can expand on them. Let us see this explicit in the following example
with output
Thus we see that even though f() is not defined in class B, the latter inherits the definition from class A. Furthermore, the g() in class B, expands the methods in class A
The child class can also override functions from their parent class:
The overwritten function can still be accessed from inside the child class using the
super()function
This is especially useful when initializing an object
Why use inheritance?
Code Reusability
Enables Subtyping (IsA relation): Dog > Mammal > Animal
Logical Hierarchy
Use Polymorphism: Calling code is agnostic to the specific implementation
Example: Dark Matter Halos
In this example we will look at different analytic Dark Matter halo profiles. Since they all share the same idea of describing the density and mass profile of a halo, it makes sense to use classes and inheritance features. To this end, we define the abstract basis class DMHalo, which doesn't describe a specific halo, but tells us how what properties a halo class should have.
The 3 profiles we will look at are
a constant density profile
an NFW profile
a NFW profile with a central density spike with the parameters chosen such that
The constant and NFW profiles can inherit from this base class, and the SpikedNFW profile can inherit from the NFW and modify its density in the appropriate region.
Exercise 2
Implement a mass function inside the classes and plot it alongside the density profile. Recall the mass can be computed as the volume integral of the DM density.
There are two possible approaches to this:
Implement the analytical mass function for each class individually
Implement a numerical integration scheme in the base class and let inheritance do the rest (tip: use solve_ivp)
What are the pros and cons of each approach?
Implement both and compare the results
Exercise 3
In this exercise we will automatize Exercise 3 in Lecture 5 for all data present in the Rotation curves directory, using classes. As usual, we will divide the exercise in several steps:
Load the pandas, and pyplot libraries; they will be used in your code
Define a new class
rotation_curve, and initialize it. The initialization function should take the inputGalaxy_ID.In the
__init__function, define the instance variabledf_circular, which creates a DataFrame for the givenGalaxy_ID. (This corresponds to step 2 in Exercise 3 Tutorial 5.) Create a test class instance ofrotation_curve, for a given galaxy ID. Check that for instance, forGalaxy_ID = 'IC2574'the following lines
are produced as output

Define the class method
update_df_circularwhich takes as input the variableskeyanddata. The method adds a new columndatato the DataFramedf_circular, with keywordkey.Define the class method
add_M_DM, which will add the columnDM Mass (M_sol)to the DataFramedf_circular. (This corresponds to step 3 in Exercise 3, Tutorial 5. Hint, use theupdate_df_circulardefined above.) Check that when you run the lines
you get as output

Now you can test that the
rotation_curveclass works well with all of the otherGalaxy_IDsin theRotation curvesdirectory. (You will probably have troubles loading the data for galaxy'F563-1'.)We will now proceed to do define a function outside of our class, that plots two columns of the DataFrame
df_circular, for all of the Galaxy_IDs in theRotation curvesdirectory. First we need to have a list with all of theGalaxy_IDsin theRotation curvesdirectory.
Use the os library, and the
os.listdir()function, to create such a list, and remove'F563-1'from the list if you got error message when loading it in step 6.Define the function
plotthat takes as entries two keywordsxandy. It will loop over all of the elements ofGalaxy_IDs, creating an instance of the classrotation_curvefor every iteration, and plot the columnsxvsyin thedf_circularclass function. For instance, if you use that function to plot'radius (kpc)'vs'DM Mass (M_sol)', one should get a plot similar to: