What is a natural way to represent real-world "things" in code?
How can we organize code to make it easier to understand, test, and extend?
Are these the same thing?
A program is just another data structure.
def defines a variable whose value is the function's instructionsdef example():
print("in example")
alias = example
alias()
in example
class Shape:
def __init__(self, name):
self.name = name
def perimeter(self):
raise NotImplementedError("perimeter")
def area(self):
raise NotImplementedError("area")
class Square(Shape):
def __init__(self, name, side):
super().__init__(name)
self.side = side
def perimeter(self):
return 4 * self.side
def area(self):
return self.side ** 2
class Circle(Shape):
def __init__(self, name, radius):
super().__init__(name)
self.radius = radius
def perimeter(self):
return 2 * math.pi * self.radius
def area(self):
return math.pi * self.radius ** 2
examples = [Square("sq", 3), Circle("ci", 2)]
for thing in examples:
n = thing.name
p = thing.perimeter()
a = thing.area()
print(f"{n} has perimeter {p:.2f} and area {a:.2f}")
def square_perimeter(thing):
return 4 * thing["side"]
def square_area(thing):
return thing["side"] ** 2
def square_new(name, side):
return {
"name": name,
"side": side,
"perimeter": square_perimeter,
"area": square_area
}
An object is just a (specialized) dictionary
A method is just a function that takes the object as its first parameter
def call(thing, method_name):
return thing[method_name](thing)
examples = [square_new("sq", 3), circle_new("ci", 2)]
for ex in examples:
n = ex["name"]
p = call(ex, "perimeter")
a = call(ex, "area")
print(f"{n} {p:.2f} {a:.2f}")
Look up the function in the object
Call it with the object as its first argument
obj.meth(arg) is obj["meth"](obj, arg)
def square_perimeter(thing):
return 4 * thing["side"]
def square_area(thing):
return thing["side"] ** 2
Square = {
"perimeter": square_perimeter,
"area": square_area,
"_classname": "Square"
}
def square_new(name, side):
return {
"name": name,
"side": side,
"_class": Square
}
def call(thing, method_name):
return thing["_class"][method_name](thing)
examples = [square_new("sq", 3), circle_new("ci", 2)]
for ex in examples:
n = ex["name"]
p = call(ex, "perimeter")
a = call(ex, "area")
c = ex["_class"]["_classname"]
print(f"{n} is a {c}: {p:.2f} {a:.2f}")
Look in the class for the method
Call it with the object as the first parameter
And we can now reliably identify objects' classes
def show_args(title, *args, **kwargs):
print(f"{title} args '{args}' and kwargs '{kwargs}'")
show_args("nothing")
show_args("one unnamed argument", 1)
show_args("one named argument", second="2")
show_args("one of each", 3, fourth="4")
nothing args '()' and kwargs '{}'
one unnamed argument args '(1,)' and kwargs '{}'
one named argument args '()' and kwargs '{'second': '2'}'
one of each args '(3,)' and kwargs '{'fourth': '4'}'
def show_spread(left, middle, right):
print(f"left {left} middle {middle} right {right}")
all_in_list = [1, 2, 3]
show_spread(*all_in_list)
all_in_dict = {"right": 30, "left": 10, "middle": 20}
show_spread(**all_in_dict)
left 1 middle 2 right 3
left 10 middle 20 right 30
Shape that uses methods defined in derived classesclass Shape:
def __init__(self, name):
self.name = name
def perimeter(self):
raise NotImplementedError("perimeter")
def area(self):
raise NotImplementedError("area")
def density(self, weight):
return weight / self.area()
examples = [Square("sq", 3), Circle("ci", 2)]
for ex in examples:
n = ex.name
d = ex.density(5)
print(f"{n}: {d:.2f}")
sq: 0.56
ci: 0.40
def shape_density(thing, weight):
return weight / call(thing, "area")
Shape = {
"density": shape_density,
"_classname": "Shape",
"_parent": None
}
def call(thing, method_name, *args):
method = find(thing["_class"], method_name)
return method(thing, *args)
def find(cls, method_name):
while cls is not None:
if method_name in cls:
return cls[method_name]
cls = cls["_parent"]
raise NotImplementedError("method_name")
examples = [square_new("sq", 3), circle_new("ci", 2)]
for ex in examples:
n = ex["name"]
d = call(ex, "density", 5)
print(f"{n}: {d:.2f}")
sq: 0.56
ci: 0.40
def shape_new(name):
return {
"name": name,
"_class": Shape
}
Shape = {
"density": shape_density,
"_classname": "Shape",
"_parent": None,
"_new": shape_new
}
def square_new(name, side):
return make(Shape, name) | {
"side": side,
"_class": Square
}
Square = {
"perimeter": square_perimeter,
"area": square_area,
"_classname": "Square",
"_parent": Shape,
"_new": square_new
}
examples = [make(Square, "sq", 3), make(Circle, "ci", 2)]
for ex in examples:
n = ex["name"]
d = call(ex, "density", 5)
print(f"{n}: {d:.2f}")
sq: 0.56
ci: 0.40