Python 中的 self,揭秘

如果您已经用Python(面向对象编程)编程一段时间了,那么您肯定遇到过第一个参数是self的方法。

让我们先来理解一下这个反复出现的self参数是什么。


Python中的self是什么?

在面向对象编程中,每当我们为类定义方法时,我们都将self作为每个方法的第一参数。让我们看看一个名为Cat的类的定义。

class Cat:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def info(self):
        print(f"I am a cat. My name is {self.name}. I am {self.age} years old.")

    def make_sound(self):
        print("Meow")

在这种情况下,所有方法,包括__init__,都以self作为第一个参数。

我们知道类是对象的蓝图。可以使用此蓝图创建多个对象。让我们从上面的类创建两个不同的对象。

cat1 = Cat('Andy', 2)
cat2 = Cat('Phoebe', 3)

self关键字用于表示给定类的实例(对象)。在这种情况下,两个Cat对象cat1cat2各自拥有自己的nameage属性。如果没有self参数,同一个类就无法同时存储这两个对象的信息。

但是,由于类只是一个蓝图,self允许访问Python中每个对象的属性和方法。这使得每个对象都可以拥有自己的属性和方法。因此,即使在创建这些对象很久之前,我们在定义类时就将对象引用为self


为什么每次都要显式定义self?

即使我们理解了self的用法,对于来自其他语言的程序员来说,每次定义方法时都要显式地将self作为参数传递仍然显得有些奇怪。正如Python之禅所说:“明确优于隐晦”。

那么,我们为什么需要这样做呢?让我们从一个简单的例子开始。我们有一个Point类,它定义了一个distance方法来计算到原点的距离。

class Point(object):
    def __init__(self,x = 0,y = 0):
        self.x = x
        self.y = y

    def distance(self):
        """Find distance from origin"""
        return (self.x**2 + self.y**2) ** 0.5

现在让我们实例化这个类并计算距离。

>>> p1 = Point(6,8)
>>> p1.distance()
10.0

在上面的示例中,__init__()定义了三个参数,但我们只传递了两个(6和8)。同样,distance()需要一个参数,但我们没有传递任何参数。为什么Python没有抱怨参数数量不匹配?


内部发生了什么?

在上面的示例中,Point.distancep1.distance是不同的,并非完全相同。

>>> type(Point.distance)
<class 'function'>
>>> type(p1.distance)
<class 'method'>

我们可以看到,前者是函数,后者是方法。Python中方法的一个特点是,对象本身被作为第一个参数传递给相应函数。

在上面的示例中,方法调用p1.distance()实际上等同于Point.distance(p1)

通常,当我们用一些参数调用方法时,相应的类函数是通过将方法的对象放在第一个参数之前来调用的。所以,任何像obj.meth(args)这样的调用都会变成Class.meth(obj, args)。调用过程是自动的,而接收过程不是(它是显式的)。

这就是为什么类中的函数的第一个参数必须是对象本身。将此参数写为self仅仅是一种约定。它不是关键字,在Python中也没有特殊含义。我们可以使用其他名称(如this),但强烈不推荐这样做。使用self以外的名称会受到大多数开发者的诟病,并会降低代码的可读性(可读性很重要)。


可以避免使用self

到现在为止,您应该清楚对象(实例)本身被自动作为第一个参数传递。在创建静态方法时,可以避免这种隐式行为。请看以下简单示例

class A(object):

    @staticmethod
    def stat_meth():
        print("Look no self was passed")

这里,@staticmethod是一个函数装饰器,它使stat_meth()成为静态方法。让我们实例化这个类并调用该方法。

>>> a = A()
>>> a.stat_meth()
Look no self was passed

从上面的例子中,我们可以看到在使用静态方法时,我们避免了将对象作为第一个参数隐式传递的行为。总而言之,静态方法就像普通的函数一样(因为类的所有对象都共享静态方法)。

>>> type(A.stat_meth)
<class 'function'>
>>> type(a.stat_meth)
<class 'function'>

Self 依然存在

显式的self并非Python独有。这个思想是从Modula-3借鉴来的。以下是一个它能派上用场的使用场景。

Python中没有显式的变量声明。它们在第一次赋值时就生效了。使用self可以更容易地区分实例属性(和方法)与局部变量。

在第一个示例中,self.x是实例属性,而x是局部变量。它们不相同,并且位于不同的命名空间。

许多人曾提议将self像C++和Java中的this一样,使其成为Python中的关键字。这样就可以消除方法中形式参数列表里显式self的冗余使用。

虽然这个想法似乎很有希望,但它不会实现。至少在不久的将来不会。主要原因是向后兼容性。这是Python创始人本人写的一篇博客,解释了为什么显式self必须保留


__init__()不是构造函数

到目前为止,一个可以得出的重要结论是,__init__()方法不是构造函数。许多初级的Python程序员会感到困惑,因为它在创建对象时会被调用。

仔细观察会发现,__init__()的第一个参数就是对象本身(对象已经存在)。__init__()函数在对象创建之后立即被调用,用于初始化对象。

严格来说,构造函数是创建对象本身的方法。在Python中,这个方法是__new__()。这个方法的一个常见签名是

__new__(cls, *args, **kwargs)

当调用__new__()时,类本身作为第一个参数(cls)被隐式传递。

同样,与self一样,cls也只是一个命名约定。此外,*args**kwargs用于在Python中调用方法时接受任意数量的参数。

在实现__new__()时需要记住的一些重要事项是

  • __new__()总是在__init__()之前被调用。
  • 第一个参数是类本身,它是隐式传递的。
  • 始终从__new__()返回一个有效的对象。这不是强制性的,但它的主要用途是创建和返回一个对象。

让我们看一个例子

class Point(object):

    def __new__(cls,*args,**kwargs):
        print("From new")
        print(cls)
        print(args)
        print(kwargs)

        # create our object and return it
        obj = super().__new__(cls)
        return obj

    def __init__(self,x = 0,y = 0):
        print("From init")
        self.x = x
        self.y = y

现在,让我们实例化它。

>>> p2 = Point(3,4)
From new
<class '__main__.Point'>
(3, 4)
{}
From init

这个例子说明__new__()__init__()之前被调用。我们还可以看到__new__()中的参数cls是类本身(Point)。最后,通过在object基类上调用__new__()方法来创建对象。

在Python中,object是所有其他类的基类。在上面的例子中,我们通过super()实现了这一点。


使用 __new__ 还是 __init__?

您可能经常看到__init__(),但很少使用__new__()。这是因为大多数时候您不需要重写它。通常,__init__()用于初始化新创建的对象,而__new__()用于控制对象的创建方式。

我们也可以使用__new__()来初始化对象的属性,但从逻辑上讲,它应该在__init__()中。

然而,__new__()的一个实际用途是限制从类创建的对象数量。

假设我们想要一个SqPoint类来创建表示正方形四个顶点的实例。我们可以继承我们之前的Point类(本文的第二个示例),并使用__new__()来实现此限制。以下是一个将类限制为仅创建四个实例的示例。

class SqPoint(Point):
    MAX_Inst = 4
    Inst_created = 0

    def __new__(cls,*args,**kwargs):
        if (cls.Inst_created >= cls.MAX_Inst):
            raise ValueError("Cannot create more objects")
        cls.Inst_created += 1
        return super().__new__(cls)

一个样本运行

>>> p1 = SqPoint(0,0)
>>> p2 = SqPoint(1,0)
>>> p3 = SqPoint(1,1)
>>> p4 = SqPoint(0,1)
>>> 
>>> p5 = SqPoint(2,2)
Traceback (most recent call last):
...
ValueError: Cannot create more objects
你觉得这篇文章有帮助吗?

我们的高级学习平台,凭借十多年的经验和数千条反馈创建。

以前所未有的方式学习和提高您的编程技能。

试用 Programiz PRO
  • 交互式课程
  • 证书
  • AI 帮助
  • 2000+ 挑战