类和实例
类是对象创建实例的模板,而实例则是对象的实体。类使用class关键字定义:
class MyClass:
pass
python中创建实例直接使用工厂函数(类名加上一对括号),和其他的语言使用new关键字有所不同:
my_obj = MyClass()
一般来说,类名以大写字母开头,而对象名则以小写字母或者下划线开头。
实例化对象时,会执行类定义中的__init__()方法,该方法执行时包含实例的各种初始化操作。
方法和函数的区别:方法由对象调用,在方法定义中,第一个参数必须是显示的self,表示当前对象,而函数则不传入当前对象self,可以说方法是特殊的函数,他指定了调用对象,并且可以获得调用对象的属性。函数和方法都是function类的实例:
class MyClass(object):
def __init__(self):
pass
def method1(a,b):
print(a+b)
def method2(self,a,b):
print(a+b)
obj = MyClass()
MyClass.method1(1,2)
obj.method2(1,2)
print(type(MyClass.method1))
print(type(MyClass.method2))
上述代码中,obj.method2就是方法,而MyClass.method1是一个普通的函数,为什么要使用MyClass.method1是因为method1的命名空间包含在MyClass中,而使用obj.method2的原因是因为method2是obj所在类的一个方法,并obj作为method1调用的对象(method1获得obj的绑定)。
从上述代码还可以延生出以下结论:
1. MyClass.method2(1,2)将会产生错误:TypeError: method2() takes exactly 3 arguments (2 given)。 而MyClass也是一个对象,执行MyClass.method2()时,并不能将MyClass作为self代表的当前对象,这是因为实例的方法必须存在于类中,而实例的属性则存在于实例本身,MyClass调用method2不成功是因为MyClass所在的类:type并没有该方法,MyClass.method2不能获得MyClass的绑定,所以MyClass.method2()仅仅是执行MyClass命名空间中的一个函数,而该函数要求传入一个对象,所以导致了TypeError错误。需要显式地传入对象来正确调用:MyClass.method2(MyClass,1,2)
2. obj.method1(1,2)将会产生错误:TypeError: method1() takes exactly 2 positional arguments (3 given)。虽然调用报错,但是报错的原因仅仅是因为参数数量不匹配,由此可见,一个对象调用方法时,该方法必须要显式的第一个对象为self。并且,obj调用时,并没有使用MyClass命名空间,因为obj是MyClass的实例,将会集成MyClass的命名空间。
注意:__init__()方法不应该返回任何对象,而应该返回None,因为该方法本身是一个构造器,实例对象通过该构造器实例化后返回,如果在该方法中显示返回了其他对象,会造成冲突。
对象的属性
在python中,实例可以在任何时刻添加对象,即使在类中并没有定义该属性,但是该语句并不会有任何错误,并且在之后一样可以调用该属性,这样的特性可能非常的方便,但是也隐藏了很多弊端,容易对已知的对象传入不必要的属性,从而搞得一团糟:
class MyClass:
def __init__(self,a,b):
self.a = a
self.b = b
obj = MyClass(1,2)
print(obj.a)
obj.a = 3
print(obj.a)
def a_method(self,c):
self.c = c
from types import MethodType
obj.a_method = MethodType(a_method,obj)
obj.a_method(4)
print(obj.c)
上述通过类定义之外的途径和对象添加方法和属性,在其他的同类的对象中是不具有的,在ruby中通过单件类来完成,python中,方法名是一个变量,该变量对方法进行了引用,本质上都是对象,也可以看成是属性,所谓的单件方法在python中就是实例的特有属性,方法和属性都遵循python从局部命名空间到全局命名空间到内建命名空间的顺序查找。
通过__slots__可以限制类的属性,以防止对实例添加不必要的变量:
class MyClass:
__slots___ = (‘a’,’b')
...
此时,如果再给obj添加其他的属性将会告知MyClass中没有该属性的错误,从而限制了对象的属性。
对象的属性是不再能被任意添加了,但是仍然可以在外部可以被外部代码轻松访问,通过在变量前添加两个下划线来表示私有变量:
class Student:
__slots__ = ('score','grade')
def __init__(self,score):
self.score = score
def getScore(self):
if self.score >= 90:
self.grade = 'A'
elif self.score >= 60:
self.grade = 'B'
elif self.score < 60:
self.grade = 'C'
return self.grade
xiaoming = Student(47)
xiaoming.score = 90
print(xiaoming.getScore())
在上述代码中,虽然在初始化小明的成绩的时候输入了47分,但是仍然可以在外部对该成绩进行修改,由于小明懂了一点python,瞬间就从差等生变成优等生,这样的结果是不希望被看到的。将score改为__score后:
class Student:
__slots__ = ('__score','grade')
def __init__(self,score):
self.__score = score
def getGrade(self):
if self.__score >= 90:
self.grade = 'A'
elif self.__score >= 60:
self.grade = 'B'
elif self.__score < 60:
self.grade = 'C'
return self.grade
此时,小明的投机取巧修改自己分数的手段就不再生效。访问xiaoming.__score时,将出现以下错误:AttributeError: 'Student' object has no attribute '__score'
尽管如此,其实小明还是可以这样做:
xiaoming._Student__score = 100
因为在使用双下划线时,python只不过是讲变量的名字之前加入了_Classname前缀以避免被直接访问到(不同版本的解释器会有所不同),而不是真正的不可访问,python中并没有真正不可操作的机制,而类似于_Student这些单下划线开头的变量名,虽然可以被外部访问到,但是最好不要这样做,单下划线的意思就是,虽然我不是私有变量,但是请你把我当成私有变量,这或许是对python中没有私有变量的一种妥协吧。
此时,我也没有办法来对付小明了。这是很有意思的故事,在Ruby中,任何实例变量在设置set方法和get方法之前,都是不能被外部访问到的,只有通过给私有变量设置setter方法或者getter方法,该变量才真正的成为属性,总之为了实例变量成为属性要做一些必要工作。而python中,几乎不能阻止一个对象访问在类中定义的变量,甚至如果你不用__slots__,都无法阻止添加各种属性到对象中,我们要做的工作反而变成想方设法让属性不能被访问到。
使用真正的属性
python也可以和其他语言一样,通过@property修饰器,实现getter、setter方法从而访问和设置的属性,从而可以“掩耳盗铃”地说,你不能设置该属性:
class Student:
__slots__ = ('_score','_grade')
def __init__(self,score):
self._score = score
@property
def score(self):
return _score
@property
def grade(self):
if self._score >= 90:
self._grade = 'A'
elif self._score >= 60:
self._grade = 'B'
elif self._score < 60:
self._grade = 'C'
return self._grade
当使用了@property修饰器之后,其实就是实现了getter方法,使外部可以被看到,如果想要设置getter方法,则再添加:
@score.setter
def score(self,value):
score._score = value
和其他语言一样,方法名字就是属性名字,小明可以通过xiaoming.score来看到自己的成绩,当没有定义setter方法时,小明是不能通过xiaoming.score来修改自己的成绩的,但是这是不是就意味着小明真的不能改了呢?其实通过上面的代码已经可以看到,python内部的变量名字并不是score,而是_score,这就是为什么我说“掩耳盗铃”的原因,小明依然可以通过xiaoming._score来修改自己的成绩。总之,python中是不会真正阻止一个对象访问类定义中的变量的,这实在有点令人苦恼。
关于属性的补充
1.和其他动态语言一样,可以在执行过程中查看对象中的属性,通过dir()方法,可以打印一个对象的所有属性组成的列表。另外一个不幸的消息,python3中取消了对象的__dict__属性,我不知道从哪里可以找回来。
2.或许会有类属性这样的说法,其实完全可以看做是类的命名空间中的一个变量。因为在type中并没有使用__slots__,所以在类中添加的任何变量,类本身和实例都可以访问,其中,实例访问该属性的时候,其实是建立了一个该属性的引用。所以当实例尝试去改变这个值的时候,如果值是不可变类型,则此时实例其实是添加了一个同名的属性,引用的其实是其他的值,而并非类中定义的属性。到这里可以发现,如果该属性是可变类型,则当实例去改变该值的时候,并没有重新引用其他的值,而是真正的改变了该属性:
class A:
a = 1
b = {'a':'A'}
o = A()
print(o.a)
print(id(o.a))
o.a = 2
print(id(o.a))
print(id(A.a))
A.a = 3
print(id(A.a))
print(o.a)
print(o.b)
print(id(o.b))
o.b['b'] = 'B'
print(o.b)
print(id(o.b))
print(A.b)
以上代码结果为(id会有所不同):
1
4296853120
4296853152
4296853120
4296853184
2
{'a': 'A'}
4301317296
{'a': 'A', 'b': 'B'}
4301317296
{'a': 'A', 'b': 'B'}
需要注意的是,第六个print函数结果为2,而不是A.a修改过的3,如果理解了整个过程,这是显而易见的:因为现在实例引用的已经是新的变量,而不是类属性,类属性的更改已经不会对其造成影响。