Effective Python - 59 Specific Ways to Write Better Python.

Chapter 4 - Metaclasses and Attributes

Book by Brett Slatkin. Summary notes by Tyler Banks.

  • Metaclasses let you intercept Python's class statement and provide special behavior each time a class is defined

Item 29: Use Plain Attributes Instead of Get and Set Methods

  • Conventionally other programming languages use getters and setters
In [4]:
class OldResistor(object):
    def __init__(self, ohms):
        self._ohms = ohms
    def get_ohms(self):
        return self._ohms
    def set_ohms(self, ohms):
        self._ohms = ohms
        
r0 = OldResistor(50e3)
print('Before: %5r' % r0.get_ohms())
r0.set_ohms(10e3)
print('After: %5r' % r0.get_ohms())
Before: 50000.0
After: 10000.0
  • Python has a more pythonic way of doing this
  • You'll implement the @property decorator and setter attribute
In [6]:
class Resistor(object):
    def __init__(self, ohms):
        self.ohms = ohms
        self.voltage = 0
        self.current = 0

class VoltageResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)
        self._voltage = 0
    @property
    def voltage(self):
        return self._voltage
    @voltage.setter
    def voltage(self, voltage):
        self._voltage = voltage
        self.current = self._voltage / self.ohms
        
r2 = VoltageResistance(1e3)
print('Before: %5r amps' % r2.current)
r2.voltage = 10
print('After: %5r amps' % r2.current)
Before:     0 amps
After:  0.01 amps
In [9]:
# Using setters to check values and throw exceptions
class BoundedResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)
    @property
    def ohms(self):
        return self._ohms
    @ohms.setter
    def ohms(self, ohms):
        if ohms <= 0:
            raise ValueError('%f ohms must be > 0' % ohms)
        self._ohms = ohms
        
r3 = BoundedResistance(1e3)
r3.ohms = 0
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-16fe06f3ea7a> in <module>()
     13 
     14 r3 = BoundedResistance(1e3)
---> 15 r3.ohms = 0

<ipython-input-9-16fe06f3ea7a> in ohms(self, ohms)
      9     def ohms(self, ohms):
     10         if ohms <= 0:
---> 11             raise ValueError('%f ohms must be > 0' % ohms)
     12         self._ohms = ohms
     13 

ValueError: 0.000000 ohms must be > 0
  • You could also use @property to make attributes immutable
  • Define new class interfaces using simple public attributes instead of setters and getters

Item 30: Consider @property Instead of Refactoring Attributes

  • Use @property to give existing instance attributes new functionallity
  • Make incremental progress toward better data models by using @property
  • Consider refactoring a class and all call sites when you find yourself using @property too heavily

Item 31: Use Descriptors for Reusable @property Methods

  • Problem with @property is that the methods it decorates can't be reused for multiple attributes of the same class. They also can't be reused by unrelated classes
  • Ex:
In [11]:
class Homework(object):
    def __init__(self):
        self._grade = 0
    @property
    def grade(self):
        return self._grade
    @grade.setter
    def grade(self, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        self._grade = value
In [12]:
person = Homework()
person.grade = 100
In [ ]: