8. Classes & Objects
The primary focus in this article is to provide common programming patterns related to class definitions. Some of the topics include:
- Making objects support common Python features.
- usage of special methods.
- Encapsulation techniques.
- Inheritence.
- Memory management.
- Useful Design Patterns.
8.1: str() & repr()
Problem
You want to change the output produced by printing or viewing instances to something more sensible.
Solution
To change the string representation of the class instances define
__str__()
& __repr__()
methods as shown below.
1 2 3 4 5 6 7 8 |
|
The
__repr__()
method returns the code represntation of the instance,
and is usually the code you would type to re-create the instance.
The built-in repr()
function returns this text and so does the interactive interpreter
while inspecting values.
The
__str__()
method converts the instance to a string and is the output returned by print()
or str()
methods.
1 2 3 4 5 |
|
The implementation of the above class also show how different string representations
may be used during formatting. Specifically, the special
!r
formatting code
indicates that the output of __repr__()
should be used instead of __str__()
, the default.
An example below shows this behavior:
1 2 3 4 5 |
|
Discussion
It is standard practice to for the output of
__repr__()
to produce text such that eval(repr(x)) == x
If it is not prossible or desired then it is common to create a textual represntation enclosed in <
& >
instead
as shown in below snippet.
1 2 3 |
|
If not
__str__()
is provided then __repr__()
is used as a fallback.
8.2: Customizing string formatting
Problem
You want an object to support customized formatting through the formaat()
function and string method.
Solution
To customize string formatting, define a custom __format__()
method in the class definition.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Instances of Date
now supports formatting options such as the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
The __format__()
method provides HOOK into Python's string formatting functionality.
Its important to know that the interpretation of the format codes (e.g.
_formats
in above code)
is entirely upto the class defintion and developer.
8.6: Creating managed attributes
Problem
You want to add extra processing (e.e. type checking, validation) to the getting and setting of the instance attributes
Solution
A simple way to customize access to an attribute is to define it as
property
.
For example, in below code, we define a property
that adds simple type checking for its
getter
and setter
methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
8.9: Descriptor Class
Creating a new kind of class or instance attribute
You want to create a new kind of class with instance atrribute type with some extra functionality
such as type checking
Solution
If you want to create an entirely new kind of instance attribute, define its functionality in the form of a descriptor class. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
A Descriptor is a class that implements three core attribute access operations
(get, set, delete) via the special methods
__get__(), __set__(), __delete__()
. These methods
work by receiving an instance as input. The underlying dictionary of the instance is then manipulated
as appropriate.
To use a descriptor, instances of the descriptor are placed into the class defintion
as class variables as shown below.
1 2 3 4 5 6 7 8 |
|
x, y
) are captured by
__get__(), __set__(), __delete__()
methods of the descriptor class. For example:
1 2 3 4 5 6 7 8 |
|
As input, each method of the descriptor receives the instance being manipulated.
To carry out the requested operation, the underlying instance dictionary
(underlying
__dict__()
attribute) is manipulated. The self.name
attribute of the
descriptor holds the dictionary key being used to store the actual data in the instance dictionary.
Discussion
Descriptors provide the underlying logic for most of the Python's class features
such as @classmethod
, @staticmethod
, @property
and even __slots__
attribute.
By defining descriptors, you can capture the core instance oprtations (get, set, delete) at a very low level and completely customoze what they do. It is one of the most important tool used by writers of most advance librarues & frameeeeworks
One confusion with the descriptors is that they can only be defined at the class level
and not at the per-instance basis. The following descriptor usage will not work.
1 2 3 4 5 6 7 8 |
|
8.13: Implementing a Data Model or Type System
Problem
You want to define various kinds of Data Structures, but want to enforce constraints on the values that are allowed to be assigned to certain constraints.
Solution
...