Python's Class Development Toolkit by Raymond Hettinger
Short but thorough tutorial on the Python's built-in toolset for creating classes. We look at commonly encountered challenges and how to solve them using Python.
whoami
• Python
Core
Developer
•
Buil;ns:
set(),
frozenset(),
sorted(),
reversed(),
enumerate(),
any(),
all()
and
the
python3
version
of
zip()
•
Standard
library:
collec;ons,
itertools,
lru_cache
•
Language
features:
key-‐func;ons,
and
generator
expressions
•
Op;miza;ons:
peephole
op;mizer,
length-‐hint,
fast
sum,
etc.
• Python
Instructor
–
Adconion,
Cisco,
HP,
EBay,
Paypal,
…
• Python
evangelist
and
former
PSF
Board
Member
id
-‐u
Our
plan
• Learn
Python’s
development
toolkit
• See
the
various
ways
users
will
exercise
your
classes
in
unexpected
ways
• Have
a
liUle
fun
teasing
the
followers
of
the
Lean
Startup
Methodology
Agile
Methodology
• Out
with
waterfall:
Design
Code
Test
Ship
• In
with:
Tight
itera;ons
• Let
liUle
bits
of
design,
coding,
and
tes;ng
inform
later
bits
of
design,
coding
and
tes;ng
• The
core
idea
is
iterate
and
adapt
quickly
Lean
Startup
Methodology
• Out
with:
Raise
capital,
spend
it,
go
to
market,
then
fail
• In
with:
Ship
early,
get
customer
feedback,
pivot,
and
iterate
• Lean
Startup
==
Agile
applied
to
businesses
Regular
method
class Circle(object): 'An advanced circle analytic toolkit' def __init__(self, radius): self.radius = radius def area(self): 'Perform quadrature on a shape of uniform radius' return 3.14 * self.radius ** 2.0 Regular
methods
have
“self”
as
first
argument.
Hmm,
what
about
the
3.14?
Modules
for
code
reuse
import math # module for code reuse class Circle(object): 'An advanced circle analytic toolkit' def __init__(self, radius): self.radius = radius def area(self): 'Perform quadrature on a shape of uniform radius' return math.pi * self.radius ** 2.0
Class
variables
for
shared
data
import math class Circle(object): 'An advanced circle analytic toolkit' version = '0.1' # class variable def __init__(self, radius): self.radius = radius def area(self): 'Perform quadrature on a shape of uniform radius' return math.pi * self.radius ** 2.0
Minimum
viable
product:
Ship
it!
# Tutorial print 'Circuituous version', Circle.version c = Circle(10) print 'A circle of radius', c.radius print 'has an area of', c.area() print
First
customer:
Academia
from random import random, seed seed(8675309) print 'Using Circuituous(tm) version', Circle.version n = 10 circles = [Circle(random()) for i in xrange(n)] print 'The average area of', n, 'random circles' avg = sum([c.area() for c in circles]) / n print 'is %.1f' % avg print
Second
customer:
Rubber
sheet
company
cuts = [0.1, 0.7, 0.8] circles = [Circle(r) for r in cuts] for c in circles: print 'A circlet with with a radius of', c.radius print 'has a perimeter of', c.perimeter() print 'and a cold area of', c.area() c.radius *= 1.1 print 'and a warm area of', c.area() print Hmm,
how
do
we
feel
about
exposing
the
radius
aUribute?
Third
customer:
Na;onal
;re
chain
class Tire(Circle): 'Tires are circles with a corrected perimeter' def perimeter(self): 'Circumference corrected for the rubber' return Circle.perimeter(self) * 1.25 t = Tire(22) print 'A tire of radius', t.radius print 'has an inner area of', t.area() print 'and an odometer corrected perimeter of', print t.perimeter() print
Next
customer:
Na;onal
graphics
company
bbd = 25.1 c = Circle(bbd_to_radius(bbd)) print 'A circle with a bbd of 25.1' print 'has a radius of', c.radius print 'an an area of', c.area() print The
API
is
awkward.
A
converter
func;on
is
always
needed.
Perhaps
change
the
constructor
signature?
Client
code:
Na;onal
graphics
company
c = Circle.from_bbd(25.1) print 'A circle with a bbd of 25.1' print 'has a radius of', c.radius print 'an an area of', c.area() print
It
should
also
work
for
subclasses
class Tire(Circle): 'Tires are circles with a corrected perimeter' def perimeter(self): 'Circumference corrected for the rubber' return Circle.perimeter(self) * 1.25 t = Tire.from_bbd(45) print 'A tire of radius', t.radius print 'has an inner area of', t.area() print 'and an odometer corrected perimeter of', print t.perimeter() print Hmm,
this
code
doesn’t
work.
New
customer
request:
add
a
func;on
def angle_to_grade(angle): 'Convert angle in degree to a percentage grade' return math.tan(math.radians(angle)) * 100.0 Will
this
also
work
for
the
Sphere
class
and
the
Hyperbolic
class?
Can
people
even
find
this
code?
Move
func;on
to
a
regular
method
class Circle(object): 'An advanced circle analytic toolkit' version = '0.4b' def __init__(self, radius): self.radius = radius def angle_to_grade(self, angle): 'Convert angle in degree to a percentage grade' return math.tan(math.radians(angle)) * 100.0 Really?
You
have
to
create
an
instance
just
to
call
func;on?
Well,
findability
has
been
improved
and
it
won’t
be
called
in
the
wrong
context.
Move
func;on
to
a
sta;c
method
class Circle(object): 'An advanced circle analytic toolkit' version = '0.4' def __init__(self, radius): self.radius = radius @staticmethod # attach functions to classes def angle_to_grade(angle): 'Convert angle in degree to a percentage grade' return math.tan(math.radians(angle)) * 100.0
Client
code:
Trucking
company
print 'A inclinometer reading of 5 degrees' print 'is a %0.1f%% grade.' % Circle.angle_to_grade(5) print Nice,
clean
call.
No
instance
is
required.
The
correct
context
is
present.
The
method
is
findable.
Problem
with
the
;re
company
class Tire(Circle): 'Tires are circles with an odometer corrected perimeter' def perimeter(self): 'Circumference corrected for the rubber' return Circle.perimeter(self) * 1.25
Tire
company
adopts
the
same
strategy
class Tire(Circle): 'Tires are circles with an odometer corrected perimeter‘ def perimeter(self): 'Circumference corrected for the rubber' return Circle.perimeter(self) * 1.25 _perimeter = perimeter
Class
local
reference
using
the
double
underscore
class Circle(object): 'An advanced circle analytic toolkit' version = '0.5' def __init__(self, radius): self.radius = radius def area(self): p = self.__perimeter() r = p / math.pi / 2.0 return math.pi * r ** 2.0 def perimeter(self): return 2.0 * math.pi * self.radius
Government
request:
ISO-‐22220
• We
insist
on
one
“liUle
change”
• You’re
not
allowed
to
store
the
radius
• You
must
store
the
diameter
instead!
How
hard
could
this
be?
Just
write
some
geUer
and
seUer
methods.
Government
request:
ISO-‐22220
class Circle(object): 'An advanced circle analytic toolkit' version = '0.6' def __init__(self, radius): self.radius = radius def get_radius(self): 'Radius of a circle' return self.diameter / 2.0 def set_radius(self, radius): self.diameter = radius * 2.0 Oh
no,
this
is
going
to
be
terrible!
I
wish
that
all
aUribute
access
would
magically
transform
to
these
method
calls.
User
request:
Many
circles
n = 10000000 seed(8675309) print 'Using Circuituous(tm) version', Circle.version circles = [Circle(random()) for i in xrange(n)] print 'The average area of', n, 'random circles' avg = sum([c.area() for c in circles]) / n print 'is %.1f' % avg print I
sense
a
major
memory
problem.
Circle
instances
are
over
300
bytes
each!
Summary:
Toolset
for
New-‐Style
Classes
1. Inherit
from
object().
2. Instance
variables
for
informa;on
unique
to
an
instance.
3. Class
variables
for
data
shared
among
all
instances.
4. Regular
methods
need
“self”
to
operate
on
instance
data.
5. Thread
local
calls
use
the
double
underscore.
Gives
subclasses
the
freedom
to
override
methods
without
breaking
other
methods.
6. Class
methods
implement
alterna;ve
constructors.
They
need
“cls”
so
they
can
create
subclass
instances
as
well.
7. Sta;c
methods
aUach
func;ons
to
classes.
They
don’t
need
either
“self”
or
“cls”.
Sta;c
methods
improve
discoverability
and
require
context
to
be
specified.
8. A
property()
lets
geUer
and
seUer
methods
be
invoked
automa;cally
by
aUribute
access.
This
allows
Python
classes
to
freely
expose
their
instance
variables.
9. The
“__slots__”
variable
implements
the
Flyweight
Design
PaUern
by
suppressing
instance
dic;onaries.