0

I have the following class:

import pandas as pd


class CashFlowSchedule:
    flows = {}
    annual_growth_rate = None
    df = None

    daterange = None
    months = None
    amount = None
    growth = None
    growth_month = None
    name = None

    def copy(self):
        return CashFlowSchedule(daterange=self.daterange, months=self.months, amount=self.amount, growth=self.growth,
                                growth_month=self.growth_month)

    def render_named_df(self):
        temp = self.df.copy()
        temp.columns = [self.name]
        return temp

    def re_init_df(self):
        self.df = pd.DataFrame([self.flows]).T

    def __init__(self, daterange=pd.date_range('19900101', '20010101'), months=range(1, 13), amount=0, growth=0,
                 growth_month=1, name=None, years=None):

        self.daterange = daterange
        self.months = months
        self.amount = amount
        self.growth = growth
        self.growth_month = growth_month
        self.name = name

        self.annual_growth_rate = growth

        if years is None:
            years = list({x.year: 0 for x in daterange}.keys())

        for dt in daterange:

            if dt.month == growth_month:
                amount *= (1. + self.annual_growth_rate)

            if dt.month in months and dt.year in years:
                self.flows[dt] = amount
            else:
                self.flows[dt] = 0.

        self.df = pd.DataFrame([self.flows]).T

and another class in another module:

class SinglePropertyValuation:
    # descriptive
    desc_zipcode = None
    desc_property_type = None
    desc_units = None
    desc_beds = None
    desc_smooth_annual_expenses = None

    # dates
    date_purchase = None
    date_sale = None
    date_distance_months = None
    date_daterange_ownership = None

    # revenue
    rev_rent = None

    # recurring expenses
    exp_taxes = None
    exp_insurance = None
    exp_maintenance = None
    exp_management = None
    exp_hoa = None

    cash_flows = {}

    monthly_raw_rents = []

    desc_nearby_zips = None

    def establish_rent_cashflows(self, monthly_per_unit_rent, growth_rate=None,
                                 growth_month=None, default_growth_rate=.02):

        self.rev_rent =copy.copy(cfs.CashFlowSchedule(daterange=copy.copy(self.date_daterange_ownership),
                                             amount=monthly_per_unit_rent * self.desc_units, growth=growth_rate,
                                             growth_month=growth_month, name='rev_rent'))    

    def establish_tax_cashflows(self, annual_tax, growth):

        if self.desc_smooth_annual_expenses:
            self.exp_taxes = copy.copy(cfs.CashFlowSchedule(daterange=self.date_daterange_ownership, amount=annual_tax / 12.,
                                                  growth=growth, growth_month=5, name='exp_taxes'))
        else:
            self.exp_taxes = copy.copy(cfs.CashFlowSchedule(daterange=self.date_daterange_ownership, amount=annual_tax,
                                                  months=[4],
                                                  growth=growth, growth_month=5, name='exp_taxes'))

    def establish_vacancy_data(self, override_vacancy_rate=None):

        self.exp_vacancy_data = self.rev_rent.copy()
        self.exp_vacancy_data.flows = {dt: self.exp_vacancy_data.flows[dt] * override_vacancy_rate for dt in
                                       self.exp_vacancy_data.flows.keys()}

        self.exp_vacancy_data.re_init_df()
        self.exp_vacancy_data.name = 'exp_vacancy'

When I call:

spv = SinglePropertyValuation()
spv.establish_rent_cashflows(monthly_per_unit_rent=1063, growth_rate=.01)
spv.establish_vacancy_data(override_vacancy_rate=1. / 24.)
spv.establish_tax_cashflows(annual_tax=8000., growth=.01)

My spv.rev_rent.flows dictionary instantiates properly on the first call, and does not change on my spv.establish_vacancy_data call but when spv.establish_tax_cashflows gets triggered, spv.rev_rent.flows CHANGES.

How do I correct this behavior? By googling and trial and error I realize this is a common error but I do not see how to fix mine, as most answers suggest copy (which I am doing).

Now, as I am viewing it,spv.rev_rent and spv.exp_maintenance should be more related via the copy(), but it is the tax call that is causing the unexpected behavior.

Driving me nuts - any help is really appreciated!!

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
jason m
  • 6,519
  • 20
  • 69
  • 122
  • 1
    Why are you creating a bunch of class variables only to Shadow them with instance variables in `__init__` in the first place? – juanpa.arrivillaga Aug 24 '18 at 00:45
  • In any event, your `CashFlowSchedule.flow` is a class variable, so of course it is shared across instances. The same is true for most of your variables in `SinglePropertyValuation`,, except for a few that you shadow in some methods. Python is not Java. Look at a tutorial on Python classes – juanpa.arrivillaga Aug 24 '18 at 00:55
  • @juanpa.arrivillaga Yes, sadly it is not. Do you know of any good tutorials, or can you provide a basic example that would act as an answer? – jason m Aug 24 '18 at 01:01
  • Start with the [official tutorial](https://docs.python.org/3/tutorial/classes.html) – juanpa.arrivillaga Aug 24 '18 at 01:02
  • 1
    Possible duplicate of [How to avoid having class data shared among instances?](https://stackoverflow.com/questions/1680528/how-to-avoid-having-class-data-shared-among-instances) – juanpa.arrivillaga Aug 24 '18 at 01:04
  • But basically, all variables defined in the class scope are *class level*, they are all "static" using Java parlance. You need to either assign to an instance directly or to `self.attribute` in a method. Typically, this happens in `__init__`. You basically created two classes with all static variables and are wondering why your instances are sharing these variables. – juanpa.arrivillaga Aug 24 '18 at 01:06
  • 1
    If you really want to define attributes at the class level but have them work like instance attributes, then you probably really want a `@dataclass` or `@attr.s` class. But if you don’t have a good reason for that, and are just doing it because you want to turn Python into Java, then you should instead learn to write idiomatically in each language. Python is a really bad Java, and Java is a really bad Python, but Python is a great Python, and Java is a pretty good Java. – abarnert Aug 24 '18 at 01:09
  • I moved away from Java a while back due to other massive python gains and have (somehow) avoided this issue until a recent project. – jason m Aug 24 '18 at 01:12
  • Will read and figure this out. – jason m Aug 24 '18 at 01:12

0 Answers0