Skip to the content.

Code 401 Class 09 Reading Notes

Dunder Methods

What are Dunder Methods?

Special methods are a set of predefined methods you can use to enrich your classes.

Dunder methods let you emulate the behavior of built-in types. But an empty class definition doesn’t support this behavior out of the box:

Example:

class LenSupport:
    def __len__(self):
        return 42

>>> obj = LenSupport()
>>> len(obj)
42

You can implement a __getitem__ method which allows you to use Python’s list slicing syntax: obj[start:stop].

Special Methods and the Python Data Model

Object Initialization: __init__

class Account:
    """A simple account class"""

    def __init__(self, owner, amount=0):
        """
        This is the constructor that lets us create
        objects from this class
        """
        self.owner = owner
        self.amount = amount
        self._transactions = []

(Matt Note: How does the below take in the above?)

>>> acc = Account('bob')  # default amount = 0
>>> acc = Account('bob', 10)

Object Representation: __str__, __repr__

  1. __repr__: The “official” string representation of an object. This is how you would makean object of the class. The goal of __repr__ is to be unambiguous.
  2. __str__: The “informal” or nicely printable string representation of an object. This is for the enduser.

Example:

class Account:
    # ... (see above)

    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)

    def __str__(self):
        return 'Account of {} with starting amount: {}'.format(
            self.owner, self.amount)

If you wanted to implement just one of these to-string methods on a Python class, make sure it’s __repr__.

Now we can query the object in various ways and always get a nice string representation:

>>> str(acc)
'Account of bob with starting amount: 10'

>>> print(acc)
"Account of bob with starting amount: 10"

>>> repr(acc)
"Account('bob', 10)"

Iteration: __len__,__getitem__, __reversed__

Defining a simple method to add transactions.

def add_transaction(self, amount):
    if not isinstance(amount, int):
        raise ValueError('please use int for amount')
    self._transactions.append(amount)

The below defines a property to calculate the balance on the account so we can conveniently access it with account.balance. This method takes the start amount and adds a sum of all the transactions.

@property
def balance(self):
    return self.amount + sum(self._transactions)

Some deposits and withdrawals on the account.

>>> acc = Account('bob', 10)

>>> acc.add_transaction(20)
>>> acc.add_transaction(-10)
>>> acc.add_transaction(50)
>>> acc.add_transaction(-20)
>>> acc.add_transaction(30)

>>> acc.balance
80

By implementing dunder methods, the below code makes the class iterable.

class Account:
    # ... (see above)

    def __len__(self):
        return len(self._transactions)

    def __getitem__(self, position):
        return self._transactions[position]

>>> len(acc)
5

>>> for t in acc:
...    print(t)
20
-10
50
-20
30

>>> acc[1]
-10

To reverse list of transaction, we can use Pythons reverse list slice syntax. We had to wrapp the result of reversed(acc) in a list()call because reversed() returns a reverse iterator, not a list object we can print nicely in the REPL.

Operator Overloading for Comparing Accounts

Using functools.total_ordering decorator which allows me to take a shortcut, only implementing __eq__ and __lt__:

from functools import total_ordering

@total_ordering
class Account:
    # ... (see above)

    def __eq__(self, other):
        return self.balance == other.balance

    def __lt__(self, other):
        return self.balance < other.balance

This allows us to compare Account instances no problem:

>>> acc2 > acc
True

>>> acc2 < acc
False

>>> acc == acc2
False

Operator Overloading for Merging Accounts

__add__

Below we will implement __add__ to be able to merge two accounts. The expected behavior would be to merge all attributes together: the owner name, as well as starting amounts and transactions.

def __add__(self, other):
    owner = '{}&{}'.format(self.owner, other.owner)
    start_amount = self.amount + other.amount
    acc = Account(owner, start_amount)
    for t in list(self) + list(other):
        acc.add_transaction(t)
    return acc

The above is more involved than the other dunder implementations. But it shows you can implement addition however you please.

Below is a more realistic use though.

def __add__(self, other):
    owner = self.owner + other.owner
    start_amount = self.balance + other.balance
    return Account(owner, start_amount)


>>> acc3 = acc2 + acc
>>> acc3
Account('tim&bob', 110)

>>> acc3.amount
110
>>> acc3.balance
240
>>> acc3._transactions
[20, 40, 20, -10, 50, -20, 30]

Callable Python Objects: __call__

You can make an object callable like a regular function by adding the __call__ dunder method. Below you will see printing a nice report of all the transactions that make up its balance:

class Account:
    # ... (see above)

    def __call__(self):
        print('Start amount: {}'.format(self.amount))
        print('Transactions: ')
        for transaction in self:
            print(transaction)
        print('\nBalance: {}'.format(self.balance))

Now when Iwe call the object with the double-parantheses acc() syntax, I get a nice account statement with an overview of all transactions and the current balance:

>>> acc = Account('bob', 10)
>>> acc.add_transaction(20)
>>> acc.add_transaction(-10)
>>> acc.add_transaction(50)
>>> acc.add_transaction(-20)
>>> acc.add_transaction(30)

>>> acc()
Start amount: 10
Transactions:
20
-10
50
-20
30
Balance: 80

An explicit method to the class should be added to make the output more transparent Account.print_statement() method.

Intro to Statistics

Data Science uses statistics to be used for identifying risk factors for a disease, customize spam detection, and establish the relationship between variables.

Boxplots help visualualize the median third quartile, first quartile and mean.

define it between 0 to 1, 0 = least likely to occur, 1 to most likely to occur.

Poisson Distribution is the distributions of intensity.

Frequentists statistics have fixed parameters, data varies, probability P(D/O), confidence interval, and no prior data.

Bayesian Statistcs have varied parameters, data is fixed, Likelihood P(O/D), Credible interval, strength of prior.

Matt note, what is P(D/O) and P(D/O)

Statistical Features like Bias, kVariance, and many other help us explore a dataset to gain valuable insights.

Probability distributions define the percent chance that some event will occur, and we can use them to understand the spread of data.

Bayesian statistics expresses probability as a degree of belief in an event which can change as a degree of belief in an event which can change as new information is gathered rather than a fixed value based on frequency.

Things I want to know more about

Statistics with Data Science. Playing with dunders with step by step implementations.

<—BACK