So far, we've been using the + operator for combining string literals with the results of expressions.
artist = "Lil Nas X"
song = "Industry Baby"
place = 2
print("Debuting at #" + str(place) + ": '" + song + "' by " + artist)
But that's not ideal:
str()
ing non-strings
String interpolation is the process of combining string literals with the results of expressions.
Available since Python 3.5, f strings (formatted string literals) are the best way to do string interpolation.
Just put an f
in front of the quotes and then
put any valid Python expression in curly brackets inside:
artist = "Lil Nas X"
song = "Industry Baby"
place = 2
print(f"Debuting at #{place}: '{song}' by {artist}")
😍😍😍😍😍😍
Any valid Python expression can go inside the parentheses, and will be executed in the current environment.
greeting = 'Ahoy'
noun = 'Boat'
print(f"{greeting.lower()}, {noun.upper()}yMc{noun}Face")
print(f"{greeting*3}, {noun[0:3]}yMc{noun[-1]}Face")
What are the objects in this code?
class Lamb:
species_name = "Lamb"
scientific_name = "Ovis aries"
def __init__(self, name):
self.name = name
def play(self):
self.happy = True
lamb = Lamb("Lil")
owner = "Mary"
had_a_lamb = True
fleece = {"color": "white", "fluffiness": 100}
kids_at_school = ["Billy", "Tilly", "Jilly"]
day = 1
lamb
, owner
, had_a_lamb
, fleece
,
kids_at_school
, day
, etc.
We can prove it by checking object.__class__.__bases__
, which reports the base class(es) of the object's class.
All the built-in types inherit from object
:
If all the built-in types and user classes inherit from object
,
what are they inheriting?
Just ask dir()
, a built-in function that returns
a list of all the attributes on an object.
dir(object)
__repr__
, __str__
, __format__
__eq__
, __ge__
, __gt__
, __le__
, __lt__
, __ne__
__bases__
, __class__
, __new__
, __init__
,
__init_subclass__
, __subclasshook__
, __setattr__
, __delattr__
, __getattribute__
__dir__
, __hash__
, __module__
, __reduce__
, __reduce_ex__
Python calls these methods behind these scenes, so we are often not aware
when the "dunder" methods are being called.
💡 Let us become enlightened! 💡
The __str__
method returns a human readable
string representation of an object.
from fractions import Fraction
one_third = 1/3
one_half = Fraction(1, 2)
float.__str__(one_third) # '0.3333333333333333'
Fraction.__str__(one_half) # '1/2'
The __str__
method is used in multiple places by Python: print()
function,
str()
constructor, f-strings, and more.
from fractions import Fraction
one_third = 1/3
one_half = Fraction(1, 2)
print(one_third) # '0.3333333333333333'
print(one_half) # '1/2'
str(one_third) # '0.3333333333333333'
str(one_half) # '1/2'
f"{one_half} > {one_third}" # '1/2 > 0.3333333333333333'
When making custom classes, we can override __str__
to define our human readable string representation.
class Lamb:
species_name = "Lamb"
scientific_name = "Ovis aries"
def __init__(self, name):
self.name = name
def __str__(self):
return "Lamb named " + self.name
lil = Lamb("Lil lamb")
str(lil) # 'Lamb named Lil lamb'
print(lil) # Lamb named Lil lamb
lil # 〈__main__.Lamb object at 0x7fc1489b82d0〉
The __repr__
method returns
a string that would evaluate to an object with the same values.
from fractions import Fraction
one_half = Fraction(1, 2)
Fraction.__repr__(one_half) # 'Fraction(1, 2)'
If implemented correctly, calling eval()
on the result should return back that same-valued object.
another_half = eval(Fraction.__repr__(one_half))
The __repr__
method is used multiple places by Python:
when repr(object)
is called
and when displaying an object in an interactive Python session.
from fractions import Fraction
one_third = 1/3
one_half = Fraction(1, 2)
one_third
one_half
repr(one_third)
repr(one_half)
When making custom classes, we can override __repr__
to return a more appropriate Python representation.
class Lamb:
species_name = "Lamb"
scientific_name = "Ovis aries"
def __init__(self, name):
self.name = name
def __str__(self):
return "Lamb named " + self.name
def __repr__(self):
return f"Lamb({repr(self.name)})"
lil = Lamb("Lil lamb")
repr(lil) "Lamb('Lil lamb')"
lil Lamb('Lil lamb')
Certain names are special because they have built-in behavior. Those method names always start and end with double underscores.
Name | Behavior |
---|---|
__init__
| Method invoked automatically when an object is constructed |
__repr__
| Method invoked to display an object as a Python expression |
__str__
| Method invoked to stringify an object |
__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) |
zero = 0
one = 1
two = 2
🍭 Syntactic sugar 🍩 | Dunder equivalent |
---|---|
|
|
|
|
|
|
Consider the following class:
from math import gcd
class Rational:
def __init__(self, numerator, denominator):
g = gcd(numerator, denominator)
self.numer = numerator // g
self.denom = denominator // g
def __str__(self):
return f"{self.numer}/{self.denom}"
def __repr__(self):
return f"Rational({self.numer}, {self.denom})"
Will this work?
Rational(1, 2) + Rational(3, 4)
🚫 TypeError: unsupported operand type(s) for +: 'Rational' and 'Rational'
We can make instances of custom classes addable by defining the __add__
method:
class Rational:
def __init__(self, numerator, denominator):
g = gcd(numerator, denominator)
self.numer = numerator // g
self.denom = denominator // g
def __add__(self, other):
new_numer = self.numer * other.denom + other.numer * self.denom
new_denom = self.denom * other.denom
return Rational(new_numer, new_denom)
# The rest...
Now try...
Rational(1, 2) + Rational(3, 4)
Polymorphic function: A function that applies to many (poly) different forms (morph) of data
str
and repr
are both polymorphic; they apply to any object
repr
invokes a zero-argument method __repr__ on its argument:
one_half = Rational(1, 2)
one_half.__repr__() # 'Rational(1, 2)'
str
invokes a zero-argument method __str__ on its argument:
one_half = Rational(1, 2)
one_half.__str__() # '1/2'
The behavior of repr
is slightly more complicated than invoking __repr__
on its argument:
__repr__
is ignored! Only class attributes are found
def repr(x):
return type(x).__repr__()
def repr(x):
return x.__repr__(x)
def repr(x):
return type(x).__repr__(x)
def repr(x):
return super(x).__repr__()
def repr(x):
return x.__repr__()
The behavior of str
is also complicated:
__str__
is ignored
__str__
attribute is found, uses repr
string
str
is a class, not a function)
A generic function can apply to arguments of different types.
def sum_two(a, b):
return a + b
What could a
and b
be? Anything summable!
The function sum_two
is generic in the type of a
and b
.
def sum_em(items, initial_value):
"""Returns the sum of ITEMS,
starting with a value of INITIAL_VALUE."""
sum = initial_value
for item in items:
sum += item
return sum
What could items
be? Any iterable with summable values.
What could initial_value
be? Any value that can be summed with the values in iterable.
The function sum_em
is generic in the type of items
and the type of initial_value
.
Another way to make generic functions is to select a behavior based on the type of the argument.
def is_valid_month(month):
if isinstance(month, str) and len(month) == 1:
return month in ["J", "F", "M", "A", "S", "O", "N", "D"]
if isinstance(month, int):
return month >= 1 and month <= 12
elif isinstance(month, str):
return month in ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"]
return False
What could month
be? Either an int or string.
The function is_valid_month
is generic in the type of month
.
Another way to make generic functions is to coerce an argument into the desired type.
def sum_numbers(nums):
"""Returns the sum of nums"""
sum = Rational(0, 1)
for num in nums:
if isinstance(num, int):
num = Rational(num, 1)
sum += num
return sum
What could nums
be? Any iterable with ints or Rationals.
The function sum_numbers
is generic in the type of nums
.