带你学python基础:面向对象编程
面向对象编程是个啥呢,其实,在传统的语言中,比如 C 语言,是不存在面向对象编程这个概念的,那时候的语言只有面向过程编程,也就是我们写代码从头写到底,最多也就是有函数。所以,这样的代码风格是比较难维护的。
后来,随着编程语言的改进,在很多的语言都有了面向对象的思想,比如 C++、Java、C#等,而 Python也是如此。
一、那什么是面向对象呢?
拿个简单的例子说说,比如我们一个人,有头、身体、腿、手等,这些东西在面向对象的思想中,都可以把他们拆分为一个一个的对象,而不会把人就看做一个对象。
在我们经常玩的游戏中,每一个英雄,每一个兵器,都是一个个的对象,每个事物都是对象。
在面向对象编程中,我们还会谈到另外一个概念:类
。
那么什么是类呢,类是一个抽象的概念,我们知道有动物,动物下面有各种各样不同的动物,狗,老虎等。所以,类
是就是动物,也就是不同类型的动物的总称,也是抽象。而对象
就是具体的类别的动物。
类是对象的类型,具有相同属性和行为事物的统称。类是抽象的,在使用的时候通常会找到这个类的一个具体存在。
万物皆对象,对象拥有自己的特征和行为。
打个比方,我们每个人都可以看做是一个对象,而我们每个人都有我们自己的不同的特征,同时,我们也会产生我们的各种各样的行为。
这个图是不是看了就知道对象的特性了。
相信讲了这么多了,我们应该知道什么是类和对象了。下面我们讲一下,如何定义类。
二、定义类
首先,我们通过一个案例来说说如何定义类,最后再给出定义类的方法。
# 定义类
class pen():
def __init__(self, str, len):
self.str = str
self.len = len # 实例变量通过init初始化声明
# 定义类变量
width = 5
'''
获取信息
'''
def getStr(self):
print('str:%s,len:%s' % (self.str, self.len))
print('width:', pen.width)
pen = pen('初始化', 10)
pen.getStr()
上面定义了一个类,这个类名为pen
,然后,我们在类中定义了它的特征属性str
和len
,同时,我们还定义了一个行为(获取信息)。
通过这个例子,我们就可以看出怎么定义类的。
定义类规则
class 类名:
属性列表
方法列表
在上面这个例子中,我们发现这里存在两种变量,一种是实例属性,一种是类属性。下面我们就说说这两种变量有什么区别。
- 类变量:也可以说类属性,类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。如果需要用在函数中使用
类名.类属性
访问,如例子中的width = 5
。 - 实例变量:也可以说实例属性,定义在方法中的变量,只作用于当前实例的类, 如例子中的
len
。
好了,我们知道怎么定义类和定义类变量和实例变量,那么如何访问这些变量呢?
三、访问变量
方法
实例对象.属性
举例
例如,我们需要访问上面的pen的变量,则可以使用下面的方式。
# 访问变量
print(pen.len)
print(pen.width)
print(pen.str)
当然,你可能会想,还有其他方式吗,确实,还有其他方式,Python也提供了类似JavaScript的访问方式。
getattr(obj, name[, default]) #访问对象的属性
hasattr(obj,name) # 检查是否存在一个属性
setattr(obj,name,value) # 设置一个属性。如果属性不存在,会创建一个新属性
delattr(obj, name) # 删除属性
举例
# -*- coding:utf-8 -*-
# 定义类
class pen():
def __init__(self, str, len):
self.str = str
self.len = len # 实例变量通过init初始化声明
# 定义类变量
width = 5
'''
获取信息
'''
def getStr(self):
print('str:%s,len:%s' % (self.str, self.len))
print('width:', pen.width)
pen = pen('初始化', 10)
# 通过内置方法访问属性
print(getattr(pen, 'len'))
print(hasattr(pen, 'len'))
setattr(pen, 'len', 20)
print(pen.len)
delattr(pen, 'len')
print(pen.len)
内置类属性
另外,Python本身还提供了自己内置的类属性,分别有下面这些。
__dict__ : 类的属性(包含一个字典,由类的属性名:值组成) 实例化类名.__dict__
__doc__ :类的文档字符串 (类名.) 实例化类名.__doc__
__name__: 类名,实现方式 类名.__name__
__bases__ : 类的所有父类构成元素(包含了以个由所有父类组成的元组)
举例
我们还是以上面的例子来讲
print(pen.__dict__) #会将实例对象的属性和值通过字典的形式返回
print(pen.__doc__)
特殊说明
在前面的例子中,我们看到了init
和self
这两个关键字,下面讲解一下。
__init__()
:是一个特殊的方法属于类的专有方法,被称为类的构造函数或初始化方法,方法的前面和后面都有两个下划线。
这是为了避免Python默认方法和普通方法发生名称的冲突。每当创建类的实例化对象的时候,__init__()
方法都会默认被运行。作用就是初始化已实例化后的对象,这就是构造函数的意思。
在方法定义中,第一个参数self
是必不可少的。类的方法和普通的函数的区别就是self,self并不是Python的关键字,你完全可以用其他单词取代他,只是按照惯例和标准的规定,推荐使用self
。
既然是面向对象编程,那么,接下来肯定要说一下面向对象的三大特性了。
四、面向对象的三大特性
封装
封装这个特性,其实在前面就已经接触到了,只是没有明白的说而已。
封装字面上的意思就是把东西包裹起来,那么,在面向对象的编程中,其实封装也就是这个意思,常见的,比如,前面我们说的的类 class,我们把一些对象的属性和行为包裹在一个类里面,这就是封装的特性
。
继承
我们都知道,我们有父子关系,很多时候,儿子都会去继承父亲的财产的,好像都是这样的吧,哈哈。
在面向对象的编程中也是这么个意思,但是不叫父亲和儿子,我们把父亲叫做父类,儿子称为子类,我们子类去继承父类的财产,这个就是一个继承的特性
。
那我们如何用 Python 来表达这种关系呢,下面,我们用一个例子先讲一下,后面再讲规则。
父亲,儿子和女儿的故事
# 定义类
class Father():
'''
定义一个父亲类
'''
def __init__(self, money, house):
self.money = money
self.house = house
def wealth(self):
print('父亲给我 %d w, %d 套房子' % (self.money, self.house))
class Son(Father):
'''
定义一个儿子类,继承自父亲类
'''
def __init__(self, money, house):
super().__init__(money, house)
class Daughter(Father):
'''
定义一个女儿类,继承自父亲类
'''
def __init__(self, money, house):
super().__init__(money, house)
# 上面定义了一个父亲类,一个儿子类,一个女儿类。
# 儿子类和女儿类都继承自父亲类,所以,他们就拥有了父亲的财,在编程中也就是拥有了变量和方法。
son = Son(100, 10)
daughter = Daughter(200, 5)
# 继承自父亲类,所以自己不定义这个wealth方法,也会自动继承这个方法
son.wealth()
daughter.wealth()
通过这个例子,定义了一个父亲类,一个儿子类,一个女儿类。
儿子类和女儿类都继承自父亲类,所以,他们就拥有了父亲的财,在编程中也就是拥有了变量和方法。
在上面的例子中发现,我们只要在定义类的时候,在括号()中写上父类的名字,这就是继承了。
其中,我们也要注意,一个 super()
方法,它的作用是用来继承父类的属性。
所以,下面我们就大概知道继承的规则怎么写了。
规则
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
注意:圆括号中父类的顺序,如果继承的父类中有相同的方法名,而在子类中使用时未指定,python将从左至右查找父类中是否包含方法,在圆括号中有多个类名时,我们称为:多继承。也就是说,我们从多个父类继承了。
好了,继承我们就说到这里了。下面我们再说说最后一个面向对象的特性:多态!
多态
在谈到多态时,我们就不得不提到另外一个概念了,这个概念就是重写。例如,我们的容貌有一些地方是会跟父母很相像的,但是,我们也会有很多我们自己的特点。在面向对象编程里面也是这样的,我们会继承父类的特性,但是,在继承的同时,我们也会发生改变的,这就是重写。
那么如何实现重写呢?接着看!
# 定义类
class Father():
'''
定义一个父亲类
'''
def __init__(self, money, house):
self.money = money
self.house = house
def wealth(self):
print('父亲给我 %d w, %d 套房子' % (self.money, self.house))
def character(self):
print('我很高!')
class Son(Father):
'''
定义一个儿子类,继承自父亲类
'''
def __init__(self, money, house):
super().__init__(money, house)
def character(self):
print('我很胖!')
class Daughter(Father):
'''
定义一个女儿类,继承自父亲类
'''
def __init__(self, money, house):
super().__init__(money, house)
def character(self):
print('我很瘦!')
# 上面定义了一个父亲类,一个儿子类,一个女儿类。
# 儿子类和女儿类都继承自父亲类,所以,他们就拥有了父亲的财,在编程中也就是拥有了变量和方法。
father = Father(350, 20)
son = Son(100, 10)
daughter = Daughter(200, 5)
# 继承自父亲类,所以自己不定义这个wealth方法,也会自动继承这个方法
son.wealth()
daughter.wealth()
# 多态
father.character()
son.character()
daughter.character()
在继承的那个例子的基础上,我们又在父亲类中加了一个 character
方法,然后儿子类和女儿类在继承这个方法的同时,还对这个 character
方法进行了修改。
所以,此时,儿子类和女儿类在调用这两个方法时,显示的内容就不一样了。
也就是说,当子类和父类都存在相同的 character()
方法时,子类的 character()
覆盖了父类的 character()
,在代码运行时,会调用子类的 character()
。
这样,我们就获得了继承的另一个好处:多态。
多态的好处就是,当我们需要传入更多的子类,例如新增 Teenagers、Adult 等时,我们只需要继承 Father 类型就可以了,而 character()方法既可以直不重写(即使用Father的),也可以重写一个特有的。这就是多态的意思。调用方只管调用,不管细节,而当我们新增一种Father的子类时,只要确保新方法编写正确,而不用管原来的代码。这就是著名的“开闭”原则:
- 对扩展开放(Open for extension):允许子类重写方法函数
- 对修改封闭(Closed for modification):不重写,直接继承父类方法函数
ok,面向对象的三大特性就讲到这里。接下来讲讲关于类的其他知识!
五、类属性与实例属性
我们在前面的几个例子中,我们发现,在类中我们都定义了一些属性或者说变量。那什么是类属性,什么是实例属性呢?
类属性是类本身的属性,该类的实例都能调用,而实例属性是某个具体的实例特有的属性,不会影响到类,也不会影响到其他实例。
1.实例属性
关于实例属性,我们只要记住下面三点就可以了。
- 在
__init__(self,...)
中初始化 - 内部调用时都需要加上self.
- 外部调用时用
对象名.属性名
调用
2.类属性
- 在内部用
类名.类属性名
调用 -
外部既可以用
类名.类属性名
,又可以用对象名.类属性名
来调用,但是,后者是实例属性的值。
下面我们看一个简单例子:
# 定义类
class Father():
'''
定义一个父亲类
'''
age = 40 # 定义一个类属性
def __init__(self, money, house):
self.money = money # 实例属性
self.house = house
def wealth(self):
print('父亲给我 %d w, %d 套房子,类属性: %d ,实例属性:%d' % (self.money, self.house, Father.age, self.age))
def character(self):
print('我很高!')
father = Father(350, 20)
father.house = 4 # 修改实例变量的值,对象名.属性
Father.age = 41 # 修改类变量的值:类名.属性 或 对象名.属性(但后者修改的值是实例属性)
father.age = 42
father.wealth()
下面,我们再用类属性实现一个数值自增
# 定义类
class Father():
'''
定义一个父亲类
'''
age = 40 # 定义一个类属性
def __init__(self, money, house):
self.money = money # 实例属性
self.house = house
Father.age += 1 # 类属性自增
father = Father(350, 20)
print(Father.age)
father2 = Father(350, 20)
print(Father.age)
但是,在类中,我们不能用 self.age
来自增,这样是不行的,就像在类外不能用对象名.类变量
来改变值一样。
下面,我们用self.age
来自增试试什么效果。
# -*- coding:utf-8 -*-
# 定义类
class Father():
'''
定义一个父亲类
'''
age = 40 # 定义一个类属性
def __init__(self, money, house):
self.money = money # 实例属性
self.house = house
self.age += 1 # 类属性自增
father = Father(350, 20)
print(Father.age)
father2 = Father(350, 20)
print(Father.age)
这段代码就将 Father
改为 self
。但输出结果却不改变,如下
接下来,我们再深入的分析一下,看类属性和实例属性到底有什么联系?
# 定义类
class Father():
'''
定义一个父亲类
'''
age = 40 # 定义一个类属性
def __init__(self, money, house):
self.money = money # 实例属性
self.house = house
father1 = Father(350, 20)
father2 = Father(350, 20)
father1.age += 1
print(father1.age, father2.age, Father.age)
Father.age += 1
print(father1.age, father2.age, Father.age) # father2.age 因为这个实例属性不存在,所以找类属性为41
Father.age += 1
print(father1.age, father2.age, Father.age)
从这个结果我们可以看出,类属性自增,实例属性是不会跟着自增的,实例属性自增,每个实例属性之间也是独立的,但是,当实例属性不存在时,编译器是会去找类属性的值。
所以说,在Python中属性的查找机制
是自下而上的,即首先在实例属性中查找,如果实例属性不存在,再到类属性中查找。
六、访问权限(来自:https://www.cnblogs.com/Lambda721/p/6130213.html)
在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的name
、score
属性:
>>> bart = Student('Bart Simpson', 98)
>>> bart.score
98
>>> bart.score = 59
>>> bart.score
59
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__
,在Python中,实例的变量名如果以__
开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def print_score(self):
print('%s: %s' % (self.__name, self.__score))
改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name
和实例变量.__score
了:
>>> bart = Student('Bart Simpson', 98)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
但是如果外部代码要获取name和score怎么办?可以给Student类增加get_name
和get_score
这样的方法:
class Student(object):
...
def get_name(self):
return self.__name
def get_score(self):
return self.__score
如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score
方法:
class Student(object):
...
def set_score(self, score):
self.__score = score
你也许会问,原先那种直接通过bart.score = 59
也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数:
class Student(object):
...
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')
需要注意的是,在Python中,变量名类似__xxx__
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__
、__score__
这样的变量名。
有些时候,你会看到以一个下划线开头的实例变量名,比如_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name
是因为Python解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量:
>>> bart._Student__name
'Bart Simpson'
但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name
改成不同的变量名。
总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
最后注意下面的这种错误写法:
>>> bart = Student('Bart Simpson', 98)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'
表面上看,外部代码“成功”地设置了__name
变量,但实际上这个__name
变量和class内部的__name
变量不是一个变量!内部的__name
变量已经被Python解释器自动改成了_Student__name
,而外部代码给bart
新增了一个__name
变量。不信试试:
>>> bart.get_name() # get_name()内部返回self.__name
'Bart Simpson'
例子:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def get_name(self):
return self.__name
def get_score(self):
return self.__score
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')
def get_grade(self):
if self.__score >= 90:
return 'A'
elif self.__score >= 60:
return 'B'
else:
return 'C'
bart = Student('Bart Simpson', 59)
print('bart.get_name() =', bart.get_name())
bart.set_score(60)
print('bart.get_score() =', bart.get_score())
print('DO NOT use bart._Student__name:', bart._Student__name)
终于到最后一个知识点了,这篇文章写的真的久!
七、类方法与静态方法
这个知识点就说说,因为没什么好说的。
普通方法我们都知道怎么定义了。
1.普通方法
def fun_name(self,...):
pass
2.静态方法
- 通过装饰器
@staticmethod
装饰 - 不能访问实例属性
- 参数不能传入
self
- 与类相关但是不依赖类与实例的方法
3.类方法
- 通过装饰
@classmethod
修饰 - 不能访问实例属性
- 参数必须传入cls
必须传入cls参数(此类对象和self代表实例对象),并且用此来调用类属性:cls.类属性名。
最后,再总结一下。
- 静态方法与类方法都可以通过类或者实例来调用,其两个的特点都是不能够调用实例属性。
- 静态方法不需要接收参数,使用
类名.类属性
。
下面,再举一个例子看看。
# -*- coding:utf-8 -*-
# 定义类
class Father():
'''
定义一个父亲类
'''
age = 40 # 定义一个类属性
def __init__(self, money, house):
self.money = money # 实例属性
self.house = house
# 创建普通方法
def getMoney(self):
# 类属性的使用通过类名.属性名使用 这是规范
# 私有属性在类里面使用正常使用
print('我有:%d w' % (self.money)) # 在方法里面使用实例属性
# 创建一个静态方法
@staticmethod
def aa(): # 不需要传递实例
# 静态方法不能访问实例属性
# 静态方法只能访问类属性
print('我:%d 岁' % Father.age) # 在方法里面使用实例属性
# 类方法
@classmethod
def bb(cls, n): # class 也不是关键字
# 类方法不能访问实例属性
cls.age = n
print('我:%d 岁' % cls.age) # 就用cls.类属性
father1 = Father(350, 20)
father2 = Father(350, 20)
# 通过对象来调用静态方
father1.aa()
# 通过对象来调用类方法
father1.bb(18)
# 静态方法和类方法的调用,推荐使用类名的方式去调用
# 通过类名来调用静态方法
Father.aa()
# 通过类名来调用类方法
Father.bb(18)
八、总结
这一节讲了很多,需要好好消化。