Repository: modin-project/modin

Issue:
BUG: Modin misuses astype `copy=False` parameter
### Modin version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the latest released version of Modin.

- [X] I have confirmed this bug exists on the main branch of Modin. (In order to do this you can follow [this guide](https://modin.readthedocs.io/en/stable/getting_started/installation.html#installing-from-the-github-master-branch).)


### Reproducible Example

```python
import modin.pandas as pd

df = pd.DataFrame([1])
# prints None
print(df.astype(str, copy=False))
# prints <class 'str'>
print(df.iloc[0, 0])
```


### Issue Description

modin interprets `copy=False` as "modify the data in place". while that interpretation seems reasonable, `copy=False` in pandas actually means to change types without copying the underlying data. I don't see any reasonable way to support this behavior, given that Modin is copy on write. We should probably raise a warning saying that we can't actually change types in place, and then do the regular `astype`.

### Expected Behavior

should match pandas

### Error Logs

N/A	

### Installed Versions

<details>


INSTALLED VERSIONS
------------------
commit           : 4114183f84d3d152126510655db5698fd4ea1071
python           : 3.8.13.final.0
python-bits      : 64
OS               : Darwin
OS-release       : 21.5.0
Version          : Darwin Kernel Version 21.5.0: Tue Apr 26 21:08:22 PDT 2022; root:xnu-8020.121.3~4/RELEASE_X86_64
machine          : x86_64
processor        : i386
byteorder        : little
LC_ALL           : None
LANG             : en_US.UTF-8
LOCALE           : en_US.UTF-8

Modin dependencies
------------------
modin            : 0.18.0+3.g4114183f
ray              : 2.1.0
dask             : 2022.12.0
distributed      : 2022.12.0
hdk              : None

pandas dependencies
-------------------
pandas           : 1.5.2
numpy            : 1.23.1
pytz             : 2022.1
dateutil         : 2.8.2
setuptools       : 61.2.0
pip              : 22.1.2
Cython           : None
pytest           : 7.1.2
hypothesis       : None
sphinx           : 5.1.1
blosc            : None
feather          : 0.4.1
xlsxwriter       : None
lxml.etree       : 4.9.1
html5lib         : None
pymysql          : None
psycopg2         : None
jinja2           : 3.1.2
IPython          : 8.4.0
pandas_datareader: None
bs4              : 4.11.1
bottleneck       : None
brotli           : None
fastparquet      : 2022.11.0
fsspec           : 2022.7.1
gcsfs            : None
matplotlib       : 3.5.2
numba            : None
numexpr          : 2.8.3
odfpy            : None
openpyxl         : 3.0.10
pandas_gbq       : 0.17.7
pyarrow          : 8.0.0
pyreadstat       : None
pyxlsb           : None
s3fs             : 2022.7.1
scipy            : 1.9.0
snappy           : None
sqlalchemy       : 1.4.39
tables           : 3.7.0
tabulate         : 0.9.0
xarray           : 2022.6.0
xlrd             : 2.0.1
xlwt             : None
zstandard        : None
tzdata           : None


</details>



Discussion:

Resolving patch:
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -983,8 +983,21 @@ def astype(self, dtype, copy=True, errors="raise"):  # noqa: PR01, RT01, D200
             # Assume that the dtype is a scalar.
             col_dtypes = {column: dtype for column in self._query_compiler.columns}
 
-        new_query_compiler = self._query_compiler.astype(col_dtypes, errors=errors)
-        return self._create_or_update_from_compiler(new_query_compiler, not copy)
+        if not copy:
+            # If the new types match the old ones, then copying can be avoided
+            frame_dtypes = self._query_compiler._modin_frame._dtypes
+            if frame_dtypes is not None:
+                for col in col_dtypes:
+                    if col_dtypes[col] != frame_dtypes[col]:
+                        copy = True
+                        break
+            else:
+                copy = True
+
+        if copy:
+            new_query_compiler = self._query_compiler.astype(col_dtypes, errors=errors)
+            return self._create_or_update_from_compiler(new_query_compiler)
+        return self
 
     @property
     def at(self, axis=None):  # noqa: PR01, RT01, D200
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -172,7 +172,8 @@ def __init__(
             if index is not None:
                 self.set_axis(index, axis=0, inplace=True)
             if dtype is not None:
-                self.astype(dtype, copy=False)
+                casted_obj = self.astype(dtype, copy=False)
+                self._query_compiler = casted_obj._query_compiler
         # Check type of data and use appropriate constructor
         elif query_compiler is None:
             distributed_frame = from_non_pandas(data, index, columns, dtype)


Test patch:
diff --git a/modin/pandas/test/dataframe/test_map_metadata.py b/modin/pandas/test/dataframe/test_map_metadata.py
--- a/modin/pandas/test/dataframe/test_map_metadata.py
+++ b/modin/pandas/test/dataframe/test_map_metadata.py
@@ -505,6 +505,23 @@ def test_astype_errors(errors):
     eval_general(modin_df, pandas_df, lambda df: df.astype("int", errors=errors))
 
 
+@pytest.mark.parametrize("has_dtypes", [False, True])
+def test_astype_copy(has_dtypes):
+    data = [1]
+    modin_df, pandas_df = pd.DataFrame(data), pandas.DataFrame(data)
+    if not has_dtypes:
+        modin_df._query_compiler._modin_frame._dtypes = None
+    eval_general(modin_df, pandas_df, lambda df: df.astype(str, copy=False))
+
+    # trivial case where copying can be avoided, behavior should match pandas
+    s1 = pd.Series([1, 2])
+    if not has_dtypes:
+        modin_df._query_compiler._modin_frame._dtypes = None
+    s2 = s1.astype("int64", copy=False)
+    s2[0] = 10
+    df_equals(s1, s2)
+
+
 @pytest.mark.parametrize("dtypes_are_dict", [True, False])
 def test_astype_dict_or_series_multiple_column_partitions(dtypes_are_dict):
     # Test astype with a dtypes dict that is complex in that: