PyCharm Refactoring Tutorial
What this tutorial is about
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:
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
Adding magic methods
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 and choose . 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)
Last modified: 17 March 2022