Python类方法:什么时候不需要自己

Python class methods: when is self not needed

我试图用类重写一些代码。在某个时刻,我想要的是使用对象的每个实例的参数值为成员函数分配一个特定的定义。

来自其他语言(JavaScript、C++、Haskell、Fortran……),我很难理解Python上的一些事情。一件事是在类方法中对self的以下区分。

例如,以下代码显然不起作用:

1
2
3
4
5
6
7
8
9
10
11
class fdf:
    def f(x):
        return 666

class gdg(fdf):
    def sq():
        return 7*7

hg = gdg()
hf = fdf()
print(hf.f(),hg.f(),hg.sq())

错误是"sq()接受0个位置参数,但给出了1"。

据我所知,原因是在执行时,函数作为第一个参数传递给调用对象(调用sq的实例)的引用,而不是我们定义/调用sq的任何其他参数/参数。所以解决方案很简单:将sq的代码改为def sq(self):。实际上,python教程1似乎建议始终使用self作为第一个参数来定义对象方法。这样我们就得到了预期的效果。到现在为止,一直都还不错。

但是,当我尝试像这样实现我的类时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Activation:
    def nonLinearBipolarStep(self,x,string=None):
        if not string: return (-1 if x<0 else 1 )
        else: return ('-' if x<0 else '1')

    default='bipolar'
    activationFunctions = {
        'bipolar': nonLinearBipolarStep ,
    }
    def _getActivation(self,func=default):
        return self.activationFunctions.get(func,self.activationFunctions.get(self.default))

    def __init__(self,func=None):
        if func == None: func=self.default
        self.run = self._getActivation(func)


ag = Activation()
print(ag.run(4))

我得到错误

1
nonLinearBipolarStep() missing 1 required positional argument: 'x'

然而,一个解决方法(解决方案??)正在定义没有参数self的step函数!作为

1
def nonLinearBipolarStep(x,string=None):

然后我得到了1的预期行为(至少对于这个微不足道的测试而言)。所以,这里不仅不需要self,而且这里的用法也不正确!

但是根据上面提到的教程,或者像2或3这样的线程中的答案,在我看来,这段代码不应该工作……或者在某个时刻会有一些意想不到的结果(?).事实上,如果我删除了_getActivation定义中对self的所有引用,我会收到错误消息_getActivation() takes from 0 to 1 positional arguments but 2 were given,根据该规则我可以理解。

线程"why is self not used in this method"4没有给我一个明确的答案:上面代码的语法细节告诉我不需要self?例如,该代码与本教程示例有什么不同?

1
2
3
4
5
6
class MyClass:
   """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

?实例化这个类可以如预期的那样工作,但是它抱怨如果用none定义的话缺少参数(我知道它可以是任何标签)。

这让我怀疑我的代码是否以某种方式隐藏了一个定时炸弹:是否将self作为x的值传递?它按预期工作,所以我会说不,但随后我面临着这个难题。

我想我遗漏了一些关于语言的关键概念。我承认,我也很难回答参考文献3中的op所问的问题。

[^]:在JS中,只在函数体中使用this,函数本身被定义为对象原型的成员,或者是使用…this正确分配的实例成员。

编辑:这线很长。对于那些浏览以寻求帮助的用户,如果您对Python不熟悉,那么您可能需要检查所选解决方案及其注释。但是,如果您已经了解了Python中绑定/未绑定的方法,那么您只需要直接检查描述符的使用情况,如blckknght的答案中所述。我最终在代码中选择了这种方式,在运行分配中使用__get__


您遇到了Python方法实现中更微妙的部分之一。归根结底,这取决于普通方法调用(如some_instance.method()self参数是如何绑定的。它使用的是"描述符"协议,这个协议没有很好的文档记录(至少,它对于新的Python程序员来说并不明显)。

描述符是一个对象,它有一个__get__方法(也可以选择__set__和/或__delete__方法,但这里我只讨论__get__方法)。当此类对象存储在类变量中时,只要在实例上查找相应的名称,python就会调用其__get__方法。注意,对于存储在实例变量中的描述符对象,这种特殊行为不会发生,只会发生在类变量中。

函数是描述符。这意味着当您将函数保存为类变量时,当您在实例上查找它时,将调用它的__get__方法。该方法将返回一个"绑定方法"对象,该对象将自动沿着self参数传递给函数。

如果将函数存储在顶级类变量之外的其他地方(例如在字典或实例变量中),则不会获得这种绑定行为,因为在查找对象时不会调用描述符协议。这通常意味着您要么需要手动传递self,要么首先应该从函数定义中省略self参数(在这种情况下,我建议将函数移出类,以使其清楚地表明它不打算用作方法)。

但如果您愿意,也可以手工构造绑定方法。该类型在types模块中公开,如types.MethodType。因此,您可以像这样更改代码,它应该可以工作:

1
2
3
def __init__(self,func=None):
    if func == None: func=self.default
    self.run = types.MethodType(self._getActivation(func), self) # be sure to import types


什么是self

在Python中,每个普通方法都必须接受一个通常名为self的参数。这是类的一个实例-一个对象。这就是Python方法如何与类状态交互。

您可以随意重命名此参数。但它始终具有相同的值:

1
2
3
4
5
6
7
8
9
>>> class Class:
    def method(foo): #
        print(foo)


>>> cls = Class()
>>> cls.method()
<__main__.F object at 0x03E41D90>
>>>

但是为什么我的例子有效呢?

但是,您可能会困惑的是,此代码的工作方式不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class Class:
    def method(foo):
        print(foo)

    methods = {'method': method}

    def __init__(self):
        self.run = self.methods['method']


>>> cls = Class()
>>> cls.run(3)
3
>>>

这是因为Python中绑定方法和未绑定方法之间的区别。

当我们在__init__()中这样做时:

1
self.run = self.methods['method']

我们指的是未绑定方法method。这意味着我们对method的引用不受Class的任何特定实例的约束,因此python不会强制method接受对象实例。因为它没有一个可以给予。

上述代码与执行此操作相同:

1
2
3
4
5
6
7
8
>>> class Class:
    def method(foo):
        print(foo)


>>> Class.method(3)
3
>>>

在这两个例子中,我们调用类对象Class的方法method,而不是Class对象的实例。

我们可以通过检查repr中的绑定和未绑定方法来进一步了解这一区别:

1
2
3
4
5
6
7
8
9
10
11
>>> class Class:
    def method(foo):
        print(foo)


>>> Class.method
<function Class.method at 0x03E43D68>
>>> cls = Class()
>>> cls.method
<bound method Class.method of <__main__.Class object at 0x03BD2FB0>>
>>>

如您所见,在第一个示例中,当我们执行Class.method时,python显示:。我对你撒了一点谎。当我们有一个类的未绑定方法时,python将它们当作普通函数来处理。因此,method只是一个不绑定到任何class实例的函数。

但是在第二个例子中,当我们创建一个Class的实例,然后访问它的method对象时,我们看到打印的:>

需要注意的关键部分是bound method Class.method。这意味着methodcls有联系——Class的特殊实例。

总备注

正如@jonshapre所提到的,像在示例中那样编写代码会导致混淆(作为这个问题的证据)和错误。如果您简单地定义nonLinearBipolarStep()Activation之外,并从Activation.activation_functions的内部引用该定义,这将是一个更好的想法:

1
2
3
4
5
6
7
8
9
10
11
def nonLinearBipolarStep(self,x,string=None):
        if not string: return (-1 if x<0 else 1 )
        else: return ('-' if x<0 else '1')

class Activation:

    activation_functions = {
        'bipolar': nonLinearBipolarStep,
    }

    ...

I guess a more specific question would be: what should I pay attention to on that code in order to become evident that ag.run(x) would be a call to an unbound function?

如果你还是想让nonLinearBipolarStep不被束缚,那么我建议你小心点。如果您认为您的方法可以生成最干净的代码,那么就继续使用它,但要确保您知道自己在做什么,以及您的代码将具有的行为。

如果你仍然想让你的类的用户明白ag.run()是静态的,你可以在某个地方用docstring来记录它,但这是用户根本不应该关心的。


您在代码中使用了未绑定方法(非线性极性步骤):

1
2
3
activationFunctions = {
    'bipolar': nonLinearBipolarStep ,
}

更长的答案:方法是在类体中定义的函数,并且总是使用至少一个自变量,即self(除非使用@staticfunction并将其转换为普通函数)。"自我"是给定类的对象,在该类上调用方法(类似于C++中的方法)。在Python中,这个参数几乎没有什么特别之处,它不必被命名为self。现在,当您调用未绑定的方法时,您给出的第一个参数将被解释为self和consumed。如果调用绑定方法,则不会发生这种消耗(该方法已经有自己的对象)。例如:

1
2
3
4
5
class A:
  def foo(self, x): print(x)
a = A()
a.foo(1) # a.foo is bound method, a is self, prints 1
A.foo(a, 2) # A.foo is unbound method, first argument becomes self, prints 2

更新:为什么它会起作用。简短回答:因为点(.)运算符将在可以更新未绑定方法时将其更新为绑定。

考虑一下,当你写a.foo(1)时会发生什么。首先,python检查对象a中的foo,但没有找到任何内容(foo不是分配给a的值)。所以它进入了一个类(名为a),你看,foo就在那里并且被使用了。但这里有个诀窍。python会将对象a绑定到未绑定的方法a.foo(细节现在不在这里了,所以想象一下龙会这样做),并将其绑定到绑定方法中。所以a.foo是有约束的,从参数中不再需要自我,所以1进入参数x,一切都正常。

现在来看看你的代码:在map中使用"双极":非线性极性步骤,这是未绑定的方法。然后在constructor(init)中,将self.run设置为从u getactivation返回的值,该值取自activationfunctions映射。在给定的示例中,您返回非线性极性步骤未绑定方法并将其分配给self.run。现在你给ag.run打电话。按照前面段落ag.run中的逻辑,首先查看ag对象内部。这是你的错误-找到了。因为python在ag对象中找到ag.run值,所以它从未为run对象咨询ag类型(activation),也从未有机会绑定它。所以ag.run是一个未绑定的方法,首先需要自变量。

一般来说,你有两个选择。要么执行ag.run(ag,4),这将起作用,但它很难看,要么手动将方法绑定到构造函数中的self。后者您可以这样做:

1
self.run = self._getActivation(func).__get__(self)


我认为这里让您困惑的是,您通过类属性activationFunctions访问方法,而不是通过实例本身(通常是访问实例的)访问方法。例如,给定:

1
2
3
4
5
6
class Class:

    def method(self, foo, bar):
        print(self, foo, bar)

    methods = {'method': method}

当我们直接从字典调用方法时:

1
2
>>> Class.methods['method'](1, 2, 3)
1 2 3

您可以看到我们正在将1作为self参数传递;没有对实例调用该方法,因此没有注入实例。相反,当我们在实例上调用它时:

1
2
3
>>> instance = Class()
>>> instance.method(1, 2)
<__main__.Class object at 0x...> 1 2

现在我们的论点是foobar,实例是self。这就是为什么你认为需要不同数量的参数。

在这种情况下,由于您实际上不需要方法中的实例状态,因此只需将其设置为常规函数(注意对PEP-8遵从性的细微修改):

1
2
3
4
5
6
7
8
9
10
11
12
def non_linear_bipolar_step(x, string=None):
    if string is not None:
        return -1 if x < 0 else 1
    return '-' if x < 0 else '1'

class Activation:

    activation_functions = {
        'bipolar': non_linear_bipolar_step,
    }

    ...

这可能不那么令人困惑。