16.13. OOP Methods and Attributes

  • Methods are functions in the class

  • First argument is always instance (self)

  • While calling function you never pass self

  • Prevents copy-paste code

  • Improves readability

  • Improves refactoring

  • Decomposes bigger problem into smaller chunks

Syntax:

>>> class MyClass:
...     def __init__(self):
...         self.myfield = 'some value'
...
...     def mymethod(self):
...         return self.myfield
>>>
>>>
>>> my = MyClass()
>>> my.mymethod()
'some value'

16.13.1. Methods Accessing Fields

Methods Accessing Fields:

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
...
...     def say_hello(self):
...         return f'My name... {self.name}'
>>>
>>>
>>> jose = Astronaut('José Jiménez')
>>> jose.say_hello()
'My name... José Jiménez'

self.name must be defined before accessing:

>>> class Astronaut:
...     def say_hello(self):
...         return f'My name... {self.name}'
>>>
>>>
>>> jose = Astronaut()
>>> jose.say_hello()
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'name'

16.13.2. Methods Calling Other Methods

Methods Calling Other Methods:

>>> class Astronaut:
...     def get_name(self):
...         return 'José Jiménez'
...
...     def say_hello(self):
...         name = self.get_name()
...         return f'My name... {name}'
>>>
>>>
>>> jose = Astronaut()
>>> jose.say_hello()
'My name... José Jiménez'

Methods calling other methods:

>>> class Iris:
...     def __init__(self):
...         self.sepal_length = 5.1
...         self.sepal_width = 3.5
...         self.petal_length = 1.4
...         self.petal_width = 0.2
...
...     def sepal_area(self):
...         return self.sepal_length * self.sepal_width
...
...     def petal_area(self):
...         return self.petal_length * self.petal_width
...
...     def total_area(self):
...         return self.sepal_area() + self.petal_area()
>>>
>>>
>>> flower = Iris()
>>> flower.total_area()
18.13

Since Python 3.7 there is a @dataclass decorator, which automatically generates __init__() method with arguments and set-up fields for you. More information in OOP Dataclass.

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Iris:
...     sepal_length = 5.1
...     sepal_width = 3.5
...     petal_length = 1.4
...     petal_width = 0.2
...     species: str = 'Iris'
...
...     def sepal_area(self):
...         return self.sepal_length * self.sepal_width
...
...     def petal_area(self):
...         return self.petal_length * self.petal_width
...
...     def total_area(self):
...         return self.sepal_area() + self.petal_area()
>>>
>>>
>>> flower = Iris()
>>> flower.total_area()
18.13

16.13.3. Examples

>>> class Point:
...     def __init__(self, x, y, z):
...         self.x = x
...         self.y = y
...         self.z = z
...
...     def get_coordinates(self):
...         return self.x, self.y, self.z
...
...     def show(self):
...         return f'Point(x={self.x}, y={self.y}, z={self.z})'
>>>
>>>
>>> point = Point(x=1, y=2, z=3)
>>>
>>> vars(point)
{'x': 1, 'y': 2, 'z': 3}
>>>
>>> point.get_coordinates()
(1, 2, 3)
>>>
>>> point.show()
'Point(x=1, y=2, z=3)'

16.13.4. Use Case - 0x01

>>> class Counter:
...     current_value: int
...
...     def __init__(self):
...         self.current_value = 0
...
...     def increment(self):
...         self.current_value += 1
...
...     def decrement(self):
...         self.current_value -= 1
...         if self.current_value < 0:
...             raise ValueError('Cannot decrement below zero')
...
...     def show(self):
...         return self.current_value
>>>
>>>
>>> c = Counter()
>>>
>>> c.increment()
>>> c.increment()
>>> c.show()
2
>>>
>>> c.decrement()
>>> c.decrement()
>>> c.show()
0
>>>
>>> c.decrement()
Traceback (most recent call last):
ValueError: Cannot decrement below zero

16.13.5. Use Case - 0x02

>>> from typing import Literal
>>>
>>>
>>> class Car:
...     engine: Literal['on', 'off']
...
...     def __init__(self):
...         self.engine = 'off'
...
...     def engine_start(self):
...         self.engine = 'on'
...
...     def engine_stop(self):
...         self.engine = 'off'
...
...     def drive_to(self, location: str):
...         if self.engine != 'on':
...             raise RuntimeError('Engine must be turned on to drive')
...         else:
...             return f'Driving to {location}'
>>>
>>>
>>> maluch = Car()
>>>
>>> maluch.drive_to('Cologne, Germany')
Traceback (most recent call last):
RuntimeError: Engine must be turned on to drive
>>>
>>> maluch.engine
'off'
>>>
>>> maluch.engine_start()
>>> maluch.engine
'on'
>>>
>>> maluch.drive_to('Cologne, Germany')
'Driving to Cologne, Germany'
>>>
>>> maluch.engine_stop()
>>> maluch.engine
'off'

16.13.6. Use Case - 0x03

$ pip install atlassian-python-api
>>> 
... from atlassian import Jira
...
... jira = Jira(
...     url='https://example.com:8080',
...     username='myusername',
...     password='mypassword')
...
... JQL = 'project = DEMO AND status IN ("To Do", "In Progress") ORDER BY issuekey'
...
... result = jira.jql(JQL)
... print(result)
>>> 
... from atlassian import Confluence
...
... confluence = Confluence(
...     url='https://example.com:8090',
...     username='myusername',
...     password='mypassword')
...
... result = confluence.create_page(
...     space='DEMO',
...     title='This is the title',
...     body='This is the body. You can use <strong>HTML tags</strong>!')
...
... print(result)

16.13.7. Assignments

"""
* Assignment: OOP State Auth
* Type: class assignment
* Complexity: easy
* Lines of code: 10 lines
* Time: 8 min

English:
    1. Modify class `User`
    2. Add method `__init__`:
         a. Arguments: `firstname`, `lastname`
         b. Method should set arguments as instance fields
         c. Method should set field `_authenticated` to `False`
         d. It's not an error, field name starts with underscore `_`
    3. Add method `login`, setting field `_authenticated` to `True`
    4. Add method `logout`, setting field `_authenticated` to `False`
    5. Add method `is_authenticated`, returning value of field `_authenticated`
    6. Run doctests - all must succeed

Polish:
    1. Zmodyfikuj klasę `User`
    2. Dodaj metodę `__init__`:
       a. Argumenty: `firstname`, `lastname`
       b. Metoda ma ustawiać argumenty jako pola instancji
       c. Metoda ma ustawiać pole `_authenticated` na `False`
       d. To nie błąd, nazwa pola zaczyna się od podkreślenia `_`
    3. Dodaj metodę `login`, ustawiającą pole `_authenticated` na `True`
    4. Dodaj metodę `logout`, ustawiającą pole `_authenticated` na `False`
    5. Dodaj metodę `is_authenticated`, zwracającą wartość pola `_authenticated`
    6. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

"""

# Add method `__init__`:
# - Arguments: `firstname`, `lastname`
# - Method should set arguments as instance fields
# - Method should set field `_authenticated` to `False`
# - It's not an error, field name starts with underscore `_`
# Add method `login`, setting field `_authenticated` to `True`
# Add method `logout`, setting field `_authenticated` to `False`
# Add method `is_authenticated`, returning value of field `_authenticated`
# type: type
class User:
    ...


"""
* Assignment: OOP State EditProfile
* Type: class assignment
* Complexity: easy
* Lines of code: 5 lines
* Time: 5 min

English:
    1. Modify class `User`
    2. Add method `edit_profile()`:
       a. Arguments: `firstname`, `lastname`
       b. Method checks if user is authenticated
       c. If so, set instance fields values
       d. If not, raise `PermissionError` with message `User is not authenticated`
    3. Run doctests - all must succeed

Polish:
    1. Zmodyfikuj klasę `User`
    2. Dodaj metodę `edit_profile()`:
       a. Argumenty: `firstname`, `lastname`
       b. Metoda sprawdza czy użytkownik jest uwierzytelniony
       c. Jeśli tak, to ustawia wartości pól instancji
       d. Jeśli nie, to rzuć wyjątek `PermissionError`
          z komunikatem `User is not authenticated`
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

"""

# Add method `edit_profile()`:
# a. Arguments: `firstname`, `lastname`
# b. Method checks if user is authenticated
# c. If so, set instance fields values
# d. If not, raise `PermissionError` with message `User is not authenticated`
# type: type
class User:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname
        self._authenticated = False

    def login(self):
        self._authenticated = True

    def logout(self):
        self._authenticated = False

    def is_authenticated(self):
        return self._authenticated