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.
Make sure that the following prerequisites are met:
You are working with PyCharm version 2016.2 or later.
A project is already created.
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)
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)
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 . In the dialog 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
Let's get rid of the variable factor
, by using Inline variable refactoring. To do that, place the caret at the variable and press . 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 . 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
Now, let's convert the existing static method to a function. To do that, press , from the suggestion list choose Convert static method to function and press :
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
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 . 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)
Next, let's 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
Next, we'll extract an expression Rational(other, 1)
into a separate method. To do that, place the caret at the aforementioned expression, press and in the dialog that opens, type the new method name from_int
.
Finally, place the caret at the method from_int
declaration, press , select Make method static from the suggestion list, and then press :
@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 at the parameter and press .
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 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. For more information, refer to Extract superclass.
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)