Python Book
πŸ‡ΊπŸ‡¦ Stand with UkraineπŸŽ“Training Suite
  • Book overview
  • Notes about this book
  • 1. Introduction to Python
    • What is Python
    • Basic syntax
    • Objects in Python
    • Python overview
    • Installation, IDEs etc.
    • ipython
    • Sources for self-learning
  • 2. Strings and numbers
    • Getting help
    • Introspection
    • Basic types
    • None object
    • Numbers
    • Strings
    • Unicode
    • String Formatting
    • Regular expressions
    • Sources for self-learning
  • 3. Containers
    • Data Structures
    • Lists
    • Tuples
    • Dictionaries
    • Sets
    • Conditions
    • Loops
    • Additional modules
    • Sources for self-learning
  • 4. Functions
    • Functions
    • Scopes of visibility
    • Generators
    • Lambdas
    • Type hints
    • Function internals
    • Sources for self-learning
  • 5. Functional Programming
    • Builtins
    • Iterable
    • Iterator
    • Functional Programming
    • Functools
    • Comprehensions
    • Additional modules
    • Sources for self-learning
  • 6. Code Styling
    • Zen of Python
    • Lint
    • PEP 8
    • Modules
    • Packages
    • Sources for self-learning
  • 7. OOP
    • OOP Basics
    • Code design principles
    • Classes
    • Method Resolution Order
    • Magic attributes and methods
    • Super
    • Sources for self-learning
  • 8. Decorators, Exceptions
    • Decorators
    • Exceptions
    • Sources for self-learning
  • 9. Testing
    • Basic Terminology
    • Testing theory
    • Dev unit testing vs QA automated testing
    • Best Practices
    • Doctest
    • Unittest
    • Test Runners
    • Pytest
    • Nose
    • Continuous Integration
  • 10. System Libs
    • Working with files
    • System libraries
    • Subprocess
    • Additional CLI libraries
Powered by GitBook
On this page
  • OOP in Python
  • Inheritance
  • Multiple Inheritance
  • Methods
  • Old and New classes

Was this helpful?

Edit on Git
  1. 7. OOP

Classes

OOP in Python

Class

Classes created with operator class:

class A:
    attr = 10

When a class definition is entered, a new namespace is created, and used as the local scope β€” thus, all assignments to local variables go into this new namespace. In particular, function definitions bind the name of the new function here

Class objects support two kinds of operations: attribute references and instantiation.

Simple OOP examples

Here is the simplest class for Cat and two objects of this class:

πŸͺ„ Code:

class Cat:
    pass 

agata = Cat()
blanka = Cat()

print(agata, id(agata))
print(blanka, id(blanka))

πŸ“Ÿ Output:

<__main__.Cat object at 0x7ff57dd25360> 140692354650976
<__main__.Cat object at 0x7ff57dd25540> 140692354651456

Cat class and object is "empty" - doesn't define any attributes and methods. They even don't have their proper name.

Let's add names:

πŸͺ„ Code:

agata.name = "Agata"

print(agata.name)

πŸ“Ÿ Output:

Agata

We can assign attributes during initialization of the object - it would simplify things a lot.

The method starts with __ ("dunder") is called a magic method. __init__ is one example, __str__ - method that defines a string representation of the object - is another.

self in examples below is the reference to the object itself so we could use it in the function code - for example during str(agata) Python will call agata.__str__() which will need to return a string including agata.name, and it does this using the reference self.

πŸͺ„ Code:

class Cat:
    def __init__(self, name="Stray"):
        self.name = name
        
    def __str__(self):
        return f'<Cat "{self.name}">'
    
    def __repr__(self):
        return f'Cat("{self.name}")'
        
agata = Cat("Agata")
street_cat = Cat()

print(agata)
print(street_cat)
print(repr(agata))

πŸ“Ÿ Output:

<Cat "Agata">
<Cat "Stray">
Cat("Agata")

Magic methods are not the only ones we can define - in fact we can add any method that would describe some action involving an object.

Here we also will set a class attribute accessible from all children.

πŸͺ„ Code:

class Cat:
    default_sound = "Meow"
    
    def __init__(self, name="Stray"):
        self.name = name
        #self.default_sound = "Rrrr"  # We can override "global" class attribute with "local" instance attribute
        
    def __str__(self):
        return f'<Cat "{self.name}">'
    
    def sound(self, times=3):
        return " ".join([self.default_sound] * times)
    
blanka = Cat("Blanka")
print(blanka.sound())

πŸ“Ÿ Output:

Meow Meow Meow

Examples with more methods/attributes:

class Bus:
    """ Sample Bus class """
    # Class attributes
    buses_count = 0         # IDEA: change with classmethod property -- get len(buses)
    people_transferred = 0  # IDEA: change with dict()
    money_collected = 0     # IDEA: change with dict()
    buses = []
 
    # Instance Initializer
    def __init__(self, name="Some Bus", rate=7):
        # instance attribute
        self.name = name
        self.people_transferred = 0
        self.rate = rate
        self.money_collected = 0
        self.__class__.buses_count += 1   # self.__class__ instead of hardcoding Bus
        self.__class__.buses.append(self) # self.__class__ instead of hardcoding Bus
 
    # Instance method
    def transfer(self, num=1):
        self.people_transferred += num
        self.__class__.people_transferred += num
        self.money_collected += self.rate * num
        self.__class__.money_collected += self.rate * num
        
    def __str__(self):
        return f"<Bus '{self.name}'>"
 
    def info(self): # change to __str__
        data = dict(
            name=self.name, total_buses=Bus.buses_count, rate=self.rate,
            num=self.people_transferred, total=Bus.people_transferred
        )
        return "Bus '{name}' (rate: {rate}€) (total: {total_buses}), " \
               "transferred {num} from {total} ppl".format(**data)
    
    def __repr__(self):
        return f"Bus('{self.name}', {self.rate})"
b = Bus("Bus #40")
b.transfer(100) # --> Bus.transfer(b, 100)
b

πŸ“Ÿ Output:

Bus('Bus #40', 7)

πŸͺ„ Code:

print(b)

πŸ“Ÿ Output:

<Bus 'Bus #40'>

πŸͺ„ Code:

b.transfer(50)
print(b.info()) # --> Bus.info(b)

πŸ“Ÿ Output:

Bus 'Bus #40' (rate: 7€) (total: 1), transferred 150 from 150 ppl

πŸͺ„ Code:

b2 = Bus("Tram #1", 8)
b2.transfer(50)
print(b2.info())

πŸ“Ÿ Output:

Bus 'Tram #1' (rate: 8€) (total: 2), transferred 50 from 200 ppl

πŸͺ„ Code:

print(f"Bus.people_transferred = {Bus.people_transferred}")
print(f"Bus.money_collected = {Bus.money_collected}")
print(f"Bus.buses_count = {Bus.buses_count}")
print(f"Bus.buses = {Bus.buses}")

πŸ“Ÿ Output:

Bus.people_transferred = 200
Bus.money_collected = 1450
Bus.buses_count = 2
Bus.buses = [Bus('Bus #40', 7), Bus('Tram #1', 8)]

Creation of an instance of the class - like calling a function (in fact it is exactly like this - firstly we calling magic method __new__() then __init__()

πŸͺ„ Code:

bus_317 = Bus("# 317")
bus_317.transfer(20)
print(bus_317.info())

πŸ“Ÿ Output:

Bus '# 317' (rate: 7€) (total: 3), transferred 20 from 220 ppl

πŸͺ„ Code:

b.transfer(23)
bus_317.transfer()
bus_317.transfer(55)  
print(bus_317.info())

πŸ“Ÿ Output:

Bus '# 317' (rate: 7€) (total: 3), transferred 76 from 299 ppl

Class variables and instance variables were changed:

πŸͺ„ Code:

print(Bus.people_transferred)
print(bus_317.people_transferred)

πŸ“Ÿ Output:

299
76

Inheritance

Inheritance from some class (called "base" or super class) allows to create a new class "borrowing" all attributes and methods definitions as they were in the super class. It allows to:

  1. Reuse the code (DRY principle).

  2. Create abstractions.

For example here is the class for the Robot:

class Robot:
    sounds = ["Beeep", "Bzzzt", "Oooooh"]
    
    def __init__(self, name, weigth=1000):
        self.weigth = weigth
        self.name = name
        
    def __str__(self):
        """Info about this robot"""
        return "Robot {obj.name} ({obj.weigth} kg)".format(obj=self)
 
    def say(self):
        """Say something"""
        import random 
        return f"{self.name} says: {random.choice(self.sounds)}"

It is completely usable:

πŸͺ„ Code:

bip = Robot("Bip 1.0")
print(bip)
print(bip.say())

πŸ“Ÿ Output:

Robot Bip 1.0 (1000 kg)
Bip 1.0 says: Oooooh

We can re-use this class to create a robot from Futurama using the inheritance. For this we need to specify base/super class in parenthesis during new class definition.

class BendingRobot(Robot):
    sounds = ["Kill all humans", "Kiss my shiny metal face", "Oh, your God!",
              "Oh wait you’re serious. Let me laugh even harder."]

πŸͺ„ Code:

bender = BendingRobot("Bender")
print(bender)
print(bender.say())

πŸ“Ÿ Output:

Robot Bender (1000 kg)
Bender says: Oh, your God!

As we can we still can use say method defined in the base class.

Multiple Inheritance

Python supports a limited form of multiple inheritance as well. A class definition with multiple base classes looks like this:

πŸͺ„ Code:

class A:
    a = "a from A"
    
class B(A):
    x = "x from B"
    
class C(A):
    a = "a from C"
    x = "x from C",
    
class D(B, C):  # change to D(C, B) and check...
    pass

d = D()
print(D.__mro__) # D.mro()
print(d.a, d.x)

πŸ“Ÿ Output:

(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
a from C x from B

Let's enhance our Robot example by inheriting from two classes at once.

πŸͺ„ Code:

class Mail:
    def send_message(self, msg):
        print(f"*** SENDING MESSAGE: <<<{msg}>>>  ***")
        
Mail().send_message("Test")

πŸ“Ÿ Output:

*** SENDING MESSAGE: <<<Test>>>  ***

πŸͺ„ Code:

class BendingMailingRobot(Robot, Mail):
    sounds = ["Kill all humans", "Kiss my shiny metal face", "Oh, your God!",
              "Oh wait you’re serious. Let me laugh even harder."]
    
bender2_0 = BendingMailingRobot("Bender 2.0")
bender2_0.send_message(bender2_0.say())

πŸ“Ÿ Output:

*** SENDING MESSAGE: <<<Bender 2.0 says: Kiss my shiny metal face>>>  ***

More advanced example:

πŸͺ„ Code:

class BendingMailingRobot(BendingRobot, Mail):
    def mail(self):
        return self.send_message(self.say())
    
bender3_0 = BendingMailingRobot("Bender 3.0")
bender3_0.mail()

πŸ“Ÿ Output:

*** SENDING MESSAGE: <<<Bender 3.0 says: Oh, your God!>>>  ***

And even more advanced example (with overloading of the existing send_message method with super() function covered later):

πŸͺ„ Code:

class BendingMailingRobot3(BendingRobot, Mail):
    def send_message(self):
        return super().send_message(self.say())

bender4_0 = BendingMailingRobot3("Bender 4.0")
bender4_0.send_message()

πŸ“Ÿ Output:

*** SENDING MESSAGE: <<<Bender 4.0 says: Kiss my shiny metal face>>>  ***

Methods

Methods can be:

  • instance methods

  • class methods

  • static methods

Instance method

By default all methods (except of __new__ are instance methods).

The method that should be called with the instance as it's first argument. This method is bound to instance so if calling as it's method passing instance is not required. Example:

πŸͺ„ Code:

class Example:
    def cool_method(self):
        print(f"I am instance method, my instance is: {self}")
        
ex = Example()
ex.cool_method()
# Example.cool_method(ex)  # <-- same 

print(ex.cool_method)

πŸ“Ÿ Output:

I am instance method, my instance is: <__main__.Example object at 0x7ff57df07610>
<bound method Example.cool_method of <__main__.Example object at 0x7ff57df07610>>

Class methods

The method with class as the first argument. Useful to run some code without need of creating the instace.

To mark the method as class method it is required to use builtin decorator @classmethod

πŸͺ„ Code:

class Example:
    attr = 5
    
    @classmethod
    def class_method(cls):
        print("CLS:", cls.attr)
        
    def instance_method(self):
        print("INSTANCE:", self.attr)
        
ex = Example()
ex.attr = 25
ex.class_method()
ex.instance_method()
Example.class_method() # Example.class_method(Example)

πŸ“Ÿ Output:

CLS: 5
INSTANCE: 25
CLS: 5

Let's add some class method to Bus class. Please note that we don't need any existing buses to call general_info method on Bus2:

πŸͺ„ Code:

class Bus2(Bus):
    # creating isolated class attributes to not use the ones from base Bus class
    buses_count = 0
    buses = []
    people_transferred = 0
    money_collected = 0
    
    @classmethod
    def general_info(cls):
        print(f"* People_transferred = {cls.people_transferred}")
        print(f"* Money_collected = {cls.money_collected}")
        print(f"* Total buses = {cls.buses_count}")
        print(f"* Buses = {cls.buses}")
        
    @classmethod
    def remove_bus(cls, bus):
        try:
            cls.buses.remove(bus)
            cls.buses_count -= 1
            print(f"{bus} removed")
        except ValueError:
            print("No such bus registered!")
            
Bus2.general_info()

πŸ“Ÿ Output:

* People_transferred = 0
* Money_collected = 0
* Total buses = 0
* Buses = []

Let's add some buses, check the general info and remove buses:

πŸͺ„ Code:

bus = Bus2()
bus.transfer(100)
Bus2.general_info()

πŸ“Ÿ Output:

* People_transferred = 100
* Money_collected = 700
* Total buses = 1
* Buses = [Bus('Some Bus', 7)]

Removing also works good:

πŸͺ„ Code:

Bus2.remove_bus(bus)
Bus2.remove_bus(bus)

πŸ“Ÿ Output:

<Bus 'Some Bus'> removed
No such bus registered!

Static method

This method doesn't require to pass instance/class at all.

To mark the method as static method it is required to use builtin decorator @staticmethod

πŸͺ„ Code:

class Example:
    @staticmethod
    def cool_method():
        Example.attr = 123123
        return "I am a static method, I don't have access to anything :("

    @staticmethod
    def other_stat_method(str_):
        return str_[::-1]
        
ex = Example()
print(ex.cool_method())
print(Example.cool_method())
print(ex.other_stat_method("Radar"))
print(Example.attr)

πŸ“Ÿ Output:

I am a static method, I don't have access to anything :(
I am a static method, I don't have access to anything :(
radaR
123123

We can use static method for creating some helper function for `Bus` class, let's add simple one for calculating money collected:

class Bus:
    ...
    @staticmethod
    def calc_money(ppl, rate):
        return ppl * rate
    
    def transfer(self, num=1):
        self.people_transferred += num
        self.__class__.people_transferred += num
        self.money_collected += self.calc_money(num, self.rate)
        self.__class__.money_collected += self.calc_money(num, self.rate)
    ...

Old and New classes

This chapter is only viable for Python 2 - as in Python 3 there are no such distinguishing as "old/new" classes.

Before Python 2.5 the format for creating a class was:

class Old:
    pass

This was resulted in various problems with MRO and types. So some code redesigned, for this new format introduced:

class New(object):
    pass

Differences:

  1. classobj type as builtin types

  2. Correct horizontal-then-vertical MRO (earlier it was vertical)

  3. No __new__() method in old class

  4. Additional functionality:

    1. __slots__ (Python 3.x)

    2. __getattribute__

    3. __getattr__ and __getattribute__ don't look for magic methods in instance

PreviousCode design principlesNextMethod Resolution Order

Last updated 2 years ago

Was this helpful?