如果您已经用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
对象cat1
和cat2
各自拥有自己的name
和age
属性。如果没有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.distance
和p1.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