Welcome to part 24 of the intermediate Python programming tutorial series. In this tutorial, we're going to cover two new special methods: __str__
and __repr__
.
There are many different explanations about what __str__
and __repr__
are each used for. The main confusion, at least from what I can tell, is where and how __repr__
actually differs from __str__
.
The __str__
method is useful for a string representation of the object, either when someone codes in str(your_object)
, or even when someone might do print(your_object)
. The __str__
method is one that should be the most human-readable possible, yet also descriptive of that exact object.
The __repr__
method is present either when doing something like repr(your_object)
, or when a programmer might actually just type the object directly into the interpreter. The repr method is really meant to be just for developers, and more for debugging than actual use of the module. For this reason, you might have never even heard of __repr__
until now, but you've probably noticed the difference between simply calling the object and the str() version of that object. Let's see an example with datetime.datetime.now()
.
import datetime print(datetime.datetime.now())
If you have both a __repr__
and __str__
method, then the __str__
method will be favored when you use print()
, so you see a fairly clean date:
2016-12-04 19:11:30.072375
To prove this, we can do:
print(str(datetime.datetime.now()))
2016-12-04 19:11:30.074376
To see the __repr__
, we can do:
print(repr(datetime.datetime.now()))
datetime.datetime(2016, 12, 4, 19, 13, 49, 596592)
Also, in the interactive interpreter / shell, you could just type the object:
>>> datetime.datetime.now() datetime.datetime(2016, 12, 4, 19, 11, 40, 804063) >>>
Just typing the object out will give you the __repr__
, or "representation" of the object.
Let's make our own __str__
and __repr__
methods for our blobs. Currently, our blob.py
file looks like:
import random class Blob: def __init__(self, color, x_boundary, y_boundary, size_range=(4,8), movement_range=(-1,2)): self.size = random.randrange(size_range[0],size_range[1]) self.color = color self.x_boundary = x_boundary self.y_boundary = y_boundary self.x = random.randrange(0, self.x_boundary) self.y = random.randrange(0, self.y_boundary) self.movement_range = movement_range def move(self): self.move_x = random.randrange(self.movement_range[0],self.movement_range[1]) self.move_y = random.randrange(self.movement_range[0],self.movement_range[1]) self.x += self.move_x self.y += self.move_y def check_bounds(self): if self.x < 0: self.x = 0 elif self.x > self.x_boundary: self.x = self.x_boundary if self.y < 0: self.y = 0 elif self.y > self.y_boundary: self.y = self.y_boundary
Let's add the __repr__
first:
def __repr__(self): return 'Blob({},{},({},{}))'.format(self.color, self.size, self.x, self.y)
So, this will give us just a quick and dirty representation of this object. It's not super human-readable, but someone familiar with what to expect will know what this means.
Now, let's do the __str__
method:
def __str__(self): return "Color: {} blobject of size {}. Located at {},{}".format(self.color, self.size, self.x, self.y)
Very similar, and both of these methods contain the same information, but the __str__
is a bit more human friendly.
Now, taking our blobworld.py file, which is:
import pygame import random from blob import Blob import numpy as np import logging ''' DEBUG Detailed information, typically of interest only when diagnosing problems. INFO Confirmation that things are working as expected. WARNING An indication that something unexpected happened, or indicative of some problem in the near future (e.g. ‘disk space low’). The software is still working as expected. ERROR Due to a more serious problem, the software has not been able to perform some function. CRITICAL A serious error, indicating that the program itself may be unable to continue running. ''' logging.basicConfig(filename='logfile.log',level=logging.INFO) STARTING_BLUE_BLOBS = 15 STARTING_RED_BLOBS = 15 STARTING_GREEN_BLOBS = 15 WIDTH = 800 HEIGHT = 600 WHITE = (255, 255, 255) BLUE = (0, 0, 255) RED = (255, 0, 0) game_display = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("Blob World") clock = pygame.time.Clock() class BlueBlob(Blob): def __init__(self, x_boundary, y_boundary): Blob.__init__(self, (0, 0, 255), x_boundary, y_boundary) def __add__(self, other_blob): logging.info('Blob add op {} + {}'.format(str(self.color), str(other_blob.color))) if other_blob.color == (255, 0, 0): self.size -= other_blob.size other_blob.size -= self.size elif other_blob.color == (0, 255, 0): self.size += other_blob.size other_blob.size = 0 elif other_blob.color == (0, 0 , 255): pass else: raise Exception('Tried to combine one or multiple blobs of unsupported colors.') class RedBlob(Blob): def __init__(self, x_boundary, y_boundary): Blob.__init__(self, (255, 0, 0), x_boundary, y_boundary) class GreenBlob(Blob): def __init__(self, x_boundary, y_boundary): Blob.__init__(self, (0, 255, 0), x_boundary, y_boundary) def is_touching(b1,b2): return np.linalg.norm(np.array([b1.x, b1.y])-np.array([b2.x, b2.y])) < (b1.size + b2.size) def handle_collisions(blob_list): blues, reds, greens = blob_list for blue_id, blue_blob, in blues.copy().items(): for other_blobs in blues, reds, greens: for other_blob_id, other_blob in other_blobs.copy().items(): logging.debug('Checking if blobs are touching {} + {}'.format(str(blue_blob.color), str(other_blob.color))) if blue_blob == other_blob: pass else: if is_touching(blue_blob, other_blob): blue_blob + other_blob if other_blob.size <= 0: del other_blobs[other_blob_id] if blue_blob.size <= 0: del blues[blue_id] return blues, reds, greens def draw_environment(blob_list): blues, reds, greens = handle_collisions(blob_list) game_display.fill(WHITE) for blob_dict in blob_list: for blob_id in blob_dict: blob = blob_dict[blob_id] pygame.draw.circle(game_display, blob.color, [blob.x, blob.y], blob.size) blob.move() blob.check_bounds() pygame.display.update() return blues, reds, greens def main(): blue_blobs = dict(enumerate([BlueBlob(WIDTH,HEIGHT) for i in range(STARTING_BLUE_BLOBS)])) red_blobs = dict(enumerate([RedBlob(WIDTH,HEIGHT) for i in range(STARTING_RED_BLOBS)])) green_blobs = dict(enumerate([GreenBlob(WIDTH,HEIGHT) for i in range(STARTING_GREEN_BLOBS)])) while True: try: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() blue_blobs,red_blobs,green_blobs = draw_environment([blue_blobs,red_blobs,green_blobs]) clock.tick(60) except Exception as e: logging.critical(str(e)) pygame.quit() quit() break if __name__ == '__main__': main()
We can just change what happens in the if __name__ == '__main__':
part to:
if __name__ == '__main__': blue_blobs = dict(enumerate([BlueBlob(WIDTH,HEIGHT) for i in range(STARTING_BLUE_BLOBS)])) red_blobs = dict(enumerate([RedBlob(WIDTH,HEIGHT) for i in range(STARTING_RED_BLOBS)])) green_blobs = dict(enumerate([GreenBlob(WIDTH,HEIGHT) for i in range(STARTING_GREEN_BLOBS)])) pygame.quit()
Run that, then we can play in the shell. Just referencing the object:
>>> blue_blobs[0] Blob((0, 0, 255),7,(332,528))
The above gives us the __repr__
method. We can next do:
>>> str(blue_blobs[0]) 'Color: (0, 0, 255) blobject of size 7. Located at 332,528'
The above gives us the __str__
method, which we also get if we use print()
:
>>> print(green_blobs[0]) Color: (0, 255, 0) blobject of size 6. Located at 370,223
If we were to remove the __str__
method, then Python will just by default give us the __repr__
method, like:
>>> print(red_blobs[1]) Blob((255, 0, 0),6,(742,265))
If we remove the __repr__
, any reference to the object will go back to the default, like:
>>> red_blobs[1] <__main__.RedBlob object at 0x0000021E56750198>
For the typical Python programmer, this information is worthless, which is why it is helpful if you as the developer of a program actually add in the __str__
and __repr__
methods, assuming of course that you make their outputs actually useful.