13.5. OOP Methods and Attributes

13.5.1. Rationale

  • 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):
...         print(self.myfield)
>>>
>>>
>>> my = MyClass()
>>> my.mymethod()
some value

13.5.2. Methods Accessing Fields

Methods Accessing Fields:

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
...
...     def say_hello(self):
...         print(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):
...         print(f'My name... {self.name}')
>>>
>>>
>>> jose = Astronaut()
>>> jose.say_hello()
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'name'

13.5.3. 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()
...         print(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()
>>> print(flower.total_area())
18.13

Since Python 3.7 there is a @dataclass decorator, which automatically generates __init__() arguments and fields. 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()
>>> print(flower.total_area())
18.13

13.5.4. Examples

$ pip install atlassian-python-api
>>> # doctest: +SKIP
... from atlassian import Jira
>>>
>>> # doctest: +SKIP
... jira = Jira(
...     url='http://example.com:8080',
...     username='myusername',
...     password='mypassword')
>>>
>>> JQL = 'project = DEMO AND status IN ("To Do", "In Progress") ORDER BY issuekey'
>>>
>>> # doctest: +SKIP
... result = jira.jql(JQL)
>>> # doctest: +SKIP
... print(result)
>>> # doctest: +SKIP
... from atlassian import Confluence
>>>
>>> # doctest: +SKIP
... confluence = Confluence(
...     url='http://example.com:8090',
...     username='myusername',
...     password='mypassword')
>>>
>>> # doctest: +SKIP
... result = confluence.create_page(
...     space='DEMO',
...     title='This is the title',
...     body='This is the body. You can use <strong>HTML tags</strong>!')
>>>
>>> # doctest: +SKIP
... print(result)
>>> 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):
...         print(f'Point(x={self.x}, y={self.y}, z={self.z})')
>>>
>>>
>>> point = Point(x=1, y=2, z=3)
>>>
>>> print(point.x)
1
>>> print(point.y)
2
>>> print(point.z)
3
>>>
>>> point.get_coordinates()
(1, 2, 3)
>>>
>>> point.show()
Point(x=1, y=2, z=3)

13.5.5. Assignments

Code 13.9. Solution
"""
* Assignment: OOP Method Sequence
* Required: yes
* Complexity: easy
* Lines of code: 9 lines
* Time: 8 min

English:
    1. Create class `Iris` with `features: list[float]` and `label: str` attributes
    2. For each row in `DATA` create `Iris` instance with row values
    3. Set class attributes at the initialization from positional arguments
    4. Create method which sums values of all `features`
    5. In `result` gather species and sum of each row
    6. Run doctests - all must succeed

Polish:
    1. Stwórz klasę `Iris` z atrybutami `features: list[float]` i `label: str`
    2. Dla każdego wiersza w `DATA` twórz instancję `Iris` z danymi z wiersza
    3. Ustaw atrybuty klasy przy inicjalizacji z argumentów pozycyjnych
    4. Stwórz metodę sumującą wartości wszystkich `features`
    5. W `result` zbieraj nazwę gatunku i sumę z każdego wiersza
    6. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    {'setosa': 9.4,
     'versicolor': 16.299999999999997,
     'virginica': 19.3}
"""


DATA = [
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
    (7.0, 3.2, 4.7, 1.4, 'versicolor'),
    (7.6, 3.0, 6.6, 2.1, 'virginica'),
]

result = {}


Code 13.10. Solution
"""
* Assignment: OOP Method Nested
* Required: yes
* Complexity: medium
* Lines of code: 17 lines
* Time: 13 min

English:
    1. Define class `Iris`
    2. `Iris` has:
        a. "Sepal length" type `float`
        b. "Sepal width" type `float`
        c. "Petal length" type `float`
        d. "Petal width" type `float`
        e. "Species" type `str`
    3. `Iris` can:
        a. Return number of `float` type attributes
        b. Return list of all `float` type attributes
        c. Return sum of values of all `float` type attributes
        d. Return mean of all `float` type attributes
    4. Use `vars(self)` iteration to return values of numeric fields
    5. Create `setosa` object with attributes set at the initialization
    6. Create `virginica` object with attributes set at the initialization
    7. Method `.show()` returns sum, mean and species name, example:
       a. 'total=10.20 mean=2.55 setosa'
       b. 'total=15.50 mean=3.88 virginica'
    8. Do not use `@dataclass`
    9. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Iris`
    2. `Iris` ma:
        a. "Sepal length" typu `float`
        b. "Sepal width" typu `float`
        c. "Petal length" typu `float`
        d. "Petal width" typu `float`
        e. "Species" typu `str`
    3. `Iris` może:
        a. Zwrócić liczbę pól typu `float`
        b. Zwrócić listę wartości wszystkich pól typu `float`
        c. Zwrócić sumę wartości pól typu `float`
        d. Zwrócić średnią arytmetyczną wartość pól typu `float`
    4. Użyj iterowania po `vars(self)` do zwrócenia wartości pól numerycznych
    7. Method `.show()` returns sumę, średnią oraz nazwę gatunku, przykład:
       a. 'total=10.20 mean=2.55 setosa'
       b. 'total=15.50 mean=3.88 virginica'
    8. Nie używaj `@dataclass`
    9. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `type(value) is float`
    * `vars(self).values()`
    * `{total=:.2f}`
    * `{mean=:.2f}`

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

    >>> setosa = Iris(5.1, 3.5, 1.4, 0.2, 'setosa')
    >>> virginica = Iris(5.8, 2.7, 5.1, 1.9, 'virginica')

    >>> setosa.show()
    'total=10.20 mean=2.55 setosa'
    >>> virginica.show()
    'total=15.50 mean=3.88 virginica'
"""


class Iris:
    def __init__(self, sepal_length, sepal_width, petal_length, petal_width, species):
        self.sepal_length = sepal_length
        self.sepal_width = sepal_width
        self.petal_length = petal_length
        self.petal_width = petal_width
        self.species = species

    def get_numeric_values(self):
        return [x for x in vars(self).values() if type(x) is float]