🐍 Python - Classes
Class definition. Always use self
as the first argument to instance methods. Always use cls
as the first argument to class methods.
class Human:
# Class attributes are shared by all instances of this class.
species = 'H. sapiens'
# Constructor
def __init__(self, name: str) -> None:
self.name: str = name
# Method
def say(self, msg: str) -> str:
return '%s: %s' % (self.name, msg)
# Class methods are shared among all instances of this class.
@classmethod
def get_species(cls) -> str:
return cls.species
# Static methods are called without instance reference.
@staticmethod
def grunt() -> str:
return '*grunt*'
Class usage.
ian = Human('Ian')
joel = Human(name='Joel')
assert ian.say('hi') == 'Ian: hi'
assert ian.get_species() == 'H. sapiens'
Human.species = 'H. neanderthalensis'
assert joel.get_species() == 'H. neanderthalensis'
assert Human.grunt() == '*grunt*'
Classes should inherit object
, but Python 3 does this automatically. Makes properties work properly and defines special methods that implement default semantics of object.
class OuterClass:
class InnerClass:
...
class ChildClass(OuterClass):
...
Custom exceptions should derive from Exception
. Consider naming custom exceptions ending with Error
if exception is an error.
class ValidationError(Exception):
pass
raise ValidationError("value is too high")
# => ValidationError: value is too high
Consider if you want to offer direct member access or wrap them.
# direct access
class Point:
def __init__(self, x: float, y: float) -> None:
self.x: float = x
self.y: float = y
# indirect access
class Point:
def __init__(self, x: float, y: float) -> None:
self._x: float = x
self._y: float = y
@property
def x(self) -> float:
return self._x
@x.setter
def x(self, value) -> None:
self._x = value
Offer constructors that help with the basic class usage.
# bad
point = Point()
point.x = 12
point.y = 5
# good
point = Point(x=12, y=5)
Define __repr__
for your classes. Defining instance representation for your classes improves maintainability of your code. __repr__
is for debugging.
# BAD CLASS
class Car:
def __init__(self, color) -> None:
self.color = color
my_car = Car('red')
assert '<__main__.Car object at' in str(my_car)
assert '<__main__.Car object at' in repr(my_car)
# GOOD CLASS
class Bike:
def __init__(self, color) -> None:
self.color = color
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.color!r})'
# !r here does repr(self.color) instead of str(self.color)
my_bike = Bike('blue')
assert "Bike('blue')" == str(my_bike)
assert "Bike('blue')" == repr(my_bike)
Optionally define __str__
for your classes. Python calls dunder string whenever an instance of the class is converted to a string e.g. when printing. If __str__
is missing, Python will use __repr__
if defined. __str__
is should be ok to be shown to end-user, not just for debugging.
Watch out for instance variables "shadowing" class variables. Always refer to class variables though __class__
or the class itself when modifying them.
class Counted:
num_instances: int = 0
def __init__(self) -> None:
self.__class__.num_instances += 1 # GOOD, CLASS VARIABLE
assert Counted.num_instances == 0
assert Counted().num_instances == 1
assert Counted().num_instances == 2
assert Counted().num_instances == 3
assert Counted.num_instances == 3
class BuggyCounted:
num_instances: int = 0
def __init__(self) -> None:
self.num_instances += 1 # BAD, INSTANCE VARIABLE
assert BuggyCounted.num_instances == 0
assert BuggyCounted().num_instances == 1
assert BuggyCounted().num_instances == 1
assert BuggyCounted().num_instances == 1
assert BuggyCounted.num_instances == 0
Python is smart in duck typing and tries various methods to fulfill an interface.
class Foo:
def __getitem__(self, position: int) -> int:
return range(0, 30, 10)[position]
f = Foo()
assert f[1] == 10
# __iter__ fallbacks to __getitem__
assert [n for n in f] == [0, 10, 20]
# __contains__ fallbacks to __getitem__
assert 20 in f
__new__
can be used to change how to construct an instance of the class. What __new__
returns is passed to __init__
if __new__
returns an instance of the class the __new__
is defined in.
import json
import keyword
import typing
class FrozenJSON:
def __new__(cls: typing.Type['FrozenJSON'], arg: typing.Any):
if isinstance(arg, typing.Mapping):
return super().__new__(cls)
elif isinstance(arg, typing.MutableSequence):
return [cls(item) for item in arg]
else:
return arg
def __init__(self, mapping: typing.Mapping):
self._data = {}
for key, value in mapping.items():
if keyword.iskeyword(key):
key += '_'
self._data[key] = value
def __getattr__(self, name: str) -> 'FrozenJSON':
if hasattr(self._data, name):
return getattr(self._data, name)
else:
return FrozenJSON(self._data[name])
raw = '{"people": [{"name": "John", "class": "6A"}, {"name": "Alice", "class": "6B"}]}'
f = FrozenJSON(json.loads(raw))
assert len(f.people) == 2
assert f.people[0].name == 'John' and f.people[0].class_ == '6A'
assert f.people[1].name == 'Alice' and f.people[1].class_ == '6B'
Other useful built-in functions.
getattr(obj, name[, default]) # also searches parents
hasattr(obj, name) # also searches parents
setattr(obj, name, value) # only assigns to the obj
dir(obj) # show attributes in the obj
vars(obj) # show __dict__
Sources
- Style Guide For Python Code
- Django Coding Style
- Google Python Guide
- Python Newbie Mistakes
- Learn Python in Y minutes
- Python’s null equivalent: None
- Python Idioms
- Python Best Practice Patterns
- Become More Advanced: Master the 10 Most Common Python Programming Problems
- Python Tricks The Book, Dan Bader
- Fluent Python, Luciano Ramalho