Using Decorators

Decorators is a Python-specific feature, virtue of which you can define a function called a decorator function. This decorator function takes an object, manipulates it using its own object and returns this latter object. For example, in the event of decorating a function:

Significance of @ Notation

Any call to functionToBeDecorated() translates to decoratorFunction() with functionToBeDecorated as its argument i.e. functionToBeDecorated() becomes decoratorFunction(functionToBeDecorated)(). For example:

Understanding Different Types of Attributes in Object Oriented Python

There are 4 conventions followed while naming variables in Python:

Dunder attributes/Magic attributes such as __doc__, __name__ etc. are named with double underscores wrapped around the attribute name. It is general consensus to use the dunder attributes that Python is shipped with, and not create new ones.

Variables/Attributes intended for private use by the class or module are prefixed with a single underscore e.g. _attr. Note that this is merely a convention, naming an attribute this way does not make it unusable by external classes and modules. It is merely a hint for fellow developers.

Variables/Attributes intended for public use should be named in lower case i.e. variable_name. Note that this is also a convention only, you need not follow it strictly. You can opt for camelCasing as well, like I have done in examples all across the djangoSpin website. The important thing is that you know that this is a popular convention, and while writing professional Python code, you should use lower case variable names with underscores rather than camel casing.

Variables/Attributes that are intended for private use by the class or module and not open for subclassing are named with double underscores in the beginning i.e. __variable_name. These can be accessed using a special syntax as shown below.

Defining Class Methods in Python: The @classmethod decorator

In Object Oriented Python, an instance method perceives its argument as the object on which the method is being called. It can operate on class attributes as well as instance attributes. Here's an example:

@classmethod: A method following the @classmethod decorator will perceive its first argument to be the class, and not the instance. The @classmethod decorator denotes a method that operates on the class attributes rather than instance attributes. Let's segregate the initializiation and count increment operation into two different methods to demonstrate this.

Note that the class method can be invoked by the object as well. We can call it using the Toy.incrementCount() notation as well, but that would defeat the purpose of the example. Also, the argument can be called anything apart from 'cls', it makes more sense to call it something that is synonymous to the word 'class' (remember, class is a keyword).

Defining Static Methods in Python: The @staticmethod decorator

In Object Oriented Python, an instance method perceives its argument as the object on which the method is being called. It can operate on class attributes as well as instance attributes. I'll cite the example I used while demonstrating the @classmethod decorator.

@staticmethod: A method following the @staticmethod decorator will NOT perceive its first argument to be the class or the instance. Rather, it will take the first argument to mean a regular positional argument. This is used to denote utility methods, which belong in the class code, but don't operate on the instance or the class. In the below example, checkForName() performs a validation, something like a utility method. It does not operate the instance or the class, and hence neither of these needs to be passes as an argument to it.

>>> class Toy:
'''Toy class'''
count = 0
def __init__(self, name, color):
'''sets instance attributes to provided values; increments counter and prints it.'''
self.name = name
self.color = color
self.checkForName(self.name)
@staticmethod
def checkForName(name):
if name.startswith('w') or name.startswith('W'):
print("Hey! This is a random check to see if your name begins with 'W', and it does!")
else:
print("Oh no! Your name does not start with W, you just missed out on a goodie!")
>>> woody = Toy('Woody', 'Brown')
Hey! This is a random check to see if your name begins with 'W', and it does!
>>> buzz = Toy('Buzz Lightyear', 'White & Purple')
Oh no! Your name does not start with W, you just missed out on a goodie!

Enforcing Encapsulation: The property class

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:

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:

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.

Read-only attribute

To make an attribute read-only, only specify the getter method of the attribute.

You can make any data structure from any other data structure using the builtin constructors list(), tuple(), dict(), set(), frozenset() of their respective classes. This interchanging operation is incredibly helpful in certain situations e.g. while reversing a tuple, while removing duplicates from a list, extracting keys and values from a dictionary, making a set immutable by initializing a frozenset from it etc. Expand the following for examples.

Using builtin chr() & ord() functions

The ord() function takes a single character (special ones too) and returns its Unicode value. Unicode is a collection of codes for more than 1,20,000 characters covering letters, numbers in a variety of languages, including symbols. Unicode values for letters a through z are 97 to 122 & A through Z are 65 to 90. It is inverse of the builtin chr() function.

The chr() function takes a Unicode value and returns the symbol or letter represented by the provided code in the Unicode dictionary. It is inverse of the builtin ord() function.

>>> ord('a')
97
>>> chr(97)
'a'

Sorting an iterable: sorted()

Python's builtin sorted() function sorts the elements an iterable object in ascending order and returns a list of the ordered elements.

This is a rudimentary application of the sorted() function. It has two optional keyword arguments: reverse and key. To know more about them, read this piece on the sorted() function.

Raising your own Exceptions: the raise keyword

The raise keyword is used to explicitly raise an exception. This is in contrast to when you make a mistake in your code, because then the exception is raised implicitly. Here, you are deliberately raising an exception. The raise keyword is useful in 2 scenarios, one is for reraising the exception, and other is for using exception for error-checking. Expand the following sourcecode for more on these 2 scenarios and other details related to the raise keyword.

######### RERAISING AN EXCEPTION: raise ###########
# The raise keyword in this situation is found in the except clause, as a standalone keyword. This is used when you want to do some processing in the except clause before you re-raise the most recently caught exception, also known as active exception.
>>> import io
>>> try:
fh = open('someExistentFile.txt', 'r')
fh.write("Attempting to write to a file opened in read-only mode.")
except io.UnsupportedOperation:
fh.close()
print("File closed?", fh.closed)
raise
File closed? True
Traceback (most recent call last):
File "<pyshell#292>", line 3, in <module>
fh.write("Attempting to write to a file opened in read-only mode.")
io.UnsupportedOperation: not writable
######### ERROR CHECKING: raise ExceptionName(“Optional message string.”) ###########
# You can intentionally raise errors as part of error-checking. The raise keyword is followed by the Exception to be thrown if a particular check fails. You can even pass a custom message to provide a situation-specific explanation to the user.
>>> temperature = 450
>>> if temperature > 400:
raise ValueError()
Traceback (most recent call last):
File "<pyshell#257>", line 2, in <module>
raise ValueError()
ValueError
>>>
>>>
>>>
>>> if temperature > 400:
raise ValueError("Temperature greater than 400 degrees!!! CODE GREEN! CODE GREEN!")
Traceback (most recent call last):
File "<pyshell#262>", line 2, in <module>
raise ValueError("Temperature greater than 400 degrees!!! CODE GREEN! CODE GREEN!")
ValueError: Temperature greater than 400 degrees!!! CODE GREEN! CODE GREEN!
# You can choose from common builtin Exceptions and just supply a custom message string, as we did in the above example. And if there is no suitable builtin Exception, you can make your own exception in the following fashion:
############ CREATING AN EXCEPTION OBJECT FROM AN EXISTING EXCEPTION TYPE #############
>>> TemperatureError = ValueError()
>>> raise TemperatureError
Traceback (most recent call last):
File "<pyshell#220>", line 1, in <module>
raise TemperatureError
ValueError
>>> TemperatureError = ValueError("Temperature greater than 400 degrees!!! CODE GREEN! CODE GREEN!")
>>> raise TemperatureError
Traceback (most recent call last):
File "<pyshell#246>", line 1, in <module>
raise TemperatureError
ValueError: Temperature greater than 400 degrees!!! CODE GREEN! CODE GREEN!
>>> def check_temperature(temperature):
if temperature < 45:
raise TemperatureError
print("Temperature under control.")
>>> check_temperature(450)
Traceback (most recent call last):
File "<pyshell#249>", line 1, in <module>
check_temperature(450)
File "<pyshell#248>", line 3, in check_temperature
raise TemperatureError
File "C:\Python34\lib\idlelib\run.py", line 353, in runcode
exec(code, self.locals)
File "<pyshell#246>", line 1, in <module>
raise TemperatureError
ValueError: Temperature greater than 400 degrees!!! CODE GREEN! CODE GREEN!
# Note that even though you are raising TemperatureError, the exception that is raised is ValueError. This is because all Exceptions are actually classes(parent of all these is the class called Exception), and classes can be instantiated i.e. objects can be made out of them. The very first line of the example (TemperatureError = ValueError()) is creating an instance/object called TemperatureError of the class ValueError. If you wish to create your own Exception class, you can do that in the following way. It is a brief write-up, which can be extended based on your knowledge of object-oriented concepts.
############# CREATING CUSTOM EXCEPTION CLASS DERIVED FROM THE PARENT CLASS OF ALL EXCEPTION CLASSES i.e. Exception. ##########
>>> class TemperatureError(Exception): pass
>>> myCustomExceptionObject = TemperatureError("Temperature greater than 400 degrees!!! CODE GREEN! CODE GREEN!")
>>> def check_temperature(temperature):
if temperature > 400:
raise myCustomExceptionObject
print("Temperature under control.")
>>> check_temperature(450)
Traceback (most recent call last):
File "<pyshell#309>", line 1, in <module>
check_temperature(450)
File "<pyshell#308>", line 3, in check_temperature
raise myCustomExceptionObject
TemperatureError: Temperature greater than 400 degrees!!! CODE GREEN! CODE GREEN!
# Note that some other languages like Java use the keyword throw instead of raise for the same functionality.