【问题标题】:Method-chaining without permanently mutating the object无需永久改变对象的方法链
【发布时间】:2019-10-20 14:18:30
【问题描述】:

我现在正在学习如何编写 Python 类和方法链。基本上,我想要一个 python (2.7) 类来保存我的数据并具有(可链接的)方法,允许我过滤数据而不改变我的原始数据。我做了一些谷歌搜索,似乎我的答案可能与return self 有关,但我不确定如何实现它以使这些方法不会改变我的原始数据。

假设我有一个数据存储在一个名为 file 的 excel 文件中,如下所示:

+--------+-----+-------+
| Person | Sex | Score |
+--------+-----+-------+
| A      | M   |    10 |
| B      | F   |     9 |
| C      | M   |     8 |
| D      | F   |     7 |
+--------+-----+-------+

我想编写一个名为MyData 的类,这样我就可以进行一些基本的数据调用和过滤。

这是我目前得到的

class MyData:
    def __init__ (self, file):
        import pandas as pd
        self.data = pd.read_excel (file)
        self.Person = self.data['Person']
        self.Sex = self.data['Sex']
        self.Score = self.data['Score']

    def male_only(self):
        self.data = self.data[self.Sex=="M"]
        self.Person = self.Person[self.Sex=="M"]
        self.Score = self.Score[self.Sex=="M"]
        self.Sex = self.Sex[self.Sex=="M"]
        return self

    def female_only(self):
        self.data = self.data[self.Sex=="F"]
        self.Person = self.Person[self.Sex=="F"]
        self.Score = self.Score[self.Sex=="F"]
        self.Sex = self.Sex[self.Sex=="F"]
        return self

这似乎可行,但遗憾的是,我的原始数据已被此代码永久变异。例如:

Data = MyData(file)
Data.data
>>> Data.data
  Person Sex  Score
0      A   M     10
1      B   F      9
2      C   M      8
3      D   F      7

Data.male_only().data
>>> Data.male_only().data
  Person Sex  Score
0      A   M     10
2      C   M      8

Data.data
>>> Data.data
  Person Sex  Score
0      A   M     10
2      C   M      8

我想要一个对Data.male_only().PersonData.Person.male_only()Data.male_only().dataData.data.male_only() 返回相同答案的类,而不会永久改变Data.dataData.Person

【问题讨论】:

  • 如果您不想改变调用这些方法的对象,那么显然您需要返回一个新对象,而不是self。您当前的 __init__() 并不适合此,因为它只允许您从文件而不是现有数据框创建新的 MyData
  • @jasonharper 我对此很陌生,但我目前的理解是return self 对于方法链接是必不可少的......一个代码示例返回相同的答案 Data.male_only().PersonData.Person.male_only()不永久更改Data.Person 将不胜感激。非常感谢。

标签: python pandas python-2.x method-chaining


【解决方案1】:

我同意@Demi-Lune。

我更改了 OP 的代码,以便 male_only()female_only() 方法始终返回其所属对象的副本。我更改了__init__() 方法,因为我认为您不想每次创建新对象时都调用pd.read_csv() 方法。所以male_only()female_only() 方法总是返回新对象,不会影响其他对象。

import pandas as pd

# Added for creating file on memory.
import io
csv = '''Person,Sex,Score
p1,M,1
p2,M,2
p3,M,3
p4,F,4
p5,F,5
p6,F,6'''
file = io.StringIO(csv)

class MyData:
    def __init__ (self, file=None, data=None):
        import pandas as pd
        if file:
            self.data = pd.read_csv(file)
        else:
            self.data = data
        self.Person = self.data['Person']
        self.Sex = self.data['Sex']
        self.Score = self.data['Score']

    def copy_d(self):
        return MyData(data=self.data.copy())

    def male_only(self):
        d = self.copy_d()
        d.data = self.data[self.Sex=="M"]
        d.Person = self.Person[self.Sex=="M"]
        d.Score = self.Score[self.Sex=="M"]
        d.Sex = self.Sex[self.Sex=="M"]
        return d

    def female_only(self):
        d = self.copy_d()
        d.data = self.data[self.Sex=="F"]
        d.Person = self.Person[self.Sex=="F"]
        d.Score = self.Score[self.Sex=="F"]
        d.Sex = self.Sex[self.Sex=="F"]
        return d

d = MyData(file)
print(d.female_only().data)
#   Person Sex  Score
# 3     p4   F      4
# 4     p5   F      5
# 5     p6   F      6

print(d.male_only().data)
#   Person Sex  Score
# 0     p1   M      1
# 1     p2   M      2
# 2     p3   M      3

print(d.data)
#   Person Sex  Score
# 0     p1   M      1
# 1     p2   M      2
# 2     p3   M      3
# 3     p4   F      4
# 4     p5   F      5
# 5     p6   F      6

但如果您只使用pandas.DataFrame,另一种方法是使用裸pandas.DataFrame。第一件事是,在大多数情况下,pandas.DataFrame 对象已经具有等于列名称的属性名称。所以实际上你不需要定义像PersonSexScore这样的属性,因为它已经存在于DataFrame对象中了。

即:

import pandas as pd
import numpy as np
df = pd.DataFrame(np.eye(3,3), columns=['Person', 'Sex', 'Score'])

# `df` already has these properteis.
df.Person
df.Sex
df.Score
# In [986]: df.Person
# Out[986]: 
# 0    1.0
# 1    0.0
# 2    0.0
# Name: Person, dtype: float64

# In [987]: df.Sex
# Out[987]: 
# 0    0.0
# 1    1.0
# 2    0.0
# Name: Sex, dtype: float64

# In [988]: df.Score
# Out[988]: 
# 0    0.0
# 1    0.0
# 2    1.0
# Name: Score, dtype: float64

因此,您的 male_only()female_only() 方法编写如下。

import pandas as pd

# Added for creating file on memory.
import io
csv = '''Person,Sex,Score
p1,M,1
p2,M,2
p3,M,3
p4,F,4
p5,F,5
p6,F,6'''
file = io.StringIO(csv)

def male_only(df):
    return df[df.Sex=='M']

def female_only(df):
    return df[df.Sex=='F']

df = pd.read_csv(file)
male_only(df)
# In [1034]: male_only(df)
# Out[1037]: 
#   Person Sex  Score
# 0     p1   M      1
# 1     p2   M      2
# 2     p3   M      3

female_only(df)
# In [1038]: female_only(df)
# Out[1041]: 
#   Person Sex  Score
# 3     p4   F      4
# 4     p5   F      5
# 5     p6   F      6

希望对你有帮助。

【讨论】:

  • 非常感谢。您的回答非常有帮助,我认为这就是我想要的。澄清一下,这不是专门使用pandas.DataFrame。这只是一个关于 python 类的练习,其方法允许用户操作返回的对象而无需永久修改它们。我认为核心魔法是copy_d 方法,它允许在不改变原始数据的情况下操作对象。非常感谢!
【解决方案2】:

我想详细说明@Demi-Lune 的回答。我认为没有办法可以绕过创建MyData 实例,对其进行修改,然后从链方法中返回它。这种事情首先起作用的全部原因是您的所有链方法都属于同一个类,并且它们返回该类的一个实例。

例如str.swapcasestr.zfillstr.replace都是str的一部分,它们都返回strs。

>>> string = "Hello World"
>>> string.swapcase().zfill(16).replace("L", "T")
'00000hETTO wORTD'
>>> string
'Hello World'
>>> 

您尝试做的 (Data.Person.male_only()) 打破了这种模式,因为现在暗示方法 male_only 不是 MyData 类的一部分,而是属于 @987654331 的方法@ 目的。 self.Personself.data["Person"] 是什么?我对熊猫不是很熟悉。它是一个字符串吗?字符串列表?无论如何,无论它是什么,您想要实现的基本上都涉及向该类型的类添加一个名为 male_only 的新方法。

【讨论】:

    【解决方案3】:

    当您编写self.data = ... 时,您在第一行显式修改了self.data。你可以返回一个新的数据实例:

        def male_only(self):
            newdata = MyData()
            newdata.data = self.data[self.Sex=="M"]
            newdata.Person = self.Person[self.Sex=="M"]
            newdata.Score = self.Score[self.Sex=="M"]
            newdata.Sex = self.Sex[self.Sex=="M"]
            return newdata
    

    根据你的 cmets,这里有一个过滤器解决方案的建议:有激活一些标志/过滤器的函数,然后你必须编写函数来获取属性:

    # self.filters should be initialized to [] in __init__
    def male_only(self):
        self.filters.append('male_only')
    def person(self):
        if "male_only" in self.filters:
            return self.Person[self.Sex=="M"]
        else: 
            return self.Person
    

    要看看这是否可以去某个地方,你应该真正完成你的测试用例来帮助你修正你的想法(首先编写测试用例,然后是类总是好的做法)。

    【讨论】:

    • 感谢您的回答。但是,我认为这不是我想要的。例如,此解决方案适用于 Data.male_only().Person,但不适用于 Data.Person.male_only()...
    • 那么另一种可能性是使用某种“查看/过滤”功能来管理您的对象。你应该用更多的例子或你想做的测试用例来丰富你的问题。我会用“过滤器”解决方案的想法来编辑我的答案。
    • 这不是我想要的,但肯定是一个很酷的技巧。 person 方法实际上非常有用。非常感谢您更新的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-10
    • 1970-01-01
    • 2018-11-01
    • 2012-05-28
    • 2017-10-04
    • 2020-03-16
    相关资源
    最近更新 更多