5.5. OOP Access Modifiers

5.5.1. Rationale

  • Attributes and methods are always public

  • No protected and private keywords

  • Protecting is only by convention 1

Attributes:

  • name - public attribute

  • _name - protected attribute (non-public by convention)

  • __name - private attribute (name mangling)

  • __name__ - system attribute

  • name_ - avoid name collision

Methods:

  • name(self) - public method

  • _name(self) - protected method (non-public by convention)

  • __name(self) - private method (name mangling)

  • __name__(self) - system method

  • name_(self) - avoid name collision

5.5.2. Example

>>> class Public:
...     firstname: str
...     lastname: str
>>>
>>> class Protected:
...     _firstname: str
...     _lastname: str
>>>
>>> class Private:
...     __firstname: str
...     __lastname: str

5.5.3. DataClasses

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Public:
...     firstname: str
...     lastname: str
>>>
>>>
>>> @dataclass
... class Protected:
...     _firstname: str
...     _lastname: str
>>>
>>>
>>> @dataclass
... class Private:
...     __firstname: str
...     __lastname: str

5.5.4. Public Attribute

  • name - public attribute

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> vars(astro)
{'firstname': 'Mark', 'lastname': 'Watney'}
>>>
>>> print(astro.firstname)
Mark
>>>
>>> print(astro.lastname)
Watney

5.5.5. Protected Attribute

  • _name - protected attribute (non-public by convention)

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     _firstname: str
...     _lastname: str
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')

To list all the attributes once again we can use vars():

>>> vars(astro)
{'_firstname': 'Mark', '_lastname': 'Watney'}

Python will allow the following statement, however your IDE should warn you "Access to a protected member _firstname of a class":

>>> print(astro._firstname)
Mark
>>>
>>> print(astro._lastname)
Watney

5.5.6. Private Attribute

  • __name - private attribute (name mangling)

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     __firstname: str
...     __lastname: str
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> vars(astro)
{'_Astronaut__firstname': 'Mark', '_Astronaut__lastname': 'Watney'}
>>>
>>> print(astro._Astronaut__firstname)
Mark
>>>
>>> print(astro._Astronaut__lastname)
Watney
>>>
>>> print(astro.__firstname)
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute '__firstname'
>>>
>>> print(astro.__lastname)
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute '__lastname'

5.5.7. Show Attributes

  • vars() display obj.__dict__

>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         self._firstname = firstname
...         self._lastname = lastname
...         self.publicname = f'{firstname} {lastname[0]}.'
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> vars(astro)
{'_firstname': 'Mark', '_lastname': 'Watney', 'publicname': 'Mark W.'}
>>>
>>> public_attributes = {attribute: value
...                      for attribute, value in vars(astro).items()
...                      if not attribute.startswith('_')}
>>>
>>> protected_attributes = {attribute: value
...                         for attribute, value in vars(astro).items()
...                         if attribute.startswith('_')}
>>>
>>>
>>> print(public_attributes)
{'publicname': 'Mark W.'}
>>>
>>> print(protected_attributes)
{'_firstname': 'Mark', '_lastname': 'Watney'}

5.5.8. System Attributes

  • __name__ - Current module

  • obj.__class__

  • obj.__dict__ - Getting dynamic fields and values

  • obj.__doc__ - Docstring

  • obj.__annotations__ - Type annotations of an object

  • obj.__module__

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> vars(astro)
{'firstname': 'Mark', 'lastname': 'Watney'}
>>>
>>> print(astro.__dict__)
{'firstname': 'Mark', 'lastname': 'Watney'}

5.5.9. Protected Method

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     _firstname: str
...     _lastname: str
...
...     def _get_fullname(self):
...         return f'{self._firstname} {self._lastname}'
...
...     def get_publicname(self):
...         return f'{self._firstname} {self._lastname[0]}.'
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>> astro._get_fullname()  # IDE should warn: "Access to a protected member _get_fullname of a class"
'Mark Watney'

5.5.10. Private Method

>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         self._firstname = firstname
...         self._lastname = lastname
...
...     def __get_fullname(self):
...         return f'{self._firstname} {self._lastname}'
...
...     def get_publicname(self):
...         return f'{self._firstname} {self._lastname[0]}.'
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> astro.__get_fullname()
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute '__get_fullname'
>>>
>>> astro._Astronaut__get_fullname()  # IDE should warn: "Access to a protected member _Astronaut__get_fullname of a class"
'Mark Watney'

5.5.11. System Method

>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         self._firstname = firstname
...         self._lastname = lastname
...
...     def __str__(self):
...         return 'stringification'
...
...     def __repr__(self):
...         return 'representation'
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(str(astro))
stringification
>>>
>>> print(repr(astro))
representation

5.5.12. Show Methods

  • dir()

>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         self._firstname = firstname
...         self._lastname = lastname
...
...     def __get_fullname(self):
...         return f'{self._firstname} {self._lastname}'
...
...     def get_publicname(self):
...         return f'{self._firstname} {self._lastname[0]}.'
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(dir(astro))  
['_Astronaut__get_fullname', '__class__', '__delattr__', '__dict__',
 '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
 '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
 '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
 '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
 '__weakref__', '_firstname', '_lastname', 'get_publicname']
>>>
>>> [method for method in dir(astro) if callable(getattr(astro, method))]  
['_Astronaut__get_fullname', '__class__', '__delattr__', '__dir__', '__eq__',
 '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
 '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__',
 '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
 '__subclasshook__', 'get_publicname']
>>>
>>> public_methods = [method
...                   for method in dir(astro)
...                   if callable(getattr(astro, method))
...                   and not method.startswith('_')]
>>>
>>> protected_methods = [method
...                      for method in dir(astro)
...                      if callable(getattr(astro, method))
...                      and method.startswith('_')]
>>>
>>> private_methods = [method
...                    for method in dir(astro)
...                    if callable(getattr(astro, method))
...                    and method.startswith(f'_{astro.__class__.__name__}')]

5.5.13. References

1

https://docs.python.org/3/tutorial/classes.html#private-variables

5.5.14. Assignments

Code 5.13. Solution
"""
* Assignment: OOP Access Dataclass
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min

English:
    1. Modify dataclass `Iris` to add attributes:
        a. Protected attributes: `sepal_length, sepal_width`
        b. Private attributes: `petal_length, petal_width`
        c. Public attribute: `species`
    2. Run doctests - all must succeed

Polish:
    1. Zmodyfikuj dataclass `Iris` aby dodać atrybuty:
        a. Chronione atrybuty: `sepal_length, sepal_width`
        b. Private attributes: `petal_length, petal_width`
        c. Publiczne atrybuty: `species`
    2. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Iris)
    >>> assert hasattr(Iris, '__annotations__')

    >>> assert '_sepal_width' in Iris.__dataclass_fields__
    >>> assert '_sepal_length' in Iris.__dataclass_fields__
    >>> assert '_Iris__petal_width' in Iris.__dataclass_fields__
    >>> assert '_Iris__petal_length' in Iris.__dataclass_fields__
    >>> assert 'species' in Iris.__dataclass_fields__
"""
from dataclasses import dataclass


@dataclass
class Iris:
    pass


Code 5.14. Solution
"""
* Assignment: OOP Access Init
* Complexity: easy
* Lines of code: 6 lines
* Time: 5 min

English:
    1. Modify class `Iris` to add attributes:
        a. Protected attributes: `sepal_length, sepal_width`
        b. Private attributes: `petal_length, petal_width`
        c. Public attribute: `species`
    2. Do not use `dataclass`
    3. Run doctests - all must succeed

Polish:
    1. Zmodyfikuj klasę `Iris` aby dodać atrybuty:
        a. Chronione atrybuty: `sepal_length, sepal_width`
        b. Private attributes: `petal_length, petal_width`
        c. Publiczne atrybuty: `species`
    2. Nie używaj `dataclass`
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Iris)

    >>> result = Iris(5.1, 3.5, 1.4, 0.2, 'setosa')
    >>> assert hasattr(result, '_sepal_width')
    >>> assert hasattr(result, '_sepal_length')
    >>> assert hasattr(result, '_Iris__petal_width')
    >>> assert hasattr(result, '_Iris__petal_length')
    >>> assert hasattr(result, 'species')
"""


class Iris:
    pass


Code 5.15. Solution
"""
* Assignment: OOP Access Members
* Complexity: easy
* Lines of code: 3 lines
* Time: 8 min

English:
    1. Define `result: list[dict]`
    2. Define class `Iris` with attributes
    3. Protected attributes: `sepal_length`, `sepal_width`, `petal_length`, `petal_width`
    4. Public attribute: `species`
    5. Iterate over `DATA` and add all public attributes to `result`
    6. Run doctests - all must succeed

Polish:
    1. Zdefiniuj `result: list[dict]`
    2.  klasę `Iris`
    3. Chronione atrybuty: `sepal_length`, `sepal_width`, `petal_length`, `petal_width`
    4. Publiczne atrybuty: `species`
    5. Iteruj po `DATA` i dodaj wszystkie publiczne atrybuty do `result`
    6. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert type(public) is dict
    >>> assert all(type(k) is str for k,v in public.items())
    >>> assert all(type(v) is str for k,v in public.items())

    >>> assert type(protected) is dict
    >>> assert all(type(k) is str for k,v in protected.items())
    >>> assert all(type(v) is float for k,v in protected.items())

    >>> assert type(private) is dict
    >>> assert all(type(k) is str for k,v in private.items())
    >>> assert all(type(v) is float for k,v in private.items())

    >>> assert len(public) > 0, \
    'public: list[dict] must not be empty'

    >>> assert len(protected) > 0, \
    'protected: list[dict] must not be empty'

    >>> assert len(private) > 0, \
    'private: list[dict] must not be empty'

    >>> public
    {'species': 'virginica'}

    >>> protected
    {'_sepal_width': 5.8, '_sepal_length': 2.7}

    >>> private
    {'_Iris__petal_width': 5.1, '_Iris__petal_length': 1.9}

"""
from dataclasses import dataclass


@dataclass
class Iris:
    _sepal_width: float
    _sepal_length: float
    __petal_width: float
    __petal_length: float
    species: str


DATA = Iris(5.8, 2.7, 5.1, 1.9, 'virginica')


# dict[str,float|str]: all public attributes and their values
public = ...

# dict[str,float|str]: all protected attributes and their values
protected = ...

# dict[str,float|str]: all private attributes and their values
private = ...

Code 5.16. Solution
"""
* Assignment: OOP Access Dict
* Complexity: medium
* Lines of code: 8 lines
* Time: 8 min

English:
    1. Create `result: list[Iris]`
    2. Iterate over `DATA` skipping header
    3. Separate `features` from `species` in each row
    4. Append to `result`:
        a. if `species` is "setosa" append instance of a class `Setosa`
        b. if `species` is "versicolor" append instance of a class `Versicolor`
        c. if `species` is "virginica" append instance of a class `Virginica`
    5. Initialize instances with `features` using `*args` notation
    6. Run doctests - all must succeed

Polish:
    1. Stwórz `result: list[Iris]`
    2. Iterując po `DATA` pomijając header
    3. Odseparuj `features` od `species` w każdym wierszu
    4. Dodaj do `result`:
        a. jeżeli `species` jest "setosa" to dodaj instancję klasy `Setosa`
        b. jeżeli `species` jest "versicolor" to dodaj instancję klasy `Versicolor`
        c. jeżeli `species` jest "virginica" to dodaj instancję klasy `Virginica`
    5. Instancje inicjalizuj danymi z `features` używając notacji `*args`
    6. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `globals()[classname]`

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

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [Virginica(5.8, 2.7, 5.1, 1.9),
     Setosa(5.1, 3.5, 1.4, 0.2),
     Versicolor(5.7, 2.8, 4.1, 1.3),
     Virginica(6.3, 2.9, 5.6, 1.8),
     Versicolor(6.4, 3.2, 4.5, 1.5),
     Setosa(4.7, 3.2, 1.3, 0.2)]
"""

from dataclasses import dataclass


DATA = [('Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'),
        (5.8, 2.7, 5.1, 1.9, 'virginica'),
        (5.1, 3.5, 1.4, 0.2, 'setosa'),
        (5.7, 2.8, 4.1, 1.3, 'versicolor'),
        (6.3, 2.9, 5.6, 1.8, 'virginica'),
        (6.4, 3.2, 4.5, 1.5, 'versicolor'),
        (4.7, 3.2, 1.3, 0.2, 'setosa')]


@dataclass(repr=False)
class Iris:
    _sepal_length: float
    _sepal_width: float
    _petal_length: float
    _petal_width: float

    def __repr__(self):
        name = self.__class__.__name__
        args = tuple(self.__dict__.values())
        return f'{name}{args}'


class Setosa(Iris):
    pass


class Versicolor(Iris):
    pass


class Virginica(Iris):
    pass


result: list = []