Skip to content

A Pythonic Object🔗

Thanks to the Python Data Model, your user-defined types can behave as naturally as the built-in types. And this can be accomplished without inheritance, in the spirit of duck typing: you just implement the methods needed for your objects to behave as expected.

But if you are writing a library or a framework, the programmers who will use your classes may expect them to behave like the classes that Python provides. Fulfilling that expectation is one way of being “Pythonic.”

Object Representation🔗

Every object-oriented language has at least one standard way of getting a string representation from any object. Python has 2

  • repr() : return a string representing the object as the developer want to see it. Its what we get using python console or debugger.
  • str() : returns a string representing the object as user want to see it. print() calls it.

There are two additional special methods to support alternative representations of objects: __bytes__ and __format__.

Vector Class Redux🔗

from array import array
import math


class Vector2d:
    typecode = 'd'  # Class Attribute

    def __init__(self, x, y):
        self.x = float(x)   # early conversion to prevent bugs
        self.y = float(y)

    def __iter__(self):
        return (i for i in (self.x, self.y))    # makes vector iterable

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self) # builds string

    def __str__(self):
        return str(tuple(self)) # tuple for display

    def __bytes__(self): # to generate `bytes` we convert typecode to bytes and concatenate
        return (bytes([ord(self.typecode)]) +
                bytes(array(self.typecode, self)))  

    def __eq__(self, other):
        return tuple(self) == tuple(other) # comparison

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

An Alternative Contructor🔗

Since we can export a Vector2d as bytes, naturally we need a method that imports a Vector2d from a binary sequence. Looking at the standard library for inspiration, we find that array.array has a class method named .frombytes that suits our purpose

    @classmethod # modifies method to be directly callable on a class
    def frombytes(cls, octets): # note we pass cls args rather than self.
        typecode = chr(octets[0]) # read typecode from first byte
        memv = memoryview(octets[1:]).cast(typecode) # create a memory view
        return cls(*memv) # unpack memory view resulting from the cast

classmethod v/s staticmethod🔗

The classmethod decorator is not mentioned in the Python tutorial, and neither is staticmethod. Anyone who has learned OO in Java may wonder why Python has both of these decorators and not just one of them.

  • classmethod : to define a method that operates on the class and not on instances.
  • classmethod changes the way the method is called, so it receives the class itself as the first argument, instead of an instance. Its most common use is for alternative constructors
  • the staticmethod decorator changes a method so that it receives no special first argument. In essence, a static method is just like a plain function that happens to live in a class body, instead of being defined at the module level.
class Demo:
  @classmethod
  def Klassmeth(*args):
    return args
  @staticmethod
  def statmeth(*args):
    return args

Demo.klassmeth()    # (<class '__main__.Demo'>,)
Demo.klassmeth('spam') # (<class '__main__.Demo'>, 'spam')

Demo.statmeth() # ()
Demo.statmeth('spam') # ('spam',)

The classmethod decorator is clearly useful, but good use cases for staticmethod are very rare in my experience.

Formatted Displays🔗

The f-strings, the format() built-in function, and the str.format() method delegate the actual formatting to each type by calling their .__format__(format_spec) method.

The format_spec is a formatting specifier, which is either:

  • The second argument in format(my_obj, format_spec), or
  • Whatever appears after the colon in a replacement field delimited with {} inside an f-string or the fmt in fmt.str.format()
brl = 1/4.82
brl # 0.20746887966804978
format(brl, '0.4f') # 0.2075
"1 BRL = {rate:0.2f} USD".format(rate=brl)
f"1 USD = {1/brl:0.2f} BRL" # f string syntax

The notation used in the formatting specifier is called the Format Specification Mini-Language.

The Format Specification Mini-Language is extensible because each class gets to interpret the format_spec argument as it likes. For instance, the classes in the datetime module use the same format codes in the strftime() functions and in their __format__ methods.

from datetime import datetime
now = datetime.now()
format(now, "%H%M:%s")
"It's now {:%I:%M %p}".format(now)
  # inside the Vector2d class

    def __format__(self, fmt_spec=''):
        components = (format(c, fmt_spec) for c in self) # apply spec to each component
        return '({}, {})'.format(*components) # plug formatted string in (x,y)


# more improved version that support polar co-ordinates using p
    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)


# output
format(Vector2d(1, 1), 'p')
format(Vector2d(1, 1), '.3ep')
format(Vector2d(1, 1), '0.5fp')

A Hashable Vector2d🔗

So far Vector2d instances are not hashable so we can’t put them in a set

To make it hashable we need to implement __hash__ and __eq__ (already done) and make vector instances immutable. Because anyone can change them right now using v1.x = 0 and there is nothing in code to prevent that.

class Vector2d:
    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x) # use __ to make attribute private
        self.__y = float(y)

    @property # marks the getter method of a property
    def x(self): # getter must be named after the public property it exposes
        return self.__x  # just return self.__x

    @property
    def y(self):
        return self.__y

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __hash__(self):
        return hash((self.x, self.y))   # hashed the tuple of co-ordinates

    # remaining methods: same as previous Vector2d

Supporting Positional Pattern Matching🔗

So far, Vector2d instances are compatible with keyword class patterns—covered in “Keyword Class Patterns”.

def keyword_pattern_demo(v: Vector2d) -> None:
    match v:
        case Vector2d(x=0, y=0):
            print(f'{v!r} is null')
        case Vector2d(x=0):
            print(f'{v!r} is vertical')
        case Vector2d(y=0):
            print(f'{v!r} is horizontal')
        case Vector2d(x=x, y=y) if x==y:
            print(f'{v!r} is diagonal')
        case _:
            print(f'{v!r} is awesome')

However if try to use a position pattern like this: it fails

                case Vector2d(_, 0):
            print(f'{v!r} is horizontal')
TypeError: Vector2d() accepts 0 positional sub-patterns (1 given)

To make Vector2d work with positional patterns, we need to add a class attribute named __match_args__ , listing the instance attributes in the order they will be used for positional pattern matching:

class Vector2d:
    __match_args__ = ('x', 'y')

Now we can use positional patterns

def positional_pattern_demo(v: Vector2d) -> None:
    match v:
        case Vector2d(0, 0):
            print(f'{v!r} is null')
        case Vector2d(0):
            print(f'{v!r} is vertical')
        case Vector2d(_, 0):
            print(f'{v!r} is horizontal')
        case Vector2d(x, y) if x==y:
            print(f'{v!r} is diagonal')
        case _:
            print(f'{v!r} is awesome')

Complete Listing of Vector2d, version3🔗

from array import array
import math

class Vector2d:
    __match_args__ = ('x', 'y')

    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
                bytes(array(self.typecode, self)))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __hash__(self):
        return hash((self.x, self.y))

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

    def angle(self):
        return math.atan2(self.y, self.x)

    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(*memv)

Private and “Protected” Attributes in Python🔗

In Python, there is no way to create private variables like there is with the private modifier in Java. What we have in Python is a simple mechanism to prevent accidental overwriting of a “private” attribute in a subclass.

Consider this scenario: someone wrote a class named Dog that uses a mood instance attribute internally, without exposing it. You need to subclass Dog as Beagle. If you create your own mood instance attribute without being aware of the name clash, you will clobber the mood attribute used by the methods inherited from Dog. This would be a pain to debug.

To prevent this, if you name an instance attribute in the form __mood (two leading underscores and zero or at most one trailing underscore), Python stores the name in the instance __dict__ prefixed with a leading underscore and the class name, so in the Dog class, __mood becomes _Dog__mood, and in Beagle it’s _Beagle__mood. This language feature goes by the lovely name of name mangling.

The name mangling functionality is not loved by all Pythonistas, and neither is the skewed look of names written as self.__x. Some prefer to avoid this syntax and use just one underscore prefix to “protect” attributes by convention (e.g., self._x)

The single underscore prefix has no special meaning to the Python interpreter when used in attribute names, but it’s a very strong convention among Python programmers that you should not access such attributes from outside the class.

Saving Memory with __slots__🔗

By default, Python stores the attributes of each instance in a dict named __dict__. A dict has a significant memory overhead - even with the optimizations implemented.

  • if you define a class attribute named __slots__ holding a sequence of attribute names, Python uses an alternative storage model for the instance attributes: the attributes named in __slots__ are stored in a hidden array or references that use less memory than a dict
class Pixel:
  __slots__ = ('x', 'y')

p = Pixel()
p.__dict__  # throws attribute error
p.x = 10
p.y = 20
p.color = 'Red' # throws attribute error

# slots are not inherited
class OpenPixel(Pixel):
  pass
op = OpenPixel()
op.__dict__ # works
op.color = 'green'  # works
class ColorPixel(Pixel):
  __slots__ = ('color',)

cp = ColorPixel()
cp.__dict__ # raises attribute error
cp.x = 10
cp.color = 'red'
cp.flavor = 'banana'    # raises attribute error

Issues with __slots__🔗

  • you must remember to redeclare __slots__ in each subclass to prevent their instance having __dict__
  • instances will only be able to have attributes listed in __slots__, unless we include __dict__ in slots.
  • Classes using __slots__ can’t use @chached_property decorator, unless they explicitly name __dict__ in __slots__
  • Instances can’t be targets of weak references unles you add __weakref__ in __slots__

Overriding Class Attributes🔗

  • class attributes can be used as default values for instance attributes
  • we can access them as self.attribute_name
  • if we declare a instance attribute then the class attribute is left untouched and self.attribute_name will always reference to instance attribute.
  • If you want to change a class attribute, you must set it on the class directly, not through an instance.
  • In python more idiomatic way to achieve it how Django does it. Create subclasses which change class attributes.
from vector2d_v3 import Vector2d
class ShortVector2d(Vector2d)
    typecode = 'f'
  • Another thing to note is following function which doesn’t hardcodes class name in its representation
    # inside class Vector2d:

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)

This way f typecodes work fine in representation as well.