- Enforcing Encapsulation: The property class
- Customizing builtin data types
- Adding arguments to the __init__() of superclass: super()
- Anomaly in defining the base class in Python 2 & 3: New Style Classes and Old Style Classes
Getters and setters in Python can be used to set and get attributes. However, they don't strictly impose encapsulation, since attributes can be get and set without these getter-setter methods as well. Pythonic convention is that the users are trusted to access the attributes via getters-setters rather than mangling them directly. It is the Pythonic way.
An anti-Pythonic way to enforce encapsulation is to use the builtin property class and its decorator @property. The property class returns a property object. It is used to manage attributes. Its constructor takes four arguments:
- getter method of attribute x (keyword argument 'fget', defaults to None)
- setter method of attribute x (keyword argument 'fset', defaults to None)
- deleter method of attribute x (keyword argument 'fdel', defaults to None)
- docstring (help text) of attribute x (keyword argument 'doc', defaults to None)
The help(property) reveals a handy example. Consider a class C with an attribute 'x':
>>> class C(object): def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x x = property(getx, setx, delx, "I'm the 'x' property.") >>> c = C() >>> c.x = 5 # invokes the setter setx() >>> c.x # invokes the getter getx() 5 >>> del c.x # invokes the deleter delx() >>> help(C) # Among other help information | x | I'm the 'x' property.
The @property decorator offers an easy-on-the-eye way of achieving the above functionality:
>>> class C(object): @property def x(self): "I am the 'x' property." return self._x @x.setter def x(self, value): self._x = value @x.deleter def x(self): del self._x # @property implies that the function declared below (x) is passed to the property constructor as its first argument, return value (a property object) of which is assigned to the function itself i.e. x = property(fget = x). @x.setter translates to x = property(fset = x), the x on the right is the function declared after the @x.setter notation. Similarly, @x.deleter translates to x = property(fdel = x). # The 'doc' keyword argument of the property constructor gets set to the docstring of the function after the @property notation. So, in actuality, the @property gets translated to x = property(fget = x, fset = None, fdel = None, doc = "I am the 'x' property.")
Don't get confused by the attribute x and the property object x. The example in the documentation is ambiguous. The property object provides us with a handle to the attribute x. Here's a much cleaner example:
>>> class C(object): @property def propertyObjectOfAttrX(self): "I'm the 'x' property." return self._x @propertyObjectOfAttrX.setter def propertyObjectOfAttrX(self, value): self._x = value @propertyObjectOfAttrX.deleter def propertyObjectOfAttrX(self): del self._x >>> c = C() >>> c.propertyObjectOfAttrX = 5 >>> c.propertyObjectOfAttrX 5 >>> del c.propertyObjectOfAttrX
Note that in order for the @property decorator to work, the names of additional functions (setter and deleter) must be the same as the name of the property object i.e. the getter propertyObjectOfAttrX. Otherwise, you'll get an Attribute Error(s). I'll leave that to you to explore.
To make an attribute read-only, only specify the getter method of the attribute.
>>> class C(object): def __init__(self): self._x = 2 @property def propertyObjectOfAttrX(self): "I'm the 'x' property." return self._x >>> c = C() >>> c.propertyObjectOfAttrX 2 >>> c.propertyObjectOfAttrX = 10 Traceback (most recent call last): c.propertyObjectOfAttrX = 10 AttributeError: can't set attribute
This happens because the @property decorator call translates to propertyObjectOfAttrX = property(fget = propertyObjectOfAttrX, fset = None, fdel = None, doc = "I'm the 'x' property."). Unless the user knows the variable name( i.e. _x), he/she cannot set it. This is how the property objects enforce encapsulation.
str, int, dict, tuple, list, float, bool, set, are aa Python classes. This means that we can subclass them and devise our own classes which behave the same way as the built-in types, and at the same time, give us the option of customize selected functionality.
>>> class MyInt(int): def __add__(self, value): print("Adding...") return int.__add__(self, value) >>> a = MyInt(5) >>> a + 6 Adding... 11
Like all other attributes and methods, the __init__() method of the superclass is inherited by the subclasses. If there is no __init__() method in the subclass, then the __init__() method of the superclass is called when the subclass is instantiated. If the subclass also defines an __init__() method, then it is called instead of the __init__() method of the superclass.
>>> class Automobile(object): def __init__(self, make): self.make = make print("Printing make from inside the base class:", make) def ignition(self): print("Igniting...") >>> class Car(Automobile): def __init__(self, make): self.make = make print("Printing make from inside the derived class:", make) >>> ferrari = Car('Ferrari') Printing make from inside the derived class: Ferrari
Now, consider a situation where you want the __init__() method of your subclasses to have additional functionality along with the functionality already defined in the __init__() method of the superclass. To achieve this, you can use the builtin super() function.
>>> class Automobile(object): def __init__(self, make): self.make = make print("Doing something with the make") def ignition(self): print("Igniting...") >>> class Car(Automobile): def __init__(self, make, horsepower): super(Car, self).__init__(make) self.horsepower = horsepower print("Doing something with the horsepower") >>> ferrari = Car('Ferrari', 3200) Doing something with the make Doing something with the horsepower
super(Car, self).__init__(make) calls the __init__() method of the super class of Car passing the instance as an argument to it, in addition to the positional argument make. We could have easily replaced this statement with Automobile.__init__(self, make), but the super() function helps in decoupling the classes. If tomorrow, the superclass of Car changes from Automobile to, say, Cars, then we will have to change this particular statement inside the Car class as well.
The parent class of each and every class in Python is the class called object. Everything in Python inherits from this class. That is why it is said that everything in Python is an object. You can do a quick >>> object or >>> help(object) or >>> dir(object) to explore this further.
In Python, there are two alternative ways of defining a base class: class BaseClass(object): and class BaseClass:. In Python 3, it does not make any difference which syntax you use. In Python 2, however, it makes a big difference. Python 3 allows you to drop the word object while declaring a base class, and it will treat the base class the same way. But Python 2 does not, which may lead to a problem. This has to do with Old-style classes and New-style classes.
Python 2 has two types of classes: Old-style classes and New-style classes. The latter were introduced in Python 2.2. The difference between Old-style classes and New-style classes is that
- In Old-style classes, obj.__class__ and type(obj) evaluate to different things. obj.__class__ gives the class name, where as type(obj) gives
. This is due to the fact that all old-style objects/instances, irrespective of their classes, are implemented with a single built-in type called instance.
- In New-style classes, obj.__class__ and type(obj) both evaluate to the same thing i.e. the class name.
Apart from this significant difference, there are two additional methods that New-style classes support which Old-style classes don't: mro() and super(). We will get to these later in the chapter.
Python 3 does not support Old-style classes. In fact, there is little chance that you will find a topic in the discussion forums relating to these in Python 3 docs. In Python 2 docs, it lies here.
In Python 2, class BaseClass(object): refer to new-style classes and class BaseClass: refer to old-style classes.
In Python 3, both class BaseClass(object): and class BaseClass: refer to the new-style classes, the latter syntax inherits the object class implicitly.
As you can see, the alternative syntax in Python 3 is the same as the syntax to declare old-style classes in Python 2. Python 2 is still very widely used, such as in Pygame, Web2Py etc. Any attempt to apply the Python 3 class definitions in Python 2 will lead to outdated base objects. And since, this is not a prominently known issue, debugging will be even more difficult.
See also: 50+ Know-How(s) Every Pythonista Must Know