3.2. OOP Access Modifiers

3.2.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

3.2.2. Example

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

3.2.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

3.2.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

3.2.5. Protected Attribute

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

  • IDE should warn: "Access to a protected member _firstname of a class"

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     _firstname: str
...     _lastname: str
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> vars(astro)
{'_firstname': 'Mark', '_lastname': 'Watney'}
>>>
>>> print(astro._firstname)       # IDE should warn: "Access to a protected member _firstname of a class"
Mark
>>>
>>> print(astro._lastname)        # IDE should warn: "Access to a protected member _lastname of a class"
Watney

3.2.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'

3.2.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'}

3.2.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'}

3.2.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')
>>>
>>> print(dir(astro))  
['__annotations__', '__class__', '__dataclass_fields__', '__dataclass_params__', '__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',
 '_get_fullname', '_lastname', 'get_publicname']
>>>
>>> public_methods = [method
...                   for method in dir(astro)
...                   if not method.startswith('_')]
>>>
>>> print(public_methods)
['get_publicname']

3.2.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'

3.2.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

3.2.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']
>>>
>>> public_methods = [method
...                   for method in dir(astro)
...                   if not method.startswith('_')]
>>>
>>> print(public_methods)
['get_publicname']

3.2.13. References

1

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

3.2.14. Assignments

Code 3.34. Solution
"""
* Assignment: OOP AccessModifiers Protected
* Complexity: easy
* Lines of code: 7 lines
* Time: 8 min

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

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

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

    >>> isclass(Iris)
    True
    >>> iris = Iris(5.8, 2.7, 5.1, 1.9, 'virginica')

    >>> assert hasattr(iris, '_sepal_length'), \
    'Iris class instance should have protected _sepal_length attribute'

    >>> assert hasattr(iris, '_sepal_width'), \
    'Iris class instance should have protected _sepal_width attribute'

    >>> assert hasattr(iris, '_petal_length'), \
    'Iris class instance should have protected _petal_length attribute'

    >>> assert hasattr(iris, '_petal_width'), \
    'Iris class instance should have protected _petal_width attribute'

    >>> assert hasattr(iris, 'species'), \
    'Iris class instance should have public species attribute'

    >>> assert hasattr(Iris, '__annotations__'), \
    'Iris class should have type annotations'

    >>> Iris.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'_sepal_length': <class 'float'>,
     '_sepal_width': <class 'float'>,
     '_petal_length': <class 'float'>,
     '_petal_width': <class 'float'>,
     'species': <class 'str'>}

    >>> DATA = [Iris(5.8, 2.7, 5.1, 1.9, 'virginica'),
    ...         Iris(5.1, 3.5, 1.4, 0.2, 'setosa'),
    ...         Iris(5.7, 2.8, 4.1, 1.3, 'versicolor')]
    >>>
    >>> result = [{attribute: value}
    ...           for row in DATA
    ...           for attribute, value in row.__dict__.items()
    ...           if not attribute.startswith('_')]
    >>>
    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [{'species': 'virginica'},
     {'species': 'setosa'},
     {'species': 'versicolor'}]
"""

class Iris:
    pass


Code 3.35. Solution
"""
* Assignment: OOP AccessModifiers 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 `*features` notation
    6. Run doctests - all must succeed

Polish:
    1. Stwórz `result: list[Iris]`
    2. Iterując po `DATA` pomiń nagłówek
    3. Odseparuj `features` od `species` w każdym wierszu
    4. Dodaj do `result`:
        a. jeżeli `species` to "setosa" to dodaj instancję klasy `Setosa`
        b. jeżeli `species` to "versicolor" to dodaj instancję klasy `Versicolor`
        c. jeżeli `species` to "virginica" to dodaj instancję klasy `Virginica`
    5. Instancje inicjalizuj danymi z `features` używając notacji `*features`
    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