Blueprint = Class and Renault 4 = object

Python 3.4 Class and Instance Attribute

Object Oriented Programming Adventures

After a few months of procedural programming, Holberton School introduced us to object oriented programming. As we are learning about classes and instances and attributes and decorators, this post is there for me to clarify my understanding of classes and instances attributes. I will do that through examples, creating and developing a Car class.

Let’s start with an empty class.

>>> class Car:
... pass
...
>>> help(Car)
Help on class Car in module __main__:
class Car(builtins.object)
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
(END)
>>> Car.__dict__
mappingproxy({'__doc__': None, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Car' objects>, '__dict__': <attribute '__dict__' of 'Car' objects>})

Surprise, my empty class comes with 2 goodies. One I am not sure what it means yet, and __dict__ which can list variables (other name for attributes).

What happens if I create a Car , i.e. I use the class as a blueprint and create an object ? Let’s make a 4L.

>>> quatreL = Car()
>>> quatreL.__dict__
{}

We see the attribute __dict__ is different for the object and for the class.

Let’s make a class attribute for Car . We will create nb_wheels and set it to 4.

>>> Car.nb_wheels = 4  #not the best way to do it
>>> quatreL.nb_wheels
4
>>> deuxCV = Car()
>>> deuxCV.nb_wheels
4
>>> Car.nb_wheels
4
>>> deuxCV.__dict__
{}
>>> Car.__dict__
mappingproxy({'__doc__': None, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Car' objects>, 'nb_wheels': 4, '__dict__': <attribute '__dict__' of 'Car' objects>})

We see that the Car __dict__ has a new entry, ’nb_wheels’: 4. We also see that both my cars, my previous quatreL and my new deuxCV have that attribute, but it does not appear in their __dict__ . I created a class attribute. This attribute is the same for all elements in that class, and accessible by all of them, and by the class itself.

However, one need to be cautious when going that way, an instance can easily overwrite it. See the Reliant Robin.

>>> reliantrobin = Car()
>>> reliantrobin.nb_wheels
4
>>> reliantrobin.__dict__
{}
>>> reliantrobin.nb_wheels = 3
>>> reliantrobin.__dict__
{'nb_wheels': 3}
>>> reliantrobin.nb_wheels
3
>>> quatreL.nb_wheels
4

The Reliant Robin has 3 wheels. So we overwrote the class variable. And what happened is that actually, this way we created an instance variable nb_wheels that holds the value 3. When I request Python to print a variable, it first goes in to check the instance __dict__ and since it finds it there, it prints 3. But for all other cars, like the 4L, it has to go one step further to the class __dict__ and so it prints 4.

Let’s make a more complex Car class:

>>> class Car:
... nb_wheels = 4
... def __init__(self, name, make):
... self.name = name
... self.make = make
...
>>> help(Car)
Help on class Car in module __main__:
class Car(builtins.object)
| Methods defined here:
|
| __init__(self, name, make)
|
------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
------------------------------------------------------------------
| Data and other attributes defined here:
|
| nb_wheels = 4
(END)
>>> Car.__dict__
mappingproxy({'__doc__': None, '__module__': '__main__', '__dict__': <attribute '__dict__' of 'Car' objects>, '__init__': <function Car.__init__ at 0x7f2e8623f2f0>, '__weakref__': <attribute '__weakref__' of 'Car' objects>, 'nb_wheels': 4})
>>> quatreL = Car("4", "Renault")
>>> quatreL.__dict__
{'name': '4', 'make': 'Renault'}
>>> Car.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'Car' has no attribute 'name'

This time we are declaring the class variable nb_wheels in the class definition. We are also creating an __init__ method to instantiate Car objects. It takes in 3 arguments:

  • selfis always present for all instance methods or variables. It is like a token to remember we are talking about one object.
  • name the name of the car, can be different for all Car objects
  • make the make of the car, can be different for all Car objects

I can create another car, with different values. The Car class requires objects to be created with 2 variables, but they can be anything. And again I have the class variable which is 4 for all objects. Car has a class variable nb_wheels which is accessible by all Car objects, and the Car class. And Car objects have 2 variables, which are only accessible by the Car instances. See above Car.name throws an error.

>>> deuxCV = Car("2CV", "Citroen")
>>> deuxCV.name
'2CV'
>>> deuxCV.make
'Citroen'
>>> deuxCV.nb_wheels
4
>>> quatreL.nb_wheels
4
>>> Car.nb_wheels
4

What distinguishes Python from other languages is how we have been able to declare new class and instance variable on the fly. Let’s see that again here.

>>> Car.nb_driver = 1
>>> modelS = Car("model S", "Tesla")
>>> modelS.engine = "electric"
>>> modelS.__dict__
{'engine': 'electric', 'name': 'model S', 'make': 'Tesla'}
>>> deuxCV.engine = "gas"
>>> deuxCV.__dict__
{'engine': 'gas', 'name': '2CV', 'make': 'Citroen'}
>>> modelS.nb_driver
1
>>> Car.nb_driver
1

What about encapsulation ? What about robust code if we can change things so fast ? In other programming languages, people would use private variables, only available through the filters of getter and setter functions. Like that:

class Car:
nb_wheels = 4
nb_driver = 1
   def __init__(self, name, make):
self.__name = name # use __ to make private variable
   def get_name(self):
return self.__name

def set_name(self, value):
self.__name = value
# ...
if __name__ == "__main__":
deuxCV = Car("2C", "Citroen")
print(deuxCV.get_name())
#Oups
deuxCV.set_name("2CV")
print(deuxCV.get_name())
print(deuxCV.name)

That would print

~/medium$ python3 Car.py 
2C
2CV
Traceback (most recent call last):
File "Car.py", line 24, in <module>
print(deuxCV.name)
AttributeError: 'Car' object has no attribute 'name'

The variable is private, I have to use get_name to retrieve it

However, Python users like simple, readable, and one way to do it. So they came up with a different solution, using decorators this way:

class Car:
nb_wheels = 4
nb_driver = 1
    def __init__(self, name, make):
self.name = name # no __ here to call the setter
self.make = make
    @property
def name(self):
return self.__name # keep __ here

@name.setter
def name(self, value):
print("in setter")
self.__name = value # keep __ here

if __name__ == "__main__":
deuxCV = Car("2C", "Citroen")
print(deuxCV.name)
#Oups
deuxCV.name = "2CV"
print(deuxCV.name)
print(deuxCV.make)

Which prints:

~/medium$ python3 Car2.py 
in setter
2C
in setter
2CV
Citroen

That way we can use the object_name.variable syntax for both private (__) and public attributes.

However, this code is not pythonic. Indeed, if I do not need any particular validation for my variable, and if I want to make it available for all users, then name should really be public like make.

Thanks for reading.


Reference: class and attributes

Link I liked about __dict__