1.1 面向对象


1.1.1 初识对象

  1. 在程序中设计表格,我们称之为:设计类(class)

    class Student:
        name = None
        gander = None
        nationality = None
        native_place = None
        age = None
  2. 在程序中打印生产表格,我们称之为:创建对象

    # 基于类创建对象
    stu_1 = Student()
  3. 在程序中填写表格,我们称之为:对象属性赋值

    stu_1.name = '顶针'
    stu_1.gander = '男'
    stu_1.nationality = '中国'
    stu_1.native_place = '妈妈生的'
    stu_1.age = 18

1.1.2 成员方法


  1. 类由两部分组成

    • 类的属性,称之为:成员变量
    • 类的行为,称之为:成员方法

    ==注意:函数是写在类外的,定义在类内部,我们都称之为方法哦==

  2. 类和成员方法的定义语法

    class 类名称:
        成员变量
        
        def 成员方法(self,参数列表):
            成员方法体
            
            
    对象 = 类名称()
  3. self的作用

    • 表示类对象本身的意思
    • 只有通过self,成员方法才能访问类的成员变量
    • self出现在形参列表中,但是不占用参数位置,无需理会

1.1.3 类和对象


  1. 现实世界的事物由什么组成?

    • 属性
    • 行为

    类也可以包含属性和行为,所以使用类描述现实世界事物是非常合适的

  2. 类和对象的关系是什么?

    类是程序中的“设计图纸”

    对象是基于图纸生产的具体实体

  3. 什么是面向对象编程?

    面向对象编程就是,使用对象进行编程。

    即,设计类,基于类创建对象,并使用对象来完成具体的工作

1.1.4 构造方法


Python类可以使用:__init__()方法,称之为构造方法。

可以实现:

  • 在创建类对象(构造类)的时候,会自动执行。
  • 在创建类对象(构造类)的时候,将传入参数自动传递给__init__方法使用。

image-20240504222958776

注意:

  • 构造方法也是成员方法,不要忘记在参数列表中提供:==self==
  • 在构造方法内定义成员变量,需要使用==self==关键字

    image-20240504223348813

    • 这是因为:变量是定义在构造方法内部,如果要成为成员变量,需要用self来表示。

1.1.5 其它内置方法


常见的5个内置方法:

方法功能
__init__构造方法,可用于创建类对象的时候设置初始化行为
__str__用于实现类对象转字符串的行为
__lt__用于2个类对象进行小于或大于比较
__le__用于2个类对象进行小于等于或大于等于比较
__eq__用于2个类对象进行相等比较

1. __str__ 字符串方法

当类对象需要被转换为字符串之时,会输出内存地址(<__main__.Student object at 0x000001AD070F81D0>)

内存地址没有多大作用,我们可以通过__str__方法,控制类转换为字符串的行为。


语法:

def __str__(self):
    return f'Student类对象:name:{self.name},age:{self.age}'
  • 方法名:__str__
  • 返回值:字符串
  • 内容:自行定义
2. __lt__ 小于符号比较方法

直接对2个对象进行比较是不可以的,但是在类中实现__lt__方法,即可同时完成:==小于符号 和 大于符号 2种比较==

比较大于符号的魔术方法是:__gt__,不过,实现了__lt____gt__就没必要实现了


语法:

def __lt__(self, other):
    return self.age < other.age
  • 方法名:__lt__
  • 传入参数:other,另一个类对象
  • 返回值:True 或 False
  • 内容:自行定义
3. __le__ 小于等于比较符号方法

魔术方法:__le__可用于:<=、>=两种比较运算符上。

大于等于符号实现的魔术方法是:__ge__不过,实现了__le____ge__就没必要实现了


语法:

def __le__(self, other):
    return self.age <= other.age
  • 方法名:__le__
  • 传入参数:other,另一个类对象
  • 返回值:True 或 False
  • 内容:自行定义
4.__eq__ 比较运算符实现方法

不实现__eq__方法,对象之间可以比较,但是是比较内存地址,也即是:不同对象==比较一定是False结果。

实现了__eq__方法,就可以按照自己的想法来决定2个对象是否相等了。


语法:

def __eq__(self, other):
    return self.age == other.age
  • 方法名:__eq__
  • 传入参数:other,另一个类对象
  • 返回值:True 或 False
  • 内容:自行定义

1.1.6 封装


将现实世界事物在类中描述为属性和方法,即为封装。

私有成员:

在类中提供仅供内部使用的属性和方法,而不对外开放(类对象无法使用)

  1. 私用成员又分为两种:

    • 私有成员变量
    • 私有成员方法
  2. 定义语法:

    • 私有成员变量:变量名以__开头(2个下划线)

      __变量名 = None
    • 私有成员方法:方法名以__开头(2个下划线)

      def __方法名(self):
          成员方法体
  3. 私有成员的访问限制

    • 类对象无法访问私有成员
    • 类中的其它成员可以访问私有成员

1.1.7 继承


1. 继承的基础语法

继承表示将从父类那里继承(复制)来成员变量和成员方法(不含私有)

继承分为:单继承和多继承

单继承:

一个类继承另一个类

class 类名(父类名):
    类内容体

多继承:

一个类继承多个类,按照顺序从左向右依次继承

class 类名(父类名1, 父类名2, ……, 父类名n):
    类内容体

==注意:多个父类中,如果有同名的成员,那么默认以继承顺序(从左到右)为优先级。
即:先继承的保留,后继承的被覆盖==

pass关键字:

pass是占位语句,用来保证函数(方法)或类定义的完整性,表示无内容,空的意思

例:

class 类名(父类名1, 父类名2, ……, 父类名n):
    pass
2. 复写和使用父类成员

复写表示对父类的成员属性或成员方法进行重新定义

语法:

在子类中重新实现同名成员方法或成员属性即可,例:

class Phone:
    producer = "XIAOMI"
    

class MyPhone(Phone):
    producer = "HUAWEI"

在子类中调用父类成员:

方法一:

  • 使用成员变量:父类名.成员变量
  • 使用成员方法:父类名.成员方法(self)

方法二:

  • 使用成员变量:super().成员变量
  • 使用成员方法:super().成员方法()

==注意:只可以在子类内部调用父类的同名成员,子类的实体类对象调用默认是调用子类复写的==

1.1.8 类型注解


1. 变量的类型注解

Python在3.5版本的时候引入了类型注解,以方便静态类型检查工具,IDE等第三方工具。

类型注解:在代码中涉及数据交互的地方,提供数据类型的注解(显式的说明)。

主要功能:

  • 帮助第三方IDE工具(如PyCharm)对代码进行类型推断,协助做代码提示
  • 帮助开发者自身对变量进行类型注释

支持:

  • 变量的类型注解
  • 函数(方法)形参列表和返回值的类型注解

语法一:

变量: 类型

例:

# 基础数据类型注解
name: str = '张三'
# 基础容器类型注解
my_list: list = [1, 2, '好好好']
# 容器类型详细注解
my_list: list[int, str, bool] = [1, '好好好', True]

# 类对象类型注解
class Student:
    pass
stu: Student = Student()

注意:

  • 元组类型设置类型详细注解,需要将每一个元素都标记出来
  • 字典类型设置类型详细注解,需要2个类型,第一个是key第二个是value

语法二:

# type: 类型

例:

class Student:
    pass

var_1 = random.randit(1, 10)    # type: int
var_2 = json.loads(data)    # type: dict[str, int]
var_3 = func()        #type: Student

注意事项:类型注解只是提示性的,并非决定性的。数据类型和注解类型无法对应也不会导致错误

2. 函数(方法)的类型注解

分为:

  • 形参的类型注解
  • 返回值的类型注解

语法:

def 函数方法名(形参: 类型, ……, 形参: 类型) -> 返回值类型:
    方法内容体

注意:返回值类型注解的符号使用: ->

3. Union类型

Union可以定义联合类型注解

使用方式:

  • 导包:from typing import Union
  • 使用:Union[类型, ......, 类型]

例:

from typing import Union

my_list: [Union[str, int]] = [1, 2, '好好好', '呵呵呵']


def func(data: Union[int, str]) -> Union[int, str]:
    return data

1.1.9 多态


多态的概念:

多态指的是,同一个行为,使用不同的对象获得不同的状态。

如,定义函数(方法),通过类型注解声明需要父类对象,实际传入子类对象进行工作,从而获得不同的工作状态

什么是抽象类(接口):

包含抽象方法的类,称之为抽象类。抽象方法是指:没有具体实现的方法(pass)称之为抽象方法

抽象类的作用:

多用于做顶层设计(设计标准),以便子类做具体实现。

也是对子类的一种软性约束,要求子类必须复写(实现)父类的一些方法,并配合多态使用,获得不同的工作状态。

来源

Python中的抽象类
abc库实现抽象类和抽象函数

Python的内置库abc(abstract base class的简称)可以实现抽象类的实现。通过让你的类继承abc.ABC即可让这个类被声明为一个抽象类,比如我们现在来创建一个商品类Good,它用来派生具体的商品,要求如下:

  • 有一个静态变量TAX,代表税率
  • 私有变量price代表商品价格
  • 公有成员函数buy负责打印商品账单

简单实现如下:

import abc

class Good(abc.ABC):
    TAX = 0.01
    def __init__(self, price=None) -> None:
        self.__price = price
    
    # 通过装饰器申明抽象函数
    @abc.abstractmethod
    def buy(self):
        pass

如果是版本小于3.3,那么可以通过改变元类属性metaclass的方式实现:

import abc

class Good(metaclass=abc.ABCMeta):
    TAX = 0.01
    def __init__(self, price=None) -> None:
        self.__price = price
    
    @abc.abstractmethod
    def buy(self):
        pass

理想的效果是,我们实例化这个抽象类,解释器要报错:

g = Good()

out:

Traceback (most recent call last):
  File "e:\python draft\test.py", line 12, in <module>
    g = Good()
TypeError: Can't instantiate abstract class Good with abstract methods buy

需要注意,这个报错检查需要你编写的类同时满足两个条件:

  • 是抽象类ABC的子类
  • 含有抽象函数(即有成员函数被abc.abstractmethod装饰)

换句话说,在Python中,你编写的类需要满足以上两个条件,才能称得上一个抽象类。

collections.abc Python内置抽象类 实现自定义基本数据结构

熟悉Python内置库的同学可能还知道Python常用模块collection中有一个子库也叫abc,那么这个abc和之前的abc有什么区别吗?我们不妨打印一下__all__看看collection.abc中都有什么:

from collections import abc
print(abc.__all__)

out:

['Awaitable', 'Coroutine', 'AsyncIterable', 'AsyncIterator', 'AsyncGenerator', 'Hashable', 'Iterable', 'Iterator', 'Generator', 'Reversible', 'Sized', 'Container', 'Callable', 'Collection', 'Set', 'MutableSet', 'Mapping', 'MutableMapping', 'MappingView', 'KeysView', 'ItemsView', 'ValuesView', 'Sequence', 'MutableSequence', 'ByteString']

不卖关子了,collection.abc中定义的,都是基本数据类型的抽象父类。比如中间的Sequence,代表不可变序列类的抽象父类,很明显,Python中的基本数据结构tuple就是Sequence的子类,我们可以检查一下:

from collections import abc
print(issubclass(tuple, abc.Sequence))

out:

True

可以看到,tuple确实是Sequence的子类。

tuple是Sequence的子类不代表tuple是基于Sequence实现的,tuple,list这些内置数据类型是直接基于C源码中的PyObject结构体实现的,感兴趣的同学可以去看这篇文章zpoint's blog-CSDN博客_python tuple底层实现

这意味着,我们可以使用Sequence自己来实现一个tuple!那么我们可以试试看,首先,我们需要知道Sequence中有哪些抽象函数需要实现,最简单粗暴的方式就是直接实例化Sequence,所有的抽象函数都会在报错信息中展示出来:

from collections import abc
seq = abc.Sequence()

out:

Traceback (most recent call last):
  File "e:\python draft\test.py", line 3, in <module>
    seq = abc.Sequence()
TypeError: Can't instantiate abstract class Sequence with abstract methods __getitem__, __len__

可以看到,Sequence中我们只需要实现__getitem__, __len__即可。因此,当你想要实现属于自己的基本数据对象时,可以尝试使用collections.abc类预定好的抽象类作为父类来实现。


白鹅类型和鸭子类型

在Python中实现多态主要有两种机制:白鹅类型和鸭子类型。emm,请允许我解释清除:白鹅类型和鸭子类型不仅是两种机制,也是两种不同的编程风格。

我们先来举个例子来直观感受一下两者的区别:假设我们现在需要实现N个商品类,在入口函数中,我们希望调用这N个商品类实例对象的price方法来打印出对应的价格。这是非常典型的多态实现。

假设我们需要实现的类为Food, Clothes 和 Coffee。

我们先来写一下入口函数中需要执行的语句:

goods = [Food(), Clothes(), Coffee()]
for good in goods:
    good.price()
鸭子类型实现

我们希望上述代码可以打印出三种商品的价格,先来看看鸭子类型机制的实现:

class Food:
    def price(self):
        print("{} price:$4".format(__class__.__name__))

class Clothes:
    def price(self):
        print("{} price:$5".format(__class__.__name__))

class Coffee:
    def price(self):
        print("{} price:$6".format(__class__.__name__))

打印结果:

Food price:$4
Clothes price:$5
Coffee price:$6
白鹅类型实现

再来看看白鹅类型的实现:

import abc

class Good(abc.ABC):
    @abc.abstractmethod
    def price(self):
        pass

class Food(Good):
    def price(self):
        print("{} price:$4".format(__class__.__name__))

class Clothes(Good):
    def price(self):
        print("{} price:$5".format(__class__.__name__))

打印结果:

Food price:$4
Clothes price:$5
Coffee price:$6

Fine,我们可以看到白鹅类型和鸭子类型都是实现了完全相同的效果。我们接下来慢慢分析一下:可以看到,在鸭子类型的实现中,我们只需要保证调用price方法的对象身上有price方法即可,别的我们关心;而白鹅类型中,为了在一个对象列表中调用每一个对象的price方法,我们直接让所有对象的类派生自含有price虚函数的抽象类Good。

如果你学过强类型语言,譬如C++,Java,那么白鹅类型你应该丝毫不陌生,Python中的白鹅类型机制就是强类型语言中实现多态的标准模式,即通过调取父类的虚函数或者继承的函数来完成不同的行为。

但是Python是一门弱类型的语言,并不强制要求保证类型一致,因此,为了实现多态,白鹅类型和鸭子类型都只是一种手段或者项目开发时实现商量好的协议。特别是鸭子类型,由于鸭子类型不限制展现多态的对象的类型,所以鸭子类型更像是一种不受编程语言语法限制的“协议”。

比如我们在开发项目时,你现在要拿你的好朋友的模块来进行多态操作,那么如果你的朋友使用的是白鹅类型的编程风格,你只需要确保进行多态的各个对象的父类是同一个即可;但是如果你的好朋友是弱类型动态脚本大师,用了鸭子类型进行了模块编写,那么你不得不去确认后续多态涉及到的每个操作是否每个类中都有与之同名的类函数对应。

鸭子类型的历史由来

看到这个section的最后了,我们来闲聊几句吧。为什么要把弱类型索取不同类中同名函数的多态称为鸭子类型,而将索取同父类虚函数的多态称为白鹅类型呢?

img

鸭子类型(Duck Typing)源于一种名叫鸭子测试(Duck Test)的思想。鸭子测试有着非常悠久的历史,根据维基百科上的考据,最早发明这个词的人大概是美国诗人James Whitcomb Riley(1849–1916):

The American poet James Whitcomb Riley (1849–1916) may have coined the phrase when he wrote:
When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

翻译过来就是「当我看到一只像鸭子一样走路、像鸭子一样游泳、像鸭子一样叫的鸟时,我称那只鸟为鸭子。」

最早让这个词流行起来的人是美国驻危地马拉(拉丁美洲国家)大使Richard Cunningham Patterson Jr 。具体事宜大家可以去查wiki,在此我就不再赘述了。后来又在Douglas Adams的侦探小说中被提及,然后或许被某个喜欢侦探小说的程序员写入他自己喜欢的note或者blog中了吧hhh

Fine,所以说,鸭子类型是动态语言的一种特性。拿刚才商品类的实现来说,就是,我如何保证遍历goods列表中的对象都是good呢?因为good对象一定有price方法,那么我后面的程序就不大可能出错。

这个场景中,商品good就是“鸭子”,而我们列表中的商品都有price方法,也就是它们调用price的姿势很像商品(叫起来很像“鸭子”),那么这些对象就大概就是商品类(那么它大概就是一只鸭子!)

这种点对点对应的校验方式确实很适合动态语言的这种特性,因此,我们将这种只需蕴含执行操作方法实现的多态实现机制称为鸭子类型。

而白鹅类型则是相对于鸭子类型而言的,白鹅类型的对象必须有明确的接口定义。关于白鹅类型的历史,我暂时没有查到,但是从网上提及到过这个词的口吻而言,应该出自于某本Python编程教材的作者。知道的读者欢迎在评论区留言。


猴子补丁
什么是猴子补丁

Wiki上给出的解释为:

the term monkey patch only refers to dynamic modifications of a class or module at runtime, motivated by the intent to patch existing third-party code as a workaround to a bug or feature which does not act as desired

简单说,猴子补丁是动态为已经创建出的对象增加新的方法和属性成员的一种机制,也就是动态打补丁。

来举几个例子你就懂了。比如一个非常简单的例子:

class Test:
    def func1(self, x, y):
        print(x + y)

test = Test()
test.func1(1, 1)

out:

2

但是你发现环境发生了变化,我们需要修改这个func1的计算逻辑,由于我们很多时候是将Test类通过import导入的,Test的源代码对我们而言不可修改,甚至Test的源代码可能是二进制文件,我们也改不了,那么这个时候要怎么办呢?

答案很简单:直接移花接木!

实例化对象的猴子补丁

我们可以直接动态修改已经实例化出来的对象的func方法:

class Test:
    def func1(self, x, y):
        print(x + y)


test = Test()
test.func1 = lambda x, y : print(x + 2 * y)
test.func1(1, 1)

out:

3

是不是很爽?你可以在运行时直接瞎改对象成员,就好像对象就只是一个容器一样!

但是请注意,这种方式下“接上去”的函数是静态函数,这意味着我们无法直接通过猴子补丁函数来访问原本对象的内部成员变量,比如下面这个代码就会报错:

class Test:
    def __init__(self) -> None:
        self.a = 1
    def func1(self, x, y):
        print(x + y)


test = Test()
test.func1 = lambda x, y : print(x + 2 * y + self.a)
test.func1(1, 1)

out:

Traceback (most recent call last):
  File "e:\python draft\tempCodeRunnerFile.py", line 10, in <module>
    test.func1(1, 1)
  File "e:\python draft\tempCodeRunnerFile.py", line 9, in <lambda>
    test.func1 = lambda x, y : print(x + 2 * y + self.a)
NameError: name 'self' is not defined

我们可以这么改:

class Test:
    def __init__(self) -> None:
        self.a = 1
    def func1(self, x, y):
        print(x + y)


test = Test()
test.func1 = lambda self, x, y : print(x + 2 * y + self.a)
test.func1(test, 1, 1)

out:

4

但是说实话,这么改太不优雅了,而且函数接口都变了,如果我们只想改逻辑,不想改接口,这个方法就行不通,还有别的方法吗?

答案也很简单:直接改类!

类的猴子补丁

Python的猴子补丁的神奇的地方在于你甚至可以直接改类,因为Python中的类也是一个对象,读者可以简单理解为Python中的所有定义出来的class都是元类(meta class)的实例化对象。既然是对象,那当然可以打猴子补丁~~~

不过类的函数补丁一定要带指向实例化对象的self参数:

class Test:
    def __init__(self) -> None:
        self.a = 1
    def func1(self, x, y):
        print(x + y)


Test.func1 = lambda self, x, y : print(x + 2 * y)

test = Test()
test.func1(1, 1)

out:

3

你当然也可以优雅访问内部成员:

class Test:
    def __init__(self) -> None:
        self.a = 1
    def func1(self, x, y):
        print(x + y)


Test.func1 = lambda self, x, y : print(x + 2 * y + self.a)

test = Test()
test.func1(1, 1)

out:

4

是不是很爽?

除了给对象和类打补丁,你还可以给导入进__main__的模块,文件,动态链接库打补丁,做法都差不多,在此就不赘述了。

增加函数

既然猴子补丁可以修改函数,那自然也能增加函数:

class Test:
    def func1(self, x , y):
        print(x + y)
    
Test.func1 = lambda self, p, q: print(p + 2 * q)   # 修改函数
Test.func2 = lambda self, p, q: print(p + 3 * q)   # 增加函数
test = Test()
test.func1(1, 2)
test.func2(1, 3)

out:

5
10

同理,属性什么的也是可以随意修改和添加的。

补丁应用

gevent就是大规模使用猴子补丁的一个最好的例子,这个库能够在不改变第三方库源代码的情况下将大部分阻塞式库实现协程式运作,其中就是通过猴子补丁实现的,感兴趣的同学可以移步如下链接:

python并发编程gevent模块以及猴子补丁学习

禁用猴子补丁

虽然猴子补丁很好用,但是在实际开发中未免存在诸多的不安全,所以如何禁用猴子补丁呢?

这个需要具体情况具体分析,使用猴子补丁时,对象/类本质是在调用什么函数?

情况一:对象的猴子补丁

当我们对一个对象使用猴子补丁时,本质上就是在调用这个对象的类的setattr方法,所以以下两句话等价:

# 语句1
test.func1 = lambda x, y : print(x + 2 * y)

# 语句2
setattr(test, "func1", lambda x, y : print(x + 2 * y))

既然知道了是在调用setattr方法,那么我们直接复写对象的__setattr__即可禁用猴子补丁,锦恢简单做个示范:

class Test:
    def func1(self, x, y):
        print(x + y)

    def __setattr__(self, __name, __value) -> None:
        if __name in self.__dir__() and callable(getattr(self, __name)):
            print("Test的类函数不可在实例化中被覆盖!")
        else:
            super().__setattr__(__name, __value)


test = Test()
# 尝试使用猴子补丁,预想是失败的,但是你仍然可以创建新的函数并挂载在这个对象上,具体的逻辑你就可以自己定义啦!
test.func1 = lambda x, y : print(x + 2 * y)

out:

Test的类函数不可在实例化中被覆盖!

情况二:类的猴子补丁

类的猴子补丁禁用会麻烦一点,我们都知道Python中的class也是一个对象cls,那么我们只需要重写cls对象的__setattr__方法即可,那么问题来了——整个类编写的哪一个环节可以让我们操作cls对象呢?这个时候,对Python类创建生命周期熟悉的同学就忍不住蹦出来说了——在__new__方法中可以碰到cls!但是很抱歉,这不对,因为我们之前操作Test时,test实例还没被创建出来,__new__方法根本不会被调用到。

那么怎么做呢?给你们三秒时间思考 :D

答案就是,修改Test的元类,为了完成对类本身的更加抽象的操作,我们必须这么做。

对元类编程不熟悉的同学可以参考小明大佬写的文章,当然,后续我也会写一篇文章来讲讲元类编程

代码如下:

class MetaTest(type):
    def __setattr__(cls, __name, __value) -> None:
        if __name in dir(cls) and callable(getattr(cls, __name)):
            print("Test 元类函数不可覆盖!")
        else:
            super().__setattr__(__name, __value)

class Test(metaclass=MetaTest):
    def func1(self, x, y):
        print(x + y)

    
    def __setattr__(self, __name, __value) -> None:
        if __name in self.__dir__() and callable(getattr(self, __name)):
            print("Test的类函数不可在实例化中被覆盖!")
        else:
            super().__setattr__(__name, __value)

Test.func1 = lambda self, x, y : print(x + y)

out:

Test 元类函数不可覆盖!

但是你还是可以附加新的函数到类上的 :D

猴子补丁的说法来源

Wiki上给出的来源解释如下:

The term monkey patch seems to have come from an earlier term, guerrilla patch, which referred to changing code sneakily – and possibly incompatibly with other such patches – at runtime.The word guerrilla, homophonous with gorilla (or nearly so), became monkey, possibly to make the patch sound less intimidating. An alternative etymology is that it refers to “monkeying about”with the code (messing with it).

很有意思,用锦恢的奇妙比喻来说就是,有些程序员编码习经常更换代码、接口、工具,就像补给不充足的游击队一样,经常更换人员,武器和作战地点。所以这种频繁更换接口的坏习惯下造成的软件更新被人们称为游击队补丁,在英文中游击队(guerrilla,英式发音[ɡəˈrɪlə])和猩猩(gorilla,英式发音[ɡəˈrɪlə])发音完全相同,人们就玩梗,把“游击队补丁”戏称为“猩猩补丁”,后来又转化为更加amusing的“猴子补丁”。