面向对象简介

image.png

面向对象使得我们可以只去关注一件事情的参与者就好了,而不用关注参与者以外的东西。

面向对象的好处

一般来说,提及面向对象而不是直接使用全局变量或者函数的好处有以下三个:

多态

多态( polymorphism)源自希腊语,意思是“有多种形态”。多态赋予了我们不需要关注对象的类型就可以对其进行操作的能力。不同对象可以定义相同的方法,进而通过object.method()即可对对象进行操作,只要我们调用的对象拥有我们需要的方法,我们就可以对对象进行操作(操作的行为也会随着对象所属的类而不一样)。
拿方法count()举一个简单的例子:

x=['hello, World',[1,2,'e','e',4]]
print(x.count('e'))

我们并不关心x到底是什么对象(list or str),我们关心的只是x里边到底有多少个e。只要x里有一个名为 count()的方法,借助这个方法我们能实现我们希望返回的整数,这就足够了。
每当无需知道对象是什么样的就能对其执行操作时,都是多态在起作用

如果走起来像鸭子,叫起来像鸭子,那么它就是鸭子

封装

封装( encapsulation)指的是向外部隐藏不必要的细节。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封装让你无需知道对象的构造就能使用它。

继承

在谈继承之前,需要先了解一下类之间的关系。类可以看成是类型的同义词,是python中一种超级数据结构(They’re like super data structures, with code thrown in)
如果一个类的对象为另一个类的子集,前者就是后者的子类,后者是前者的超类

例如,如果你在窗外看到一只鸟,这只鸟就是“鸟类”的一个实例。鸟类是一个非常通用(抽象)的类,它有多个子类:你看到的那只鸟可能属于子类“云雀”。你可将“鸟类”视为由所有鸟组成的集合,而“云雀”是其一个子集。因此“云雀”为“鸟类”的子类,而“鸟类”为“云雀”的超类

面向对象的继承是指,一个类可以继承另一个类的属性和方法。继承的好处是,子类获得了父类的全部属性和方法,这样就不必从零开始编写,只需要编写新增的属性和方法。这增强了代码的复用性,子类在继承超类时,也可以将超类的方法进行重写

继承:即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。例如,有这样一个设计:一个Dog类型的对象派生自Animal类,这是模拟"是一个(is-a)"关系(例图,Dog是一个Animal)。Dog对象可以作为Animal对象对待,但是Animal对象不可以作为Dog对象对待。

python对属性的搜索遵从以下的原则:

Find the first occurrence of attribute by looking in object, then in all classes above it,from bottom to top and left to right.

l1,l2为两个实例,C1,C2,C3是三个类
反应在上图中,当调用l1.att时,搜索的顺序为l1,C1,C2,C3。这样的搜索顺序保证了子类的属性能够覆盖父类的属性,这也是在C1继承C2、C3时,重写x属性的原因。
redefining and replacing the attribute, C1 effectively customizes what it inherits from its superclasses.

类属性和方法

类(Class): 用来描述具有相同的属性和方法的对象(object)的集合。它定义了该集合中每个对象所共有的属性(attribute)和方法(method)。对象是类的实例(instance)
类属性:类中方法外的变量称为类属性,被该类的所有对象所共享。因为生成的实例中都有一个类指针指向定义的类,因此所有的实例可以共享该类属性,且类属性可以被访问和直接赋值修改;类属性定义在类中且在函数体之外。

实例可以直接访问类属性的取值,但无法直接对其进行修改,直接通过实例属性对其复制会创建一个新的实例属性,而不会修改类属性的值(覆盖类属性),python的属性搜索是自底向上的(instance->class->parent class),因此实例属性会覆盖类属性,但是通过类名访问类属性的值不会受到实例属性的影响。

类的属性定义在类下边,缩进与def命令相同,所有类均可访问该属性:

class People(object):
name = 'Jack' #类属性(公有)
__age = 12 #类属性(私有)

类的特殊属性

类的特殊属性:
dict:用来获得对象或者实例对象所绑定的所有属性和方法的字典
class:对象所属的类
bases:对象的父类类型元素
mro:类的层次结构
subclasses:子类
doc:类的注释

类的特殊方法

  • init : 对创建的对象进行初始化
  • del : 析构函数,释放对象时使用
  • repr : 打印,转换
  • setitem : 按照索引赋值
  • getitem: 按照索引获取值
  • len: 获得长度
  • cmp: 比较运算
  • call: 函数调用
  • add: 加运算
  • sub: 减运算
  • mul: 乘运算
  • truediv: 除运算
  • mod: 求余运算
  • pow: 乘方

init()方法

类定义了 init() 方法,类的实例化操作会自动调用 init() 方法。需要注意的是有的时候可能需要初始化属性,但不确定给该属性赋什么值或者就是想让该属性为空,那么可以给该属性赋值为None

class Complex:
def __init__(self, realpart, imagpart):
self.r = realpart
self.i = imagpart
x = Complex(3.0, -4.5)
print(x.r, x.i) # 输出结果:3.0 -4.5

new()方法

该方法主要用来写实例的创建方法。
image.png
先通过new方法来创建需要的对象,再通过init方法来初始化对象

类方法和静态方法定义

当我们在解决实际问题时,可能会遇到有一些方法是属于类而不属于实例的方法,如对于一个三角形类,判断输入参数是否是三角形的方法就可以看做是属于类的方法。定义类的私有方法有两个办法,一个办法是利用装饰器@staticmethod定义一个静态方法。另外一种方法是利用装饰器@classmethod用装饰器定义一个类的方法
两者的区别是类方法会默认第一个输入参数是类的名称参数cls(类比self):

from math import sqrt


class Triangle(object):

def __init__(self, a, b, c):
self._a = a
self._b = b
self._c = c

@staticmethod
def is_valid(a, b, c):
return a + b > c and b + c > a and a + c > b

def perimeter(self):
return self._a + self._b + self._c

def area(self):
half = self.perimeter() / 2
return sqrt(half * (half - self._a) *
(half - self._b) * (half - self._c))


def main():
a, b, c = 3, 4, 5
# 静态方法和类方法都是通过给类发消息来调用的
if Triangle.is_valid(a, b, c):
t = Triangle(a, b, c)
print(t.perimeter())
# 也可以通过给类发消息来调用对象方法但是要传入接收消息的对象作为参数
# print(Triangle.perimeter(t))
print(t.area())
# print(Triangle.area(t))
else:
print('无法构成三角形.')


if __name__ == '__main__':
main()
class Student():
count=0
def __init__(self,name) -> None:
self.name=name
Student.count+=1
@classmethod
def Count(cls):
print(cls.count,"instances have been distributed")
stu1=Student("xiaoming")
Student.Count()

instance

实例变量:在类的声明中,属性是用变量来表示的,这种变量就称为实例变量,实例变量就是一个用 self 修饰的变量。

动态语言

Python作为一门动态语言,我们可以根据自己的需要为创建的实例动态地单独绑定我们需要的属性和方法。

def test():
print("My name is Xiaoming")

class Student():
def __init__(self,name,age) -> None:
self.name=name
self.age=age
def print_name(self):
print("my name is %s"%self.name)
stu1=Student("张三",20)
stu2=Student("李四",18)
stu1.print_1=test
stu1.print_1()
#stu2.print_1()
#AttributeError: 'Student' object has no attribute 'print_1'

限制实例属性的绑定

有的时候我们并不希望我们我们对实例可以绑定任意属性,这时我们可以定义__slots__变量来进行限定属性绑定的范围

class Student():
__slots__=('age',''body')

对于上述类生成的实例,我们只能绑定age和body两个属性。但需要注意的是这一变量在类的继承时并不会进行继承。

属性或者方法的私有化

在属性和方法定义时前边加上一个"__"可以把类的名称或者属性私有化,私有化之后的类的名称或者属性是不能在外部进行访问的。

class Site:
def __init__(self, name, url):
self.name = name # public
self.__url = url # private

def who(self):
print('name : ', self.name)
print('url : ', self.__url)

def __foo(self): # 私有方法
print('这是私有方法')

def foo(self): # 公共方法
print('这是公共方法')
self.__foo()

x = Site('菜鸟教程', 'www.runoob.com')
x.who() # 正常输出
x.foo() # 正常输出
x.__foo() # 报错

私有属性的访问和更改

在属性或者方法前加’__'可以实现属性私有化,但这也会导致外部代码不能访问该属性(仍然可以通过instance.class name__attribute name访问,实际上是对变量名字进行了重整),于是python添加了一个在类或者属性前加一个’'对类的访问加以限制(虽然仍然可以通过_obj._var实现对该变量的访问,这个符号的含义就是希望我们不要去轻易修改和访问)。但是这样会使得访问和更改都变得比较麻烦,于是python提供了装饰器@property来将有限制的属性或者方法的访问或者修改变得更加便捷。可以理解为将负责把一个方法变成属性进行调用:

from unicodedata import name
class Person(object):
def __init__(self,name,age) -> None:
self._name=name
self._age=age
@property
def name(self):
return self._name
@name.setter
def name(self,name):
self._name=name
@property
def age(self):
return self._age
stu=Person('小明',18)
print(stu.name)
print(stu.age)
stu.name='小王'
#stu.age=22
print(stu.name)
print(stu.age)

image.png

基于类的编程

类的继承

类的继承使得我们的代码可以实现"自动更新"。继承类的代码格式如下:

class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>

BaseClassName(实例中的基类名)必须与派生类定义在一个作用域内。除了类,还可以用表达式,基类定义在另一个模块中时这一点非常有用:

class DerivedClassName(modname.BaseClassName):

多继承格式与单继承类似,只需在类名称后面的括号内依次写出父类的名称即可,不过需要注意的是当方法在多个父类中出现且有差异时 ,会调用最左边的包含该父类的方法

Superclasses provide behavior shared by all their subclasses, but because the search proceeds from the bottom up(left right), subclasses may override behavior defined in their superclasses by redefining superclass names lower in the tree

object类

object类是所有类的父类,在我们声明类不指明继承的类时,默认继承该类。

  • 内置方法dir()可以查看指定对象的所有属性
  • str(),如何打印对象信息,我们经常对该方法进行重写。不进行重写的时候在打印对象的时候会默认输出对象所属的类以及在内存中的位置#return string,不然无效

创建自定义类

首先引入一个简单的示例:

class Person:
def set_name(self, name):
self.name = name
def get_name(self):
return self.name
def greet(self):
print("Hello, world! I'm {}.".format(self.name))
foo=Person()
print(foo.get_name())
foo.set_name('xiaoming')

至于self是什么,其实从例子里也能看出,self就是方法的第一个参数,而在这个例子中,每次使用set_name和greet这些方法时,foo都会作为第一个参数传递给这个方法,所以我们用self来命名,因为它指向对象本身。显然, self很有用,甚至必不可少。如果没有它,所有的方法都无法访问对象本身——要操作的属性所属的对象。与以前一样,也可以从外部访问这些属性。 比如可以写print(foo.name)

方法覆盖

方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。

例如,我们可以通过覆盖父类的方法实现对子类的专有方法进行更改:

class Vector:
def __init__(self, a, b):
self.a = a
self.b = b

def __str__(self):
return 'Vector (%d, %d)' % (self.a, self.b)

def __add__(self,other):
return Vector(self.a + other.a, self.b + other.b)

v1 = Vector(2,10)
v2 = Vector(5,-2)
print (v1 + v2)

方法重写

有的时候我们希望对类的方法进行重写时或许希望可以借助原有类的内容,而不是将父类中的方法进行覆盖,这时python中的super()方法提供了这样的一个方法。

#在需要借助父类方法的地方写入
super().method name(*args,**kwargs)#不需要写self
class Person():
def __init__(self,name) -> None:
self.name=name
print("My name is %s"%self.name)
class student(Person):
def __init__(self, name,ID) -> None:
self.ID=ID
super().__init__(name)
#亦可super(student,self).__init__(name)
print("My ID is %s"%self.ID)
ming=student("xiaoming",88800)

使用super()调用父类方法的一个好处是不需要写父类的名称,这增加了代码的可移植性和重用性。

组合(composition)或聚合(aggregation)

类的继承适用于子类和父类存在很多相似点的两个类之间。但还存在一种情况是一个类是另外一个类的组成部分,这时用组合或者聚合是一种不错的选择。比如鸭子有“尾巴”这样的关系

class Hair():
def __init__(self,height) -> None:
self.height=height
class Student():
def __init__(self,hair) -> None:
self.hair=hair
def ppp(self):
print(self.hair.height)
hair_xiaoming=Hair(88)
stu=Student(hair_xiaoming)
print(id(stu.hair))
print(id(hair_xiaoming))
stu.ppp()

命名空间和作用域

在python中,类中变量的作用域与类的方法内的变量的作用域以及类定义外边的作用域是三个独立的作用域,也就是说在这三个域内的变量是互不影响的。如果要在类的方法内部使用类中的变量,需要借助nonlocal对需要使用的作用域进行声明。如果要在类外使用同一变量,则需要用global进行声明。

def scope_test():
def do_local():
spam = "local spam"

def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"

def do_global():
global spam
spam = "global spam"

spam = "test spam"
do_local()
print("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)
def p():
#直接访问会报错的
#print(a)
global a
a=3
print(a)
a=1
p()

‘__’,’_'保留用法

以两个或者一个下划线开头和结尾的名称都是python的保留用法。
function.name:
function.doc:

类和模块的使用场景

image.png

上下文管理器

上下文管理器是定义在python中一种特殊的类,它需要有两个特殊的方法,分别是__enter____exit__
with语法块就是一个很好的例子:

with context_expression [as target(s)]:
with-body

当使用with语句时,python会自动调用__enter__方法,然后将返回值赋值给as后面的变量,然后执行with-body中的语句,最后调用__exit__方法,换言之,写在context_expression中的类必须有__enter____exit__方法,在平时的编程过程中,使用with的场景多见于文件的读写,数据库的连接等,这里简单定义一个上下文管理器然后用with语句来调用它。

class ReadFile():
def __init__(self,path):
self.path=path
def __enter__(self):
self.f_obj = open(self.path, 'r')
print('文件读取成功')
return self.f_obj
def __exit__(self, exc_type, exc_val, exc_tb):
"""
@exc_type:错误的类型
@exc_val:错误类型对应的值
@exc_tb:代码中错误发生的位置
"""
if exc_type:
print('出错了')
print('错误类型:', exc_type)
print('错误值:', exc_val)
print('错误位置:', exc_tb)
self.f_obj.close()
print('退出操作')
with ReadFile('./test.txt') as f:
#raise SyntaxError
print(f"对文件数据的处理工作\n打开的文件对象:{f}")
print(f"文件是否关闭:{f.closed}")
print(f"文件是否关闭:{f.closed}")

#无报错运行的结果:
# 文件读取成功
# 对文件数据的处理工作
# 打开的文件对象: < _io.TextIOWrapper name = './test.txt' mode = 'r' encoding = 'cp936' >
# 文件是否关闭: False
# 退出操作
# 文件是否关闭: True


# 有报错运行的情况:
# 文件读取成功
# 出错了
# 错误类型: < class 'SyntaxError' >
# 错误值: None
# 错误位置: < traceback object at 0x000001D759B21C00 >
# 退出操作

实际上,在with后面的代码块抛出异常时,exit()方法被执行。开发库时,清理资源,关闭文件等操作,都可以放在exit()方法中。
with语句实际上是对try-except语法的进一步封装,省去了每次在最后写finally语句的麻烦。