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:
- Redefinition of
__init__()in a child class requiressuper().__init__(). - Avoid hard-coding class name in
__repr__()othewise child classes may give misleading information. - Inheriting from built-in classes like
dictmay surprise. Because,dict.update()manipulates the dictionary data directly inC, 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
collectionmodule’sUserDict,UserList,UserStringto derive fromdict,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