Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

PyCon 2013

March 16, 2013
Tweet

More Decks by PyCon 2013

Other Decks in Programming

Transcript

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

    View Slide

  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  

    View Slide

  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  

    View Slide

  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  

    View Slide

  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  

    View Slide

  6. Gradua;on  dilemma  
    •  Get  haircut  and  get  a  real  job  
    •  Be  CEO  of  our  own  startup  
     
     
    I  say,  “Go  for  it!  

    View Slide

  7. Company  name  and  elevator  pitch  
    ''' Circuitous, LLC –
    An Advanced Circle Analytics Company
    '''

    View Slide

  8. Start  coding:    Circle  class  
    class Circle:
    'An advanced circle analytic toolkit'
    Hmm,  is  that  really  advanced  technology?  

    View Slide

  9. New  style  classes  
    class Circle(object): # new-style class
    'An advanced circle analytic toolkit'
    Yes,  this  is  advanced  technology!  

    View Slide

  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.  

    View Slide

  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?  

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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?  

    View Slide

  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

    View Slide

  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?  

    View Slide

  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'])

    View Slide

  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)

    View Slide

  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

    View Slide

  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.  

    View Slide

  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)

    View Slide

  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?  

    View Slide

  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.  

    View Slide

  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

    View Slide

  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.  

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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.  

    View Slide

  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.  

    View Slide

  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

    View Slide

  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!  

    View Slide

  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

    View Slide

  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.  

    View Slide

  40. Q  &  A  

    View Slide