面向对象编程
主要包括:
- 基本概念
- 属性私有化(类的封装)
- 实例属性与类属性
- 静态方法、类方法、与实例方法
- 继承与多态
- 获取对象信息
1. 基本概念
-
面向对象是一种编写代码的思维方式:程序是由什么构成的。
-
在面向对象程序结构中:
程序(类) = 数据结构(属性) + 算法(方法)
-
类是一种抽象,例如:User 类:把用户相关信息抽象到一起。
-
一般来说,类是由属性和方法组成,属性表示了这个类的特点,方法规定了这个类的功能。
-
实例(对象)的概念:实例是类的具体化。是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
先来看一个例子:
class Cat(object):
def __new__(cls, *args, **kwargs):
# __new__ 方法主要用来分配内存,首先运行
print("__new__")
return super().__new__(cls)
def __init__(self, color, name):
# 构造方法,该方法会在创建对象(实例)的时候自动调用
self.color = color
self.name = name
def catch_rat(self):
print(self.name + '抓到了老鼠')
# 创建一个实例
tom = Cat('Blue', 'Tom')
tom.catch_rat()
__new__
Tom抓到了老鼠
class
后面紧接着是类名,即Cat
,类名通常使用‘驼峰法’命名,首字母大写,紧接着是(object)
,表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object
类,这是所有类最终都会继承的类。
定义好了类,就可以根据Cat
类创建出Cat
的实例,创建实例是通过类名+()实现的。
可以自由地给一个实例变量绑定属性,比如,给实例tom
绑定一个sex
属性:
tom.sex = 'male'
tom.sex
'male'
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__
方法,在创建实例的时候,就把name
,color
等属性绑上去:
这个 __init__
方法,它是构造方法,该方法会在创建对象(实例)的时候自动调用。(是不是有点像 C++ 中的构造函数)
另外__init__
方法的第一个参数永远是 self
,表示方法的调用者(有点像 this 指针的感觉),有了__init__
方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__
方法匹配的参数,但self
不需要传,Python解释器自己会把实例变量传进去。
接下来我们先简单看看实例化对象的过程:

正如上图描述的那样:
- 分配内存(
__new__
方法)(类也是一个对象,或者说是一个模板对象,这个我们后面再说) - 初始化属性值(
__init__
方法)
python 中没有属性控制符,对于每一个实例我们可以修改私有属性:
print(tom.name)
tom.name = 'JiaFeiCat'
print(tom.name)
Tom
JiaFeiCat
2. 属性私有化(类的封装)
python 中没有属性控制符,这就有点难受了,外部函数不就可以随便改我的属性了吗,不要慌一般有下面两种方法控制访问:
- 约定:单下划线开头,不要修改,但是!!小白来了给我修改了就不爽了对不对!!还有下面强制的方法:(后面就会看到还是挡不住大佬啊,挡不住挡不住)
- 属性名两个下滑线开头,
class Cat:
def __init__(self, color, name):
# 构造方法
self.__color = color
self.__name = name
def catch_rat(self):
print(self.__name + '抓到了老鼠')
# 创建一个实例
tom2 = Cat('Blue', 'Tom')
print(tom2.name)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-9-631bcdb4adc7> in <module>
10 # 创建一个实例
11 tom2 = Cat('Blue', 'Tom')
---> 12 print(tom2.name)
AttributeError: 'Cat' object has no attribute 'name'
ok 报错了,外部无法访问。只能自己的方法访问 or 修改。
双下滑线开头为什么可以隐藏变量呢?
我们使用__dict__
来查看一下实例的所有属性:(储存在一个字典中)
print(tom.__dict__)
print(tom2.__dict__)
{'color': 'Blue', 'name': 'JiaFeiCat'}
{'_Cat__color': 'Blue', '_Cat__name': 'Tom'}
从上面可以看出:双下滑线开头的属性名字会被自动转换: _类名+属性名
有一个小坏的想法我们其实还是可以通过访问 _类名+属性名
修改,(这谁能抗的住啊)
print(tom2._Cat__color)
Blue
在上面的例子中我们确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
但是如果外部代码要获取和修改name
属性怎么办?可以给Student
类增加get_name
和set_name
这样的方法:
在这个例子中我们加了一个函数 set_name
用于修改对象的 name
属性,和一个用于访问的 get_name
函数。
class Student:
'''
学生类
'''
def __init__(self, name, age):
self.__name = name
self.__age = age
def set_name(self, name):
if not len(name)<1:
self.__name = name
def get_name(self):
return self.__name
s = Student("Vector", 20)
print(s.get_name())
s.set_name("HHM")
print(s.get_name())
Vector
HHM
我们在上面的例子中是自己实现的 get
函数和 set
函数。有小可爱问,反正都要被外部访问,为啥不直接把name
属性设置成公有的,其实我们手动设置外部访问函数还有一个好处就是,我们可以对传进来的参数先进行一个处理(或者说检查),
但是我们每次修改或访问属性都要调用函数,能不能有一个方法让我们像对普通变量一样进行修改和赋值,又可以进行处理呢?
可以的!装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property
装饰器就是负责把一个方法变成属性调用的,@property
的实现比较复杂,我们先考察如何使用。
把一个getter
方法变成属性,只需要加上@property
就可以了,此时,@property
本身又创建了另一个装饰器@score.setter
,负责把一个setter
方法变成属性赋值,于是,我们就拥有一个可控的属性操作。
这个东西很好呀 如果我们把函数名字就设置成我们想要修改和访问的属性名,那么我们就可以像一个普通变量一样访问啦~(语法糖哦)
class Student:
'''
学生类
'''
def __init__(self, name, age):
self.__name = name
self.__age = age
@property
def age(self):
print('get函数')
return self.__age
@age.setter
def age(self,age):
print('set函数')
if age > 0:
self_age = age
s = Student("Vector", 20)
s.age = 50 # 调用了 set 函数
print(s.age) # 调用了 get 函数,进行了检查。
set函数
get函数
20
@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
3. 实例属性与类属性
由于Python是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过self
变量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
但是,如果Student
类本身需要绑定一个属性呢?可以直接在class
中定义属性,这种属性是类属性,归Student
类所有:
class Student(object):
name = 'Student'
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:
s = Student() # 创建实例s
print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
print(Student.name) # 打印类的name属性
print('-'*20)
s.name = 'Michael' # 给实例绑定name属性
print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
print('-'*20)
del s.name # 如果删除实例的name属性
print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
Student
--------------------
Michael
Student
--------------------
Student
从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
4. 静态方法、类方法、与实例方法
静态方法是指类中无需实例参与即可调用的方法(不需要self
参数),在调用过程中,无需将类实例化,直接在类之后使用.
号运算符调用方法。
通常情况下,静态方法使用@staticmethod
装饰器来声明。
class ClassA(object):
@staticmethod
def func_a():
print('Hello Python')
if __name__ == '__main__':
ClassA.func_a()
# 也可以使用实例调用,但是不会将实例作为参数传入静态方法
ca = ClassA()
ca.func_a()
Hello Python
Hello Python
类方法在Python中使用比较少,类方法传入的第一个参数为cls
,是类本身。并且,类方法可以通过类直接调用,或通过实例直接调用。但无论哪种调用方式,最左侧传入的参数一定是类本身。
通常情况下,类方法使用@classmethod
装饰器来声明。
class ClassA(object):
@classmethod
def func_a(cls):
print(type(cls), cls)
if __name__ == '__main__':
ClassA.func_a() # 类调用
ca = ClassA()
ca.func_a() # 实例调用
<class 'type'> <class '__main__.ClassA'>
<class 'type'> <class '__main__.ClassA'>
从运行结果可以看出,无论是类调用还是实例调用,类方法都能正常工作。且通过打印cls
,可以看出cls
传入的都是类实例本身。
我们再来看一个实例:
class User:
number = 0
def __init__(self, name):
self.name = name
User.number = User.number + 1
def print_name(self):
print(self.name)
@classmethod
def creat_user(cls, name, age):
print(cls)
user = cls(name)
user.age = age
return user
u2 = User.creat_user('zs', 20)
print(u2.age)
print(u2.name)
<class '__main__.User'>
20
zs
这里需要注意的是,如果存在类的继承,那类方法获取的类是类树上最底层的类。
实例方法,除静态方法与类方法外,类的其他方法都属于实例方法。
实例方法需要将类实例化后调用,如果使用类直接调用实例方法,需要显式地将实例作为参数传入。最左侧传入的参数self,是实例本身。
class User:
number = 0
def __init__(self, name):
self.name = name
User.number = User.number + 1
@staticmethod
def sum(a,b):
return a+b
print(User.sum(2,3))
5
以上的三种方法调用方式总结成一张表就是:

所以这么多方法到底有啥用呀??我们来看一个实例:类方法用在模拟java定义多个构造函数的情况。由于python类中只能有一个初始化方法,不能按照不同的情况初始化类。我们可以采取下面的方法:
class Book(object):
def __init__(self, title):
self.title = title
@classmethod
def class_method_create(cls, title):
book = cls(title=title)
return book
@staticmethod
def static_method_create(title):
book= Book(title)
return book
book1 = Book("use instance_method_create book instance")
book2 = Book.class_method_create("use class_method_create book instance")
book3 = Book.static_method_create("use static_method_create book instance")
print(book1.title)
print(book2.title)
print(book3.title)
use instance_method_create book instance
use class_method_create book instance
use static_method_create book instance
顺便提一下:在静态方法中直接使用类名,在类方法中使用cls
,在实例方法中使用self
5. 继承与多态
继承
在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Animal
的class,有一个run()
方法可以直接打印:
class Animal(object):
def run(self):
print('Animal is running...')
当我们需要编写Dog
和Cat
类时,就可以直接从Animal
类继承:
class Dog(Animal):
pass
class Cat(Animal):
pass
对于Dog
来说,Animal
就是它的父类,对于Animal
来说,Dog
就是它的子类。Cat
和Dog
类似。
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial
实现了run()
方法,因此,Dog
和Cat
作为它的子类,什么事也没干,就自动拥有了run()
方法:
dog = Dog()
dog.run()
cat = Cat()
cat.run()
Animal is running...
Animal is running...
当然,也可以对子类增加一些方法,比如Dog类:
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
继承的第二个好处需要我们对代码做一点改进。你看到了,无论是Dog
还是Cat
,它们run()
的时候,显示的都是Animal is running...
,符合逻辑的做法是分别显示Dog is running...
和Cat is running...,
因此,对Dog
和Cat
类改进如下:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
dog = Dog()
dog.run()
cat = Cat()
cat.run()
Dog is running...
Cat is running...
当子类和父类都存在相同的run()
方法时,我们说,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
。这样,我们就获得了继承的另一个好处:多态。
多态
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str
、list
、dict
没什么两样。
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:
def run_twice(animal):
animal.run()
animal.run()
当我们传入Animal
的实例时,run_twice()
就打印出:
run_twice(Animal())
Animal is running...
Animal is running...
当我们传入Dog
的实例时,run_twice()
就打印出:
run_twice(Dog())
Dog is running...
Dog is running...
当我们传入Cat
的实例时,run_twice()
就打印出:
run_twice(Cat())
Cat is running...
Cat is running...
看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise
类型,也从Animal
派生:
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
当我们调用run_twice()
时,传入Tortoise
的实例,你会发现,新增一个Animal
的子类,不必对run_twice()
做任何修改,实际上,任何依赖Animal
作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
多态的好处就是,当我们需要传入Dog
、Cat
、Tortoise
……时,我们只需要接收Animal
类型就可以了,因为Dog
、Cat
、Tortoise
……都是Animal
类型,然后,按照Animal
类型进行操作即可。由于Animal
类型有run()
方法,因此,传入的任意类型,只要是Animal
类或者子类,就会自动调用实际类型的run()
方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Animal
类型,无需确切地知道它的子类型,就可以放心地调用run()
方法,而具体调用的run()
方法是作用在Animal
、Dog
、Cat
还是Tortoise
对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal
的子类时,只要确保run()
方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增Animal
子类;
对修改封闭:不需要修改依赖Animal
类型的run_twice()
等函数。
继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object
,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:

动态语言的“鸭子类型”
对于静态语言(例如Java)来说,如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者它的子类,否则,将无法调用run()
方法。
对于Python这样的动态语言来说,则不一定需要传入Animal
类型。我们只需要保证传入的对象有一个run()
方法就可以了:
class Timer(object):
def run(self):
print('Start...')
run_twice(Timer())
Start...
Start...
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()
方法,返回其内容。但是,许多对象,只要有read()
方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()
方法的对象。
动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
关于继承的其他细节
在子类中如果想使用父类使用super()
表示。
class A:
def __init__(self):
self.name = 'A'
class B:
def __init__(self):
self.name = 'B'
class C(A):
def __init__(self):
super().__init__() # super 指定是 父类
self.age = 20
c = C()
print(c.name)
print(c.age)
A
20
另外python支持多继承,继承内容与继承顺序相关,例如下面的例子中,在搜索方法时先搜索C 再搜索 B 再搜索A ,最后搜索对象。
可以使用__mro__
查看搜索顺序。
class A:
def __init__(self):
self.name = 'A'
class B:
def __init__(self):
self.name = 'B'
class C(B, A):
def __init__(self):
super().__init__() # super 指定是 父类
self.age = 20
c = C()
print(c.name)
print(c.age)
B
20
print(C.__mro__)
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
6. 获取对象信息
当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
- 使用
type()
- 使用
isinstance()
- 使用
die()
此处详见https://www.liaoxuefeng.com/wiki/1016959663602400/1017499532944768
class User:
number = 0
def __init__(self, name):
self.name = name
def __str__(self):
_str = ''
for k ,v in self.__dict__.items():
_str +=
return self.__dict__
u = User('kk')
print(u)