String Representations
- An object value should behave like the kind of data it is meant to represent
- Part of this ability is to provide a string representation of a program
- There are only two string representations of python objects.
- The
str
representation is legible for humans - The
repr
representation is legible to the Python interpreter
- The
- The
str
andrepr
strings are often the same, but not always
The repr
String for an Object
- The
repr
function returns a Python expression (a string) that evaluates to an equal objectrepr(object) -> string
- Return the canonical string representation of the object.
- For most object types, eval(repr(object)) == object
- Complicated objects such as classes or functions do not have a simple Python-readable string.
- This is because not everything that is pertaining to the object could be described simply.
- Ex: The
repr
of a function
1
repr(max)
1
'<built-in function max>'
The str
String for an Object
- Human interpretable strings are useful as well:
- Sometimes, our programs and objects interact and communicate with the user.
1
2
3
4
from fractions import Fraction
half = Fraction(1,2)
print("str representation:", repr(str(half)))
print("repr representation", repr(repr(half)))
1
2
str representation: '1/2'
repr representation 'Fraction(1, 2)'
- The result of calling
str
on the value of an expression displays what Python would print using the print expression.
1
2
s = "Hello, World"
s
1
'Hello, World'
1
print(repr(s))
1
'Hello, World'
1
print(str(s))
1
Hello, World
1
repr(s)
1
"'Hello, World'"
F-Strings
- We may generate strings out of expressions within a string literal
- String interpolation is evaluating a string literal that contains expressions
1
2
from math import pi
f"pi starts with {pi}..."
1
'pi starts with 3.141592653589793...'
- When we evaluate an f-string literal, we incorporate a str string of the value of each sub-expression.
- Sub-expressions are evaluated in the current environment.
- Expressions within an F-string are evaluated in the order that they appear in.
Polymorphic Functions
- Polymorphic Function: A function that applies to many (poly) different forms (morph) of data
- Both
str
andrepr
are polymorphic functions, they apply to any object.- The way that this behavior is created in python is that
repr
invokes a zero-argument method__repr__
on its argument. - The
str
function also invokes a zero-argument method__str__
on its argument. - In other words,
repr
asks the argument to display itself.
- The way that this behavior is created in python is that
- The
str
andrepr
strings do not know how to obtain thestr
orrepr
representation of the object. It’s the object itself that understands how to represent itself. - Important Idea: We can defer the logic of the function to the methods of the argument itself.
Implementing repr
and str
- Slightly more complicated than just invoking
__repr__
or__str__
on the argument.- If there is an instance attribute called
__repr__
it is ignored. - Only class attributes are found
- If there is an instance attribute called
1
2
def repr(x):
return type(x).__repr__(x)
- The breakdown of this code is that
type(x)
returns the class that defines the object. Thus, applying__repr__
on the class would access the class attribute rather than the instance attribute. We also pass te instance into the__repr__
function because the function accepts aself
parameter. The__repr__
that we call is not a bound method, but rather a function. - The behavior of
str
is also complicated- The instance attribute
__str__
is ignored - If no
__str__
attribute is found, use therepr
string. - This is implemented through interfaces
- The instance attribute
Interfaces
- Message passing: Objects interact by looking up attributes on each other (passing messages)
- Attribute look-up rules enable different data types to respond to the same message
- This is achived by giving each object the same name. This also creates a standard for communication.
- A shared message (attribute name) elicits similar behavior from different object classes is a powerful method of abstraction.
- These shared messages forms an interface between different classes, types, and objects.
- Classes that implement
__repr__
and__str__
methods that return Python-interpretable and human-readable strings implement an interface for producing string representations. - At a higher level, as long as we have a collection of classes that have methods of the same name with similar behavior, we have effectively created an interface between the objects.
- Example: Ratio
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Ratio:
def __init__(self, n, d):
self.numer = n
self.denom = d
def __repr__(self):
return 'Ratio({0}, {1})'.format(self.numer, self.denom)
def __str__(self):
return '{0}/{1}'.format(self.numer, self.denom)
half = Ratio(1,2)
print(half)
print(repr(half))
1
2
1/2
Ratio(1, 2)
Special Method Names
- Certain names are special because they have built-in behavior
- These names always start and end with two underscores
Special Method | Behavior |
---|---|
__init__ | Method invoked automatically when an object is constructed |
__repr__ | Method invoked to display an object as a Python expression |
__add__ | Method invoked to add one object to another |
__bool__ | Method invoked to convert an object to True or False |
__float__ | Method invoked to convert an object to a float (real number) |
- Ex: This piece of code…
1
2
3
zero, one, two = 0, 1, 2
print(one + two)
print(bool(zero), bool(one))
1
2
3
False True
is the same as this
1
2
print(one.__add__(two))
print(zero.__bool__(), one.__bool__())
1
2
3
False True
Special Methods
- When we add instnaces of user-defined classes, we invoke either the
__add__
or__radd__
method. __add__
and__radd__
are methods that both perform addition.__add__
is left hand addition__radd__
is right hand additionThere is a subtle, yet important distinction between
__add__
and__radd__
. The__add__
method adds an object to another object when the instance itself is the expression to the left of the dot-expression. In other words, it is addition from the left-hand side. The__radd__
method happens in the reverse, and adds an object to another object when the instance itself is the argument to the right and wrapped in parenthesis of the dot-expression. Here is an example of why this is helpful:
A classmyClass
creating an instancemyObj
could be created to handle the addition of a number to it, say 4:
myObj + 4
However, if we attempted to add 4 ontomyObj
it would raise an error as the integer type does not have the implementation to add our object to an integer
4 + myObj –> NotImplemented –> TypeError
We can avoid this unwanted behavior (as some addition is commutative) by implementing the__radd__
operation formyClass
. Thus, when running 4 + myObj, python would first try to run4.__add__(myObj)
to find the NotImplemented return value, but will then runmyObj.__radd__(4)
. This allows an interface that enables our class to avoid handle cases where the other object’s implementation of addition does not support our current object’s
- It is possible to also use
__radd__
to specify addition operations where the action is not commutative between different objects. - We could now define the addition property for our ratio class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Ratio:
def __init__(self, n, d):
self.numer = n
self.denom = d
def __repr__(self):
return 'Ratio({0}, {1})'.format(self.numer, self.denom)
def __str__(self):
return '{0}/{1}'.format(self.numer, self.denom)
def __add__(self, other):
# Type Dispatching
if isinstance(other, int):
n = self.numer + self.denom * other
d = self.denom
elif isinstance(other, Ratio):
n = self.numer * other.denom + self.denom * other.numer
d = self.denom * other.denom
elif isinstance(other, float):
# Type Coercion
return float(self) + other
g = gcd(n, d)
return Ratio(n//g, d//g)
def __float__(self):
return self.numer/self.denom
__radd__ = __add__
def gcd(n, d):
while n != d:
n, d = min(n, d), abs(n-d)
return n
print(Ratio(1, 3) + Ratio(1, 6))
print(Ratio(1,4) + 8)
print(3 + Ratio(1,7))
print(0.2 + Ratio(1,3))
1
2
3
4
1/2
33/4
22/7
0.5333333333333333
- In the
Ratio
implementation above, we implemented two important ideas:- Type Casting: We inspect the type of an argument and decide what to do.
- Type Coercion: We take an object of one type and we convert it into another type to combine it with another value.
- By combining these two methods, we may create classes that we may have different classes interact with each other.