6
\$\begingroup\$

I have a class that imports the FX Rates produced by another department.

This works as intended, which is to return a Pandas DataFrame with all FX Rates by month.

The DataFrame it returns is then used by another 5 classes, that basically import other files and do some formatting and calculations with their own columns, always using the FX Rates from the FxRates class.

I run this code in a Jupyter Notebook.

I want to know if:

  1. Code is refactored enough or over-refactored
  2. Is it good practice to update the instance variables as I have done so?
  3. Is there anything else that stands out as being bad practice?
class FxRates:
 def __init__(self, reporting_date):
 self.reporting_date = reporting_date
 self.path = self.get_path()
 def get_path(self):
 """Get path for the Fx Rates."""
 content_list = listdir(os.path.join(os.getcwd(), 'Data'))
 file_path = os.path.join(
 os.getcwd(),
 'Data',
 list(filter(lambda x: 'FX rates' in x, content_list))[0]
 )
 return file_path
 
 def reporting_date_check(self):
 """
 Check if date input follows the criteria below:
 31/12/yyyy, 31/03/yyyy, 30/06/yyyy, 30/09/yyyy
 """
 accepted_dates = [
 '31/12',
 '31/03',
 '30/06',
 '30/09'
 ]
 
 # Check if first 5 characters match accepted_dates
 if self.reporting_date[:5] in accepted_dates:
 reporting_date = pd.to_datetime(self.reporting_date,
 dayfirst=True)
 self.reporting_date = reporting_date
 else:
 # If not, raise ValueError
 raise ValueError(
 """reporting_date does not match one of the following:
 31/12/yyyy, 31/03/yyyy, 30/06/yyyy, 30/09/yyyy"""
 )
 def import_excel_rates(self):
 """Import FX Rates in Excel file from Group."""
 
 rates = pd.read_excel(self.path,
 sheet_name='historic rates',
 skiprows=2,
 header=None,
 usecols='B:O',
 skipfooter=1)
 
 return rates
 
 def EWI_check(self, rates):
 """
 Check if the reporting month already has FX Rates defined.
 If not, copy FX Rates from previous month.
 """
 # For EWI we need to use FX Rates from 1 month before
 if pd.isnull(rates.iloc[0, self.reporting_date.month]):
 print("""
 \n########## Warning ##########:
 \nThere are no FX Rates for {0}/{1}.
 \nFX Rates being copied from {2}/{3}.\n""".format(
 rates.columns[self.reporting_date.month],
 self.reporting_date.year,
 rates.columns[self.reporting_date.month - 1],
 self.reporting_date.year
 ))
 # Copy FX Rates from previous month
 rates.iloc[:, self.reporting_date.month] = \
 rates.iloc[:, self.reporting_date.month - 1]
 else:
 pass
 return rates
 def import_rates(self):
 """
 Import Group Fx rates into a Pandas Dataframe
 """
 # Check if reporting date is correct
 self.reporting_date_check()
 # Import FX Rates in Excel file
 rates = self.import_excel_rates()
 # Set column headers manually
 rates.columns = ['ISO Code',
 'December ' + str(self.reporting_date.year - 1),
 'January',
 'February',
 'March',
 'April',
 'May',
 'June',
 'July',
 'August',
 'September',
 'October',
 'November',
 'December']
 # Set ISO Code as Index
 rates.index = rates['ISO Code'].values
 rates.drop('ISO Code', axis=1, inplace=True)
 # Check if we have FX Rates for the reporting month
 # If not, copy from last month
 return self.EWI_check(rates)
 
asked Jun 15, 2020 at 20:24
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

A note on terminology

"Refactored enough" is only meaningful relative to the code's current state and how it was before; but "over-refactored" is kind of meaningless. I guess the only time that idea could even be applicable is if refactoring occupied too much time or too many corporate resources. Maybe you mean over-abstracted, but that's conjecture. Anyway.

Type hints

reporting_date could stand to get a type hint, likely : str given your later usage of this variable.

Pathlib

Consider replacing listdir, os.path.join and os.getcwd with pathlib.Path equivalents, which are typically better-structured and have nice object representations for paths.

Parsing

Don't store the string representation of reporting_date. Do something in the constructor like self.reporting_date = self.parse_date(reporting_date), where the latter is a static method to replace your current reporting_date_check. This method would not mutate member variables and would simply return the date once it's figured that out.

Sets

 accepted_dates = [
 '31/12',
 '31/03',
 '30/06',
 '30/09'
 ]

should be a class static, initialized via set literal - something like

class FxRates:
 ACCEPTED_DATES = {
 '31/12',
 '31/03',
 '30/06',
 '30/09',
 }

That said, the approach is a little backwards. You should not do string comparison on the string-formatted date. Parse it first, then do validation on its integer parts after. The accepted dates above can turn into a set of 2-tuples, (day, month).

Heredocs

This:

 print("""
 \n########## Warning ##########:
 \nThere are no FX Rates for {0}/{1}.
...

is problematic. You're packing a bunch of whitespace in there that you shouldn't. One solution is to move the string to a global constant to avoid the indentation; you should also replace the explicit \n with actual newlines in the string. Another solution is to keep the text where it is but replace it with a series of implicit-concatenated strings, one per line, i.e.

print(
 "########## Warning ##########:\n"
 "There are no FX Rates for {0}/{1}.\n"
# ...
answered Jun 17, 2020 at 4:14
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the answer! And about the refactoring, I definitely meant "over-abstracted". I've read so much about functions that "do one thing, and one thing only" that now I'm paranoid about putting 2 actions in the same function. So, in your opinion, does the code above have a good balance between what each function does? \$\endgroup\$ Commented Jun 18, 2020 at 7:06
  • \$\begingroup\$ Seems pretty good to me \$\endgroup\$ Commented Jun 18, 2020 at 12:11

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.