因为 Python 并非是工作内容需要使用的语言,所以对语法并没有系统地学习过,加上也不会使用 Python 编写什么复杂的逻辑,所以一直以来都是使用过程中遇到需要的逻辑语法就查一下
在学习 LangChain 的过程中,发现 Python 的语法糖很多,很多设计思想提供了语法层面的支持,使得阅读源码挺费劲的
所以在这里还是系统地了解下 Python 都提供了哪些语法,也了解下 Python 编写代码的主要思想
这里主要是整理下 Python 中比较特殊的、和 Java 有较大区别的操作
函数
多返回值
Python 允许函数返回多个值,并且调用时使用多个变量承接
1 | def build(): |
实际上真正的返回值是一个 Tuple 对象,Python 帮忙做了解压(unpack)操作
1 | def build(): |
默认参数
设置参数的默认值,默认参数可以简化函数的调用
需要注意:
- 必选参数在前,默认参数在后
- 实践中一般将变化大的参数放在前面,变化小的参数放在后面(简单理解,就是因为变化小才需要默认)
- 当不按顺序提供部分默认参数时,需要把参数名写上;如
default_params('李四', city='上海')
1 | def default_params(name, age=20, city='北京'): |
默认参数有一个需要注意的坑:默认参数必须指向不变对象
1 | def default_params(L=[]): |
原因是 Python 函数在定义的时候,默认参数 L
的值就被计算出来了,即 []
,因为默认参数 L
也是一个变量,它指向对象 []
,每次调用该函数都在改变
L
变量的值
实际上 PyCharm IDE 也会出现警告
Default argument value is mutable
可以使用 None
来解决上述问题
1 | def default_params(L=None): |
可变参数
可变参数指传入的参数个数是可变的,例如 0 个、1 个、2 个...
定义可变参数和定义一个 list 或 tuple
参数相比,只需要在参数前面加了一个 *
号,在函数内部实现逻辑可以完全不变
1 | def sum(*nums): |
如果我们已经有了一个 list 或
tuple,则无法直接作为参数传入该方法,也可以使用 *
号将其转换为可变参数
1 | def sum(*nums): |
关键字参数
可变参数允许传入 0 个或任意个参数,这些可变参数在函数调用时自动组装为一个 tuple
关键字参数允许传入 0 个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个 dict
使用 **
表示希望接收的是个关键字参数
1 | def build_student(name, age, **other): |
和可变参数类似,也可以先组装出一个 dict,然后,把该 dict 转换为关键字参数传入
1 | def build_student(name, age, **other): |
对于关键字参数,函数的调用方可以传入任意不受限制的关键字参数,对于参数值可以在函数内进行参数校验
对于参数名字的限制可以通过命名关键字参数来进行限制,通过
*
号分隔位置参数和关键字参数
1 | def build_student(name, age, *, city): |
命名关键字参数也可以有默认值
1 | def build_student(name, age=20, *, city='北京'): |
使用命名关键字参数时需要注意,如果没有可变参数就必须加一个
*
作为特殊分隔符,因为如果缺少 *
,Python
解释器将无法识别位置参数和命名关键字参数
组合参数
在 Python 中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这 5 种参数都可以组合使用
但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数
比如定义一个函数,包含上述若干种参数:
1 | def f1(a, b, c=0, *args, **kw): |
最神奇的是通过一个 tuple 和 dict 也可以调用上述函数
1 | args = (1, 2, 3, 4) |
即对于任意函数,都可以通过类似 func(*args, **kw)
的形式调用它,无论它的参数是如何定义的
集合特性
切片
l[0:3]
表示,从索引 0 开始取,直到索引 3
为止,但不包括索引 3(左闭右开,基本上所有的集合 API 都是这种设计)
1 | l = [1, 2, 3, 4] |
如果第一个索引是 0,还可以省略
1 | print(l[:3]) |
-1 表示取倒数第一个元素,即同样支持倒数切片
1 | print(l[-3:-1]) |
前 10 个数,每 2 个取一个
1 | l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] |
所有数每 3 个取一个
1 | l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] |
甚至什么都不写,只写 [:]
就可以原样复制一个 list
1 | l = [1, 2, 3, 4, 5] |
tuple 也是一种 list,唯一区别是 tuple 不可变,因此 tuple 也可以用切片操作,操作的结果仍是 tuple
1 | t = (1, 2, 3, 4, 5) |
字符串也可以看成是一种 list,每个元素就是一个字符;因此字符串也可以用切片操作,操作结果仍是字符串
1 | s = 'Hello World!' |
迭代
只要是可迭代对象,无论有无下标,都可以迭代,比如 dict 就可以迭代
因为 dict 的存储不是按照 list 的方式顺序排列,所以迭代出的结果顺序很可能不一样
1 | d = {'name': 'zhangsan', 'age': 20, 'city': 'beijing'} |
默认情况下,dict 迭代的是 key;如果要迭代 value,可以用
for value in d.values()
,如果要同时迭代 key 和
value,可以用 for k, v in d.items()
1 | d = {'name': 'zhangsan', 'age': 20, 'city': 'beijing'} |
如何判断一个对象是可迭代对象呢?方法是通过
collections.abc
模块的 Iterable
类型判断
1 | from collections.abc import Iterable |
如果要对 list 实现类似 Java 的下标循环,可以使用 Python 内置的
enumerate
函数可以把一个 list 变成索引-元素对,这样就可以在
for 循环中同时迭代索引和元素本身
1 | for i, value in enumerate(['A', 'B', 'C']): |
Python 中也可以同时引用多个变量进行 for 循环
1 | for name, age in ('张三', 20), ('李四', 21), ('王五', 20): |
列表生成式
列表生成式即 List Comprehensions,是 Python 内置的非常简单却强大的可以用来创建 list 的生成式
如果要生成 [1x1, 2x2, 3x3, ..., 10x10]
的 list
方法一是循环
1 | l = [] |
而列表生成式则可以用一行语句代替循环生成上面的 list
1 | l = [x * x for x in range(1, 11)] |
还可以使用两层循环,可以生成全排列
1 | l = [m + n for m in ['草莓味', '柠檬味', '菠萝味'] for n in ['棒棒糖', '口香糖', '水果茶']] |
if ... else ...
在生成式中,也可以使用 if ... else
需要注意在一个列表生成式中,for
前面的
if ... else
是表达式,而 for
后面的
if
是过滤条件,不能带 else
1 | # 在 [0,10) 范围中取出 x % 2 == 0 的数字 |
1 | # 如果 x % 2 != 0,则取 'x' |
生成器
在 Python 中,一边循环一边计算的机制,称为生成器 Generator
第一种方法,只要把一个列表生成式的 []
改成
()
,就创建了一个 generator
1 | l = (x for x in range(0, 10) if x % 2 == 0) |
获取生成器元素时,可以使用
next
,也可以将其作为迭代器循环
1 | l = (x for x in range(0, 10) if x % 2 == 0) |
1 | 0 |
同理也可以将迭代器输出为 list
1 | print(list(l.__iter__())) |
如果推算的算法比较复杂,用类似列表生成式的 for
循环无法实现的时候,还可以用函数来实现
下面是一个斐波那契数列的函数
1 | def fib(max): |
只需要把 print(b)
改为 yield b
就修改为了生成器
1 | def fib(max): |
需要注意:
- 函数生成器遇到
yield
语句返回,再次执行时从上次返回的yield
语句处继续执行 - 没有
yield
执行时会报错StopIteration
- 调用 generator 函数会创建一个 generator 对象,多次调用 generator 函数会创建多个相互独立的 generator
此外,生成器函数中的 return
会被当做
StopIteration
异常抛出,如果想要获取返回值,需要解析异常;并且迭代器不会抛出异常,需要使用
next
获取
1 | def return_gen(): |
1 | 1 |
迭代器
前面已经了解到可以直接作用于 for
循环的数据类型有以下几种
- 集合数据类型,如 list、tuple、dict、set、str 等
- 生成器,包括生成器和带 yield 的生成器函数
这些可以直接作用于 for
循环的对象统称为可迭代对象:Iterable
可以使用 isinstance
判断一个对象是否是
Iterable
对象
1 | print(isinstance([], Iterable)) # True |
可以被 next
函数调用并不断返回下一个值的对象称为迭代器:Iterator
可以使用 isinstance
判断一个对象是否是
Iterator
对象
1 | print(isinstance([], Iterator)) # False |
生成器都是 Iterator
对象,但 list、dict、str 虽然是
Iterable
,却不是 Iterator
把 list、dict、str 等 Iterable
变成
Iterator
可以使用 iter
函数
1 | print(isinstance(iter([]), Iterator)) # True |
为什么 list、dict、str 等数据类型不是 Iterator
?
因为 Python 的 Iterator
对象表示的是一个数据流,Iterator
对象可以被
next
函数调用并不断返回下一个数据,直到没有数据时抛出
StopIteration
错误
可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过
next
函数实现按需计算下一个数据,所以 Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算
函数式编程
高阶函数
一个函数接收另一个函数作为参数,这种函数就称之为高阶函数(Java 中的 Function)
1 | def add(x, y, f): |
map
map
函数接收两个参数,一个是函数,一个是
Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的
Iterator
返回
1 | o_list = [-1, 2, -3, -4, 5, 6] |
reduce
reduce
把一个函数作用在一个序列
[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果视为
1 | reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) |
1 | def fn(x, y): |
1 | DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9} |
filter
filter
也接收一个函数和一个序列
把传入的函数依次作用于每个元素,然后根据返回值是 True
还是 False
决定保留还是丢弃该元素
1 | def not_empty(s): |
需要注意 filter
使用了惰性计算,所以只有在取结果的时候,才会真正筛选并每次返回下一个筛出的元素
sort
如果是字符串或者两个 dict 直接比较数学上的大小是没有意义的,因此比较的过程必须通过函数抽象出来
直接排序
1 | list = sorted(["zhangsan", "lisi", "wangwu", "zhaoliu"]) |
sorted
函数也是一个高阶函数,它还可以接收一个
key
函数来实现自定义的排序
1 | list = sorted([10, -5, 1, 0, -7, 3], key=abs) |
reverse
可以控制顺逆序,逆序为由大到小
1 | list = sorted(["zhangsan", "lisi", "wangwu", "zhaoliu", "a"], key=str.__len__, reverse=True) |
函数返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回
1 | def lazy_sum(*args): |
调用函数 lazy_sum
时,才真正计算求和的结果
当调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数
1 | lazy_sum1 = lazy_sum(1, 2, 3, 4) |
闭包
在上述例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中
这种程序结构就被称为闭包(Closure)
使用闭包时需要注意
- 返回闭包函数不要引用任何循环变量,或者后续会发生变化的变量
- 使用闭包时,对外层变量赋值前,需要先使用
nonlocal
声明该变量不是当前函数的局部变量 - 返回的函数并不是立刻执行,而是直到调用才执行
这是一个问题示例
1 | def count(): |
实际上调用 f1
等函数会发现,结果都是
9
,原因是返回的函数引用了变量
i
,但它并非立刻执行,等到 3
个函数都返回时,它们所引用的变量 i
已经变成了
3
,因此最终结果为 9
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变
1 | def count(): |
nonlocal
使用闭包,就是内层函数引用了外层函数的局部变量,可以读取值,但是赋值时 Python 解释器会认为对局部变量赋值,就会报错
可以加上 nonlocal
声明,声明后解释器会把变量看作外层函数的局部变量
1 | def count(): |
匿名函数
1 | list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
如上,关键字 lambda
表示匿名函数,冒号前面的
x
表示函数参数
匿名函数只能有一个表达式,不用写
return
,返回值就是该表达式的结果
匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数
1 | f = lambda: print("Hello World!") |
同样,也可以把匿名函数作为返回值返回
1 | def add(x, y): |
装饰器
偏函数
functools.partial
可以帮助创建一个偏函数
把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,使得调用这个新函数会更简单
1 | def hello(name): |
创建偏函数时,实际上可以接收函数对象、*args
和
**kw
3 个参数
1 | max2 = functools.partial(max, 10) |
实际上会把 10
作为 *args
的一部分自动加到左边,也就是
1 | max2(5, 6, 7) |
面向对象
访问限制
在 Python 中,实例的变量名如果以 __
开头,就变成了一个私有变量(private)
1 | class Student(object): |
需要注意
- 在 Python 中,变量名类似
__xxx__
,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量;特殊变量是可以直接访问的,不是私有变量 - 以一个下划线开头的实例变量名,比如
_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思为“虽然我可以被访问,但是请把我视为私有变量,不要随意访问” - 在外部手动设置私有属性和对象真正的私有属性无关(因为已经被改名)
- 本质上私有的实现原理是解释器变更了属性名
继承和多态
Python 的继承和多态和 Java 类似,这里只列举一些 API 和明显区别的点
判断一个变量是否是某个类型可以用 isinstance
判断
1 | print(isinstance(123, int)) # True |
对于静态语言(例如 Java)来说,如果需要传入 Animal
类型,则传入的对象必须是 Animal
类型或者它的子类
对于 Python 这样的动态语言来说,则不一定需要传入 Animal
类型,只需要具有相同的方法即可
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子
1 | class Animal(object): |
对象信息
Python 中对象的属性可以不用定义在类结构中
那么当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢
type
获取对象类型isinstance
判断对象是否等于 or 继承于某类dir
返回一个包含字符串的 list,包含一个对象的所有属性和方法
1 | class Animal(object): |
配合 getattr
、setattr
以及
hasattr
,我们可以直接操作一个对象的属性;类似 Java
的反射
hasattr
是否具有某属性getattr
获取某属性setattr
设置某属性
1 | class Student(object): |
实例属性和类属性
对应 Java 中的静态变量和成员变量
给实例绑定属性的方法是通过实例变量,或者通过 self
变量
1 | class Student(object): |
如果 Student
类本身需要绑定一个属性呢?可以直接在 class
中定义属性,这种属性是类属性,归 Student
类所有
1 | class Student(object): |
对象可以覆盖同名类属性,也可以通过类名加属性名访问类属性
1 | class Student(object): |
__slots__
正常情况下,当定义了一个 class,创建了一个 class 的实例后可以给该实例绑定任何属性和方法,这就是动态语言的灵活性
也可以给 class 绑定方法
1 | class Student(object): |
如果想要限制实例的属性,Python 允许在定义 class
的时候,定义一个特殊的 __slots__
变量,来限制该 class
实例能添加的属性
1 | class Student(object): |
使用 __slots__
要注意,__slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的
在子类中定义 __slots__
,子类实例允许定义的属性就是自身的
__slots__
加上父类的 __slots__
@property
Python 内置的 @property
装饰器就是负责把一个方法变成属性调用(和 Java 的 Lombok 相反)
把一个 getter 方法变成属性,只需要加上 @property
就可以了
@property
本身又创建了另一个装饰器
@score.setter
,负责把一个 setter 方法变成属性赋值
1 | class Student(object): |
要特别注意:属性的方法名不要和实例变量重名
例如以下的错误代码
1 | class Student(object): |
这是因为调用 s.birth
时,首先转换为方法调用,在执行
return self.birth
时,又视为访问 self
的属性,于是又转换为方法调用,造成无限递归,最终导致栈溢出报错
RecursionError
多重继承
Python
支持多重继承,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让
Ostrich
除了继承自 Bird
外,再同时继承
Runnable
,这种设计通常称之为 MixIn
为了更好地看出继承关系,我们把 Runnable
和
Flyable
改为 RunnableMixIn
和
FlyableMixIn
类似的,你还可以定义出肉食动物 CarnivorousMixIn
和草食动物 HerbivoresMixIn
,让某个动物同时拥有好几个
MixIn
1 | class Dog(Mammal, RunnableMixIn, CarnivorousMixIn): |
MixIn 的目的就是给一个类增加多个功能,这样在设计类的时候,优先考虑通过多重继承来组合多个 MixIn 的功能,而不是设计多层次的复杂的继承关系
Python 自带的很多库也使用了 MixIn;例如 Python 自带了
TCPServer
和 UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由
ForkingMixIn
和 ThreadingMixIn
提供,通过组合,我们就可以创造出合适的服务来
1 | # 多进程模式的 TCP 服务 |
这样不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类
定制方法
看到类似 __slots__
这种形如 __xxx__
的变量或者函数名就要注意,这些在 Python 中是有特殊用途的
__str__
调用 print
时,会调用对象的该方法进行打印
1 | class Student(object): |
__iter__
如果一个类想被用于 for ... in
循环,类似 list 或 tuple
那样,就必须实现一个 __iter__()
方法
该方法返回一个迭代对象,然后 Python 的 for
循环就会不断调用该迭代对象的 __next__()
方法拿到循环的下一个值,直到遇到 StopIteration
错误时退出循环
1 | class PrimeNumber(object): |
上面的示例实现了一个 100 以内素数的迭代器
__getitem__
上面的 PrimeNumber
虽然可以迭代,但是还是不能当作 list
使用
要表现得像 list 那样按照下标取出元素,需要实现
__getitem__
方法
1 | class PrimeNumber(object): |
如果进一步希望其支持切片方法,需要判断入参是否是
slice
,再进行不同的操作
1 | class PrimeNumber(object): |
(只是举个例子,实现的很粗糙)
总之通过上面的方法,自己定义的类表现得和 Python 自带的 list、tuple、dict 没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口
__getattr__
当调用不存在的属性时,比如 score
,Python
解释器会试图调用 __getattr__(self, 'score')
来尝试获得属性
1 | class Student(object): |
也可以返回函数
1 | class Student(object): |
__call__
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用
instance.method()
来调用
而任何类,只需要定义一个 __call__
方法,就可以直接对实例进行调用
1 | class Student(object): |
完全可以把对象看成函数,把函数看成对象
那么怎么判断一个变量是对象还是函数呢,能被调用的对象就是一个
Callable
对象,比如函数和上面定义的带有
__call__
的类实例
1 | class Student(object): |
枚举类
Python 提供了 Enum
类来实现枚举类的功能
1 | Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')) |
可以直接使用 Month.Jan
来引用一个常量,或者使用
__members__
列举所有成员
value
属性则是自动赋给成员的 int
常量,默认从 1
开始计数
如果需要更精确地控制枚举类型,可以从 Enum
派生出自定义类
1 |
|
@unique
装饰器可以帮助我们检查保证没有重复值
既可以用成员名称引用枚举常量,又可以直接根据 value 的值获得枚举常量
1 | print(Weekday.Sat) |
元类
动态语言和静态语言最大的不同,就是函数和类的定义不是编译时定义的,而是运行时动态创建的
type
函数可以查看一个类型或变量的类型,一个 class
类型就是 type
创建 class 的方法就是使用 type
函数
type
函数既可以返回一个对象的类型,又可以创建出新的类型
1 | # 定义出函数 |
type
的参数:
- class 的名称
- 继承的父类集合(注意 Python 支持多重继承,如果只有一个父类,别忘了 tuple 的单元素写法)
- class 的方法名称与函数绑定;示例中将 hello 方法名绑定上面定义的
hello
函数
动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂(Java 中的 cglib、bytebuddy)
tuple 单元素写法
Python 中单元素的 tuple 应在元素后加上 ,
,若括号中没有
,
,则会被认为是其元素类型(忽略 ()
tuple
表达)
1 | tuple1 = ('a') |
metaclass
除了使用 type
动态创建类以外,要控制类的创建行为,还可以使用 metaclass
metaclass 直译为元类,简单的解释就是:先定义 metaclass,就可以创建类,最后创建实例
metaclass 允许创建类或者修改类,换句话说可以把类看成是 metaclass 创建出来的“实例”
举一个例子,给我们自定义的 MyList
增加一个
add
方法
定义 ListMetaclass
,按照默认习惯,metaclass 的类名总是以
Metaclass
结尾,以便清楚地表示这是一个 metaclass
1 | # metaclass 是类的模板,所以必须从 `type` 类型派生 |
__new__
方法接收到的参数依次是:
- 当前准备创建的类的对象
- 类的名字
- 类继承的父类集合
- 类的方法集合
有了 ListMetaclass
,我们在定义类的时候还要指示使用
ListMetaclass
来定制类,传入关键字参数
metaclass
1 | class MyList(list, metaclass=ListMetaclass): |
当我们传入关键字参数 metaclass
时,魔术就生效了,它指示
Python 解释器在创建 MyList
时,要通过
ListMetaclass.__new__()
来创建
1 | my_list = MyList() |
这里使用元类实现一个 ORM Model 作为练习
首先来定义 Field
类,它负责保存数据库表的字段名和字段类型
1 | class Field(object): |
在 Field
的基础上,进一步定义各种类型的
Field
,比如
StringField
,IntegerField
等等
1 | class StringField(Field): |
编写 ModelMetaclass
1 | class ModelMetaclass(type): |
基类 Model
1 | class Model(dict, metaclass=ModelMetaclass): |
在 ModelMetaclass
中,一共做了几件事情
- 排除掉对
Model
类的修改 - 在当前类(比如
User
)中查找定义的类的所有属性,如果找到一个Field
属性,就把它保存到一个__mappings__
的 dict 中,同时从类属性中删除该Field
属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性) - 把表名保存到
__table__
中,这里简化为表名默认为类名
定义实体类
1 | class Student(Model): |
1 | s = Student(id=12345, name='zhangsan', email='zhangsan@xxx.com') |
这里在看的时候有一个问题,定义的 Student
属性必须和
Student(id=12345, name='zhangsan', email='zhangsan@xxx.com')
参数一致
不然就会出现如下结果 ARGS: [None, None, None]
这是为什么呢,其次如果一致的话不应该实例属性影响类属性吗
- Q:为什么名称必须一致
- 因为后续是通过
getattr(self, k, None)
操作来找到对应 value 的,如果不一致self
将找不到该属性对应的 value
- 因为后续是通过
- Q:名称如果一致的话不应该实例属性影响类属性吗
- 不影响,因为
Model
重写了__getattr__
方法,实现为self[key]
- 而
Model
继承了dict
,实际上讲数据以 KV 形式保存在自己dict
中 ModelMetaclass
定义中,在保存了属性后使用attrs.pop(k)
将实例同名属性删除了
- 不影响,因为
1 | # 实例化后,s 对象其实已经没有了 id\name\email 属性 |