PyCharm Refactoring Tutorial

This tutorial shows some refactorings available in PyCharm, using the example of a simple class that makes use of the rational numbers.

Prerequisites

Make sure that the following prerequisites are met:

• You are working with PyCharm version 2016.2 or later.

• A project is already created.

Preparing an example

Create a Python file rational.py in your project and add the following code:

from collections import namedtuple class Rational(namedtuple('Rational', ['num', 'denom'])): def __new__(cls, num, denom): if denom == 0: raise ValueError('Denominator cannot be null') if denom < 0: num, denom = -num, -denom return super().__new__(cls, num, denom) def __str__(self): return '{}/{}'.format(self.num, self.denom)

Simplifying rational number

Let's simplify a rational number by dividing numerator and denominator by the greatest common divisor:

from collections import namedtuple class Rational(namedtuple('Rational', ['num', 'denom'])): def __new__(cls, num, denom): if denom == 0: raise ValueError('Denominator cannot be null') if denom < 0: num, denom = -num, -denom x = abs(num) y = abs(denom) while x: x, y = y % x, x factor = y return super().__new__(cls, num // factor, denom // factor) def __str__(self): return '{}/{}'.format(self.num, self.denom)

Extracting a method

Now, let's extract the search for a greatest common divisor to a separate method. To do that, select the statements

x = abs(num) y = abs(denom) while x: x, y = y % x, x factor = y

and press Ctrl+Alt+M. In the dialog box that opens type the method name gcd and then click OK:

@staticmethod def gcd(denom, num): x = abs(num) y = abs(denom) while x: x, y = y % x, x factor = y return factor

Inlining a local variable and changing method signature

Let's get rid of the variable factor, by using Inline variable refactoring. To do that, place the caret at the variable and press Ctrl+Alt+N. All the detected factor variables are inlined.

Next, change the parameter names using Change signature. To do that, place the caret at the method declaration line and press Ctrl+F6. In the dialog that opens, rename the parameters denom and num to x and y respectively, and click to change the order of parameters.

You end up with the following code:

@staticmethod def gcd(x, y): x = abs(x) y = abs(y) while x: x, y = y % x, x return y

Using quick fix

Now, let's convert the existing static method to a function. To do that, press Alt+Enter, from the suggestion list choose Convert static method to function and press Enter:

from collections import namedtuple class Rational(namedtuple('Rational', ['num', 'denom'])): def __new__(cls, num, denom): if denom == 0: raise ValueError('Denominator cannot be null') if denom < 0: num, denom = -num, -denom factor = gcd(num, denom) return super().__new__(cls, num // factor, denom // factor) def __str__(self): return '{}/{}'.format(self.num, self.denom) def gcd(x, y): x = abs(x) y = abs(y) while x: x, y = y % x, x return y

Moving the function to another file

Now, we'll move the function to a separate file and add an import statement. To do that, place the caret at the function gcd declaration and press F6. In the dialog that opens specify the fully qualified path of the destination file util.py. This file does not exist, but it is created automatically:

def gcd(x, y): x = abs(x) y = abs(y) while x: x, y = y % x, x return y

The import statement is also added automatically. Thus the file rational.py looks as follows:

from collections import namedtuple from util import gcd class Rational(namedtuple('Rational', ['num', 'denom'])): def __new__(cls, num, denom): if denom == 0: raise ValueError('Denominator cannot be null') if denom < 0: num, denom = -num, -denom factor = gcd(num, denom) return super().__new__(cls, num // factor, denom // factor) def __str__(self): return '{}/{}'.format(self.num, self.denom)

Further changes of the class Rational

Next, let us add declarations of the magic methods for the addition/subtraction operations on the objects of the class Rational:

from collections import namedtuple from util import gcd class Rational(namedtuple('Rational', ['num', 'denom'])): def __new__(cls, num, denom): if denom == 0: raise ValueError('Denominator cannot be null') factor = gcd(num, denom) if denom < 0: num, denom = -num, -denom return super().__new__(cls, num // factor, denom // factor) def __str__(self): return '{}/{}'.format(self.num, self.denom) def __add__(self, other): if isinstance(other, int): other = Rational(other, 1) if isinstance(other, Rational): new_num = self.num * other.denom + other.num * self.denom new_denom = self.denom * other.denom return Rational(new_num, new_denom) return NotImplemented def __neg__(self): return Rational(-self.num, self.denom) def __radd__(self, other): return self + other def __sub__(self, other): return self + (-other) def __rsub__(self, other): return -self + other

Extracting method and using a quick fix

Next, we'll extract an expression Rational(other, 1) into a separate method. To do that, place the caret at the aforementioned expression, press Ctrl+Alt+M and in the dialog box that opens, type the new method name from_int.

Finally, place the caret at the method from_int declaration, press Alt+Enter, select Make method static from the suggestion list, and then press Enter:

@staticmethod def from_int(other): return Rational(other, 1)

Finally, let's change the name of the parameter other to number. To do that, place the caret on the parameter and press Shift+F6.

Extracting a superclass

Next, we'll move the implementations of the methods __radd__, __sub__ and __rsub__ into a superclass. Also, we'll make the methods __neg__ and __add__ abstract.

This is how it's done... Place the caret at the class Rational declaration, on the context menu point to Refactor | Extract and choose Superclass.... Next, in the dialog box that opens, specify the name of the superclass (here it's AdditiveMixin) and select the methods to be added to the superclass. For the methods __neg__ and __add__, select the checkboxes in the column Make abstract.

End up with the following code:

from abc import abstractmethod, ABCMeta from collections import namedtuple from util import gcd class AdditiveMixin(metaclass=ABCMeta): @abstractmethod def __add__(self, other): pass @abstractmethod def __neg__(self): pass def __radd__(self, other): return self + other def __sub__(self, other): return self + (-other) def __rsub__(self, other): return -self + other class Rational(namedtuple('Rational', ['num', 'denom']), AdditiveMixin): def __new__(cls, num, denom): if denom == 0: raise ValueError('Denominator cannot be null') factor = gcd(num, denom) if denom < 0: num, denom = -num, -denom return super().__new__(cls, num // factor, denom // factor) def __str__(self): return '{}/{}'.format(self.num, self.denom) def __add__(self, other): if isinstance(other, int): other = self.from_int(other) if isinstance(other, Rational): new_num = self.num * other.denom + other.num * self.denom new_denom = self.denom * other.denom return Rational(new_num, new_denom) return NotImplemented def from_int(self, number): return Rational(number, 1) def __neg__(self): return Rational(-self.num, self.denom)