Objects in Python
As I learn more about Pythons idioms reflect on its unique approach to object based programming. In combination with duck typing its approach to objects feels distrubingly flexible.
Everything in Python is an object.
Special methods (dunders)⌗
Filed under Special Method Names in the docs, defines the special traits a class can implement that are invoked by special syntax, such as arithmetic operations.
Python will raise an exception (AttributeError
or TypeError
), if a class fails to provide appropriate method/s.
Foundational⌗
You want | So you write | And Python calls |
---|---|---|
to initialise an instance | x = MyClass() |
x.__init__() |
representation string that can be eval() |
repr(x) |
x.__repr__() |
the “informal” value as a string | str(x) |
x.__str__() |
the “informal” value as a byte array | bytes(x) |
x.__bytes__() |
the value as a formatted string | format(x, format_spec) |
x.__format__(format_spec) |
Iterators⌗
You want | So you write | And Python calls |
---|---|---|
to iterate through a sequence | iter(seq) |
seq.__iter__() |
to get the next value from an iterator | next(seq) |
seq.__next__() |
to create an iterator in reverse order | reversed(seq) |
seq.__reversed__() |
for x in seq:
print(x)
Python will call seq.__iter__()
to create an iterator, then call the __next__()
method on that iterator to get each value of x. When the __next__()
method raises a StopIteration
exception, the for loop ends gracefully.
Compariable classes⌗
You want | So you write | And Python calls |
---|---|---|
equality | x == y |
x.__eq__(y) |
inequality | x != y |
x.__ne__(y) |
less than | x < y |
x.__lt__(y) |
less than or equal to | x <= y |
x.__le__(y) |
greater than | x > y |
x.__gt__(y) |
greater than or equal to | x >= y |
x.__ge__(y) |
truth value in a boolean context | if x: |
x.__bool__() |
If you define a __lt__()
method but no __gt__()
method, Python will use the __lt__()
method with operands swapped.
However, methods will not be combined. For example, if you define a __lt__()
method and a __eq__()
method and try to test whether x <= y
, Python will not call __lt__()
and __eq__()
in sequence. It will only call the __le__()
method.
Serializable classes⌗
With the pickle module, Python supports serializing and deserializing objects. All of the native datatypes support pickling. If you create a custom class that you want to be able to pickle, checkout the pickle protocol to see when and how the following special methods are called.
You want | So you write | And Python calls |
---|---|---|
a custom object copy | copy.copy(x) |
x.__copy__() |
a custom object deepcopy | copy.deepcopy(x) |
x.__deepcopy__() |
to get an object’s state before pickling | pickle.dump(x, file) |
x.__getstate__() |
to serialize an object | pickle.dump(x, file) |
x.__reduce__() |
to serialize an object (new pickling protocol) | pickle.dump(x, file, protocol_version) |
x.__reduce_ex__(protocol_version) |
control over how an object is created during unpickling | x = pickle.load(file) |
x.__getnewargs__() |
to restore an object’s state after unpickling | x = pickle.load(file) |
x.__setstate__() |
To recreate a serialized object, Python first needs to create a new object that looks like the serialized object, and then set the values of all the attributes on the new object. The __getnewargs__()
method controls how the object is created, then the __setstate__()
method controls how the attribute values are restored.
Classes with computed attributes⌗
You want | So you write | And Python calls |
---|---|---|
to get a computed attribute (unconditionally) | x.my_property |
x.__getattribute__('my_property') |
to get a computed attribute (fallback) | x.my_property |
x.__getattr__('my_property') |
to set an attribute | x.my_property = value |
x.__setattr__('my_property', value) |
to delete an attribute | del x.my_property |
x.__delattr__('my_property') |
to list all attributes and methods | dir(x) |
x.__dir__() |
Tips:
- If defined
__getattribute__()
will always be called for every reference to any attribute or method name (except for special dunders) __getattr__()
on the other hand will only be called only after looking for the attribute in the normal places__dir__()
is useful if you use either of the__getattr*__()
traits, asdir(x)
only lists regular attributes and methods, by overriding__dir__()
can register dynamic attributes to the list of available attributes.
Classes that are callable⌗
You want | So you write | And Python calls |
---|---|---|
to “call” an instance like a function | my_instance() |
my_instance.__call__() |
The zipfile
module takes this approach to define a class that can decrypt an encrypted zip file with a given password. The zip decryption algorithm requires you to store state during decryption. Defining the decryptor as a class allows you to maintain this state within a single instance of the decryptor class. The state is initialized in the __init__()
method and updated as the file is decrypted. But since the class is also callable like a normie function, you can pass the instance as the first argument of the map()
function.
Stateful functions if you will.
# excerpt from zipfile.py
class _ZipDecrypter:
def __init__(self, pwd):
self.key0 = 305419896
self.key1 = 591751049
self.key2 = 878082192
for p in pwd:
self._UpdateKeys(p)
def __call__(self, c):
assert isinstance(c, int)
k = self.key2 | 2
c = c ^ (((k * (k^1)) >> 8) & 255)
self._UpdateKeys(c)
return c
# sample usage
zd = _ZipDecrypter(pwd)
bytes = zef_file.read(12)
h = list(map(zd, bytes[0:12]))
Classes that act like sets⌗
If you have a class that is a container for values, it’s may make sense to enquire if it “contains” a certain value and leverage Python’s x in s
syntax.
You want | So you write | And Python calls |
---|---|---|
the number of items | len(s) |
s.__len__() |
to know whether it contains a specific value | x in s |
s.__contains__(x) |
Classes that act like dictionaries⌗
Going beyond just the “in” operator, classes can also act as full blown dictionaries:
You want | So you write | And Python calls |
---|---|---|
to get a value by its key | x[key] |
x.__getitem__(key) |
to set a value by its key | x[key] = value |
x.__setitem__(key, value) |
to delete a key-value pair | del x[key] |
x.__delitem__(key) |
to provide a default value for missing keys | x[nonexistent_key] |
x.__missing__(nonexistent_key) |
Classes that act like numbers⌗
The classical operator overload example, most languages including Python provide syntax for working with numeric types, adding +
, subtracting -
, modulo %
, bitwise XOR ^
and so on.
You want | So you write | And Python calls |
---|---|---|
addition | x + y |
x.__add__(y) |
subtraction | x - y |
x.__sub__(y) |
multiplication | x * y |
x.__mul__(y) |
division | x / y |
x.__truediv__(y) |
floor division | x // y |
x.__floordiv__(y) |
modulo (remainder) | x % y |
x.__mod__(y) |
floor division & modulo | `divmod(x, y) | x.__divmod__(y) |
raise to power | x ** y |
x.__pow__(y) |
left bit-shift | x << y |
x.__lshift__(y) |
right bit-shift | x >> y |
x.__rshift__(y) |
bitwise and | x & y |
x.__and__(y) |
bitwise xor | x ^ y |
x.__xor__(y) |
bitwise or | x | y |
x.__or__(y) |
These overloads handle a huge number of scenarios, but fails to provide comprehensive coverage of all scenarios.
>>> from fractions import Fraction
>>> x = Fraction(1, 3)
>>> 1 / x
Fraction(3, 1)
In the above, the built-in integer has no concept of how to handle a Fraction
, that is 1.__truediv__(x)
There is a second set of arithmetic special methods with reflected operands. Given an arithmetic operation that takes two operands (x / y
), there are two ways to go about it:
- Tell
x
to divide itself byy
, or - Tell
y
to divide itself intox
The set of special methods (such as __truediv__(y)
) above take the first approach: given x / y
, they provide a way for x
to say “I know how to divide myself by y”.
The following set of special methods tackle the second approach: they provide a way for y
to say “I know how to be the denominator and divide myself into x”.
You want | So you write | And Python calls |
---|---|---|
addition | x + y |
y.__radd__(x) |
subtraction | x - y |
y.__rsub__(x) |
multiplication | x * y |
y.__rmul__(x) |
division | x / y |
y.__rtruediv__(x) |
floor division | x // y |
y.__rfloordiv__(x) |
modulo (remainder) | x % y |
y.__rmod__(x) |
floor division & modulo | `divmod(x, y) | y.__rdivmod__(x) |
raise to power | x ** y |
y.__rpow__(x) |
left bit-shift | x << y |
y.__rlshift__(x) |
right bit-shift | x >> y |
y.__rrshift__(x) |
bitwise and | x & y |
y.__rand__(x) |
bitwise xor | x ^ y |
y.__rxor__(x) |
bitwise or | x | y |
y.__ror__(x) |
Python also support numeric syntax for mutating values in-place (e.g. x += y
), which depending on your class may need to be handled:
You want | So you write | And Python calls |
---|---|---|
in-place addition | x += y |
x.__iadd__(y) |
in-place subtraction | x -= y |
x.__isub__(y) |
in-place multiplication | x *= y |
x.__imul__(y) |
in-place division | x /= y |
x.__itruediv__(y) |
in-place floor division | x //= y |
x.__ifloordiv__(y) |
in-place modulo | x %= y |
x.__imod__(y) |
in-place raise to power | x **= y |
x.__ipow__(y) |
in-place left bit-shift | x <<= y |
x.__ilshift__(y) |
in-place right bit-shift | x >>= y |
x.__irshift__(y) |
in-place bitwise and | x &= y |
x.__iand__(y) |
in-place bitwise xor | x ^= y |
x.__ixor__(y) |
in-place bitwise or | x | = y |
x.__ior__(y) |
Finally there are several unary operations that number types can perform on themselves:
You want | So you write | And Python calls |
---|---|---|
negative number | -x |
x.__neg__() |
positive number | +x |
x.__pos__() |
absolute value | abs(x) |
x.__abs__() |
inverse | ~x |
x.__invert__() |
complex number | complex(x) |
x.__complex__() |
integer | int(x) |
x.__int__() |
floating point number | float(x) |
x.__float__() |
number rounded to nearest integer | round(x) |
x.__round__() |
number rounded to nearest n digits | round(x, n) |
x.__round__(n) |
smallest integer >= x | math.ceil(x) |
x.__ceil__() |
largest integer <= x | math.floor(x) |
x.__floor__() |
truncate x to nearest integer toward 0 | math.trunc(x) |
x.__trunc__() |
number as a list index | a_list[x] |
a_list[x.__index__()] |
Classes that can be used in a with block⌗
A with
block defines a runtime context; you enter the context when you execute the with statement, and you exit the context after you execute the last statement in the block.
You want | So you write | And Python calls |
---|---|---|
do something special when entering a with block | with x: |
x.__enter__() |
do something special when leaving a with block | with x: |
x.__exit__(exc_type, exc_value, traceback) |
This is exactly how the file
idiom works:
# excerpt from io.py:
def _checkClosed(self, msg=None):
'''Internal: raise an ValueError if file is closed
'''
if self.closed:
raise ValueError('I/O operation on closed file.'
if msg is None else msg)
def __enter__(self):
'''Context management protocol. Returns self.'''
self._checkClosed()
return self
def __exit__(self, *args):
'''Context management protocol. Calls close()'''
self.close()
Context manager tips:
- The file object defines both an
__enter__()
and an__exit__()
method. The__enter__()
method checks that the file is open; if it’s not, the_checkClosed()
method raises an exception. - The
__enter__()
method should almost always returnself
— this is the object that the with block will use to dispatch properties and methods. - After the with block, the file object automatically closes. How? In the
__exit__()
method, it callsself.close()
.
Esoteric behavior⌗
You want | So you write | And Python calls |
---|---|---|
a class constructor | x = MyClass() |
x.__new__() |
a class destructor | del x |
x.__del__() |
only a specific set of attributes to be defined | x.__slots__() |
|
a custom hash value | hash(x) |
x.__hash__() |
to get a property’s value | x.color |
type(x).__dict__['color'].__get__(x, type(x)) |
to set a property’s value | x.color = 'PapayaWhip' |
type(x).__dict__['color'].__set__(x, 'PapayaWhip') |
to delete a property | del x.color |
type(x).__dict__['color'].__del__(x) |
to control whether an object is an instance of your class | isinstance(x, MyClass) |
MyClass.__instancecheck__(x) |
to control whether a class is a subclass of your class | issubclass(C, MyClass) |
MyClass.__subclasscheck__(C) |
to control whether a class is a subclass of your abstract base class | issubclass(C, MyABC) |
MyABC.__subclasshook__(C) |
Design Patterns⌗
https://python-patterns.guide/gang-of-four/composition-over-inheritance/