Python: Objects

All data in a Python program are objects. An object has: identity, type, and value. Type is also object.

def compare(a, b):
    # compare identities or memory locations
    if a is b:
        print("same object")
    if a == b:
        print("same value")
    if type(a) is type(b):
        print("same type")

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]

>>> compare(a, a)
same object
same value
same type

>>> compare(a, b)
same value
same type

>>> compare(a, [4, 5, 6])
same type

Data and methods are an object’s attributes. Even an operator like a + 10 is mapped to a method a.__add__(10) and thus is an attribute.

Assigning object creates reference. Shallow copy of a container object creates a new container object but the elements are references to the original container object.

>>> a = [1, 2, [3, 4]]
>>> b = list(a) # shallow copy
>>> a
[1, 2, [3, 4]]
>>> b
[1, 2, [3, 4]]
>>> b is a
False
>>> b[2][1] = -1000
>>> b
[1, 2, [3, -1000]]
>>> a
[1, 2, [3, -1000]]

Deep copy of a container object creates a new container and recursively copies the elements as well.

>>> from copy import deepcopy
>>> a = [1, 2, [3, 4]]
>>> b = deepcopy(a)
>>> a
[1, 2, [3, 4]]
>>> b
[1, 2, [3, 4]]
>>> b is a
False
>>> b[2][1] = 1000
>>> b
[1, 2, [3, 1000]]
>>> a
[1, 2, [3, 4]]

Copying object can be slow and it does not work with system or runtime states like files, network connections, threads, generators.

Best practice:

  • DO minimize copying.

None is a special object with no attributes and it evaluates to False in boolean expressions. Internally, None is stored as a singleton—there is only one None object in the interpreter. Pythonic way to check: optional_object is None.

Inheritance

Python supports multiple inheritance. object is the default base class that provides defaults for __str__() or __repr__(). Python lacks class-level scope, so each instance method needs self as a parameter and accessing attributes within instance method needs self.attribute.

Pitfalls:

  1. Redefinition of __init__() in a child class requires super().__init__().
  2. Avoid hard-coding class name in __repr__() othewise child classes may give misleading information.
  3. Inheriting from built-in classes like dict may surprise. Because, dict.update() manipulates the dictionary data directly in C, bypassing __setitem__().
class Account:
    ...
    def __repr__(self):
        return f"{type(self).__name__}({self.owner!r}, {self.balance!r})"

Best practice:

  • DO prefer composition over inheritance
  • DO prefer functions over single method class.
  • DO use collection module’s UserDict, UserList, UserString to derive from dict, list, str.

Quickest way to create a stack class would be to inherit from list.

class Stack(list):
  def push(self, item):
    self.append(item)
s = Stack()
s.push(1)
s.push(2)
s.push(3)
print(s.pop())
print(s.pop())

Since it is implementation inheritance, the Stack class now also has methods like sort. We got more than we asked for. With composition we can have same effect, more cleanly.

class Stack:
  def __init__(self):
      self._items = []
  def push(self, item):
    self._items.append(item)
  def pop(self):
    return self._items.pop()
  def __len__(self):
    return len(self._items)
# Users won't notice if we replace list with chained tuples
class Stack:  
  def __init__(self):
    self._items = None
    self._size = 0
  def push(self, item):
    self._items = (item, self._items)
    self._size += 1
  def pop(self):
    if self._items is None:
      return
    item, self._items = self._items
    self._size -= 1
    return item
  def __len__(self):
    return self._size
class Stack:
  def __init__(self, *, container=None):
    if container is None:
      self._items = [] 
    else: 
      self._items = container
  def push(self, item):
    self._items.append(item)
  def pop(self):
    return self._items.pop()
  def __len__(self):
    return len(self._items)
list_stack = Stack()
list_stack.push(1)
list_stack.push(2)
list_stack.push(3)
print(list_stack.pop())
print(list_stack.pop())

from array import array
array_stack = Stack(container=array('i'))
array_stack.push(1)
array_stack.push(2)
array_stack.push(3)
print(array_stack.pop())
print(array_stack.pop())

Class variable can be accessed via instances, because attribute lookup falls back to class similar to how methods (which aren’t direct instance attributes) are found.

class Account:
  num_accounts = 0
  def __init__(self, owner, balance):
    self.owner = owner
    self.balance = balance
    Account.num_accounts += 1
a = Account("John", 100)
b = Account("Jen", 200)
print(a.num_accounts) # 2
print(b.num_accounts) # 2
print(Account.num_accounts) # 2

Class methods and variables are used to provide alternate construction of instances.

import time
class Date:
  datefmt = "{year}-{month:02d}-{day:02d}"
  def __init__(self, year, month, day):
    self.year = year
    self.month = month
    self.day = day
  def __str__(self): 
    return self.datefmt.format(year=self.year, month=self.month, day=self.day)
  @classmethod
  def from_timestamp(cls, ts):
    tm = time.localtime(ts)
    return cls(tm.tm_year, tm.tm_mon, tm.tm_mday)
  @classmethod
  def today(cls):
    return cls.from_timestamp(time.time())
class MDYDate(Date):
  datefmt = "{month}/{day}/{year}"
class DMYDate(Date):
  datefmt = "{day}/{month}/{year}"
a = Date(1983, 1, 1)
print(a)
b = MDYDate(1983, 12, 1)
print(b)
c = DMYDate(1983, 12, 1)
print(c)
d = DMYDate.today()
print(d)
print(a.today()) # today() does not make sense for instance of Date but classmethod is accessible from instances, so we endup having it

A @staticmethod is a function that happens to be in a class, useful for grouping functions in a supporting class.

@property allows for the interception of get, set, del of an attribute. It can be used validate attribute type and value.

import string
class Account:
  def __init__(self, owner, balance) -> None:
    self.owner = owner
    self._balance = balance
  @property
  def owner(self):
    return self._owner
  @owner.setter
  def owner(self, value):
    if not isinstance(value, str):
      raise TypeError("Expected str")
    if not all(c in string.ascii_uppercase for c in value):
      raise ValueError("Must be uppercase ASCII")
    if len(value) > 10:
      raise ValueError("Must be 10 characters or less")
    self._owner = value

To enforce contract on subclasses, use abstract base classes (ABC) and @abstractmethod from the module abc.

from abc import ABC, abstractmethod

class Stream(ABC):
  @abstractmethod
  def send(self):
    pass
  @abstractmethod
  def receive(self):
    pass
  @abstractmethod
  def close(self):
    pass

Leave a comment