Python's Class Development Toolkit by Raymond Hettinger

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.

Afcfefa1f067d10bd021de0cc2e5e806?s=128

PyCon 2013

March 16, 2013
Tweet

Transcript

  1. Python's  Class  Development   Toolkit   Raymond  He+nger   @raymondh

     
  2. 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  
  3. 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  
  4. 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  
  5. 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  
  6. Gradua;on  dilemma   •  Get  haircut  and  get  a  real

     job   •  Be  CEO  of  our  own  startup       I  say,  “Go  for  it!  
  7. Company  name  and  elevator  pitch   ''' Circuitous, LLC –

    An Advanced Circle Analytics Company '''
  8. Start  coding:    Circle  class   class Circle: 'An advanced

    circle analytic toolkit' Hmm,  is  that  really  advanced  technology?  
  9. New  style  classes   class Circle(object): # new-style class 'An

    advanced circle analytic toolkit' Yes,  this  is  advanced  technology!  
  10. 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.  
  11. 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?  
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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?  
  18. 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
  19. 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?  
  20. 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'])
  21. 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)
  22. 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
  23. 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.  
  24. 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)
  25. 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?  
  26. 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.  
  27. 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
  28. 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.  
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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.  
  35. 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.  
  36. 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
  37. 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!  
  38. 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
  39. 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.  
  40. Q  &  A