Share
Embed
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.
PyCon 2013
March 16, 2013
Transcript
40 slides
-
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 -
Gradua;on dilemma • Get haircut and get a real
job • Be CEO of our own startup I say, “Go for it! -
Company name and elevator pitch ''' Circuitous, LLC –
An Advanced Circle Analytics Company ''' -
Start coding: Circle class class Circle: 'An advanced
circle analytic toolkit' Hmm, is that really advanced technology? -
New style classes class Circle(object): # new-style class 'An
advanced circle analytic toolkit' Yes, this is advanced technology! -
Ini;alize instance variables class Circle(object): 'An advanced circle analytic
toolkit' def __init__(self, radius): self.radius = radius # instance variable Init isn’t a constructor. It’s job is to ini;alize the instance variables. -
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 -
Next customer wants a perimeter method class Circle(object):
'An advanced circle analytic toolkit' version = '0.2' def __init__(self, radius): self.radius = radius def area(self): return math.pi * self.radius ** 2.0 def perimeter(self): return 2.0 * math.pi * self.radius -
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? -
Need an alterna;ve constructor print datetime(2013, 3, 16) print
datetime.fromtimestamp(1363383616) print datetime.fromordinal(734000) print datetime.now() print dict.fromkeys(['raymond', 'rachel', 'matthew']) -
Class methods create alterna;ve constructors class Circle(object): 'An
advanced circle analytic toolkit' version = '0.3' def __init__(self, radius): self.radius = radius def area(self): return math.pi * self.radius ** 2.0 def perimeter(self): return 2.0 * math.pi * self.radius @classmethod # alternative constructor def from_bbd(cls, bbd): 'Construct a circle from a bounding box diagonal' radius = bbd / 2.0 / math.sqrt(2.0) return Circle(radius) -
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. -
Alterna;ve constructors need to an;cipate subclassing class Circle(object):
'An advanced circle analytic toolkit' version = '0.3' def __init__(self, radius): self.radius = radius def area(self): return math.pi * self.radius ** 2.0 def perimeter(self): return 2.0 * math.pi * self.radius @classmethod # alternative constructor def from_bbd(cls, bbd): 'Construct a circle from a bounding box diagonal' radius = bbd / 2.0 / math.sqrt(2.0) return cls(radius) -
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. -
Government request: ISO-‐11110 class Circle(object): 'An advanced circle
analytic toolkit' version = '0.5b' 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 -
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 -
Class local reference: keep a spare copy class Circle(object):
'An advanced circle analytic toolkit' version = '0.5b' 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 _perimeter = perimeter -
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. -
Convert aUribute access to method access: property class
Circle(object): 'An advanced circle analytic toolkit' version = '0.6' def __init__(self, radius): self.radius = radius @property # convert dotted access to method calls def radius(self): 'Radius of a circle' return self.diameter / 2.0 @radius.setter def radius(self, radius): self.diameter = radius * 2.0 -
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! -
Flyweight design paUern: Slots class Circle(object): 'An advanced
circle analytic toolkit' # flyweight design pattern suppresses # the instance dictionary __slots__ = ['diameter'] version = '0.7' def __init__(self, radius): self.radius = radius @property # convert dotted access to method calls def radius(self): return self.diameter / 2.0 @radius.setter def radius(self, radius): self.diameter = radius * 2.0 -
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.