1
\$\begingroup\$

I'm a newbie python developer, and fortunately I have been introduced to the principle of abstract base classes and they seemed pretty interesting to tackle while going forward in my Python journey.

I've had multifarious tries in order to implement an abstract base class correctly so the child class also won't be left empty, but I am really confused in the main idea of it.

Should an abstract class even contain an __init__ method?

I'd be glad if someone would be able to point me in the right direction.

base_tokenizer_class.py

#!/usr/bin/env python3
"""
The tokenizer class base module
"""


from abc import ABC, abstractmethod


class InstanceError(BaseException):
    def __init__(self, error_message: str):
        """
        Inappropriate instanciation, ETC.
        """

        self.error_message = str(error_message)
        super().__init__(self.error_message)


class BaseTokenizer(ABC):
    """The base class for all tokenizing classes."""

    def __new__(cls, *args, **kwargs):
        instance_class_name = cls.__get_class_name(cls)

        if instance_class_name == "BaseTokenizer":
            raise InstanceError(
                "The BaseTokenizer class shall only be used for creating subclasses and cannot be instanciated directly."
            )

        return super().__new__(cls)

    def __init__(
        self,
        text: str,
        dilimeter: str = "-",
    ):
        self.default_text = text
        self.dilimeter = dilimeter

        self.token = "".join(
            [
                f"{character}{self.dilimeter}"
                if not (index + 1) == len(text)
                else f"{character}"
                for index, character in enumerate(self.default_text)
            ]
        )

    def get_formatted_token(self) -> str:
        return self.token
    
    @property
    def text(self):
        return self._second

    @text.setter
    def text_to_format(self, text):
        if not isinstance(text, str):
            argument_type = type(text).__name__
            raise ValueError(
                f"Expected argument type passed for the parameter (text_to_format): str | Not: {argument_type:}"
            )

        self._second = text

    @property
    def dilimeter(self):
        return self._dilimeter

    @dilimeter.setter
    def dilimeter(self, char):
        char_length = len(char)

        if not isinstance(char, str):
            argument_type = type(char).__name__
            raise ValueError(
                f"Expected argument type passed for the parameter (dilimeter): str | Not: {argument_type}"
            )

        if not char_length == 1:
            raise TypeError("The dilimeter must be a single-character string")

        self._dilimeter = char

    def __get_class_name(cls) -> str:
        """Returns the name of the class used for the instantiation."""

        instanciated_class_name = cls.__name__

        return instanciated_class_name

test_base_tokenizer.py

#!/usr/bin/env python3
"""
The unittest module for the BaseTokenizer class
"""


from base_tokenizer_class import BaseTokenizer, InstanceError
import unittest


class TestBaseTokenizer(unittest.TestCase):
    """
    The unit test class for the BaseTokenizer class.
    """

    def setUp(self) -> None:

        class ChildTokenizer(BaseTokenizer):
            ...

        self.ChildTokenizer = ChildTokenizer


    def test_instanciation(self):
        """Does the program raise InstanceError if the class is instantiated directly?
        """

        self.assertRaises(InstanceError, BaseTokenizer)

    def test_correct_formatting(self):
        """Does the program tokenize the default text correctly?
        """

        child_tokenizer_instance1 = self.ChildTokenizer('something!', dilimeter='-')
        child_tokenizer_instance2 = self.ChildTokenizer('foo', dilimeter='-')

        tokenized_text1 = child_tokenizer_instance1.get_formatted_token()
        tokenized_text2 = child_tokenizer_instance2.get_formatted_token()

        self.assertEqual(tokenized_text1, 's-o-m-e-t-h-i-n-g-!')
        self.assertEqual(tokenized_text2, 'f-o-o')

    def test_custom_dilimeter(self):
        """Does the program works with dilimiteres other than '-'?
        """

        child_tokenizer_instance1 = self.ChildTokenizer('foo', dilimeter='^')
        
        tokenized_text1 = child_tokenizer_instance1.get_formatted_token()

        self.assertEqual(tokenized_text1, 'f^o^o')


        


if __name__ == '__main__':
    unittest.main()
\$\endgroup\$
2
  • 2
    \$\begingroup\$ A pair of repeated spelling errors will distract reviewers -- please change "instanciation" to "instantiation", and change "dilimeter" to "delimiter". It is fine to make such an edit, since there's no Answers posted yet. Thx! \$\endgroup\$
    – J_H
    Nov 11, 2023 at 20:35
  • 1
    \$\begingroup\$ Also, "instanciated"⟶"instantiated". \$\endgroup\$ Nov 11, 2023 at 21:30

1 Answer 1

1
\$\begingroup\$

Should a abstract class even contains a init method?

In better languages, a typical pattern is to declare abstract constructors as protected so that children can delegate to them. In Python it isn't worth attempting to explicitly constrain abstract constructors to only be callable by children. But more importantly... what you've shown does not at all fit the idea of an abstract class. Presumably, the most important polymorphic behaviour in your class and its children is how the text is tokenized - but you already have a default implementation in the base. That means it isn't abstract; other languages call this virtual (which basically means "overrideable"). It's a plain-old parent class. If you really want an abstract class, then it should not provide any implementation for tokenization at all, should declare that method as abstract, and should delegate to a child. I demonstrate the first option where the default tokenizer is preserved in the parent and the abstract machinery is removed.

Otherwise:

Enable a spell checker.

Your join should remove the last-character acrobatics and simply self.delimiter.join().

Consider removing tokenization from the constructor and evaluating it as a lazy @property. The class should be immutable, which is easily achievable via NamedTuple.

get() and set() methods are not particularly Pythonic - especially thin ones that simply return or assign member variables. Delete the setters for text and delimiter (again, because the class should be immutable), and - whereas normally you should also delete get_formatted_token - the latter should be converted into the @property I mentioned to lazy-evaluate tokenization.

_second should not be assigned to an instance for the first time outside of a constructor.

Suggested

#!/usr/bin/env python3
"""
The tokenizer class base module
"""

import unittest
from typing import NamedTuple


class BaseTokenizer(NamedTuple):
    text: str
    delimiter: str

    @property
    def token(self) -> str:
        return self.delimiter.join(iter(self.text))


class TestBaseTokenizer(unittest.TestCase):
    """
    The unit test class for the BaseTokenizer class.
    """

    def setUp(self) -> None:
        class ChildTokenizer(BaseTokenizer):
            pass

        self.child_tokenizer = ChildTokenizer

    def test_correct_formatting(self):
        """Does the program tokenize the default text correctly?
        """
        tokenizer_1 = self.child_tokenizer('something!', delimiter='-')
        self.assertEqual(tokenizer_1.token, 's-o-m-e-t-h-i-n-g-!')

        tokenizer_2 = self.child_tokenizer('foo', delimiter='-')
        self.assertEqual(tokenizer_2.token, 'f-o-o')

    def test_custom_delimiter(self):
        """Does the program work with delimiters other than '-'?
        """
        tokenizer = self.child_tokenizer('foo', delimiter='^')
        self.assertEqual(tokenizer.token, 'f^o^o')


if __name__ == '__main__':
    unittest.main()

Abstract Option

This uses the implicit-abstract style where any abstract methods raise NotImplementedError. A different flavour that uses ABC and dataclass(frozen=True) is also possible.

#!/usr/bin/env python3

import unittest
from typing import NamedTuple


class BaseTokenizer(NamedTuple):
    text: str
    delimiter: str

    @property
    def token(self) -> str:
        raise NotImplementedError()


class EveryCharTokenizer(BaseTokenizer):
    @property
    def token(self) -> str:
        return self.delimiter.join(iter(self.text))


class TestEveryCharTokenizer(unittest.TestCase):
    def test_correct_formatting(self):
        """Does the program tokenize the default text correctly?
        """
        tokenizer_1 = EveryCharTokenizer('something!', delimiter='-')
        self.assertEqual(tokenizer_1.token, 's-o-m-e-t-h-i-n-g-!')

        tokenizer_2 = EveryCharTokenizer('foo', delimiter='-')
        self.assertEqual(tokenizer_2.token, 'f-o-o')

    def test_custom_delimiter(self):
        """Does the program work with delimiters other than '-'?
        """
        tokenizer = EveryCharTokenizer('foo', delimiter='^')
        self.assertEqual(tokenizer.token, 'f^o^o')


if __name__ == '__main__':
    unittest.main()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.