Python类方法:何时不需要self

16

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

作为其他语言(JavaScript、C++、Haskell、Fortran等)的开发者,我很难理解Python中的一些东西。其中之一就是类方法中“self”的区别。

例如,以下代码显然无法工作:

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作为第一个参数。这样做可以得到预期的结果666 666 49,目前为止还不错。

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

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))

我遇到了错误

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

然而,一个解决方法是在不使用参数 self 的情况下定义步进函数,即:

然而,一个解决方法是在不使用参数 self 的情况下定义步进函数,即:

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例如,那段代码与这个教程示例有什么不同。

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

? 实例化这个类的时候可以正常工作,但是如果定义为none,则会抱怨缺少参数(我知道它可以是任何标签)。

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

我想我错过了一些语言的关键思想。我承认我也在努力回答参考3的OP提出的问题。

[^]:在JS中,函数体内只使用this,函数本身要么被定义为对象的原型成员,要么被定义为实例成员,然后使用...this正确地分配。

编辑: 线程很长。对于那些正在寻找帮助的人,如果你是新手,请查看选定的解决方案及其评论。但是,如果您已经了解Python中的绑定/非绑定方法,您只需要直接检查如Blckknght的答案所述的描述符的用法。最终,我选择在我的代码中使用__get__分配到运行中。


不,你可以使用 @staticmethod 进行工作。 - Willem Van Onsem
你并不总是需要一个 self。如果你用 @staticmethod 装饰函数,就不需要使用 self - Willem Van Onsem
  • 是将自身传递作为 x 的值吗? - 是的,完全正确。
- Christian Dean
@WillemVanOnsem 但这里有一个没有使用“staticmethod”装饰的案例,但它仍然有效。 - MASL
@jonrsharpe 我有一种感觉,深入研究Python的静态区别将有助于我理解这个问题。因此,如果您有比教程更好的参考资料,我会很感激 - 这就是我所说的,仅从语法上无法理解它... - MASL
显示剩余8条评论
4个回答

16

什么是 self

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

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

>>> class Class:
    def method(foo): # 
        print(foo)

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

那么为什么我的例子能够工作呢?

然而,你可能会困惑的是,为什么这段代码会有不同的表现:

>>> 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__()中执行以下操作时:

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

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

上面的代码与执行以下操作相同:

>>> class Class:
    def method(foo):
        print(foo)

        
>>> Class.method(3)
3
>>> 
在这两个示例中,我们调用类对象 Class 的方法method,而不是该类对象的一个实例。
通过检查绑定方法和非绑定方法的repr,我们可以进一步看出这种区别:
>>> 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 显示:<function Class.method at 0x03E43D68>。我向您撒了一个小谎。当我们有一个类的未绑定方法时,Python 将它们视为普通函数。因此,method 只是一个未绑定到任何 `Class` 实例的函数。

但是在第二个示例中,当我们创建 Class 的一个实例,然后访问它的 method 对象时,我们看到打印输出:<bound method Class.method of <__main__.Class object at 0x03BD2FB0>>

需要注意的关键部分是 bound method Class.method。这意味着 method 是绑定到 cls - `Class` 的一个特定实例。

一般注释

正如 @jonshapre 所提到的,像您的示例代码一样编写代码会导致混乱(如本问题所证明的),以及错误。最好在 Activation 外部定义 nonLinearBipolarStep(),并从 Activation.activation_functions 中引用它:

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,
    }

    ...

我想更具体地问一下:在那段代码中,我应该注意什么才能清楚地看出ag.run(x)是对未绑定函数的调用?

如果你仍然希望让nonLinearBipolarStep保持未绑定状态,那我建议你谨慎对待。如果你认为这种方法可以产生最干净的代码,那就去做吧,但一定要确切知道你正在做什么以及你的代码将会有什么行为。

如果你仍然希望向你的类的用户表明ag.run()是静态的,你可能会在某个文档字符串中记录它,但这实际上用户根本不需要关心。


当我正在回答@jonrsharpe时,将其放入内部的点是封装的重点。我不希望这些函数与其他任何东西发生冲突。那么这似乎更多地是Python风格的东西,但我仍然认为将其放在内部会使代码更清晰。可以就语言的优点进行争论,以按其方式执行操作。 - MASL
@MASL:“self.run = self.methods['method']”这句话中,“unbound method”的意思是什么?- self.run被赋值为self.methods['method'],即method,或者说相当于执行了Class.method。这意味着self.run被赋值为Class.method,一个未绑定到Class的函数。 - Christian Dean
@MASL 是的,你说得对。你可以这样想。Python是否传递对象实例完全取决于你如何调用method。如果你使用Class.method,你将获得属于Class对象的函数method。它不绑定到任何特定的Class实例,而是绑定到Class对象本身。如果你使用cls = Class(); cls.method,你将获得绑定到Class的特定实例cls的函数method。如果你还有其他问题,请告诉我具体是什么问题? - Christian Dean
谢谢,但我得到了---> 44 print(ag.run(4)) TypeError: 'staticmethod' object is not callable - MASL
但现在出错了,错误是 TypeError: nonLinearBipolarStep()缺少1个必需的位置参数:'x' - MASL
显示剩余7条评论

6
你遇到了 Python 方法实现中比较微妙的部分之一。它涉及到普通方法调用(例如 some_instance.method())中的 self 参数如何绑定。它使用了“描述符”协议,这个协议并没有很好地记录下来(至少对于新的 Python 程序员来说不是很明显)。
描述符是一个具有 __get__ 方法(以及可选的 __set__ 和/或 __delete__ 方法,但我只想在这里讲述 __get__)。当这样一个对象存储在类变量中时,Python 会在实例上查找相应名称时调用它的 __get__ 方法。请注意,此特殊行为不适用于存储在实例变量中的描述符对象,仅适用于类变量中的描述符对象。
函数是描述符。这意味着当你将函数保存为类变量时,它的 __get__ 方法将在你在实例上查找它时被调用。该方法将返回一个“绑定方法”对象,它将自动传递 self 参数给函数。
如果你将函数存储在除顶级类变量之外的其他位置(例如字典或实例变量中),则不会获得此绑定行为,因为在查找对象时不会调用描述符协议。这通常意味着你需要手动传递 self,或者你应该在函数定义中省略 self 参数(在这种情况下,我建议将该函数移出类以明确它不打算用作方法)。
但是如果你愿意,也可以手动构造绑定方法。该类型在 types 模块中公开,称为 types.MethodType。因此,你可以像这样更改你的代码,它应该能够正常工作:
def __init__(self,func=None):
    if func == None: func=self.default 
    self.run = types.MethodType(self._getActivation(func), self) # be sure to import types

我之前已经监督过这个。从现在的角度来看,这可能是最完整的答案——尽管如果没有Jon和Christian的回答,它对我来说不会像现在这样有意义。谢谢! - MASL

2
我认为让您困惑的是,您是通过类属性activationFunctions访问该方法,而不是(通常情况下)在实例本身上访问。例如,假设有以下代码:
class Class:

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

    methods = {'method': method}

当我们直接从字典中调用方法时:
>>> Class.methods['method'](1, 2, 3)
1 2 3

你可以看到我们将1作为self参数传递;该方法未在实例上调用,因此没有注入实例。相比之下,当我们在实例上调用它时:
>>> instance = Class()
>>> instance.method(1, 2)
<__main__.Class object at 0x...> 1 2

现在我们的参数是foobar,实例是self。这就是为什么你认为需要不同数量的参数的原因。
在这种情况下,如果你的方法实际上不需要实例状态,那么就将它变成一个普通函数(注意对PEP-8的一些小修改以符合规范)。
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,
    }

    ...

这可能会更少令人困惑。

试图理解这个...同时:(1) 你开始说这与我的类属性使用有关,但你的例子似乎取决于我们是在实例上调用它还是在对象上调用它;(2)既然Python中的函数本身也是对象,我期望我可以在运行时只传递一个函数,就像JavaScript中可以做到的那样。所以这里似乎存在两种语言之间的微妙区别(或者我没有理解);(3) 整个重点在于将所有内容封装在一个类内,而不是留下一些垃圾-这是我在JS或C++中会做的事情。否则感觉非常不对... - MASL
  1. "在实例或对象上" - 你试图区分什么,你认为"对象"是什么?
  2. 是的,Python有一级函数。然而,绑定到实例和未绑定到实例之间仍然存在差异。
  3. "这是我在JS或C++中会做的" - Python不是那些语言。如果你不想让函数在模块外使用,请考虑将其命名为"_non_linear..."。
- jonrsharpe
在你的例子中,当你调用 Class.methods['method'](1, 2, 3) 时,只会打印出 2 3,而不是你展示的 1 2 3。该方法没有将 self 与其他参数一起打印出来。 - Blckknght

1

在这段代码中,您正在使用未绑定的方法(nonLinearBipolarStep):

activationFunctions = {
    'bipolar': nonLinearBipolarStep ,
}

较长的回答:方法是在类体内定义的函数,始终至少需要一个参数,即所谓的self(除非您使用@staticfunction并将其转换为普通函数)。Self是给定类的对象,方法在其中调用(就像在C ++中的this一样)。在Python中,关于此参数几乎没有什么特别之处,它不必被命名为self。现在,当您调用未绑定的方法时,您给定的第一个参数将被解释为self并被消耗。如果您调用绑定方法,则不会发生这种消耗(该方法已经具有其self对象)。例如:

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已经绑定,不再需要来自参数的self,因此1进入参数x并且一切正常。

现在来看你的代码:你在map中使用了非线性双极步进'bipolar': nonLinearBipolarStep,这是一个未绑定方法。然后在构造函数(init)中,你将self.run设置为从activationFunctions map中获取的_getActivation返回值。在给定的示例中,你返回了未绑定的nonLinearBipolarStep方法并将其分配给self.run。现在你调用ag.run。按照上一段的逻辑,首先查找ag对象中的ag.run。这里出现了错误-它被找到了。因为Python在ag对象内部找到了ag.run值,它从未查询过ag类型(Activation)的run对象,并且从未有机会将其绑定。所以ag.run是未绑定的方法,需要作为第一个参数传入self。
总的来说,你有两个选择。要么执行ag.run(ag, 4),这样会起作用,但很丑陋,要么在构造函数中手动绑定方法到self。后者可以像这样实现:
self.run = self._getActivation(func).__get__(self)

我现在明白了。但是,为什么我的代码(ag.run(x))按照我期望的方式工作呢?毕竟,每次调用ag.run(x),self并不是一个不同的值... - MASL
我会更新答案,因为它也稍微有些复杂。 ;) - Radosław Cybulski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接