类和实例
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是创建实例的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。各个实例拥有的数据都互相独立,互不影响。
以 Student 类为例,在 Python 中,定义类是通过 class 关键字:
1 2 3 4 5 6 |
class Student(object): 'parent class' data = "hello world" def method(self, name, score): self.name = name self.score = score |
第一行:class 后面紧接着是类名,即 Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用 object 类,这是所有类最终都会继承的类。
第二行:用来定义注释信息的。
第三行:用来定义类的属性(类中的变量叫属性)。
第四行:用来定义方法(类中的函数叫方法),method 方法的第一个参数永远是 self,表示创建的实例本身。因此,在 method 方法内部,就可以把各种属性绑定到 self,因为self就指向创建的实例本身。
第五行:定义实例属性(只有将类实例化为实例时此定义才有效)。
定义好了 Student 类,就可以根据 Student 类创建出 Student 的实例,创建实例是通过 类名+() 实现的:
1 2 3 4 5 |
>>> instance = Student() >>> instance <__main__.Student object at 0x10fde39e8> >>> Student <class '__main__.Student'> |
可以看到,变量 instance 指向的就是一个 Student 的实例,后面的 0x10fde39e8 是内存地址,每个 object 的地址都不一样,而 Student 本身则是一个类。
类实例化之后,就可以通过点操作符访问实例的属性或者调用实例的方法。调用类属性,也就是类中定义的变量(公共的):
1 2 |
>>> instance.data 'hello world' |
然后可以调用类中的方法,也就是函数:
1 |
>>> instance.method('andy', 23) |
可以发现,当我调用实例方法的时候,对于 self 参数是忽略的,上面也说了方法的第一个参数永远是 self,表示创建的实例本身,会由解释器自动传入。
调用实例属性,在 instance.method 调用之前,Student() 类不会把 name 属性附加到实例 name 上的。
1 2 3 4 |
>>> instance.name 'andy' >>> instance.score 23 |
在方法部分,通过定义一个特殊的__init__
方法,在创建实例的时候,就把 name,score 等属性绑上去:
1 2 3 4 |
class Student(object): def __init__(self, name, score): self.name = name self.score = score |
__init__
方法是用来初始化属性的函数,其第一个参数永远是 self,表示创建的实例本身,因此,在__init__
方法内部,就可以把各种属性绑定到self,因为 self 就指向创建的实例本身。其实在类内部默认有一个__init__
函数,才是类调用的第一个方法,是真正用来创建实例的,其没有属性,会变成 self 传递给__init__
函数,__init__
函数会对创建出来的实例做初始化工作。但是通常__new__
函数不需要写,除非要改变默认创建实例的行为才需要修改,这也就是元编程了。
另外当没有显示地定义__init__
方法时,会使用默认的__init__
方法,默认__init__
方法如下:
1 2 |
def __init__(self): pass |
有了__init__
方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__
方法匹配的参数,但 self 不需要传,Python 解释器自己会把实例变量传进去:
1 2 3 4 5 |
>>> instance = Student('jerry', 23) >>> instance.name 'jerry' >>> instance.score 23 |
由以上部分可以发现,类体可以包含:声明语句、类成员定义、数据属性、方法等。类中的函数和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量 self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数和关键字参数等。
实例属性和类属性
由于 Python 是动态语言,根据类创建的实例可以动态增减属性。给实例绑定属性的方法是通过实例变量,或者通过 self 变量:
1 2 3 |
class Student(object): def __init__(self, name): self.name = name |
对 Student 类可以做如下操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 操作类 >>> Student.type = 'A' #增加一个类属性 >>> Student.type 'A' >>> del Student.type # 删除一个类属性 # 操作实例 >>> s = Student('Bob') # 创建一个实例s >>> s.name # 打印实例属性 'Bob' >>> s.score = 90 # 增加新的实例属性 >>> s.score 90 >>> del s.score # 删除新的实例属性 |
但是,如果我们想要限制实例的属性怎么办?比如,只允许对 Student 实例添加 name 和 age 属性。为了达到限制的目的,Python 允许在定义类的时候,定义一个特殊的__slots__
变量,来限制该类实例能添加的属性:
1 2 3 4 |
class Student(object): __slots__ = ('name','age') def __init__(self, name): self.name = name |
然后,我们试试:
1 2 3 4 5 6 |
>>> s = Student('Bob') >>> s.age = 90 >>> s.score = 90 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute 'score' |
由于’score’没有被放到__slots__
中,所以不能绑定 score 属性,试图绑定 score 将得到 AttributeError 的错误。
使用__slots__
要注意, __slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
再来说一下作用域,定义一个类:
1 2 3 4 5 6 7 8 |
class Door: def __init__(self, number, status): self.number = number self.status = status def open(self): self.status = 'opening' def close(self): self.status = 'closed' |
实例化一下:
1 2 3 4 5 6 7 8 9 10 |
>>> d1 = Door(1, 'closed') >>> d2 = Door(2, 'opening') >>> d1.status 'closed' >>> d2.status 'opening' >>> id(d1) 4495893928 >>> id(d2) 4495893856 |
我们可以看出两个实例是不同的命名空间,也就是说不同实例之间的操作是不会影响到另外一个实例的变量。
但是,如果类本身需要绑定一个属性呢?可以直接在 class 中定义属性,这种属性是类属性,归类所有:
1 2 |
class Door: type = 'A' |
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> d1 = Door() # 创建实例d1 >>> d1.type # 打印type属性, 因为实例并没有type属性, 所以会继续查找class的type属性 'A' >>> Door.type # 打印类的type属性 'A' >>> d1.type = 'B' # 给实例绑定type属性 >>> d1.type # 打印实例type属性, 由于实例属性优先级比类属性高; 因此, 它会屏蔽掉类的type属性 'B' >>> Door.type # 打印类属性, 类属性并未消失, 仍可以访问 'A' >>> del d1.type # 可以使用del删除实例属性, 不能删除实例方法 >>> d1.type # 再次调用d1.type, 由于实例的type属性没有找到, 类的type属性就显示出来了 'A' |
从上面的例子可以看出,在编写程序的时候,千万不要把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
1 2 3 4 |
class Door: type = 'A' def __init__(self, type): self.type = type |
1 2 3 4 5 6 7 8 |
>>> d1 = Door('B') >>> d1.type 'B' >>> Door.type 'A' >>> del d1.type >>> d1.type 'A' |
对于类属性,所有实例共享。
1 2 |
class Door: type = 'A' |
1 2 3 4 5 6 7 8 9 10 |
>>> d1 = Door() >>> d2 = Door() >>> d1.type 'A' >>> d2.type 'A' >>> id(d1.type) 4495502552 >>> id(d2.type) 4495502552 |
可以看出两个实例初始化时引用同一个对象,所以当其中一个实例属性发生新的赋值时,Python 会创建一个新的对象引用,此时会看见两个实例属性就不同了。
1 2 3 4 5 6 7 8 |
>>> d1.type = 'B' # d1属性重新赋值 >>> d1.type # d1属性值发生变化 'B' >>> d2.type # d2属性没有发生变化, 还是实例化时的引用 'A' >>> del d1.type # 删除d1属性 >>> d1.type # 再次调用d1.type, 由于实例的type属性没有找到, 类的type属性就显示出来了 'A' |
当类属性发生改变时,所有实例属性都会改变(已经重新被赋值过的实例属性不会改变):
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> d1 = Door() >>> d2 = Door() >>> d1.type = 'B' >>> d1.type 'B' >>> d2.type 'A' >>> Door.type = 'C' # 类变量赋值 >>> d1.type # 重新被赋值过的实例属性不会改变 'B' >>> d2.type # 没有被重新赋值过的实例属性会随着类属性改变而改变 'C' |
为了更好地验证,我们把类属性变成一个列表试试看:
1 2 |
class Door: type = [1] |
1 2 3 4 5 6 7 8 9 10 11 |
>>> d1 = Door() >>> d2 = Door() >>> d1.type [1] >>> d2.type [1] >>> d1.type.append(2) >>> d1.type [1, 2] >>> d2.type [1, 2] |
可以发现 d1 实例追加一个元素后,d2 实例也有这个元素。由此看出两个实例都是引用同一个对象,由于 d1 属性没有发生新的赋值,所以引用对象没有发生变化。
实例与方法
通过上面的演示,基本明白了类与实例,以及类属性和实例属性了。对于类中定义的方法来说,其作用域是类的,如下演示:
1 2 3 4 5 6 7 8 |
class Door: def __init__(self, number, status): self.number = number self.status = status def open(self): self.status = 'opening' def close(self): self.status = 'closed' |
实例化一下:
1 2 3 4 5 6 7 8 9 |
>>> d1 = Door(1, 'opening') >>> d2 = Door(2, 'closed') >>> d1.status 'opening' >>> d2.status 'closed' >>> d1.close() # 调用close函数改变status值 >>> d1.status # 再次调用d1.status发现值已经改变 'closed' |
修改实例方法:
1 2 3 4 5 6 7 |
>>> Door.open <function Door.open at 0x10ea6aea0> >>> Door.open = lambda self: print("hello world") >>> d1.open() hello world >>> d2.open() hello world |
方法的作用域都属于类级,可以看出当把类的方法改变之后,所有实例都改变了。另外这种改变方法有一个术语叫 “monkey patch”,也是有安全风险的,比如一些黑客可以通过植入代码来改变你的内部函数;好处就是可以在不动第三方库源码的情况下,通过这种方法,可以改变一些内部实现了。
另外,对于类中定义的方法来说,通过类来调用与实例调用是不一样的:
1 2 3 4 5 |
class C: def f(self): pass print(C.f) # <function C.f at > print(C().f) # <bound method C.f of > |
一个返回的是 function
类型,一个返回的是 method
类型。他们的主要区别在于,函数的传参都是显式传递的,而方法传参往往都会有隐式传递的,具体根据于调用方。例如示例中的 C().f
通过实例调用的方式会隐式传递 self
数据。
类方法及静态方法
除了实例方法,还有类方法和静态方法。
定义类方法,需要一个内置的 @classmethod 装饰器,classmethod 则是要让 C.f
和 c.f
都返回方法,并且传递隐式参数 cls
,传递的是类本身。运行代码如下:
1 2 3 4 5 6 7 8 |
class C: @classmethod def f(cls): pass # 类方法的cls由@classmethod负责传递,cls可以是任何合法变量,但是通常使用cls标识 c = C() print(C.f) # <bound method C.f> print(c.f) # <bound method C.f> print(C.f is c.f) # False |
测试类方法和实例方法:
1 2 3 4 5 6 |
class A: def method_of_instance(self): print('method of instance') @classmethod def method_of_class(cls): print('method of class') |
1 2 3 4 5 6 7 8 9 10 11 |
>>> s = A() # 创建一个实例s >>> s.method_of_instance() # 实例调用实例方法正常 method of instance >>> s.method_of_class() # 实例调用类方法正常,实例是可以访问类方法的 method of class >>> A.method_of_instance() # 类调用实例方法,无法访问,报没有self关键字 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: method_of_instance() missing 1 required positional argument: 'self' >>> A.method_of_class() # 类调用类方法正常 method of class |
实例方法和类方法的区别在于传入的第一个参数,实例方法会自动传入实例本身作为第一个参数,而类方法会自动传入当前类。
定义静态方法,需要一个内置的 @staticmethod 装饰器,staticmethod 的效果是让 C.f
与 c.f
都返回函数,等价于 object.__getattribute__(c, "f")
或 object.__getattribute__(C, "f")
,运行代码如下:
1 2 3 4 5 6 7 8 |
class C: @staticmethod def f(): pass c = C() print(C.f) # <function C.f at 0x000001AEDDA64040> print(c.f) # <function C.f at 0x000001AEDDA64040> print(C.f is c.f) # True |
静态方法的作用不大,一般就是用来做组织代码用的,可以把一批方法归纳到一个命名空间。
1 2 3 4 5 6 7 8 |
class A: def method_of_instance(self): print('method of instance') @classmethod def method_of_class(cls): print('method of class') def static_method(): print('static method') |
1 2 3 4 5 6 7 8 9 10 11 |
>>> s = A() >>> s.method_of_instance() method of instance >>> s.method_of_class() method of class >>> s.static_method() # 实例无法调用静态方法,除非使用@staticmethod装饰器把这个静态方法装饰一下 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: static_method() takes 0 positional arguments but 1 was given >>> A.static_method() # 类可以调用静态方法 static method |
可以看出,实例无法调用静态方法,而类可以。当我们用实例调用方法的时候,总是会传入一个参数,要么是实例本身,要么是它的类。
虽然通常静态方法都只是类会调用,但是如果想让实例可以调用静态方法,就需要使用 @staticmethod 装饰器装饰一下(一般都会加上这个装饰器,标明一下这是一个静态变量),当我们实例调用静态方法时,@staticmethod 会把第一个参数去掉。
1 2 3 4 |
class A: @staticmethod def static_method(): print('static method') |
实例调用静态方法:
1 2 3 4 5 |
>>> s = A() >>> s.static_method() static method >>> A.static_method() static method |
总结:方法的作用域都属于类级,可以看出当把类的方法改变之后,所有实例都改变了。另外具体这个方法是实例方法、类方法、或者静态方法,都由第一个参数决定。当第一个参数是实例的时候,就属于实例方法;当第一个参数是类的时候就是类方法;当不要求第一个参数的时候是静态方法。静态方法用的一般不多,可以被普通函数或模块替代,而类方法和实例方法一般用的挺多。