语言的分类

  • 面向机器
    抽象成机器指令,机器容易理解
    代表:汇编语言
  • 面向过程
    问题规模小,可以步骤化,按部就班处理
    代表:C语言
  • 面向对象OOP
    随着计算机需要解决的问题的规模扩大,情况越来越复杂。需要很多人、很多部门协作,面向过程编程不太合适了
    代表:C++、Java、Python

面向对象

面向对象是一种认识世界、分析世界的方法论。将万事万物抽象为各种对象

类class

  • 类是抽象的概念,是万事万物的抽象,是一类事物的共同特征的集合
  • 用计算机语言来描述类,是属性和方法的集合

对象instance、object

  • 对象是类的具象,是一个实体
  • 对于我们每个人这个个体,都是抽象概念人类的不同的实体

类是抽象的,对象是具体化的

  • 属性:它是对象状态的抽象,用数据结构来描述
  • 操作:它是对象行为的抽象,用操作名和实现该操作的方法来描述

哲学

  • 一切皆对象
  • 对象是数据和操作的封装
  • 对象是独立的,但是对象之间可以相互作用
  • 目前OOP是最接近人类认知的编程范式

面向对象3要素

1. 封装

  • 组装:将数据和操作组装到一起
  • 隐藏数据:对外只暴露一些接口,通过接口访问对象。比如驾驶员使用汽车,不需要了解汽车的机动原理,会用就行

2. 继承

  • 多复用,继承来的就不用自己写了
  • 多继承少修改,OCP(Open-closed Principle),使用继承来改变,来体现个性

3. 多态

  • 面向对象编程最灵活的地方,动态绑定

Python的类

定义

class ClassName:
	语句块
  1. 必须使用class关键字
  2. 类名必须是用大驼峰命名
  3. 类定义完成后,就产生了一个类对象,绑定到了标识符ClassName上

举例

class MyClass:
    """A exapmle class"""
    x = 'abc' # 类属性

    def foo(self): # 类属性foo,也是方法
        return 'My Class'

print(MyClass.x)
print(MyClass.foo)
print(MyClass.__doc__)

# 打印结果
abc
<function MyClass.foo at 0x00000000021F3C80>
A exapmle class

类对象及类属性

  • 类对象,类的定义执行后会生成一个类对象
  • 类的属性,类定义中的变量和类中定义的方法都是类的属性
  • 类变量,上例中x是类MyClass的变量

MyClass中,x、foo都是类的属性, __doc__ 也是类的特殊属性
foo方法是类的属性,如同吃是人类的方法,但是每一个具体的人才能吃东西,也就是说吃是人的实例能调用的方法
foo是方法method,本质上就是普通的函数对象function,它一般要求至少有一个参数。第一个形式参数可以是
self(self只是个惯用标识符,不推荐修改),这个参数位置就留给了self。
self 指代当前实例本身

实例化

a = MyClass() # 实例化

使用上面的语法,在类对象名称后面加上一个括号,就调用类的实例化方法,完成实例化。
实例化就真正创建一个该类的对象(实例)。例如

tom = Person()
jack = Person()

上面的tom、jack 都是Person类的实例,通过实例化生成了2个实例。
每次实例化后获得不同的实例,即使是使用同样的参数实例化,也得到不一样的对象。
Python类实例化后,会自动调用__init__方法。这个方法第一个形式参数必须留给self,其它参数随意。

__init__方法

  • MyClass()实际上调用的是__init__(self) 方法,可以不定义,如果没有定义会在实例化后隐式调用。
  • 作用:对实例进行初始化
class MyClass:
    def __init__(self):
        print('init')

print(MyClass) # 不会调用
print(1, '-' * 50)
print(MyClass()) # 调用__init__
print(2, '-' * 50)
a = MyClass() # 调用__init__

# 打印结果
<class '__main__.MyClass'>
1 --------------------------------------------------
init
<__main__.MyClass object at 0x0000000001E90860>
2 --------------------------------------------------
init
init
  • 初始化函数可以多个参数,请注意第一个位置必须是self,例如__init__(self, name, age)
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def showage(self):
        print('{} is {}'.format(self.name,self.age))

tom = Person('Tom', 20) # 实例化, 会调用__init__方法并为实例进行属性的初始化
jack = Person('Jack', 25)
print(tom.name, jack.age) # Tom 25
jack.age += 1
print(jack.age) # 26
jack.showage()  # Jack is 26
  • 注意__init__() 方法不能有返回值,也就是只能是return None

实例对象instance

  • 类实例化后一定会获得一个类的实例,就是实例对象
  • 上例中的tom、jack就是Person类的实例
  • __init__方法的第一参数 self 就是指代某一个实例自身
  • 类实例化后,得到一个实例对象,调用方法时采用jack.showage()的方式,实例对象会绑定到方法上。
  • 但是该函数签名是showage(self),少传一个实际参数self吗?
  • 这个self就是jack,jack.showage()调用时,会把方法的调用者jack实例作为第一参数self的实参传入。
  • self.name就是jack对象的name,name是保存在了jack对象上,而不是Person类上。所以,称为实例变量

self

class MyClass:
    def __index__(self):
        print(1,'self in init = {}'.format(id(self)))

    def showself(self):
        print(2, 'self in showself() = {}'.format(id(self)))

c = MyClass() # 会调用__init__
print(3, 'c = {}'.format(id(c)))
print('-' * 30)
c.showself()

# 打印结果
3 c = 35653784
------------------------------
2 self in showself() = 35653784

上例说明,self就是调用者,就是c对应的实例对象。
self这个名字只是一个惯例,它可以修改,但是请不要修改,否则影响代码的可读性

实例变量和类变量

class Person:
    age = 3
    def __init__(self, name):
        self.name = name

tom = Person('Tom') # 实例化、初始化
jack = Person('Jack')

print(tom.name, tom.age)
print(jack.name, jack.age)
print(Person.age)
# print(Person.name)
Person.age = 30
print(Person.age, tom.age, jack.age)

# 运行结果
Tom 3
Jack 3
3
30 30 30
  • 实例变量是每一个实例自己的变量,是自己独有的
  • 类变量是类的变量,是类的所有实例共享的属性和方法
    在这里插入图片描述
  • 注意:
    Python中每一种对象都拥有不同的属性。函数、类都是对象,类的实例也是对象。
  • 举例
class Person:
    age = 3

    def __init__(self, name):
        self.name = name
print('----class----')
print(Person.__class__, type(Person))
print(sorted(Person.__dict__.items()), end='\n\n') # 属性字典

tom = Person('Tom')
print('----instance tom----')
print(tom.__class__, type(tom))
print(sorted(tom.__dict__.items()), end='\n\n')

print("----tom's class----")
print(tom.__class__.__name__)
print(sorted(tom.__class__.__dict__.items()), end='\n\n') # type(tom).__dict__

# 打印结果
----class----
<class 'type'> <class 'type'>
[('__dict__', <attribute '__dict__' of 'Person' objects>), ('__doc__', None), ('__init__', <function Person.__init__ at 0x00000000021E3C80>), ('__module__', '__main__'), ('__weakref__', <attribute '__weakref__' of 'Person' objects>), ('age', 3)]

----instance tom----
<class '__main__.Person'> <class '__main__.Person'>
[('name', 'Tom')]

----tom's class----
Person
[('__dict__', <attribute '__dict__' of 'Person' objects>), ('__doc__', None), ('__init__', <function Person.__init__ at 0x00000000021E3C80>), ('__module__', '__main__'), ('__weakref__', <attribute '__weakref__' of 'Person' objects>), ('age', 3)]

上例中,可以看到类属性保存在类的__dict__ 中,实例属性保存在实例的__dict__ 中,如果从实例访问类的属性,也可以借助__class__ 找到所属的类,再通过类来访问类属性,例如tom.__class__.age

class Person:
    age = 3
    height = 170

    def __init__(self, name, age=18):
        self.name = name
        self.age = age

tom = Person('Tom') # 实例化、初始化
jack = Person('jack', 20)

Person.age = 30
print(1, Person.age, tom.age, jack.age)  #  30, 18, 20
print(2, Person.height, tom.height, jack.height) # 170, 170, 170
jack.height = 175
print(3, Person.height, tom.height, jack.height) # 170, 170, 175

tom.height += 10
print(4, Person.height, tom.height, jack.height) # 170, 180, 175

Person.height += 15
print(5, Person.height, tom.height, jack.height) # 185, 180, 175

Person.weight = 70
print(6, Person.weight, tom.weight, jack.weight) # 70, 70, 70

# print(7, tom.__dict__['height']) # KeyError
# print(8, tom.__dict__['weight']) # KeyError
总结
  1. 是类的,也是这个类所有实例的,其实例都可以访问
  2. 是实例的,就是这个实例自己的,通过类访问不到
  3. 类变量是属于类的变量,这个类的所有实例可以共享这个变量
  4. 对象(实例或类)可以动态的给自己增加一个属性(赋值即定义一个新属性)
  5. 实例.__dict__[变量名]实例.变量名 都可以访问到实例自己的属性(注意这两种访问是有本质区别的)
  6. 实例的同名变量会隐藏掉类变量,或者说是覆盖了这个类变量。但是注意类变量还在那里,并没有真正被覆盖
实例属性的查找顺序
  • 指的是实例使用 .点号 来访问属性,会先找自己的__dict__ ,如果没有,然后通过属性__class__ 找到自己的类,再去类的 __dict__ 中找
  • 注意:如果实例使用 __dict__[变量名] 访问变量,将不会按照上面的查找顺序找变量了,这是指明使用字典的key 查找,不是属性查找
  • 一般来说,类变量可使用全大写来命名

装饰一个类

需求,为一个类通过装饰,增加一些类属性。例如能否给一个类增加一个NAME类属性并提供属性值

# 增加类变量
def add_name(name, cls):
    cls.NAME = name # 动态增加类属性

# 改进装饰器
def add_name(name):
    def wrapper(cls):
        cls.NAME = name
        return cls
    return wrapper

@add_name('Tom')
class Person:
    AGE = 3

print(Person.NAME)
  • 之所以能够装饰,本质上是为类对象动态的添加了一个属性,而Person这个标识符指向这个类对象

类方法和静态方法

前面的例子中定义的 __init__ 等方法,这些方法本身都是类的属性,第一个参数必须是self,而self必须指向一个对象,也就是类实例化之后,由实例来调用这个方法。

类方法
class Person:
    @classmethod
    def class_method(cls):
        print('class = {0.__name__} ({0})'.format(cls))
        cls.HEIGHT = 170

Person.class_method()
print(Person.__dict__)

# 打印结果
class = Person (<class '__main__.Person'>)
{'__module__': '__main__', 'class_method': <classmethod object at 0x000001CD0B334780>, \
'__dict__': <attribute '__dict__' of 'Person' objects>,\
 '__weakref__': <attribute '__weakref__' of 'Person' objects>, \
 '__doc__': None, 'HEIGHT': 170}
  1. 在类定义中,使用@classmethod装饰器修饰的方法
  2. 必须至少有一个参数,且第一个参数留给了clscls指代调用者即类对象自身
  3. cls这个标识符可以是任意合法名称,但是为了易读,请不要修改
  4. 通过cls可以直接操作类的属性
  • 注意:无法通过cls操作类的实例
    类方法,类似于C++、Java中的静态方法
静态方法
  1. 在类定义中,使用@staticmethod装饰器修饰的方法
  2. 调用时,不会隐式的传入参数 静态方法,只是表明这个方法属于这个名词空间。函数归在一起,方便组织管理
方法的调用
class Person:
    def method(self):
        print("{}' s method".format(self))

    @classmethod
    def class_method(cls):
        print('class = {0.__name__} ({0})'.format(cls))
        cls.HEIGHT = 170

    @staticmethod
    def static_method():
        print(Person.HEIGHT)

print('-----类访问')
# print(1, Person.method()) #  TypeError,需传入self
Person.class_method() # class = Person (<class '__main__.Person'>)
Person.static_method() # 170
print(Person.__dict__)
print('-----实例访问')
tom = Person() 
tom.method() # <__main__.Person object at 0x00000288330F9668>' s method
tom.class_method() # class = Person (<class '__main__.Person'>)
tom.static_method() # 170
  • 类几乎可以调用所有内部定义的方法,但是调用普通的方法时会报错,原因是第一参数必须是类的实例
  • 实例也几乎可以调用所有的方法,普通的函数的调用一般不可能出现,因为不允许这么定义
总结:
  • 类除了普通方法都可以调用,普通方法需要对象的实例作为第一参数
  • 实例可以调用所有类中定义的方法(包括类方法、静态方法),普通方法传入实例自身,静态方法和类方法需要找到实例的类
补充:
class Person:
    def method(self):
        print("{}' s method".format(self))

tom = Person()
tom.method()
# Person.method() # TypeError,需传入self
Person.method(tom)
tom.__class__.method(tom)

# 打印结果
<__main__.Person object at 0x00000176F1FB4780>' s method
<__main__.Person object at 0x00000176F1FB4780>' s method
<__main__.Person object at 0x00000176F1FB4780>' s method
  • tom.method()调用的时候,会绑定实例,调用method方法时,实例tom会注入到method中,这样第一参数就满足了。
  • Person.method(),使用类调用,不会有实例绑定,调用method方法时,就缺少了第一参数,可以手动的填入

访问控制

私有(Private)属性

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.age = age

    def growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.age += 1

p1 = Person('tom')
p1.growup(20) # 正常的范围
print(p1.age) # 19
p1.age = 160 # 超过了范围,并绕过了控制逻辑
print(p1.age) # 160

私有属性

使用__双下划线开头的属性名,就是私有属性

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.__age += 1

p1 = Person('tom')
p1.growup(20) # 正常的范围
# print(p1.__age) # 访问不到
  • 通过实验可以看出,外部已经访问不到__age了,age根本就没有定义,更是访问不到。
  • 那么,如何访问这个私有变量 __age 呢?

使用方法访问

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.__age += 1

    def getage(self):
        return self.__age
    
print(Person('tom').getage()) # 18

私有变量的本质

外部访问不到,动态增加一个 __age

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.__age += 1

    def getage(self):
        return self.__age

p1 = Person('tom')
p1.growup(20) # 正常的范围
# print(p1.__age) # 访问不到
p1.__age = 28
print(p1.__age) # 28
print(p1.getage())  # 19 # 为什么年龄不一样?__age没有覆盖吗?
print(p1.__dict__) # {'name': 'tom', '_Person__age': 19, '__age': 28}
  • 秘密都在 __dict__ 中,里面是 {’__age’: 28, ‘_Person__age’: 38, ‘name’: ‘tom’}
  • 私有变量的本质:
    类定义的时候,如果声明一个实例变量的时候,使用双下划线,Python解释器会将其改名,转换名称为 _类名__变量名 的名称,使用原来的名字访问不到

使用私有变量变更名,修改其属性

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.__age += 1

    def getage(self):
        return self.__age

p1 = Person('tom')
p1.growup(20) # 正常的范围
# print(p1.__age) # 访问不到
p1.__age = 28
print(p1.__age) # 28
print(p1.getage()) # 19
print(p1.__dict__) # {'name': 'tom', '_Person__age': 19, '__age': 28}

# 直接修改私有变量
p1._Person__age = 15
print(p1.getage()) # 15
print(p1.__dict__) # {'name': 'tom', '_Person__age': 15, '__age': 28}

保护变量

在变量名前使用一个 _ 下划线,称为保护变量。

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self._age = age

p1 = Person('tom')
print(p1._age) # 18
print(p1.__dict__) # {'name': 'tom', '_age': 18}
  • 可以看出,这个_age属性根本就没有改变名称,和普通的属性一样,解释器不做任何特殊处理。
  • 这只是开发者共同的约定,看见这种变量,就如同私有变量,不要直接使用。

私有方法

参照保护变量、私有变量,使用单下划线、双下划线命名方法

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self._age = age


    def _getname(self): # 没改名
        return self.name

    def __getage(self): # 已改名,_Person__getage
        return self._age

tom = Person('tom')
print(tom._getname()) # tom
# print(tom.__getage()) # AttributeError
print(tom.__dict__) # {'name': 'tom', '_age': 18}
print(tom.__class__.__dict__)
print(tom._Person__getage()) # 18

私有方法的本质

  • 单下划线的方法只是开发者之间的约定,解释器不做任何改变
  • 双下划线的方法,是私有方法,解释器会改名,改名策略和私有变量相同, _类名__方法名
  • 方法变量都在类的 __dict__ 中可以找到。

私有成员的总结

  1. Python中使用 _单下划线 或者 __ 双下划线来标识一个成员被保护或者被私有化隐藏起来。
  2. 但是,不管使用什么样的访问控制,都不能真正的阻止用户修改类的成员。Python中没有绝对的安全的保护成员 或者私有成员。
  3. 因此,前导的下划线只是一种警告或者提醒,遵守这个约定。除非真有必要,不要修改或者使用保护成员或者私有成员,更不要修改它们。

补丁

  • 可以通过修改或者替换类的成员。使用者调用的方式没有改变,但是,类提供的功能可能已经改变了。
  • 猴子补丁(Monkey Patch): (慎用)
  • 在运行时,对属性、方法、函数等进行动态替换。
  • 其目的往往是为了通过替换、修改来增强、扩展原有代码的能力。
# t1.py
class Person:
    def get_score(self):
        # from mysql数据库
        return {'English':25, 'Chinese':20}

# t2.py
def get_score(self):
    return {'English':80, 'Chinese':76}

# t3.py
from t1 import Person
from t2 import get_score

def mokeypatch4Person(cls):
    cls.get_score = get_score

mokeypatch4Person(Person)

if __name__ == "__main__":
    print(Person().get_score()) 

# 打印结果
{'English': 80, 'Chinese': 76}
  • 上例中,假设Person类get_score方法是从数据库拿数据,但是测试的时候,不方便
  • 为了测试时方便,使用猴子补丁,替换了get_score方法,返回模拟的数据

属性装饰器

一般好的设计是:把实例的某些属性保护起来,不让外部直接访问,外部使用getter读取属性和setter方法设置属性。

  • Python提供了属性property装饰器
class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    @property # 只读
    def age(self):
        return self.__age
 
    @age.setter # 可写
    def age(self, age):
        self.__age = age

    @age.deleter
    def age(self):
    	# del self.__age
        print('del')

tom = Person('Tom')
print(tom.age) # 18
tom.age = 20
print(tom.__dict__) # {'name': 'Tom', '_Person__age': 20}
print(tom.age) # 20
del tom.age # 执行@age.deleter下函数,打印del

特别注意

  • 使用property装饰器的时候这三个方法同名

property装饰器

  • 后面跟的函数名就是以后的属性名。它就是getter。这个必须有,有了它至少是只读属性

setter装饰器

  • 与属性名同名,且接收2个参数,第一个是self,第二个是将要赋值的值。有了它,属性可写

deleter装饰器

  • 可以控制是否删除属性。很少用

property装饰器必须在前,setterdeleter装饰器在后
property装饰器能通过简单的方式,把对方法的操作变成对属性的访问,并起到了一定隐藏效果

其它的写法

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def getage(self):
        return self.__age

    def setage(self, age):
        self.__age = age

    def delage(self):
        # def self.__age
        print('del')

    age = property(getage, setage, delage, 'age property')

tom = Person('Tom')
print(tom.age) # 18
tom.age = 20
print(tom.__dict__) # {'name': 'Tom', '_Person__age': 20}
print(tom.age) # 20
del tom.age # del

还可以如下

 class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    age = property(lambda self:self.__age)

tom = Person('Tom')
print(tom.age) # 18

对象的销毁

  • 类中可以定义 __del__ 方法,称为析构函数(方法)
  • 作用:销毁类的实例的时候调用,以释放占用的资源。其中就放些清理资源的代码,比如释放连接
  • 注意这个方法不能引起对象的真正销毁,只是对象销毁的时候会自动调用它
  • 使用del语句删除实例,引用计数减1。当引用计数为0时,会自动调用 __del__ 方法。 由于Python实现了垃圾回收机制,不能确定对象何时执行垃圾回收
class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def __del__(self):
        print('delete {}'.format(self.name))

tom = Person('tom')
tom.__del__() # delete tom
print('======statr======')
tom2 = tom
tom3 = tom
del tom
print(2, 'del') # 2 del
del tom2 # delete tom
del tom3

由于垃圾回收对象销毁时,才会真正清理对象,还会在回收对象之前自动调用 __del__ 方法,除非你明确知道自 己的目的,建议不要手动调用这个方法。

方法重载(overload)

  • 其他面向对象的高级语言中,会有重载的概念
  • 所谓重载,就是同一个方法名,但是参数个数、类型不一样,就是同一个方法的重载
  • Python没有重载
  • Python不需要重载
  • Python中,方法(函数)定义中,形参非常灵活,不需要指定类型(就算指定了也只是一个说明而非约束),参数个数也不固定(可变参数)。
  • 一个函数的定义可以实现很多种不同形式实参的调用。所以Python不需要方法的 重载。
  • 或者说Python语法本身就实现了其它语言的重载。

封装

面向对象的三要素之一,封装Encapsulation

  • 将数据和操作组织到类中,即属性和方法
  • 将数据隐藏起来,给使用者提供操作(方法)。使用者通过操作就可以获取或者修改数据。getter和setter。
  • 通过访问控制,暴露适当的数据和操作给用户,该隐藏的隐藏起来,例如保护成员或私有成员。
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐