魔术方法
总览
与计算无关
类别 | 方法名 |
---|---|
字符串/字节序列表示形式 | repr、str、format、bytes |
数值转换 | abs、bool、complex、int、float、hash、index |
集合模拟 | len、getitem、setitem、delitem、contains |
迭代枚举 | iter、reversed、next |
可调用模拟 | call |
上下文管理 | enter、exit |
实例创建和销毁 | new、init、del |
属性管理 | getattr、getattribute、setattr、delattr、dir |
属性描述符 | get、set、delete |
跟类相关的服务 | prepare、instancecheck、subclasscheck |
与计算有关
类别 | 方法名 |
---|---|
一元运算符 | neg -、pos +、abs abs() |
比较运算符 | lt <、le <=、eq ==、ne !=、gt >、ge >= |
算术运算符 | add +、sub -、mul *、truediv /、floordiv //、mod %、divmod divmod()、pow ** 或pow()、round round() |
反向算术运算符 | radd、rsub、rmul、rtruediv、rfloordiv、rmod、rdivmod、__r |
增量赋值算术运算符 | iadd、isub、imul、itruediv、ifloordiv、imod、ipow |
位运算符 | invert ~、lshift <<、rshift >>、and &、or |
反向位运算符 | rlshift、rrshift、rand、rxor、ror |
增量赋值位运算符 | ilshift、irshift、iand、ixor、ior |
len(self)
len某个对象的时候调用
xxxitem(self, position)
对某个对象使用[xx]的时候调用
class FrenchDeck:
def __init__(self):
self._cards = {}
def __len__(self):
print("len")
return len(self._cards)
def __setitem__(self, key, value):
print("--set item")
self._cards[key] = value
def __getitem__(self, position):
print("--get item")
return self._cards[position]
def __delitem__(self, key):
print("--del item")
self._cards.pop(key)
deck = FrenchDeck()
print(len(deck)) # 调用len
deck["a"] = 1 # 调用setitem
print(deck["a"]) # 调用getitem
del deck["a"] # 调用delitem
# len
# 0
# --set item
# --get item
# 1
# --del item
repr(self)/str(self)
repr 和 str 的区别在于,后者是在 str() 函数被使用, 或是在用 print 函数打印一个对象的时候才被调用的,并且它返回的 字符串对终端用户更友好。
如果你只想实现这两个特殊方法中的一个,repr 是更好的选择, 因为如果一个对象没有 str 函数,而 Python 又需要调用它的时 候,解释器会用 repr 作为替代。
getattr
属性查找失败后,解释器会调用 getattr 方法。简单来说,对 my_obj.x 表达式,Python 会检查 my_obj 实例有没有名为 x 的属性; 如果没有,到类(my_obj.class)中查找;如果还没有,顺着 继承树继续查找。4 如果依旧找不到,调用 my_obj 所属类中定义的 getattr 方法,传入 self 和属性名称的字符串形式(如 'x')
数据结构
概览
容器序列list、tuple 和 collections.deque 这些序列能存放不同类型的数据。 扁平序列str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型。
可变序列list、bytearray、array.array、collections.deque 和memoryview。 不可变序列tuple、str 和 bytes。
str 是一个例外,因为对字符串做 += 实在是太普遍了,所以 CPython 对它做了优化。为 str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并 不会涉及复制原有字符串到新位置这类操作。
list
tuple
不可变的列表
t = (1, 2, [30, 40])
t[2] += [50, 60]
# tuple' object does not support item assignment
# (1, 2, [30, 40, 50, 60])
- 不要把可变对象放在元组里面。
- 增量赋值不是一个原子操作它虽然抛出了异 常,但还是完成了操作。
具名元组
collections.namedtuple 是一个工厂函数,它可以用来构建一个 带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很 大帮助。
from collections import namedtuple
City = namedtuple('City', 'name country population coordinates') ➊
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) ➋
print(tokyo)
# City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,139.691667))
print(tokyo.population)
# 36.933
print(tokyo.coordinates)
# (35.689722, 139.691667)
print(tokyo[1])
# 'JP'
数组
数组支持所有跟可变序列有关的操作,包括 .pop、.insert 和 .extend。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes 和 .tofile。
如果我们需要一个只包含数字的列表,那么 array.array 比 list 更 高效。
memoryview
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作 同一个数组的不同切片。
数组VS列表
数组(Array) 类型专一度:数组是固定类型的数据集合,意味着数组中的所有元素都必须是相同类型的。 内存效率:数组通常比列表更节省内存,因为它们是同质的,不需要为每个元素存储类型信息。 性能:由于内存布局的连续性和类型一致性,数组在某些操作上可能比列表有更好的性能。 使用场景:数组常用于科学计算和数值分析,如使用NumPy库创建的ndarray对象。 内置类型:Python内置的array模块提供了array的功能,但它的使用没有列表那么广泛。 列表(List) 类型灵活性:列表是异质的,可以包含任意类型的元素,包括不同的数据类型和对象。 内存使用:由于需要存储类型信息,列表相比数组通常会占用更多的内存。 性能:列表是动态数组,其内存分配和元素插入可能会导致额外的内存开销和性能损失,特别是在大型数据集上。 内置类型:列表是Python的内置数据类型,使用非常广泛,支持广泛的操作,如切片、排序和内置函数。
双向队列和其他形式的队列
双向队列
collections.deque 类(双向队列)是一个线程安全、可以快速从 两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存 放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新 建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满 员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。示 例 2-23 中有几个双向队列的典型操作
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10)
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3)
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1)
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33])
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40])
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
queue
提供了同步(线程安全)类 Queue、LifoQueue 和 PriorityQueue,不同的线程可以利用这些数据类型来交换信息。这 三个类的构造方法都有一个可选参数 maxsize,它接收正整数作为输 入值,用来限定队列的大小。但是在满员的时候,这些类不会扔掉旧的 元素来腾出位置。相反,如果队列满了,它就会被锁住,直到另外的线 程移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活 跃线程的数量。
multiprocessing 这个包实现了自己的 Queue,它跟 queue.Queue 类似,是设计 给进程间通信用的。同时还有一个专门的 multiprocessing.JoinableQueue 类型,可以让任务管理变得更 方便。 asyncio Python 3.4 新提供的包,里面有 Queue、LifoQueue、PriorityQueue 和 JoinableQueue,这些 类受到 queue 和 multiprocessing 模块的影响,但是为异步编程里 的任务管理提供了专门的便利。
字典
标准库里的所有映射类型都是利用 dict 来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键(只有键有 这个要求,值并不需要是可散列的数据类型)。
tt = (1, 2, (30, 40))
hash(tt)
tl = (1, 2, [30, 40])
hash(tl)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
建立字典
a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('two', 2), ('one', 1), ('three', 3)])
e = dict({'three': 3, 'one': 1, 'two': 2})
字典推导
DIAL_CODES = [
(86, 'China'),
(91, 'India'),
(1, 'United States'),
(62, 'Indonesia'),
(55, 'Brazil'),
(92, 'Pakistan'),
(880, 'Bangladesh'),
(234, 'Nigeria'),
(7, 'Russia'),
(81, 'Japan'),
]
country_code = {country: code for code, country in DIAL_CODES}
# {'China': 86, 'India': 91, 'Bangladesh': 880, 'United States': 1,
# 'Pakistan': 92, 'Japan': 81, 'Russia': 7, 'Brazil': 55, 'Nigeria':
# 234, 'Indonesia': 62}
{code: country.upper() for country, code in country_code.items()
if code < 66}
# {1: 'UNITED STATES', 55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA'}
字典的变种
collections.OrderedDict
这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致 的。OrderedDict 的 popitem 方法默认删除并返回的是字典里的最 后一个元素,但是如果像 my_odict.popitem(last=False) 这样 调用它,那么它删除并返回第一个被添加进去的元素。
collections.Counter
这个映射类型会给键准备一个整数计数器。每次更新一个键的时候 都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或 者是当成多重集来用——多重集合就是集合里的元素可以出现不止一1次 Counter 实现了 + 和 - 运算符用来合并记录,还有像 most_common([n]) 这类很有用的方法。most_common([n]) 会按 照次序返回映射里最常见的 n 个键和它们的计数,
>>> ct = collections.Counter('abracadabra')
>>> ct
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.update('aaaaazzz')
>>> ct
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.most_common(2)
[('a', 10), ('z', 3)]
其他
散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。 在一般的数据结构教材中,散列表里的单元通常叫作表元(bucket)。 在 dict 的散列表当中,每个键值对都占用一个表元,每个表元都有两 个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大 小一致,所以可以通过偏移量来读取某个表元。
- 键必须是可散列的 一个可散列的对象必须满足以下要求。 (1) 支持 hash() 函数,并且通过 hash() 方法所得到的散列 值是不变的。 (2) 支持通过 eq() 方法来检测相等性。 163 (3) 若 a == b 为真,则 hash(a) == hash(b) 也为真。 所有由用户自定义的对象默认都是可散列
- 字典在内存上的开销巨大 由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空 间上的效率低下。举例而言,如果你需要存放数量巨大的记录,那 么放在由元组或是具名元组构成的列表中会是比较好的选择;最好 不要根据 JSON 的风格,用由字典组成的列表来存放这些记录。用 元组取代字典就能节省空间的原因有两个:其一是避免了散列表所 耗费的空间,其二是无需把记录中字段的名字在每个元素里都存一 遍。 在用户自定义的类型中,slots 属性可以改变实例属性的存 储方式,由 dict 变成 tuple, 记住我们现在讨论的是空间优化。如果你手头有几百万个对象,而 你的机器有几个 GB 的内存,那么空间的优化工作可以等到真正需 要的时候再开始计划,因为优化往往是可维护性的对立面。
- 键查询很快 dict 的实现是典型的空间换时间:字典类型有着巨大的内存开 164 销,但它们提供了无视数据量大小的快速访问——只要字典能被装 在内存里。正如表 3-5 所示,如果把字典的大小从 1000 个元素增 加到 10 000 000 个,查询时间也不过是原来的 2.8 倍,从 0.000163 秒增加到了 0.00456 秒。这意味着在一个有 1000 万个元素的字典 里,每秒能进行 200 万个键查询。
-
键的次序取决于添加顺序 当往 dict 里添加新键而又发生散列冲突的时候,新键可能会被安 排存放到另一个位置。于是下面这种情况就会发生:由 dict([key1, value1), (key2, value2)] 和 dict([key2, value2], [key1, value1]) 得到的两个字 典,在进行比较的时候,它们是相等的;但是如果在 key1 和 key2 被添加到字典里的过程中有冲突发生的话,这两个键出现在 字典里的顺序是不一样的。
```python DIAL_CODES = [ (86, 'China'), (91, 'India'), (1, 'United States'), (62, 'Indonesia'), (55, 'Brazil'), (92, 'Pakistan'), (880, 'Bangladesh'), (234, 'Nigeria'), (7, 'Russia'), (81, 'Japan'), ] d1 = dict(DIAL_CODES) print('d1:', d1.keys()) d2 = dict(sorted(DIAL_CODES)) print('d2:', d2.keys()) d3 = dict(sorted(DIAL_CODES, key=lambda x:x[1])) print('d3:', d3.keys()) assert d1 == d2 and d2 == d3
key顺序不一样,但是 == 为true
d1: dict_keys([880, 1, 86, 55, 7, 234, 91, 92, 62, 81])
d2: dict_keys([880, 1, 91, 86, 81, 55, 234, 7, 92, 62])
d3: dict_keys([880, 81, 1, 86, 55, 7, 234, 91, 92, 62])
```
-
往字典里添加新键可能会改变已有键的顺序 无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩 容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字 典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲 突,导致新散列表中键的次序变化。要注意的是,上面提到的这些 变化是否会发生以及如何发生,都依赖于字典背后的具体实现,因 此你不能很自信地说自己知道背后发生了什么。如果你在迭代一个 166 字典的所有键的过程中同时对字典进行修改,那么这个循环很有可 能会跳过一些键——甚至是跳过那些字典中已经有的键。 由此可知,不要对字典同时进行迭代和修改。如果想扫描并修改一 个字典,最好分成两步来进行:首先对字典迭代,以得出需要添加 的内容,把这些内容放在一个新字典里;迭代结束之后再对原有字 典进行更新。
集合
集合还实现了很多基础的中缀运算符。给定两个集合 a 和 b,a | b 返回的是它们的合集,a & b 得到的是交集,而 a - b 得到的是差集。合理地利用这些操作,不仅能够让代码的行数变少, 还能减少 Python 程序的运行时间。这样做同时也是为了让代码更易读, 从而更容易判断程序的正确性,因为利用这些运算符可以省去不必要的 循环和逻辑操作。
特点
实现也依赖散列表,但在它们的散列表里存放的 只有元素的引用(就像在字典里只存放键而没有相应的值)。在 set 加入到 Python 之前,我们都是把字典加上无意义的值当作集合来用的。 这些特点总结如下。
- 集合里的元素必须是可散列的。
- 集合很消耗内存。
- 可以很高效地判断元素是否存在于某个集合。
- 元素的次序取决于被添加到集合里的次序。
- 往集合里添加元素,可能会改变集合里已有元素的次序。
相关使用
切片
左闭右开 [start:end:step]
# 切片赋值
l = list(range(10))
l[2:5] = [20, 30]
# [0, 1, 20, 30, 5, 6, 7, 8, 9]
del l[5:7]
# [0, 1, 20, 30, 5, 8, 9]
# 对序列使用+和*
l = [1, 2, 3]
# l * 5
# [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
# 5 * 'abcd'
# 'abcdabcdabcdabcdabcd'
列表推导和生成器表达式
[ord(x) for x in x]
# 列表推导同filter和map的比较
symbols = '$¢£¥€¤'
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
list.sort方法和内置函数sorted
list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。 这也是这个方法的返回值是 None 的原因,提醒你本方法不会新建一个 列表。
与 list.sort 相反的是内置函数 sorted,它会新建一个列表作为返 回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不 可变序列或生成器(见第 14 章)。而不管 sorted 接受的是怎样的参 数,它最后都会返回一个列表
函数
doc
def factorial(n):
''' i am doc!'''
return 1 if n < 2 else n * factorial(n-1)
factorial.__doc__
# i am doc!
高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher order function)。
python3中map、filter 和 reduce 这三个高阶函数还能见到。
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
# 把 len 函数传给 key 参数
sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
匿名函数
lambda 关键字在 Python 表达式内创建匿名函数. 然而,Python 简单的句法限制了 lambda 函数的定义体只能使用纯表达 式。换句话说,lambda 函数的定义体中不能赋值,也不能使用 while 和 try 等 Python 语句。
# 把 lambda 表达式转换成 def 语句,使用那个名称来定义函数。
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
函数内省
代码内省是检查类、函数等以了解它们是什么、做什么以及知道什么的能力。
内省是一种自我检查(self examination)的行为。内省是在运行时确定对象的类型或属性的能力。 Python中的所有内容都是一个对象。 Python中的每个对象都可以具有属性和方法。 通过使用内省, 可以动态检查Python对象。
# 例如fastapi
# 需要传递person
# 在运行的时候去检验是否传递
from fastapi import APIRouter
router = APIRouter(prefix="/auth", tags=["用户认证"])
@router.query('/')
def hello(person):
return 'Hello %s!' % person
函数对象有个 defaults 属性,它的值是一个元组,里面保存着 定位参数和关键字参数的默认值。仅限关键字参数的默认值在 kwdefaults 属性中。然而,参数的名称在 code 属性中, 它的值是一个 code 对象引用,自身也有很多属性。
def tag(name, *content, cls=None, **attrs):
print("name---", name)
print("content---", content)
print("attrs---", attrs)
import inspect
sig = inspect.signature(tag)
my_tag = {'name': 'img', 'title': 'Sunset Boulevard','src': 'sunset.jpg', 'cls': 'framed'}
bound_args = sig.bind(**my_tag)
for name, value in bound_args.arguments.items():
print(name, '=', value)
# name = img
# cls = framed
# attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
del my_tag['name']
bound_args = sig.bind(**my_tag)
# Traceback (most recent call last):
# ...
# TypeError: 'name' parameter lacking default value
参数捕获
def tag(name, *content, cls=None, **attrs):
# cls 参数只能作为关键字参数传入
print("name---", name)
print("content---", content)
print("attrs---", attrs)
tag('br')
# 传入单个定位参数,生成一个指定名称的空标签。
# name--- br
# content--- ()
# attrs--- {}
tag('p', 'hello')
# name--- p
# content--- ('hello',)
# attrs--- {}
tag('p', 'hello', 'world')
# 第一个参数后面的任意个参数会被 *content 捕获,存入一个元组。
# name--- p
# content--- ('hello', 'world')
# attrs--- {}
tag('p', 'hello', 'world' ,id=33)
# tag 函数签名中没有明确指定名称的关键字参数会被 **attrs 捕获,存入一个字典。
# name--- p
# content--- ('hello', 'world')
# attrs--- {'id': 33}
tag(content='testing', name="img")
# name--- img
# content--- ()
# attrs--- {'content': 'testing'}
my_tag = {'name': 'img', 'title': 'Sunset Boulevard','src': 'sunset.jpg', 'cls': 'framed'}
tag(**my_tag)
# 在 my_tag 前面加上 **,字典中的所有元素作为单个参数传入,同名键会绑定到对应的具名参数上,余下的则被 **attrs 捕获。
# name--- img
# content--- ()
# attrs--- {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
函数注解
可以获取函数的注解,例如实现一个真正的类型校验
def clip(text:str, max_len:'int > 0'=80) -> str:
"""在max_len前面或后面的第一个空格处截断文本"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # 没找到空格
end = len(text)
return text[:end].rstrip()
clip.__annotations__
# {'text': str, 'max_len': 'int > 0', 'return': str}
from inspect import signature
sig = signature(clip)
sig.return_annotation
for param in sig.parameters.values():
note = param.annotation
print(note, ':', param.name, '=', param.default)
# <class 'str'> : text = <class 'inspect._empty'>
# int > 0 : max_len = 80
装饰器实现类型校验
def debug(func):
def wrapper(*args, **kwargs):
# func希望的类型
par = func.__annotations__
ans = func(*args, **kwargs)
signature = inspect.signature(func)
parameters = signature.parameters
args_dict = dict(zip(parameters.keys(), args))
kwargs_dict = {k: v for k, v in kwargs.items()}
# 实际传入的参数
data = {**args_dict, **kwargs_dict}
# 校验传入的参数类型
for key in data.keys():
if not isinstance(data[key],par[key]):
raise ValueError(f'var {data[key]} is not {par[key]}')
# 校验返回值信息
if not isinstance(ans,par["return"]):
raise ValueError(f'return {ans} is not {par["return"]}')
return ans
return wrapper
@debug
def clip(text:str, max_len:int=80) -> str:
"""在max_len前面或后面的第一个空格处截断文本"""
print(text,max_len)
# return "ok"
clip("999",max_len=1)
# 使用装饰器的另一种方式
def decorate(func):
def wrapper(*args, **kwargs):
print("9999")
return func(*args, **kwargs)
return wrapper
t = decorate(print)
t("11")
def decorate(func):
def wrapper(*args, **kwargs):
print("head ---- decorate")
func(*args, **kwargs)
print("tail ---- decorate")
return wrapper
def decorate2(func):
def wrapper(*args, **kwargs):
print("head ---- decorate2")
func(*args, **kwargs)
print("head ---- decorate2")
return wrapper
# 多个装饰器
@decorate
@decorate2
def func():
print("---")
func()
# head ---- decorate
# head ---- decorate2
# ---
# head ---- decorate2
# tail ---- decorate
# 带参数的装饰器
def with_args(xx):
def decorate(func):
def wrapper(*args, **kwargs):
print(xx)
func(*args, **kwargs)
return wrapper
return decorate
@with_args(0)
def func():
print("---")
func()
# 类装饰器
class A:
def __init__(self,func):
print("----")
self.func = func
def __call__(self,*args,**kargs):
self.func(*args,**kargs)
@A
def hah(info):
print("------",info)
# 带参数类装饰器
class A:
def __init__(self,a):
print("----")
self.a = a
def __call__(self,func):
print("aaa",self.a)
self.func = func
return self.hah
def hah(self,*args,**kargs):
print("99")
self.func(*args,**kargs)
# 带参数的相当于是 A(a=1)(hah),调用了hah的方法,在写一个方法接受参数
@A(a=1)
def hah(info):
print("------",info)
# 这里可以看出执行了init的方法和call方法
# 即执行了 c = A(a=1)(hah)
# ----
# aaa 1
hah(info="1")
# 这里才是调用了类的hah和hah本身自己的方法
# 执行了c.hah,然后在c.hah中调用了hah的方法
# 99
# ------ 1
装饰器的问题
使用装饰器包装函数,当要得到函数本身的信息的话, 如果没有@wraps(func),则只会得到装饰器里的return的那个函数的信息。 如果有@wraps(func),则只会得到函数自己的信息。
装饰器的调用时机为,当有函数使用装饰器的时候,就会调用装饰器的外函数,当具体函数执行的时候执行内函数。
from functools import wraps
def decorate(func):
# @wraps(func)
def wrapper(*args, **kwargs):
print("9999")
return func(*args, **kwargs)
return wrapper
@decorate
def hah():
print("hello world")
print(hah.__name__)
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("9999")
return func(*args, **kwargs)
return wrapper
@decorate
def hah():
print("hello world")
print(hah.__name__)
classmethod and staticmethod
就是一个装饰器,一个对象的实例都可以调用类方法和静态方法,无非是里面做了参数的传递与否。
闭包
有内部函数和外部函数,外层函数的变量被内层函数引用,内层函数被外部函数返回。 外部函数传入的参数不会消失,实现一种session的机制。 就是因为外部函数的局部变量不会被释放,所以内存占用比较高。
闭包可以是认为是轻量级的简单类,参数为属性,内部方法为函数,闭包里的信息相对于类会少很多.
- 闭包定义是在函数内再嵌套函数
- 闭包是可以访问另一个函数局部作用域中变量的函数
- 闭包可以读取另外一个函数内部的变量,可以保证外部函数的数据安全
- 闭包可以让参数和变量不会被垃圾回收机制回收圣放局部变量,最好是用完手动释放
def get_av():
prices = []
def inner(price):
prices.append(price)
return sum(prices)/len(prices)
return inner
c = get_av()
c(100)
c(200)
nonlocal
需要修改外部函数的变量,需要使用nonlocal关键字
## 优化get_av
# 把所有值存储在历史数列中,然后在每次调用 averager 时使用 sum求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
# Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。
count += 1
# 当 count 是数字或任何不可变类型时,count += 1 语句的
# 作用其实与 count = count + 1 一样。因此,我们在 averager 的
# 定义体中为 count 赋值了,这会把 count 变成局部变量。total 变
# 量也受这个问题影响。
total += new_value
return total / count
return averager
思考
函数、匿名函数、闭包、对象 当做实参时 有什么区别 ?
- 匿名函数能够完成基本的简单功能,传递是这个函数的引用 只有功能
- 普通函数能够完成较为复杂的功能,传递是这个函数的引用 只有功能
- 闭包能够将较为复杂的功能,传递是这个闭包中的函数以及数据,因此传递是功能+数据
- 对象能够完成最为复杂的功能 ,传递是很多数据+很多功能,因此传递是功能+数据
单分派泛函数
函数重载,即是传入不同的参数做不同的事情,减少if/else的判断。
from functools import singledispatch
from collections import abc
@singledispatch
def show(obj):
print (obj, type(obj), "obj")
#参数字符串
@show.register(str)
def _(text):
print("i am str")
print (text, type(text), "str")
#参数int
@show.register(int)
def _(n):
print("i am int")
print (n, type(n), "int")
#参数元祖或者字典均可
@show.register(list)
@show.register(tuple)
@show.register(dict)
def _(tup_dic):
print("i am tuple or dict list")
print (tup_dic, type(tup_dic), "int")
show(1)
show("xx")
show([1])
show((1,2,3))
show({"a":"b"})
# i am int
# 1 <class 'int'> int
# i am str
# xx <class 'str'> str
# i am tuple or dict list
# [1] <class 'list'> int
# i am tuple or dict list
# (1, 2, 3) <class 'tuple'> int
# i am tuple or dict list
# {'a': 'b'} <class 'dict'> int
对象引用、可变性和垃圾回收
a = [1, 2, 3]
b = a
a.append(4)
b
# [1, 2, 3, 4]
nums = [1,2]
def func(nums):
nums.append(9)
func(nums)
# [1, 2, 9]
is VS ==
is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是直接比较两个整数 ID。 而 a == b 是语法糖,等同于 a.eq(b)。继承自 object 的 eq 方法比较两个对象的ID,结果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了__eq__ 方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作.
深浅拷贝
浅拷贝: 仅仅是复制最顶层数据(切片[:]是浅拷贝,字典的copy也是浅拷贝) 深拷贝: 递归拷贝,任何一层数据都是会复制一份.
import copy
a = [1,[1,2,3]]
b = copy.copy(a)
a[1] is b[1]
# True
l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1)
# 构造方法或 [:] 做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)
l2
# [3, [55, 44], (7, 8, 9)]
l2 == l1
# True
l2 is l1
# False
# 因为l1的[1]的
l1[1].append(1)
l1[0]=99
l2[2] += (10, 11)
# 对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量l2[2]。这等同于 l2[2] = l2[2] + (10, 11)。现在,l1 和 l2中最后位置上的元组不是同一个对象。
# l1 [99, [55, 44, 1], (7, 8, 9)]
# l2 [3, [55, 44, 1], (7, 8, 9,10,11)]
# 使用浅拷贝的话,仅仅是复制了最外层的容器
# 如果最外层的容器的是不可变,那么改变 l1/l2互不影响
# 但是如果容器是可以变化的,那么改变其一另一方会受到影响
构造方法或 [:] 做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。
>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(4301498296, 4301499416, 4301499752)
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David']
# bus1 和 bus2 共享同一个列表对象,因为 bus2 是 bus1 的浅复制副本。
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4302658568, 4302658568, 4302657800)
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David']
# bus3 是 bus1 的深复制副本,因此它的 passengers 属性指代另一个列表。
一般来说,深复制不是件简单的事。如果对象有循环引用,那么这个朴素的算法会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用.
函数参数
>>> def f(a, b):
... a += b
... return a
...
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y
(1, 2)
# 数字 x 没变。
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b
# 列表 a 变了。
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))
# 元组 t 没变。
# 不要使用可变类型作为参数的默认值
class HauntedBus:
"""备受幽灵乘客折磨的校车"""
def __init__(self, passengers=[]):
self.passengers = passengers
# 优化
# def __init__(self, passengers=None):
# if passengers is None:
# self.passengers = []
# else:
# self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
bus2 = HauntedBus()
bus2.pick('Carrie')
bus2.passengers
# ['Carrie']
bus3 = HauntedBus()
bus3.passengers
# ['Carrie']
bus3.pick('Dave')
bus2.passengers
# ['Carrie', 'Dave']
# bus2.passengers is bus3.passengers
弱引用
有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。 弱引用不会增加对象的引用数量。引用的目标对象称为所指对象referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
import weakref
ccc = {0, 1}
wref = weakref.ref(ccc)
print(wref()) # {0, 1}
ccc = {2,3,4}
print(wref())
# 因为 {0, 1} 对象不存在了,所以 wref() 返回 None
弱引用的局限
不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本 的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地 解决这个问题:
class MyList(list):
"""list的子类,实例可以作为弱引用的目标"""
pass
a_list = MyList(range(10))
# a_list可以作为弱引用的目标
wref_to_a_list = weakref.ref(a_list)
参考:https://blog.csdn.net/lb971216008/article/details/138258447
del和垃圾回收
del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾 回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得 到对象时。重新绑定也可能会导致对象的引用数量归零,导致对象被 销毁。
在 CPython 中,垃圾回收使用的主要算法是引用计数。实际上,每个对 象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销 毁:CPython 会在对象上调用 del 方法(如果定义了),然后释 放分配给对象的内存。CPython 2.0 增加了分代垃圾回收算法,用于检 测引用循环中涉及的对象组——如果一组对象之间全是相互引用,即使 再出色的引用方式也会导致组中的对象不可获取。
>>> import weakref
>>> s1 = {1, 2, 3}
>>> s2 = s1
# s1 和 s2 是别名,指向同一个集合,{1, 2, 3}。
>>> def bye():
... print('Gone with the wind...')
...
>>> ender = weakref.finalize(s1, bye)
# 在 s1 引用的对象上注册 bye 回调。
>>> ender.alive
# 调用 finalize 对象之前,.alive 属性的值为 True。
True
>>> del s1
# del 不删除对象,而是删除对象的引用。
# 删除了s1,但是s2指向同一个集合,所以s1仍然活着
>>> ender.alive
True
>>> s2 = 'spam'
# 重新绑定最后一个引用 s2,让 {1, 2, 3} 无法获取。对象被销毁了,调用了 bye 回调,ender.alive 的值变成了 False。
Gone with the wind...
>>> ender.alive
False
面向对象
属性
属性分为类属性和实例属性,公共的属性即是类属性,私有的属性即是实例属性
因为python是动态类型语言,一个类或者对象都可以随时增加属性和函数等.
class A:
# 类在内存中只会有一份
a = '123'
def __init__(self,name):
# 实例属性 在每一个对象实例中都有一份
self.name = name
a = A("!")
a.a = 999
print(a.a)
print(A.a)
# 999 实例对象能改自己的类属性,但是改不了类的属性
# 123
# 123
A.a = 666
print(a.a)
print(A.a)
print(A("!").a)
# 999 修改类属性后,已经生成的对象的类属性不受影响
# 666
# 666 修改类属性后,新生成的对象的类属性不受影响
A.b =111 # 添加类属性
import types
def func(self):
print("func")
a.run = types.MethodType(func,a) # 添加实例函数
a.run()
动态增加
属性
class A:
def __init__(self):
self.a = 1
A.b = 2 # 添加类属性
a = A()
a.c = 3 # 对象添加属性
方法
class A:
def __init__(self):
self.a = 1
import types
def func(self):
print("func")
a.run = types.MethodType(func,a) # 添加实例函数
a.run()
@classmethod
def classmethod_1(cls):
print("classmethod")
@staticmethod
def staticmethod_1():
print("staticmethod_1")
A.classmethod_1 = classmethod_1 # 添加类方法
A.staticmethod_1 = staticmethod_1 # 添加静态方法
slots
__slots__是一个特殊的内置类属性,它可以用于定义类的属性名称的集合。一旦在类中定义了__slots__属性,Python将限制该类的实例只能拥有__slots__中定义的属性。这有助于减少每个实例的内存消耗,提高属性访问速度,同时也可以防止意外添加新属性。
slots定义类只能拥有哪些属性,而不允许添加其他属性。
class MyClass:
__slots__ = ('attr1', 'attr2', 'attr3')
使用__slots__有几个潜在的好处:
-
内存效率:通过限制实例的属性集合,__slots__可以降低每个实例的内存消耗。在大型数据结构或对象数量众多的情况下,这可能会显著减小内存占用。
-
属性访问速度:由于__slots__限制了属性的数量和名称,属性访问速度会更快。这对于需要高性能的应用程序和数据处理非常有用。
-
防止错误:通过限制可用属性,__slots__可以减少在程序中意外添加新属性的可能性,有助于提高代码的稳定性和可维护性。
-
强制规范:__slots__可以强制类的设计者明确定义属性的名称,从而使代码更易于理解和维护。
class Person:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("Alice", 30)
print(person.name) # 输出 "Alice"
print(person.age) # 输出 30
print(person.ccc) # 错误无法修改
# 内存效率比较
import sys
class WithoutSlots:
def __init__(self, name, age):
self.name = name
self.age = age
class WithSlots:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
obj1 = WithoutSlots("Alice", 30)
obj2 = WithSlots("Bob", 25)
print(sys.getsizeof(obj1)) # 输出 56
print(sys.getsizeof(obj2)) # 输出 40
# 属性访问速度比较
import timeit
class WithoutSlots:
def __init__(self, name, age):
self.name = name
self.age = age
class WithSlots:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
obj1 = WithoutSlots("Alice", 30)
obj2 = WithSlots("Bob", 25)
def access_name_age(obj):
obj.name
obj.age
time1 = timeit.timeit(lambda: access_name_age(obj1), number=1000000)
time2 = timeit.timeit(lambda: access_name_age(obj2), number=1000000)
print(f"WithoutSlots: {time1} seconds")
print(f"WithSlots: {time2} seconds")
# 当属性数量较多时,__slots__的元组形式可能会变得冗长。您可以使用元组展开来改善可读性
# __slots__与property方法一起使用可以添加属性的getter和setter,以便在属性访问时执行特定的操作
class Person:
__slots__ = ('name', 'age', 'city', 'email')
def __init__(self, name, age, city, email):
self.name = name
self.age = age
self.city = city
self.email = email
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if isinstance(value, str):
self._name = value
else:
raise ValueError("Name must be a string")
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if isinstance(value, int) and 0 <= value <= 120:
self._age = value
else:
参考:https://blog.csdn.net/Rocky006/article/details/135384857
property
看上去是掉了属性,其实是调用了一个方法,可以简化操作.
xxx.setter/deleter需要放在那个对应property的后面,且几个函数的名称需要相同, 可以只有property
class Page(object):
current_page = 1
count = 10
# 第一种使用方法
@property
def offset(self):
return (self.current_page -1)* self.count
# 需要放在那个对应property的后面
# offset的函数名称与上面property的方法一致.
@offset.setter
def offset(self,value):
print("setter")
@offset.deleter
def offset(self):
print("deleter")
# 第二种方法
def get_offset(self):
return (self.current_page -1)* self.count
def set_offset(self,value):
print("setter2")
def del_offset(self):
print("deleter2")
# 对应上面 get set del 最后一个是描述信息
# 第一个必须有
offset2 = property(get_offset, set_offset, del_offset, "doc")
a = Page()
print(a.offset) # 调用 @property/offset
a.offset = 1 # 调用 @property.setter/offset
del a.offset # 调用 @property.deleter/offset
# 0
# setter
# deleter
a.offset2
a.offset2 = 2
a.offset2.__doc__
del a.offset2
# 0
# setter2
# deleter2
基类
声明抽象基类最简单的方式是继承 abc.ABC 或其他抽象基类。
除了 @abstractmethod 之外,abc 模块还定义了 @abstractclassmethod、@abstractstaticmethod 和 @abstractproperty 三个装饰器。然而,后三个装饰器从 Python 3.3 起废弃了,因为装饰器可以在 @abstractmethod 上堆叠,那三个就 显得多余了。
import abc
class Tombola(abc.ABC):
@abc.abstractmethod
def load(self, iterable):
"""从可迭代对象中添加元素。"""
@abc.abstractmethod
def pick(self):
"""随机删除元素,然后将其返回。
如果实例为空,这个方法应该抛出`LookupError`。
"""
@classmethod
@abc.abstractclassmethod
def an_abstract_classmethod(cls, ...):
pass
def loaded(self): ➍
"""如果至少有一个元素,返回`True`,否则返回`False`。"""
return bool(self.inspect())
虚拟子类
Python 中的虚拟子类主要是为了实现动态性和解耦。
- 动态性:与传统的静态继承关系相比,虚拟子类具有更高的动态性。虚拟子类可以在运行时动态地添加或移除,而不需要修改代码。这使得程序具有更高的灵活性和可扩展性。
- 解耦:虚拟子类可以实现不同类之间的解耦,降低了类之间的依赖性。传统的继承关系可能会导致子类与父类紧密地绑定在一起,而虚拟子类则可以避免这一点。通过使用虚拟子类,一个类可以灵活地使用另一个类的功能,而不必关心具体的实现细节。
- 模块化:虚拟子类可以帮助我们更好地组织和管理代码。将类的功能提取为单独的模块,通过虚拟子类来实现不同模块之间的交互,可以使代码结构更加清晰,便于维护和复用。
import abc
class Tombola(abc.ABC):
@abc.abstractmethod
def pick(self, iterable):
"""从可迭代对象中添加元素。"""
@Tombola.register
class TomboList(list):
def pick(self):
if self:
position = randrange(len(self))
return self.pop(position)
else:
raise LookupError('pop from empty TomboList')
# 或者
Tombola.register(tuple)
继承
子类化内置类型
在 Python 2.2 之前,内置类型(如 list 或 dict)不能子类化。在 Python 2.2 之后,内置类型可以子类化了,但是有个重要的注意事项: 内置类型(使用 C 语言编写)不会调用用户定义的类覆盖的特殊方法。
直接子类化内置类型(如 dict、list 或 str)容易出 错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化 内置类型,用户自己定义的类应该继承 collections 模块 (http://docs.python.org/3/library/collections.html)中的类,例如 UserDict、UserList 和 UserString,这些类做了特殊设计, 因此易于扩展。
>>> class DoppelDict(dict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value] * 2) # ➊
>>> dd = DoppelDict(one=1) #
# 继承自 dict 的 __init__ 方法显然忽略了我们覆盖的__setitem__ 方法:'one' 的值没有重复。
>>> dd
{'one': 1}
>>> dd['two'] = 2
>>> dd
{'one': 1, 'two': [2, 2]}
# [] 运算符会调用我们覆盖的 __setitem__ 方法,按预期那样工作:'two' 对应的是两个重复的值,即 [2, 2]。
>>> dd.update(three=3)
>>> dd
{'three': 3, 'one': 1, 'two': [2, 2]}
# 继承自 dict 的 update 方法也不使用我们覆盖的 __setitem__方法:'three' 的值没有重复。
多重继承
任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关 的祖先类实现同名方法引起。
多继承中父类中有相同的方法, Python 会按照特 定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order,MRO)。类都有一个名为 mro 的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到object 类。 例如下面的pingpong里面的pong方法,因为是先解析到了B,所以调用了B的方法。
对于init方法也是会按照__mro__里面的顺序执行init方法.
class A:
def ping(self):
print('A-ping:', self)
class B(A):
def pong(self):
print('B-pong:', self)
class C(A):
def pong(self):
print('C-PONG:', self)
class D(B, C):
def ping(self):
super().ping()
print('post-ping:', self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
C.pong(self)
d = D()
d.pong()
# B-pong: <__main__.D object at 0x7f6c1c51e7f0>
d.ping()
# A-ping: <__main__.D object at 0x7f6c0c2c2160>
# post-ping: <__main__.D object at 0x7f6c0c2c2160>
d.pingpong()
# post-ping: <__main__.D object at 0x7f6c1c1a6190>
# A-ping: <__main__.D object at 0x7f6c1c1a6190>
# B-pong: <__main__.D object at 0x7f6c1c1a6190>
# B-pong: <__main__.D object at 0x7f6c1c1a6190>
# C-PONG: <__main__.D object at 0x7f6c1c1a6190>
D.__mro__
# (__main__.D, __main__.B, __main__.C, __main__.A, object)
运算符重载
+
add
radd
*
mul
rmul
中缀运算符方法
运算符 | 正向方法 | 反向方法 | 就地方法 | 说明 |
---|---|---|---|---|
+ | add | radd | iadd | 加法或拼接 |
- | sub | rsub | isub | 减法 |
* | mul | rmul | imul | 乘法或重复复制 |
/ | truediv | rtruediv | itruediv | 除法 |
// | floordiv | rfloordiv | ifloordiv | 整除 |
% | mod | rmod | imod | 取模 |
divmod() | divmod | rdivmod | idivmod | 返回由整除的商和模数组成的元组 |
pow | pow | rpow | ipow | 取幂 |
@ | matmul | rmatmul | imatmul | 矩阵乘法 |
& | and | rand | iand | 位与 |
| | or | ror | ior | 位或 |
^ | xor | rxor | ixor | 位异或 |
<< | lshift | rlshift | ilshift | 按位左移 |
>> | rshift | rrshift | irshift | 按位右移 |
可迭代的对象、迭代器和生成器
所有生成器都是迭代器,因为生成器完全实现了迭代器接口。
- for循环可以循环Iterable类型(可迭代对象)
- next则是Iterator类型(迭代器)
是否可迭代
from collections.abc import Iterable
isinstance(1, Iterable)
nums = [1,2,3]
nums_iter = iter(nums)
next(nums_iter)
next(nums_iter)
next(nums_iter)
# 最后一个没了报错 StopIteration
next(nums_iter)
序列可以迭代的原因:iter函数
- 检查对象是否实现了 iter 方法,如果实现了就调用它,获取 一个迭代器。
- 如果没有实现 iter 方法,但是实现了 getitem 方法, Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。
- 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“C object is not iterable”(C 对象不可迭代),其中 C 是目标对象所属的类。
从 Python 3.4 开始,检查对象 x 能否迭代,最准确的方法 是:调用 iter(x) 函数,如果不可迭代,再处理 TypeError 异 常。这比使用 isinstance(x, abc.Iterable) 更准确,因为 iter(x) 函数会考虑到遗留的 getitem 方法,而abc.Iterable 类则不考虑。
迭代器
next 返回下一个可用的元素,如果没有元素了,抛出 StopIteration 异常。 iter 返回 self,以便在应该使用可迭代对象的地方使用迭代器,例如 在 for 循环中。
class A:
nums = [ 1,2,3]
def __iter__(self):
return B()
# 只有iter方法就是可迭代对象,但是不是迭代器
# def __next__(self):
# pass
class B:
nums = [ 1,2,3]
current = 0
# 有iter,next方法是迭代器
def __iter__(self):
return self
def __next__(self):
# for 循环里其实调用的就是iter和next方法
# 带有异常判断StopIteration,所以next方法需要raise StopIteratio
if self.current < len(self.nums):
item = self.nums[self.current]
self.current+=1
return item
else:
self.current=0
raise StopIteration
from collections.abc import Iterable,Iterator
a = A()
iter(a) # 会调用上面的__iter__方法
print(isinstance(a,Iterable))
print(isinstance(a,Iterator))
b = B()
print(isinstance(b,Iterable))
print(isinstance(b,Iterator))
for i in b:
print(i)
for i in b:
print(i)
list(b)
[ i for i in b]
# [1,2,3]
生成器
如果需要很多数据的列表,不需要一次性全部生成,从而导致内存占用太高,而是需要的时候再去生成,就可以省内存. 只是知道数据生成方式,而不是先生成所有数据.
生成器是一种特别的迭代器.
通过next调用生成器,直到遇到yield返回出去,然后等下一次next调用生成器,执行上次yiedl后的代码,然后遇到下一次yield返回出去.
import sys
import time
total1 = 0
total2 = 0
count = 10000000
t1 = time.time()
nums = [ i for i in range(count)]
print(type(nums))
print(sys.getsizeof(nums))
for i in nums:
total1+=i
t2 = time.time()
print(t2-t1)
# (列表推导式) 生成器
t3 = time.time()
nums = (i for i in range(count))
print(type(nums))
print(sys.getsizeof(nums))
for i in nums:
total2+=i
t4 = time.time()
print(t4-t3)
# <class 'list'>
# 800984 --------内存占用
# 0.006327390670776367 ----执行时间对比
# <class 'generator'>
# 112 --------内存占用
# 0.0066471099853515625 ----执行时间对比
# 斐波那次数列
def fib():
a, b = 1 , 1
while True:
res = a
a, b = b, a + b
print("before yield")
if b > 100000:
return "b值已经大于100000"
yield res
print("after yield")
generator = fib()
# 得到是生成器对象
print(next(generator))
print(next(generator))
print(next(generator))
for i in fib():
print(i)
# before yield
# here is 1
# after yield
# before yield
# here is 1
# after yield
# ...
如果没有遇到yield,而是有return,而return的信息,会放到stopiteration异常中.
def fib():
a, b = 1 , 1
while True:
res = a
a, b = b, a + b
print("before yield")
if b > 4:
return "我是错误"
yield res
print("after yield")
gen = fib()
gen = fib()
print(next(gen))
print(next(gen))
# print(next(gen))
try:
print(next(gen))
except StopIteration as s:
print(s.value)
# 1
# 1
# 我是错误
seed
使用seed可以向生成器传入信息
def fib():
a, b = 1 , 1
while True:
res = a
a, b = b, a + b
if b > 4:
return "我是错误"
cc = yield res
print("here is ",cc)
gen = fib()
# 生成器还没开始,需要执行一次next,或者使用gen.send(None)不传入只
# print(next(gen))
print(gen.send(None))
# 将11传给上面的cc
print(gen.send((1,2)))
try:
print(gen.send(12))
except StopIteration as s:
print(s.value)
# 1
# here is (1,2)
# 1
# here is 13
# 我是错误
上下文管理
上下文管理器协议包含 enter 和 exit 两个方法。with 语 句开始运行时,会在上下文管理器对象上调用 enter 方 法。with 语句运行结束后,会在上下文管理器对象上调用 exit 方法,以此扮演 finally 子句的角色.
contextlib
编写__enter__和__exit__仍然很繁琐,因此Python的标准库contextlib提供了更简单的写法,
from contextlib import contextmanager
class Query(object):
def __init__(self, name):
self.name = name
def __enter__(self):
print('Begin')
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')
def query(self):
print('Query info about %s...' % self.name)
@contextmanager
def create_query(name):
print('Begin')
q = Query(name)
yield q
print('End')
GIL
一次只允许使用一个线程执行 Python 字节码
- GIL global interface lock 全局解释器锁 存在于Cpython
- 每个线程在执行前都需要获取GIL,保证同一时刻只会有一个线程执行代码.
- 标准库中所有执行阻塞型 I/O 操作的函数,在等待操作系统返回 结果时都会释放 GIL,如果函数中没有阻塞型 I/O 操作,在代码执行达到一定的次数后会释放GIL
- 可以使用多进程,利用多核CPU.
优化
可以通过进程的代码通过其他语言实现,然后在python中使用多进程掉对应对应的方法就OK. 就可以实现真正的多线程.
from ctypes import *
from threading import Thread
# 动态加载库
libc = cdll.LoadLibrary('./loop.so')
# thread
# loop里面有个方法是DeadLoop
t = Thread(target=libc.DeadLoop)
t.start()
# 主线程
while True:
pass
// loop.c
// 生成动态库文件
// gcc -shared -o loop.so loop.c
void DeadLoop(){
while(1){
}
}
并发/并行
真正的并行需要多个核心。现代的笔记本电脑有4个 CPU 核心,但是 通常不经意间就有超过 100 个进程同时运行。因此,实际上大多数过程 都是并发处理的,而不是并行处理。计算机始终运行着 100 多个进程, 确保每个进程都有机会取得进展,不过 CPU 本身同时做的事情不能超 过四件。十年前使用的设备也能并发处理 100 个进程,不过都在同一个 核心里。
协程
线程与协程之间的比较还有最后一点要说明:如果使用线程做过重要的 编程,你就知道写出程序有多么困难,因为调度程序任何时候都能中断 线程。必须记住保留锁,去保护程序中的重要部分,防止多步操作在执 行的过程中中断,防止数据处于无效状态。
而协程默认会做好全方位保护,以防止中断。必须显式产出才能让 程序的余下部分运行。对协程来说,无需保留锁,在多个线程之间同步 操作,协程自身就会同步,因为在任意时刻只有一个协程运行。
状态
'GEN_CREATED'
等待开始执行。
'GEN_RUNNING'
解释器正在执行。
'GEN_SUSPENDED'
在 yield 表达式处暂停。
'GEN_CLOSED'
执行结束。
>>> def simple_coro2(a):
... print('-> Started: a =', a)
... b = yield a
... print('-> Received: b =', b)
... c = yield a + b
... print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2)
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED'
>>> my_coro2.send(28)
728
-> Received: b = 28
42
>>> my_coro2.send(99)
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CLOSED'
# my_coro2.close()
比较
进程
进程有独立的数据代码,堆栈内存分页等信息,所以开销很大。且每个进程的运行环境是独立的.
线程
比进程轻量级,仅仅是包含了程序和CPU,建立在进程的基础之上,一个进程里包含多个线程. 线程分为kernel thread 内核态线程和user thread用户态线程,真正工作的是kernel thread, kernel thread去执行user thread 。 并发模型可以分为1:1(java),1:n(python),m:n(golang)
每一个User线程需要记录自己的栈信息,共享进程里的堆数据代码等。
线程的上下文切换的流程:
- 进入内核态,执行中断
- 保存线程的上下文
- 调度
- 加载cpu
- 恢复切换到的线程上下文(频繁做切换,会导致效率很低)
- 切换到用户态,执行
而线程的调度只有拥有最高权限的内核空间才可以完成,所以线程的切换涉及到用户空间和内核空间的切换,也就是特权模式切换,然后需要操作系统调度模块完成线程调度(taskstruct),而且除了和协程相同基本的 CPU 上下文,还有线程私有的栈和寄存器等 上下文切换的开销大约是2.7-5.48us左右。
https://zhuanlan.zhihu.com/p/80037638
import asyncio
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from threading import Lock
import time
num = 4
test = 0
# 修改大小测试线程安全的问题
count = 40000000
# lock = Lock()
async def task1():
print("--")
global test
for i in range(count):
test += 1
await asyncio.sleep(0.1)
def task2():
global test
for i in range(count):
# with lock:
test += 1
time.sleep(0.1)
def with_processing():
with ProcessPoolExecutor(max_workers=5) as executor:
for i in range(num):
executor.submit(task2)
def with_threading():
with ThreadPoolExecutor(max_workers=5) as executor:
for i in range(num):
executor.submit(task2)
async def main():
tasks = [task1() for i in range(num)]
await asyncio.gather(*tasks)
if __name__ == "__main__":
# time1 = time.time()
# with_processing()
# time2 = time.time()
# print("processing", time2 - time1)
# with_threading()
# time3 = time.time()
# print(test)
# print("threading", time3 - time2)
asyncio.run(main())
# print("asyncio", time.time() - time3)
print(test)
协程
比线程更为轻量,不需要创建内核线程,上下文切换快,允许用户自主调度.
协程切换非常简单,就是把当前协程的 CPU 寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载的 CPU 寄存器上就 ok 了。
一般来说一次协程上下文切换最多就是几十ns 这个量级.
代码对比
import asyncio
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
num = 500
async def task1():
await asyncio.sleep(0.1)
def task2():
time.sleep(0.1)
def with_processing():
with ProcessPoolExecutor(max_workers=5) as executor:
for i in range(num):
executor.submit(task2)
def with_threading():
with ThreadPoolExecutor(max_workers=5) as executor:
for i in range(num):
executor.submit(task2)
async def main():
tasks = [task1() for i in range(num)]
await asyncio.gather(*tasks)
if __name__ == "__main__":
time1 = time.time()
with_processing()
time2 = time.time()
print("processing", time2 - time1)
with_threading()
time3 = time.time()
print("threading", time3 - time2)
asyncio.run(main())
print("asyncio", time.time() - time3)
# processing 10.522849082946777
# threading 10.752429962158203
# asyncio 0.1018362045288086
垃圾回收
小整数对象池
为了优化速速,[-5.256]这些整数对象是提前建立好的,使用了小整数对象池,不会被垃圾回收,避免为整数频繁申请和销毁内存空间.而比较大的数,都会创建一个新的对象。
a = 1
b = 1
id(a) == id(b)
# True
l1 = [1]
l2 = [2]
id(l1) == id(l2)
# False
a = 300
b = 300
id(a) == id(b)
# False
intern机制
因为普通字符串(不包含特殊字符)时不可修改,所以相同的字符会共用一份.
a = "hello"
b = "hello"
id(a) == id(b)
# True
a = "hello world"
b = "hello world"
id(a) == id(b)
# False
GC
garbage collection 垃圾回收机制 python采用的是引用计数为主,分代收集机制为辅
引用计数
每个对象的存储里有一个变量指向引用数.
特点:
- 简单
- 实时性,一旦没有引用就会被及时GC
- 比较消耗资源
- 循环引用无法解决,会导致内存泄漏
a = A() # 计数+1
b =a # 计数+1
+1
- 对象被创建,如 a = 999
- 对象被引用,如 b = a
- 对象作为参数传递,如 func(a)
- 对象作为其他对象的元素,存储在容器里,如 c = [a,b]
-1
- 对象被del,如 del a
- 对象变化,如 a = 1000
- 一个对象离开它的作用域,如函数执行完毕,func中的局部变量(全局变量不会减少引用数)
- 对象所在的容器被销毁,或者容器中的元素被删除,如 a = []
# 在ipython中查看
import sys
a = "python"
# 取消注释查看变化
# b = a
# c = [a]
print(sys.getrefcount(a))
隔代计数
将刚创建对象的放到1代的链表中,如果存在某次GC后的存在的对象数量大于阈值,将零代的链表中的对象的计数-1,如果它的计数为0,则将对象从链表中移除,并释放内存空间,将不为0的对象放入到2代GC的链表中。再次GC则放到3代GC的链表中。python默认开启.
import gc
class A:
def __init__(self):
print("-------",id(slef))
def func():
while True:
a1 = A()
a2 = A()
a1.a = a2
a2.a = a1
# 如果关闭了gc,那么a1和a2的计数不会为0,del不掉,导致内存泄漏
del a1
del a2
# 强制回收,就可以回收
# gc.collect()
gc.disable()
func()
import gc
# 关闭gc
gc.disable()
# 查看清理条件
gc.get_threshold()
(700, 10, 10)
# 当1代的链表的垃圾对象数量大于700时,会触发一次GC。
# 当1代的链表GC10次后,触发2代链表的GC。
# 当2代的链表GC10次后,触发3代链表的GC。
gc.get_counts()
# 获取当前的信息
(40, 10, 3)
# 1代的链表上的垃圾对象数量。
# 1代的GC的数量
# 2代的GC的数量
执行时机
- 当对象数量的阈值大于阈值时,会触发一次GC。
- gc.collect
- 退出程序
元类
元类就是一个特殊的类,用于专门创建其他类.所有的类的最终的__class__都是type. type就是所有对象的元类,type可以认为是祖师爷.
type
# type(name, bases, dict)
# name: 类名
# bases: 父类元组
# dict: 属性字典
A = type("A", (), {"name": 1})
def func(self):
print("i am func",self.name)
@staticmethod
def static_func():
print("i am static_func")
@classmethod
def class_func(cls):
print("i am class_func")
# 继承A
B = type("B", (A,), {"func":func, "static_func":static_func, "class_func":class_func})
# 创建的类如下
# class B(A):
# def func(self):
# print("i am func",self.name)
#
# @staticmethod
# def static_func():
# print("i am static_func")
# @classmethod
# def class_func(cls):
# print("i am class_func")
b = B()
b.func()
b.static_func()
b.class_func()
# i am func 1
# i am static_func
# i am class_func
metaclass
在声明类的时候可以指定用的元类,元类会负责创建类,其中可以完成自己的属性定义.
# 将其中的所有类属性转换为大写
def update_attr(class_name, class_parents, class_attrs):
now_attrs = {}
for attr_name, attr_value in class_attrs.items():
if not attr_name.startswith("__"):
now_attrs[attr_name.upper()] = attr_value
return type(class_name, class_parents, now_attrs)
class MyClass(metaclass=update_attr):
name = 1
MyClass.NAME
# 使用name报错,因为已经转换为大写了,不存在小写的name
class Meta(type):
def __new__(cls, class_name, class_parents, class_attrs):
now_attrs = {}
for attr_name, attr_value in class_attrs.items():
if not attr_name.startswith("__"):
now_attrs[attr_name.upper()] = attr_value
return type(class_name, class_parents, now_attrs)
# 或着
return type.__new__(cls, class_name, class_parents, now_attrs)
class MyClass(metaclass=Meta):
name = 1
元类实现ORM
class ModelMetaclass(type):
"""数据表模型元类"""
def __new__(mcs, cls_name, bases, attrs):
# 数据表对应关系字典
mappings = dict()
# 过滤出对应数据表的字段属性
for k, v in attrs.items():
# 判断是否是对应数据表的字段属性, 因为attrs中包含所有的类属性
# 这里就简单判断字段是元组
if isinstance(v, tuple):
mappings[k] = v
# 删除这些已经在字典中存储的字段属性
for k in mappings.keys():
attrs.pop(k)
# 将之前的uid/name/email/password以及对应的对象引用、类名字
# 用其他类属性名称保存
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = cls_name # 假设表名和类名一致
return type.__new__(mcs, cls_name, bases, attrs)
class Model(object, metaclass=ModelMetaclass):
"""数据表模型基类"""
def __init__(self, **kwargs):
for name, value in kwargs.items():
setattr(self, name, value)
def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
# 因为下面的是这样('uid', "int unsigned"),取出0,即为数据库表的字段信息
args.append(getattr(self, k, None))
# 把参数数据类型对应数据表的字段类型
args_temp = list()
for temp in args:
if isinstance(temp, int):
args_temp.append(str(temp))
elif isinstance(temp, str):
args_temp.append(f"'{temp}'")
# 表名
table_name = self.__table__
# 数据表中的字段
fields = ','.join(fields)
# 待插入的数据
args = ','.join(args_temp)
# 生成sql语句
sql = f"""insert into {table_name} ({fields}) values ({args})"""
print(f'SQL: {sql}')
# 执行sql语句
# ...
class User(Model):
"""用户表模型类"""
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
def main():
user = User(uid=123, name='jumpwill', email='707979910@qq.com', password='123456')
user.save()
if __name__ == '__main__':
main()
属性描述符
描述符是Python中一种特殊的对象,它具有特殊方法__get__、set__和__delete。 描述符主要用来实现数据绑定和属性验证。
描述符需要放在类属性里,而不是实例属性.
下面的代码中name是B的描述符。
class A:
name = 1
def __get__(self,instance ,owner):
print("self",self) # A的实例对象
print("instance",instance) # B的实例对象,即是下面的b
print("owner",owner) # B的class,即使instance.__class__
return self.name
def __set__(self,instance , value):
print("self",self) # A的实例对象
print("value",value) # 等于的值
print("instance",instance) # B的实例对象,即是下面的b
self.name = value
def __delete__(self,instance):
print(self) # A的实例对象
print("instance",instance) # B的实例对象,即是下面的b
del self.name
class B:
name = A()
b = B()
b.name
# self <__main__.A object at 0x7fc8d76a54f0>
# instance <__main__.B object at 0x7fc8d7948370>
# owner <class '__main__.B'>
b.name = 2
# self <__main__.A object at 0x7fc8d76a54f0>
# value 2
# instance <__main__.B object at 0x7fc8d7948370>
del b.name
# <__main__.A object at 0x7fc8d76a54f0>
# instance <__main__.B object at 0x7fc8d7948370>
调用时机
当python解释器遇到类代码的时候,实际上会进行调用,保证解释器能知道创建类需要有哪些东西,比如属性、方法等等。
class A:
name = 1
def __get__(self,instance ,owner):
print("self",self) # A的实例对象
print("instance",instance) # B的实例对象,即是下面的b
print("owner",owner) # B的class,即使instance.__class__
return self.name
def __set__(self,instance , value):
print("self",self) # A的实例对象
print("value",value) # 等于的值
print("instance",instance) # B的实例对象,即是下面的b
self.name = value
def __delete__(self,instance):
print(self) # A的实例对象
print("instance",instance) # B的实例对象,即是下面的b
del self.name
print("888")
class B:
name = A()
print("9999")
# 会输出
# 8888
# 9999
为什么需要描述符
封装
使用描述符,可以set中验证属性的类型,且实现在类的内部,达到一种高内聚低耦合的设计.
相对于property方便
使用property,只能对一个属性进行验证,如果想校验多个属性,使用property会很多,而描述符可以对多个属性进行验证。pydantic也是这样实现的.
class A:
def __init__(self, type, value, min, max):
self.__type = type
self.__min = min
self.__max = max
self.__value = value
def __get__(self, instance, owner):
return self.__value
def __set__(self, owner, value):
if not isinstance(value, self.__type):
raise TypeError(f"类型错误 希望是{self.__type} ,传入的是{type(value)}")
if self.__type == str:
if len(value) < self.__min or len(value) > self.__max:
raise ValueError(f"值错误 最大长度{self.__max} 最小长度{self.__min}")
if self.__type == int:
if value < self.__min or value > self.__max:
raise ValueError(f"值错误 最大值{self.__max} 最小值{self.__min}")
self.__value = value
def __delete__(self, owner):
del self.__value
class B:
name = A(str, "1234", 5, 10)
age = A(int, 12, 5, 100)
def __init__(self,name,age):
# 这里会调用上面的set
self.name = name
self.age = age
b = B("11111",5)
# 因为init里会调用set
# 所以会做相关的类型校验
dict
__dict__是python中一个特殊的属性,它返回一个字典,字典的键是实例的属性名,值是属性的值。
数据描述符和非数据描述符
有get/set方法,就是数据描述符。使用数据描述符的时候,设置值调用对应类的set方法,获取值的时候调用对应类的get方法。 有get方法,就是非数据描述符。而非数据描述符,如果init函数里面也像数据描述符那样的初始化,那么这个属性就是实例属性,而不同于数据描述符,获取值的时候也不会调用get方法,因为它只是个实例属性.
如果下面的C的init方法里,去掉self.b = b,获取.b的时候就会调用B类的get方法。此时就和数据描述符一样。
在一个实例寻找它的属性的时候,有着循序以下优先级, 如果在数据描述符里找不到了某属性,就会在实例属性里找,找到了就停止了,找不到继续在非数据描述符里找。 数据描述符 > 实例属性 > 非数据描述服
class A:
def __init__(self):
self.__value = "A"
def __get__(self, instance, owner):
print("i am get in A")
return self.__value
def __set__(self, owner, value):
print("i am set in A")
self.__value = value
def __delete__(self, owner):
del self.__value
class B:
def __init__(self):
self.__value = "B"
def __get__(self, instance, owner):
print("i am get in B")
return self.__value
class C:
a = A()
b = B()
def __init__(self, a,b):
# 调用A的set
self.a = a
# 此处可以注释一下句在看程序运行的变化
self.b = b
c = C(1,2)
# i am set in A
print(c.a)
# i am get in A
# 1
print(c.b) # 如果是实例对象访问,则是因为无set方法,会将b作为一个对象的实例属性 此处没有打印i am get in B ,则也说明没有使用B类,而是仅仅将b作为了一个实例属性
# 2
print(C.b) # 如果是类对象访问,返回的将是描述符,即B本身,使用了get方法
# i am get in B
# B
练习
使用描述符实现static
class Static:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
def wrapper(*args, **kwargs):
return self.func(owner, *args, **kwargs)
return wrapper
class A:
name = "i am name"
@Static
# 此处相当于是 Static(test)
# 因为其中实现了get,
# 当调用的A.b的时候返回是wrapper,然后使用()即调用方法了
# 描述符返回了一个函数,使用()即使调用方法
def test(cls, num):
print("test,", num)
print(cls.name)
A().test(num=1)
A.test(num=1)
getattr__和__getattribute
- __getattr__只有在访问不存在的属性时被调用,而__getattribute__在每次访问属性时都被调用。
- __getattr__返回值可以是任意对象,或者抛出AttributeError异常来模拟找不到属性的情况。而__getattribute__必须始终返回一个属性的值,否则会抛出AttributeError异常
class MyClass:
name = 1
def __getattr__(self, name):
print(f"Trying to access attribute {name}")
return None
def __getattribute__(self, name):
print(f"Accessing attribute {name}")
return object.__getattribute__(self, name)
obj = MyClass()
print(obj.foo)
print(obj.name)
# Accessing attribute foo
# Trying to access attribute foo
# None
# Accessing attribute name
# 1
惰性计算
class Lazy:
def __init__(self, func):
# 使用装饰器的时候,将area传递了进来
self.func = func
def __get__(self, instance, owner):
# 因为func是一个方法,所以需要将instance作为self传递进去
value = self.func(instance)
print("i am get")
# 给Circle的实例对象添加了area的属性,保证下次再次调用的时候,直接就使用area的属性
# 数据描述符号 > 实例属性 > 非数据描述服
setattr(instance, self.func.__name__, value)
return value
class Circle:
pi = 3.14
def __init__(self, radius):
self.radius = radius
@Lazy
def area(self):
return self.radius * self.radius * self.pi
c = Circle(2)
c.area # 因为c里面没有area方法,但是它是飞数据描述符,所以第一次调用就执行方法,并且对实例对象设置一个area的属性,共给下次调用
# i am get
# 12.56
c.area
# 12.56 # 直接调用了实例对象的area属性