5
\$\begingroup\$

I wrote the following Author class in Python for a project I'm working on. I wanted to make sure that the rich comparisons and the methods I used to validate the constructor parameters were done properly.

Author class:

class Author:

    def __init__(self, id, firstname, lastname):
        self._validateid(id, "ID")
        self._validate_author(firstname, "First Name")
        self._validate_author(lastname, "Last Name")

        self._id = id
        self._firstname = firstname
        self._lastname = lastname

    def _validateid(self, id, error_message):
        if id < 1:
            raise ValueError(error_message + " is invalid")

    def _validate_author(self, parameter, error_message):
        if not parameter:
            raise TypeError(error_message + " is missing")

    @property
    def iden(self):
        return self._id

    @property
    def first_name(self):
        return self._firstname

    @property
    def last_name(self):
        return self._lastname

Rich Comparisons:

    def __lt__(self, other):
        if isinstance(other, Author):
            return (self.iden, self.first_name.lower(), self.last_name.lower()) < (other.iden, other.first_name.lower(), other.last_name.lower())
        return NotImplemented

    def __le__(self, other):
        if isinstance(other, Author):
            return (self.iden, self.first_name.lower(), self.last_name.lower()) <= (other.iden, other.first_name.lower(), other.last_name.lower())
        return NotImplemented

    def __eq__(self, other):
        if isinstance(other, Author):
            return (self.iden, self.first_name.lower(), self.last_name.lower()) == (other.iden, other.first_name.lower(), other.last_name.lower())
        return NotImplemented

    def __ne__(self, other):
        if isinstance(other, Author):
            return (self.iden, self.first_name.lower(), self.last_name.lower()) != (other.iden, other.first_name.lower(), other.last_name.lower())
        return NotImplemented

    def __ge__(self, other):
        if isinstance(other, Author):
            return (self.iden, self.first_name.lower(), self.last_name.lower()) >= (other.iden, other.first_name.lower(), other.last_name.lower())
        return NotImplemented

    def __gt__(self, other):
        if isinstance(other, Author):
            return (self.iden, self.first_name.lower(), self.last_name.lower()) > (other.iden, other.first_name.lower(), other.last_name.lower())
        return NotImplemented

    def __hash__(self):
        return hash((self.iden, self.first_name.lower(), self.last_name.lower()))
\$\endgroup\$
  • \$\begingroup\$ Why do you want your authors to sort by id first and only then by name? Also, can two authors have the same id? If not, __hash__ could just return the id... \$\endgroup\$ – Graipher Oct 13 '17 at 8:08
  • \$\begingroup\$ @Graipher - What would be a better approach to sorting. My plan is to have them sorted by last name, like you see at a library \$\endgroup\$ – user150904 Oct 13 '17 at 14:17
  • \$\begingroup\$ Then compare tuples of (lastname, firstname, id), instead of (id, lastname, firstname), which will always sort by id first. \$\endgroup\$ – Graipher Oct 13 '17 at 14:19
  • \$\begingroup\$ @Graipher - Would it be a problem is I just used last name? \$\endgroup\$ – user150904 Oct 13 '17 at 14:20
  • 1
    \$\begingroup\$ Depends, what do you want to do if you have two authors with the same last name? Python's sort is stable, so they would sort in the order of being inserted into the list/whatever you are sorting. Most people would expect it to be sorted by last name first and then by first name. I just added the id there for the rare cases where some authors have the same first and last name. \$\endgroup\$ – Graipher Oct 13 '17 at 14:24
3
\$\begingroup\$
  • Personally using a namedtuple is the best way to strip most of your code.

    If you wish to be able to mutate your data, then inheriting from a list, rather than namedtuple, could be a better idea.

  • I'm not a fan of _validateid or _validate_author, just write them out in the constructor.

And so I'd personally change your code to:

from collection import namedtuple

class Author(namedtuple('AuthorBase', 'id first_name last_name')):
    def __new__(cls, id, first, last):
        if id < 1:
            raise ValueError('id is not a valid id')
        if not first:
            raise ValueError('first_name has not been set')
        if not last:
            raise ValueError('last_name has not been set')
        return super().__new__(cls, id, first, last)
\$\endgroup\$
  • \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$ – user150904 Oct 13 '17 at 14:40
  • \$\begingroup\$ I was the OP for this question, thanks for answering it. Quick question, when inheriting and overriding the __new__ method in this case, are you supposed to use return super() or return namedtuple? Explained here: python.org/download/releases/2.2/descrintro/#__new__ \$\endgroup\$ – user168710 Jun 20 '18 at 15:19
  • \$\begingroup\$ @Sveta That document is about Python 2.2 which is very old. In the current documentation it says using super is the common way \$\endgroup\$ – Peilonrayz Jun 20 '18 at 15:28
  • \$\begingroup\$ Inheriting from namedtuple is a common way to achieve immutability? \$\endgroup\$ – user168710 Jun 20 '18 at 15:31
  • 1
    \$\begingroup\$ @Sveta Depends on usage, here I'd use a namedtuple, other places I may not. \$\endgroup\$ – Peilonrayz Jun 20 '18 at 15:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy