Repository: modin-project/modin

Issue:
pd.Series.ffill() raise the error: AttributeError: 'numpy.ndarray' object has no attribute 'ffill'
### System information
- **OS Platform and Distribution - WIndows10**:
- **Modin version** (`0.15.0+7.g4ec7f634`):
- **Python version 3.9.12**:
- **Code we can use to reproduce**:

```python
  import modin
  import modin.pandas as pd
  from distributed import Client
  
  import numpy as np
  
  
  if __name__ == '__main__':
      if pd.__name__ == 'modin.pandas':
          client = Client(n_workers=3)
          print(modin.__version__)

    df = pd.DataFrame(
        dict(
            a=[1, 2, None, None, None, ],
            b=(1, None, 3, 4, 5,),
        )
    )

    df.a.ffill(inplace=True)
    print(df)

    df['tr_id'] = 0

    df.tr_id = np.where(
        (df.b <= 4),
        3,
        None,
    )

    df.tr_id.ffill(inplace=True)
```


<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
<!-- Describe the problem clearly here. -->
I have the output:

```
0.15.0+7.g4ec7f634
UserWarning: Ray execution environment not yet initialized. Initializing...
To remove this warning, run the following python code before doing dataframe operations:

    import ray
    ray.init()

UserWarning: Distributing <class 'dict'> object. This may take some time.
     a    b
0  1.0  1.0
1  2.0  NaN
2  2.0  3.0
3  2.0  4.0
4  2.0  5.0
Traceback (most recent call last):
  File "d:\OD\OneDrive\Projects\Chud_Amaz\Soft_in_dev\moduled_way_OOP\modin_test_ffill.py", line 33, in <module>
    df.tr_id.ffill(inplace=True)
AttributeError: 'numpy.ndarray' object has no attribute 'ffill'
```

### **Problem**
When new column was filled with `np.where ` - method `ffill` does not work




Discussion:
@VasilijKolomiets thank you for reporting this issue! I can reproduce it at Modin version 4ec7f6347903f9133c65ebc5b6e0e15553b98577:

```python
import modin.pandas as pd

df = pd.DataFrame([[1]], columns=['col0'])
df.col0 = [3]
print(f'The entire dataframe: {df}')
print(f'df.col0: {df.col0}')
print(f'df["col0"]: {df["col0"]}')
print(df.col0.ffill)
```

In Modin, I get `AttributeError: 'list' object has no attribute 'ffill'`, but in pandas, the code runs without error. It seems that when we assign a list-like value that is not a series to a Modin dataframe using [`__setattr__`](https://docs.python.org/3/reference/datamodel.html?highlight=__setattr__#object.__setattr__), we assign the item to the dataframe, but we don't reset the attribute of the frame. In the snippet above, `df.col0` gets the assigned list of `[3]` instead of the new column value, which is `pd.Series([3], name = 'col0')`.

If I can see a quick fix, I'll assign this issue to myself and make the fix.


In the snippet I posted above, the Modin dataframe's `__setattr__` [calls](https://github.com/modin-project/modin/blob/4ec7f6347903f9133c65ebc5b6e0e15553b98577/modin/pandas/dataframe.py#L2501) `__setitem__` to modify the `col0` column in place. It then calls `object.__setattr__(self, key, value)`, which re-assigns the `col0` property to exactly the value that was passed in, i.e. the list. I think a fairly simple fix would be to call `object.__setattr__(self, key, self.__getitem__(key))` in the case [here](https://github.com/modin-project/modin/blob/4ec7f6347903f9133c65ebc5b6e0e15553b98577/modin/pandas/dataframe.py#L2501) where we call `__setitem__`.

I can't take this on right now, so I'll leave it unassigned.
> In the snippet I posted above, the Modin dataframe's `__setattr__` [calls](https://github.com/modin-project/modin/blob/4ec7f6347903f9133c65ebc5b6e0e15553b98577/modin/pandas/dataframe.py#L2501) `__setitem__` to modify the `col0` column in place. It then calls `object.__setattr__(self, key, value)`, which re-assigns the `col0` property to exactly the value that was passed in, i.e. the list. I think a fairly simple fix would be to call `object.__setattr__(self, key, self.__getitem__(key))` in the case [here](https://github.com/modin-project/modin/blob/4ec7f6347903f9133c65ebc5b6e0e15553b98577/modin/pandas/dataframe.py#L2501) where we call `__setitem__`.
> 
> I can't take this on right now, so I'll leave it unassigned.

So what have I to do to obtain result as in 'pure pandas'?
May beput resulting numpy in Series on right hand?

Or just wait a bit? )
> May beput resulting numpy in Series on right hand?

@VasilijKolomiets unfortunately it turns out that that won't work in Modin either 😢  Modin assigns the right-hand side as-is to the attribute instead of pointing the attribute to the the new column.

```python
import modin.pandas as pd

df = pd.DataFrame([[1]], columns=['col0'])
df.col0 = pd.Series([3])
df.iloc[0, 0] = 4
# BUG: col0 is unchanged!!!
assert df.col0.equals(df['col0'])
```

What you can do instead is use `__setitem__` instead of `__setattr__`, e.g. `df['tr_id'] = np.where(...` instead of `df.tr_id = np.where(...`. This works:

```python
import modin.pandas as pd

df = pd.DataFrame([[1]], columns=['col0'])
df['col0'] = pd.Series([3])
df.iloc[0, 0] = 4
assert df.col0.equals(df['col0'])
```

Meanwhile, @pyrito will work on a PR that should fix all the bugs identified here.
Thanks a lot!
.
'Be happy, do not worry' (c)

Resolving patch:
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -2498,6 +2498,10 @@ def __setattr__(self, key, value):
             pass
         elif key in self and key not in dir(self):
             self.__setitem__(key, value)
+            # Note: return immediately so we don't keep this `key` as dataframe state.
+            # `__getattr__` will return the columns not present in `dir(self)`, so we do not need
+            # to manually track this state in the `dir`.
+            return
         elif isinstance(value, pandas.Series):
             warnings.warn(
                 "Modin doesn't allow columns to be created via a new attribute name - see "


Test patch:
diff --git a/modin/pandas/test/dataframe/test_iter.py b/modin/pandas/test/dataframe/test_iter.py
--- a/modin/pandas/test/dataframe/test_iter.py
+++ b/modin/pandas/test/dataframe/test_iter.py
@@ -248,7 +248,8 @@ def test_inplace_series_ops(data):
         df_equals(modin_df, pandas_df)
 
 
-def test___setattr__():
+# Note: Tests setting an attribute that is not an existing column label
+def test___setattr__not_column():
     pandas_df = pandas.DataFrame([1, 2, 3])
     modin_df = pd.DataFrame([1, 2, 3])
 
@@ -257,6 +258,37 @@ def test___setattr__():
 
     df_equals(modin_df, pandas_df)
 
+    # While `new_col` is not a column of the dataframe,
+    # it should be accessible with __getattr__.
+    assert modin_df.new_col == pandas_df.new_col
+
+
+def test___setattr__mutating_column():
+    # Use case from issue #4577
+    pandas_df = pandas.DataFrame([[1]], columns=["col0"])
+    modin_df = pd.DataFrame([[1]], columns=["col0"])
+
+    # Replacing a column with a list should mutate the column in place.
+    pandas_df.col0 = [3]
+    modin_df.col0 = [3]
+
+    df_equals(modin_df, pandas_df)
+    # Check that the col0 attribute reflects the value update.
+    df_equals(modin_df.col0, pandas_df.col0)
+
+    pandas_df.col0 = pd.Series([5])
+    modin_df.col0 = pd.Series([5])
+
+    # Check that the col0 attribute reflects this update
+    df_equals(modin_df, pandas_df)
+
+    pandas_df.loc[0, "col0"] = 4
+    modin_df.loc[0, "col0"] = 4
+
+    # Check that the col0 attribute reflects update via loc
+    df_equals(modin_df, pandas_df)
+    assert modin_df.col0.equals(modin_df["col0"])
+
 
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 def test_isin(data):