Understanding classes

You can think of classes as blueprints for their instances—for example, a Car class will describe the generic properties of a car, while a specific instance of this class will describe a particular car with its own characteristics. As such, classes provide a way to store information and related functions linked to the instances. In our example, the Car class can store a drive function in its body, which will rely on the gasoline value for each specific instance. This approach is extremely useful when we describe systems of entities, whether a representation of a physical object (Car), personal information (Contact), or some abstract entity (web page representation).

Let's start with the syntax. Take a look at this example:

class Person:
'''person entity'''
greeting = 'Hi {0}. My name is {1}!'
def __init__(self, name, surname, age):
self.name = name
self.surname = surname
self.age = age
def __repr__(self):
return f'Person(name={self.name}, surname={self.surname},
age={self.age})'
def say_hi(self, name):
print(self.greeting.format(name, self.name))

To declare a new class, we will start with the class keyword, followed by the class name—the same as we do for functions. As recommended in PEP8, class names are title-cased—they should start with an uppercase letter. The name is then followed by parentheses, if the class is based on (inherits from) another class (more on that later). You don't need them if it does not. Again, as with functions, this initial line needs to end with the colon, and all the following lines should start with an indentation.

In some code samples outside of this book, you might notice that a class inherits from an object. This is a legacy style; there is no advantage in inheriting from an object in Python 3. This code was probably written to be compatible with Python 2.

Like functions, classes can (and should) contain a docstring. After that, you can describe its internals, such as the values and methods the class instance would have.

Right after the docstring, we can add class attribute—those will shared across all instances, until we overwrite them (in this case, a new value will be preserved for a specific instance). Using class attributes is a great practice to declare default values. Class attributes also save memory, as we store one value shared across all instances.

The functions within the class are called methods. While you can add any custom method or variable to a class, it is important to know the special values and methods that are bound to Python syntaxes and will change the class's behavior. To be easily distinguished, those special methods and attributes are enclosed by a double underscore and are therefore called dunder methods.

The __init__ method is used to initialize instances of each class. For example, when you call Person('Pippi', 'Longstocking', 11), the __init__ function is called under the hood. The first argument—traditionally called self (although you can name it anything you want)—is the representation of the instance itself, so you can add, extract, or change any of its values. All class methods expect to add the object itself as a first argument (unless you set method to static using the @staticmethod decorator), but you won't need to actually pass this variable on execution—see the following example.

Similarly, the class will call the __repr__ method whenever the instance needs to be represented. For example, when you put a variable at the end of the cell in Jupyter, its __repr__ method is called under the hood, as you can see from the following example (note the mood attribute we just made up):

>>> P = Person('Pippi', 'Longstocking', 11)
>>> P
Person(name=Pippi, surname=Longstocking, age=11, mood=excited)

According to general Python philosophy (remember duck typing?), Python does not care much about what any of those functions are actually doing. There are some requirements, though—for example, __repr__ is required to return a string.

The say_hi function is one we have added. Note that it prints out the string using both internal and external data:

>>> P.say_hi('Kalle')
Hi Kalle. My name is Pippi!

Note that, even after initiation, we have full access to the properties and methods of a particular instance and are free to change and modify them, simply by overriding (assigning a new value):

>>> P.name, P.surname = 'Kalle', 'Blomkvist'
>>> P
Person(name=Kalle, surname=Blomkvist, age=11, mood=excited)
Special methods, such as __repr__ , can also be overridden, but they are defined on the class level, not by instance. In order to override it, you need to write this: P.__class__.__repr__ = lambda self: lambda self: f'mr. {self.name} {self.surname}, Detective'. Note that this will also override the behavior for other instances of the same class as well.

It may be a great feature, but also a total disaster if used unintentionally.

The power of classes is unleashed as you use the same class to describe and operate on multiple instances. We loop over a small dataset, initializing a Person object on every row of data:

data = [
{'name':"Pippi", 'surname':'Longstocking', 'age':11},
{'name':"Kalle", 'surname':'Blomkvist', 'age':10},
{'name':'Karlsson-on-the-Roof', 'surname': None, 'age':12}
# not sure of the age
]

characters = [Person(**row) for row in data]
for character in characters:
character.say_hi('Reader')

The output should be the following:

>>> Hi Reader. My name is Pippi!
>>> Hi Reader. My name is Kalle!
>>> Hi Reader. My name is Karlsson-on-the-Roof!

As a result, we now have a list of objects, with every single one storing corresponding data and methods. Of course, objects can use and change global values, or interact with each other.

At the beginning of this section, we briefly mentioned two special methods for classes. Let's now go on a tour of other special methods we can use.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.254.116