1.更换 cpython 为 jpython(不建议)
2.使用多进程完成多线程的任务
3.在使用多线程可以使用c语言去实现
9. socked 通信
socket是在应用层和传输层之间的一个抽象层,socket本质是编程接口(API),它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。TCP/IP只是一个协议栈,必须要具体实现,同时还要提供对外的操作接口(API),这就是Socket接口。通过Socket,我们才能使用TCP/IP协议。
socket通信流程如图:
根据上图顺序写出代码:
说明:建立2个py文件,原则上应该是在2台电脑上各自建立1个py文件,分别是 server.py 和 client.py 。由于现在只有一台电脑,所以建立2个py文件。
server.py:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8000)) # 通过sk中的bind()方法,绑定地址和端口,参数必须是元组
sk.listen(3) # 设置访问程序的最大排队人数
conn, address = sk.accept()
inp = input('请输入需要传输的内容:')
conn.send(bytes(inp,'utf8')) # 传输的内容一定是byte类型
client.py
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8000))
data = sk.recv(1024)
print(str(data,'utf8')) # str()转换
二. 类
1. 普通方法,类方法,静态方法区别
- 普通方法,保存在类中,执行时通过对象调用
- 静态方法,保存在类中,需要加装饰器(@staticmethod),执行通过类直接调用。不需要创建对象,可直接通过类调用,相当于函数
- 类方法,保存在类中,需要加装饰器(@classmethod),执行通过类直接调用。不需要创建对象,可直接通过类调用,相当于函数
class Dog:
dogbook = {'黄色': 30, '黑色': 20, '白色': 0} # 类变量
def \_\_init\_\_(self, name, color, weight):
self.name = name # 实例变量
self.color = color # 实例变量
self.weight = weight # 实例变量
'''
实例方法
必须将self作为第一个参数
可以访问实例变量,不能访问类变量
只能通过实例名访问
'''
def bark(self):
print('{}叫了一声'.format(self.name)) # 大黄叫了一声
# print(dogbook) # name 'dogbook' is not defined
'''
类方法
必须将cls作为第一个参数
可以访问类变量,不能访问实例变量
可以通过实例名或者类名访问
'''
@classmethod
def dog\_num(cls):
num = 0
# print(self.name) # name 'self' is not defined
for v in cls.dogbook.values():
num += v
print(num) # 50
'''
静态方法:
不强制传入self,cls
不能访问类变量,也不能访问实例变量
可以通过实例名或类名访问
静态方法和普通函数没有区别,可以放在类外面。
之所以放在这里,是因为此方法和这个类有关联
'''
@staticmethod
def total\_weights(dogs_weight_list):
total = 0
for i in dogs_weight_list:
total += i
print(total) # 30
Dog('大黄', '黄色', 10).bark()
Dog('大黄', '黄色', 10).dog_num()
Dog.total_weights([10,20])
静态方法是类中的函数,不需要实例。当某个方法不需要用到对象中的任何资源,将这个方法改为一个静态方法, 加一个@staticmethod
。加上之后, 这个方法就和普通的函数没有什么区别了, 只不过写在了一个类中, 可以使用这个类的对象调用,也可以使用类直接调用。
类方法是将类本身作为对象进行操作的方法。它和静态方法的区别在于:不管这个方式是从实例调用还是从类调用,它都用第一个参数把类传递过来。
2. python 中的@property,@setter 实现成员变量的get, set属性?
@property
装饰器: 将一个方法的调用方式变成“属性调用”。
通常用在属性的get方法,set方法,通过设置@property
可以实现实例成员变量的直接访问,又保留了参数的检查。
class Student(object):
@property
def score(self):
return self.__score
@score.setter
def score(self,value):
if value>=0 and value <= 100:
self.__score = value #还记得\_\_score吗?前面加一个双下划线,表示private私有属性
else:
raise ValueError('score must between 0 ~ 100!')
s = Student()
s.score = 90 # 设置分数
print(s.score)
@property
修饰函数score(getter),将score函数变成score属性输出,此外,@property
本身又创建了另一个装饰器@score.setter
,负责把一个 setter 函数变成属性赋值,于是,我们虽然看到了类Student内部定义了两个函数score,对,没错,都是score,但是,他们却被不同的装饰器装饰,getter函数被@property
修饰,setter函数被@property
创建的函数的setter装饰器@score.setter
修饰,因此,我们可以直接用s.score=90来代替s.set_socre(90),达到给score属性赋值的效果,简单粗暴,精益求精,
我们上面创建了@property
另一个装饰器函数@xxx.setter
,对私有属性__score进行输入值的判断,如果,我们不创建装饰器@xxx.setter
可以吗?如果不创建的话,属性xxx就是一个只读属性,意味着不能被修改,
3. python 是如何实现多态的?
多态:一类事物有多种形态,(一个抽象类有多个子类,因而多态的概念依赖于继承)
class Animal():
def \_\_init\_\_(self, name):
self.name = name
def talk(self): # 抽象方法,仅由约定定义
print(self.name, '叫') # 当子类没有重写talk方法的时候调用
def animal\_talk(obj): # 多态
obj.talk()
class Cat(Animal):
def talk(self):
print('%s: 喵喵喵!' % self.name) # 重写talk方法
class Dog(Animal):
def talk(self):
print('%s: 汪汪汪!' % self.name)
a = Dog('小狗')
Animal.animal_talk(a) # 多态调用
b = Cat('小猫')
Animal.animal_talk(b)
c = Animal('111')
Animal.animal_talk(c)
执行结果:小狗: 汪汪汪!
小猫: 喵喵喵!
111 叫
4. 单元测试,单例模式
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。在python中指一个类。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个对象实例。(详见面试总结一)
5. python 如何实现单例模式?
什么是单例模式(Singleton)?
单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。
单例模式优点:
- 由于单例模式要求全局内只有一个实例,所以可以节省很多内存空间
- 全局只有一个接入点,可以更好的进行数据同步,避免多重占用
- 单例可以常驻内存,减少系统开销
单例模式应用:
- 生成唯一序列号
- 访问全局复用的唯一资源,如磁盘,总线等
- 数据库连接池
- 网站计数器
实现单例的方式?
- 全局变量:不直接调用 Config() ,而使用同一个全局变量
- 重写 __new__ 方法:重写 __new__来保证每次调用 Config() 都会返回同一个对象, __new__ 为对象分配空间,返回对象的引用
- 使用metaclass:metaclass重写__call__ 方法来保证每次调用 Config() 都会返回同一个对象
- 使用装饰器:使用装饰器来保证每次调用 Config() 都会返回同一个对象
原理:在真正调用 Config() 之前进行一些拦截操作,来保证返回的对象都是同一个:
具体实现:
- 全局变量
# config.py
from dataclasses import dataclass
@dataclass
class Config:
SQLALCHEMY_DB_URI = SQLALCHEMY_DB_URI
config = Config(SQLALCHEMY_DB_URI = "mysql://")
通过使用全局变量,我们在所有需要引用配置的地方,都使用 from config import config 来导入,这样就达到了全局唯一的目的。
2. 重写__new__方法
class Singleton(object):
_instance = None # \_instance 作为类属性,保证了所有对象的实例都是同一个
def \_\_new\_\_(cls, \*args, \*\*kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls) # 为对象分配空间
return cls._instance # 返回对象的引用
a = Singleton() # 创建对象时,new方法会被自动调用
b = Singleton()
print(a)
print(b)
执行结果:<__main__.Singleton object at 0x0000023A88E0F2B0>
<__main__.Singleton object at 0x0000023A88E0F2B0>
每次实例化一个对象时,都会先调用 __new__()
创建一个对象,再调用 __init__()
函数初始化数据。因而,在 new 函数中判断 Singleton类 是否已经实例化过,如果不是,调用父类的 new 函数创建实例;否则返回之前创建的实例。
3. 使用metaclass元类
class SigletonMetaClass(type):
_instance = None
def \_\_new\_\_(cls, \*args, \*\*kwargs):
return super().__new__(cls, \*args, \*\*kwargs)
def \_\_call\_\_(self, \*args, \*\*kwargs):
if self._instance is None:
self._instance = super().__call__(\*args, \*\*kwargs)
return self._instance
class Singleton(metaclass=SigletonMetaClass):
def \_\_new\_\_(cls, \*args, \*\*kwargs):
return super().__new__(cls)
a = Singleton()
b = Singleton()
print(a)
print(b)
执行结果:<__main__.Singleton object at 0x000001A11E4AC908>
<__main__.Singleton object at 0x000001A11E4AC908>
因此,用元类实现单例时仍需按照三步骤:1. 拦截 2. 判断是否已经创建过对象 3. 返回对象。与上个方法相比,区别在于拦截的地点不同。
4. 装饰器
4.1 函数装饰器
def SingletonDecorator(cls):
_instance = None
def get\_instance(\*args, \*\*kwargs):
nonlocal _instance
if _instance is None:
_instance = cls(\*args, \*\*kwargs)
return _instance
return get_instance
@SingletonDecorator
class Singleton(object):
pass
a = Singleton()
b = Singleton()
print(a)
print(b)
执行结果:<__main__.Singleton object at 0x0000022194E6C438>
<__main__.Singleton object at 0x0000022194E6C438>
4.2 类装饰器
class SingletonDecorator(object):
_instance = None
def \_\_init\_\_(self, cls):
self._cls = cls
def \_\_call\_\_(self, \*args, \*\*kwargs):
if self._instance is None:
self._instance = self._cls(\*args, \*\*kwargs)
return self._instance
@SingletonDecorator
class Singleton(object):
pass
a = Singleton()
b = Singleton()
print(a)
print(b)
执行结果:<__main__.Singleton object at 0x0000028F42CFC8D0>
<__main__.Singleton object at 0x0000028F42CFC8D0>
6. 装饰器有什么作用
装饰器
本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能
,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
7. 打印时间的装饰器
import time
# 装饰函数
def timer(func):
def wrapper(\*args, \*\*kw):
start =time.time()
# 这是函数真正执行的地方
func(\*args, \*\*kw)
end =time.time()
cost_time = end - start
print("花费时间:{}秒".format(cost_time))
return wrapper
@timer
def want\_sleep(sleep_time):
time.sleep(sleep_time)
want_sleep(5)
8. fun(*args,**kwargs)中的 *args,**kwargs什么意思?
*args
: 用来发送一个非键值对的可变数量的参数列表给一个函数。
**kwargs
: 用来发送一个可变的键值对的参数给一个函数。
9. python 中的闭包?
闭包函数 指定义在一个函数内部的函数,被外层函数包裹着,其特点是可以访问外层函数的变量。
def outer():
num = 1
def inner():
print num # 内层函数可以访问外层函数中的 num
return inner # 外层函数的返回值必须是内层函数
fun = outer()
num = 1
fun() # 1
10. 简述 Python 的作用域以及 Python 搜索变量的顺序
Python作用域简单说就是一个变量的命名空间。代码中变量被赋值的位置,就决定了哪些范围的对象可以访问这个变量,这个范围就是变量的作用域。
在Python中,只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域。
Python的变量名解析机制也称为 LEGB 法则:本地作用域Local
→当前作用域被嵌入的本地作用域Enclosing locals
→全局/模块作用域Global
→内置作用域Built-in
11. 简述__new__
和__init__
的区别
创建一个新实例时调用 __new__
,初始化一个实例时用__init__
,这是它们最本质的区别。
__new__
方法会返回所构造的对象,__init__
则不会.
__new__
函数必须以 cls 作为第一个参数,而__init__
则以 self 作为其第一个参数.
例如,构造单例模式:
cls sington():
_instance = None
def \_\_new\_\_(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
12. 双下划线和单下划线的区别?
“单下划线”开始的成员变量叫做保护变量,类对象和子类对象都能访问。
“双下划线”开始的成员变量叫做私有成员,只有类对象自己能访问,子类对象不能访问。
13. 列出几种魔法方法并简要介绍用途
__init__
: 对象初始化方法
__new__
:创建对象时候执行的方法,单列模式会用到
__str__
: 当使用print输出对象的时候,只要自己定义了__str__(self)方法,那么就会打印从在这个方法中return的数据
__del__
: 删除对象执行的方法
14. 什么是 python 元类?
在 Python 当中万物皆对象,我们用 class 关键字定义的类本身也是一个对象,负责产生该对象的类称之为元类,元类可以简称为类的类,
元类的主要目的是为了控制类的创建行为.
type
是 Python 的一个内建元类,用来直接控制生成类,在 python 当中任何 class 定义的类其实都是type 类实例化的结果。
只有继承了 type 类才能称之为一个元类,否则就是一个普通的自定义类,自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程.
三. 函数
1. 高阶函数:map() , reduce() , filter() ,lambda()
1.map()
:遍历序列,根据提供的函数对指定序列做映射,对序列中每个元素进行操作,最终获取新的序列
例1:
print(list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
# 输出结果:['1', '2', '3', '4', '5', '6', '7', '8', '9']
例2:
def square(x):
return x\*\*2
result = list(map(square,[1,2,3,4,5]))
print(result)
# 输出结果:[1, 4, 9, 16, 25]
备注:map() : Python 2.x 返回列表;Python 3.x 返回迭代器
2.reduce()
:对于序列内所有元素进行累计操作,即是序列中后面的元素与前面的元素做累积计算
(结果是所有元素共同作用的结果)
from functools import reduce
def addl(x,y):
return x + y
print(reduce(addl,range(1,5)))
# 输出结果:10
reduce()的作用是接收一个列表,[1,2,3,4]
首先,将1,2传给addl(),计算结果为3
接着,将3,3传给addl(),计算结果为6
接着,将6,4传给addl(),计算结果为10
…
3.filter()
:过滤序列,过滤掉不符合条件的元素,返回一个迭代器对象,如果要转换为列表,可以使用 list() 来转换
def func(x):
return x%2==0
print(list(filter(func,range(1,6))))
# 输出结果:[2, 4]
4.lambda(),匿名函数,不需要命名的函数def f ( x ,y):
def add(x,y):
return x + y
print(add(1,2))
# 等效于:
g = lambda x, y : x + y
print(g(1,2))
lambda 表达式的作用:
- python 写一些执行脚本时,使用 lambda 就可以省下定义函数过程,比如说我们只是需要写个简单的脚本来管理服务器时间,我们就不需要专门定义一个函数然后再写调用,使用 lambda 就可以使得代码更加精简。
- 对于一些比较抽象并且整个程序执行下来只需要调用一两次的函数,有时候给函数起个名字也是比较头疼的问题,使用 lambda 就不需要考虑命名的问题了。
- 简化代码的可读性,由于普通的函数阅读经常要跳到开头 def 定义部分,使用 lambda 函数可以省去这样的步骤。
四. 底层问题
1. 垃圾回收机制
python 垃圾回收主要以引用计数为主,标记-清除和分代清除为辅的机制,其中标记-清除和分代回收主要是为了处理循环引用的难题。
引用计数
引用计数算法
当有1个变量保存了对象的引用时,此对象的引用计数就会加1。
当使用del删除变量指向的对象时,如果对象的引用计数不为1,比如3,那么此时只会让这个引用计数减1,即变为2,当再次调用del时,变为1,如果再调用1次del,此时会真的把对象进行删除。
引用计数+1的四种情况:
- 对象被创建 a=14
- 对象被引用 b=a
- 对象被作为参数,传到函数中 func(a)
- 对象作为一个元素,存储在容器中 List={a,”a”,”b”,2}
对应的引用计数-1的四种情况:
- 当该对象的别名被显式销毁时 del a
- 当该对象的引别名被赋予新的对象, a=26
- 一个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
- 将该元素从容器中删除时,或者容器被销毁时。
优点: 简单实时,一旦没有引用,内存就直接释放了。处理回收内存的时间分摊到了平时。
缺点: 维护引用计数消耗资源,会造成循环引用导致无法回收,造成内存泄露
,比如:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
'''
list1与list2相互引用,即使不存在其他对象对它们的引用,list1与list2的引用计数也
仍然为1,所占用的内存永远无法被回收,这将是致命的。
'''
标记清除(Mark—Sweep)
算法是一种基于追踪回收(tracing GC)
技术实现的垃圾回收算法。
它分为两个阶段:
- 第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,
- 第二阶段是把那些没有标记的对象『非活动对象』进行回收。
那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?
- 对象之间通过引用(指针)连在一起,构成一个
有向图
,对象构成这个有向图的节点,而引用关系构成这个有向图的边。 - 从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。
根对象就是全局变量、调用栈、寄存器。
标记清除算法作为Python的辅助垃圾收集技术主要处理
的是一些容器对象,比如list、dict、tuple,instance等,
因为对于字符串、数值对象是不可能造成循环引用问题。
缺点: 清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
分代回收
分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代。
- Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),
- 他们对应的是3个
链表
,它们的垃圾收集频率与对象的存活时间的增大而减小。 - 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被
触发
,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去, - 依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
- 同时,分代回收是建立在标记清除技术基础之上。
- 说明对象存在时间越长,越可能不是垃圾,应该越少去收集。
- 分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象
2. 赋值,深拷贝,浅拷贝
浅拷贝: 拷贝对象的第一层。
深拷贝: 拷贝对象的所有层。
说大白话:
B 拷贝于 A
浅拷贝虽然说是拷贝,但也是 “ 身不由己 ” ,当修改除了第一层之外的值时,都会改动(内嵌列表表面上也拷贝过来了,但实际还是不是自己说了算的,只要改了内嵌列表的值,拷贝的也要改,这就是只拷贝一层,内嵌的就无能为力了),当A的第二层改变时,B的第二层也会随之改变。
深拷贝就是彻底的拷贝,两者就再毫无关系,虽然拷贝完不改的话长的一样,但是不管对谁改动,另一个也是毫不受影响。当A的第二层改变时,B的第二层不受影响。
3. 可变对象和不可变对象
什么是可变/不可变对象?
- 不可变对象,该对象所指向的内存中的值不能被改变,当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
- 可变对象,该对象所指向的内存中的值可以被改变,变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。
Python中
可变对象:dict、list、set
不可变对象:str、int、tuple、float
附: python函数调用时,参数传递方式是值传递还是引用传递?
不可变参数: 值传递
可变参数: 引用传递
4. python 传参数是传值还是传址?
Python中函数参数是引用传递(注意不是值传递)。
- 对于不可变类型(数值型、字符串、元组),因变量不能修改,所以运算不会影响到变量自身;
- 对于可变类型(列表、字典)来说,函数体运算可能会更改传入的参数变量。
5. def fun(a, b=[]): 这种写法有什么坑?
def func(a,b=[]):
b.append(a)
print(b)
func(1) # [1]
func(1) # [1, 1]
func(1) # [1, 1, 1]
func(1) # [1, 1, 1, 1]
函数的第二个默认参数是一个list,当第一次执行的时候实例化了一个list,第二次执行还是用第一次执行的时候实例化的地址存储,所以三次执行的结果就是 [1, 1, 1] 。
想每次执行只输出[1] ,b = [] 应该放在函数里面。
6. is 和 == 的区别?
- is 就是判断两个对象的id是否相同
- == 判断的则是内容是否相同
7. 连接字符串用 join 还是 +
字符串是不可变对象。
- 当用操作符
+
连接字符串的时候,每执行一次+
都会申请一块新的内存,然后复制上一个+
操作的结果和本次操作的右操作符到这块内存空间,因此用+
连接字符串的时候会涉及好几次内存申请和复制。 - 而
join
在连接字符串的时候,会先计算需要多大的内存存放结果,然后一次性申请所需内存并将字符串复制过去,这是为什么join
的性能优于+
的原因。 - 所以在连接字符串数组的时候,我们应考虑优先使用join。
8. Python 中的复制(赋值),底层是怎么实现的?
Python中,对象的赋值实际上是简单的对象引用。也就是说,当你创建一个对象,然后把它复制给另一个变量的时候,Python并没有拷贝这个对象,而是拷贝了这个对象的引用(指针)。
9. python with 用法+原理剖析
用法:with语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
with open("xxx") as f:
f.write() #文件操作
原理: 基本思想是with所求值的对象必须有一个__enter__()
方法,一个__exit__()
方法。
紧跟with后面的语句被求值后,__enter__()
方法的返回值将被赋值给as后面的变量。当with下面的代码块全部被执行完之后,将调用前面返回对象的__exit__()
方法,对象的__exit__
要接受三个参数:异常类型、异常对象和异常跟踪。
10. 如何捕获异常,常用的异常机制有哪些?
捕获异常需调用python默认的异常处理器,并在终端输出异常信息。
try...except...finally
语句:当try语句执行时发生异常,回到try语句层,寻找后面是否有except语句。找到except语句后,会调用这个自定义的异常处理器。except将异常处理完毕后,程序继续往下执行。finally语句表示,无论异常发生与否,finally中的语句都要执行。assert语句
:判断assert后面紧跟的语句是True还是False,如果是True则继续执行print,如果是False则中断程序,调用默认的异常处理器,同时输出assert语句逗号后面的提示信息。with语句
:如果with语句或语句块中发生异常,会调用默认的异常处理器处理,但文件还是会正常关闭。
11. python 中断言方法举例
assert()方法,断言成功,则程序继续执行,断言失败,则程序报错
a = 3
assert (a > 1)
print("断言成功,程序继续往下执行")
b = 4
assert (b > 7)
print("断言失败,程序报错")
12. 如何提高 python 的运行效率?
- 关键代码使用外部功能包(Cython,pylnlne,pypy,pyrex)
- 针对循环的优化–尽量避免在循环中访问变量的属性
- 使用较新版本的python
- 尝试多种编码方法
- 交叉编译应用程序
五. 列表
1. 迭代器和生成器
列表生成式:(还没有放到内存中)
a1 = [x for x in range(1,10)]
a2 = [x**2 for x in range(1,10)]
也可以放一个函数:
def f(n):
return n**3
a3 = [f(x) for x in range(1,10)]
生成器:
背景:通过列表生成式,我们可以直接创建一个列表,但是,受到内存限制,列表容量肯定是有限的,而且创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间,在Python中,这种一边循环一边计算的机制,称为生成器:generator
定义: 在Python中,这种一边循环一边计算的机制,称为生成器:generator 。
说明:生成器是一个特殊的程序,可以被用作控制循环的迭代行为,python中生成器是迭代器的一种,使用yield返回值函数,每次调用yield会暂停,而可以使用next()函数和send()函数恢复生成器。
两种创建方式:
- (x*2 for x in range(5,100)) # 此处注意区分列表生成式
- yield
其中:
- 生成器表达式:返回一个对象,这个对象只有在需要的时候才产生结果
- 生成器函数:也是用def定义的,利用关键字yield(相当于return)一次性返回一个结果,阻塞,重新开始
为了节省空间,生成器生成的数据并没有放到内存中,只有在调取的时候才往内存中放。
只能从前往后逐一调取。
next() 返回迭代器的下一个项目
a = (x*2 for x in range(5,100)) #元组
print(next(a)) #将5返回给x*2 10
用生成器来实现斐波那契数列的例子是:
def fib(max):
a, b = 0, 1
while max > 0:
a, b = b, a + b
yield a
max -= 1
print([i for i in fib(10)])
f.send() 和 next() 的区别:
- f.send(),将参数传给yield前一个变量,也就是说send()可以强行修改上一个yield的值;
- next()参数是迭代器函数名字;
- 第一个send()不可传值,写作f.send(None),相当于next(b)。
迭代器
定义:任何实现了__iter__和__next__()方法的对象都是迭代器(这是迭代器协议),__iter__返回迭代器自身,__next__返回容器中的下一个值,直到容器中没有更多元素时抛出StopIteration异常并停止迭代。
迭代就是循环,它是一个带状态的对象,能在你调用next()方法的时候返回容器中的下一个值。
迭代器就像一个懒加载的工厂,等到有人需要的时候,它才会生成值并返回,没调用的时候就处于休眠状态等待下一次调用。
生成器与迭代器的区别
- 迭代器(iterator)是一个实现了迭代器协议(iter__和__next()方法)的对象。
- 生成器(generator)是通过yield语句或生成器函数快速生成迭代器,可以不用iter和next方法 。
- 生成器本质上也是一个迭代器,自己实现了可迭代协议,与迭代器不同的是生成器的实现方式不同,可以通过生成器表达式和生成器函数两种方式实现,代码更简洁。
- 生成器和迭代器都是惰性可迭代对象,只能遍历一次,数据取完抛出Stopiteration异常
什么是可迭代对象?
我们已经知道,可以直接作用于for循环的数据类型有以下几种:
- 一类是集合数据类型,如list,tuple,dict,set,str等
- 一类是generator,包括生成器表达式和生成器函数
定义:实现了__iter__方法的对象就叫做可迭代对象。说白了就是可以直接作用于for 循环的对象统称为可迭代对象:Iterable
区别:一个可迭代的对象必须是定义了__iter__()方法的对象;而一个迭代器必须是同时定义了__iter__()方法和next()方法的对象。
for函数的本质:在python3中,就是不断调用next() 方法实现for循环。
x = [1,2,3]
for i in x:
print(i)
实际执行情况:
等价于:
# 首先获得Iterator对象:
iterator = iter([1, 2, 3])
# 循环:
while True:
try:
# 获得下一个值:
x = next(iterator)
print(x)
except StopIteration:
# 遇到StopIteration就退出循环
break
总结:
- 凡是可作用于for循环的对象都是可迭代对象;
- 凡是可作用于next()函数的对象都是迭代器,它们表示一个惰性计算的序列;
- 集合数据类型如list、dict、str等是可迭代对象,但不是迭代器,不过可以通过iter()方法获得一个Iterator对象;
- 文件是可迭代对象,也是迭代器。
你可能会问,为什么list、dict、str等数据类型不是迭代器?
这是因为Python的迭代器对象表示的是一个数据流,可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
迭代器甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
list,str,dict…这些可迭代的对象你可以随意的读取所以非常方便易用,但是你必须把它们的值放到内存里,当它们有很多值时就会消耗太多的内存。
如何判断是可迭代对象还是迭代器?如何把可迭代对象转换为迭代器?
from collections import Iterator # 迭代器
from collections import Iterable # 可迭代对象
s = 'hello'
print(isinstance(s, Iterator)) # 判断是不是迭代器
print(isinstance(s, Iterable)) # 判断是不是可迭代对象
print(isinstance(iter(s), Iterator)) # 把可迭代对象转换为迭代器 iter(s)
2. sorted() 函数
'''
sorted(iterable, cmp=None, key=None, reverse=False)
iterable -- 可迭代对象。
cmp -- 比较的函数,这个具有两个参数,参数的值都是从可迭代对象中取出,此函数必须遵守的规则为,大于则返回1,小于则返回-1,等于则返回0。
key -- 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
reverse -- 排序规则,reverse = True 降序 , reverse = False 升序(默认)。
'''
3. 现有字典 dict = {‘a’ : 24, ‘d’ : 55, ‘kj’ : 86},请按照字段中的的value值进行排序
dict = {'a': 24, 'd': 55, 'kj': 30}
# 按照 value 进行排序
new_dict1 = sorted(dict.items(), key=lambda c: c[1])
print new_dict1
# 按照 key 进行排序
new_dict2 = sorted(dict.items(), key=lambda c: c[0])
print new_dict2
4. 现有列表 list=[{‘name’: ‘a’, ‘age’: 24}, {‘name’: ‘d’, ‘age’: 55}, {‘name’: ‘c’, ‘age’: 67}],请按照 age 进行从大到小排序
list = [{'name': 'a', 'age': 24}, {'name': 'd', 'age': 55}, {'name': 'c', 'age': 32}]
new_list1 = sorted(list, key=lambda c: c['age'], reverse=True)
print new_list1
5. 下面代码输出结果是?
list = [1, 2, 3, 4, 5]
print list[10:]
'''
代码输出空列表[], 并不会出现 IndexError
'''
print list[10]
'''
IndexError: list index out of range
'''
6. 修改列表时,如何保证地址不变?
若要维持地址不变,需要加上[:]
l = [1, 2, 3]
print id(l) # 52733960
l = [2, 3]
print id(l) # 52661384
l[:] = [3]
print id(l) # 52661384
7.列表和元组的区别
区别:
- 语法差异:使用方括号 [ ] 创建列表,而使用括号 ( ) 创建元组
- 是否可变:列表是可变的,而元组是不可变的。可以修改列表的值,但是不修改元组的值。
- 重用与拷贝:元组无法复制。 原因是元组是不可变的。 如果运行tuple(tuple_name)将返回自己。
相同点:
- 都是序列
- 都可以存储任何数据类型
- 可以通过索引访问
什么时候用元组什么时候用列表?
- 不确定元素可以用列表
- 确定元素的用元组。
六. 常用模块
time 模块:从1970年开始
time.time() #记录当前时间戳 1556723770.0009158
time.sleep(3) #延迟3秒钟
time.localtime() #结构化时间(本地)
time.strftime('%Y-%m-%d %H:%M:%S') #自定义时间格式2019-05-01 23:05:45
time.mktime(time.localtime()) #将本地时间转化为时间戳
datetime 模块
datetime.datetime.now() #2019-05-01 23:18:38.512192
random 模块
import random
random.random() # 生成一个0到1的随机数 0.758108354250769
random.randint(1, 8) # 生成一个0到8的随机整数 4
random.choice({}/[]/()/'') # 从一个对象中随机选择一个元素
random.randrang(1, 3) # 从1到3中随机取一个整数(不包括3)
os 模块:与操作系统进行交互
import os
os.getcwd() # 获取当前工作目录
os.chdir() # 更改当前工作目录 change
os.makedirs('dirname1/dirname2') # 可生成多层递归目录
os.removedirs() #(递归)删除空文件夹
os.mkdir('dirname') # 生成一个文件夹
os.rmdir('dirname') # 删除一个空文件夹,若文件不为空,报错
os.remove() #只能删除文件,不能删除文件夹
os.listdir("G:\python") # 显示出目录下的所有文件
os.rename('oldname','newname') # 改变文件夹名称
os.path.dirname('./abc') # 返回abc所在的文件的路径
os.path.exists(path) # 如果path存在,返回Ture
os.path.join(path1,path2,) # 将多个路径合并并返回
sys 模块:与 python 解释器进行交互
sys.argv
sys.argv[]说白了就是一个从程序外部获取参数的桥梁。因为我们从外部取得的参数可以是多个,所以获得的是一个列表(list),也就是说sys.argv其实可以看作是一个列表,所以才能用[]提取其中的元素。其第一个元素是程序本身,随后才依次是外部给予的参数。
Sys.argv[ ]其实就是一个列表,里边的项为用户输入的参数,关键就是要明白这参数是从程序外部输入的,而非代码本身的什么地方,要想看到它的效果就应该将程序保存了,从外部来运行程序并给出参数。
hashlib 模块:提供了很多加密的算法
这里介绍一下hashlib的简单使用事例,用hashlib的md5算法加密数据
加密算法的转化结果是唯一的,不会改变。(唯一性)
import hashlib
def md5(arg): # 这是加密函数,将传进来的函数加密
md5_pwd = hashlib.md5(bytes('abd', encoding='utf-8'))
md5_pwd.update(bytes(arg, encoding='utf-8'))
return md5_pwd.hexdigest() # 返回加密后的数据(十六进制)
def log(user, pwd): # 登陆时候的函数,由于md5不能反解,因此登陆的时候用正解
with open('db', 'r', encoding='utf-8') as f:
for line in f:
u, p = line.strip().split('|')
if u == user and p == md5(pwd): # 登陆的时候验证用户名以及加密的密码跟之前保存的是否一样
return True
def register(user, pwd): # 注册的时候把用户名和加密的密码写进文件,保存起来
with open('db', 'a', encoding='utf-8') as f:
temp = user + '|' + md5(pwd)
f.write(temp)
i = input('1表示登陆,2表示注册:')
if i == '2':
user = input('用户名:')
pwd = input('密码:')
register(user, pwd)
elif i == '1':
user = user = input('用户名:')
pwd = input('密码:')
r = log(user, pwd) # 验证用户名和密码
if r == True:
print('登陆成功')
else:
print('登陆失败')
else:
print('账号不存在')
logging 模块:输出日志
import logging
# 1.创建一个logger
logger = logging.getLogger() # get一个Logger对象,日志对象
logger.setLevel(logging.INFO) # Log等级总开关
# 2.创建一个handler,用于写入日志文件
fh = logging.FileHandler('txt') # 用于写入文件,文件对象
fh.setLevel(logging.DEBUG) # 输出到file的log等级的开关
# 3.创建一个handler,用于输出控制台
ch = logging.StreamHandler() # 输出控制台,屏幕对象
ch.setLevel(logging.WARNING) # 输出到console的log等级的开关
# 4.定义handler的输出格式
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
fh.setFormatter(formatter) # fh创建格式,和formatter一样
ch.setFormatter(formatter) # ch创建格式,和formatter一样
# 5.控制输出
logger.addHandler(fh) # 输出文件
logger.addHandler(ch) # 输出控制台
# 6.日志
logger.debug('this is a logger debug message')
logger.info('this is a logger info message')
logger.warning('this is a logger warning message')
logger.error('this is a logger error message')
logger.critical('this is a logger critical message')
json 模块
json.dumps() # 字典转json字符串
json.loads() # json字符串转字典
re 模块:正则表达式
1. 限定符
字符 | 描述 |
---|---|
* | 匹配前面的子表达式零次或多次。* 等价于{0,}。 |
+ | 匹配前面的子表达式一次或多次。+ 等价于 {1,}。 |
? | 匹配前面的子表达式零次或一次。? 等价于 {0,1}。 例如,do(es)? 可以匹配 do 、 does 中的 does 、 doxy 中的 do 。 |
^ | 1.匹配输入字符串的开始位置 2.在[ ]中使用时,表示不匹配[ ]中的字符集合。 |
$ | 匹配输入字符串的结尾位置。 |
{n} | n 是一个非负整数。匹配确定的 n 次。 例如,o{2} 不能匹配 Bob 中的 o ,但是能匹配 food 中的oo 。 |
{n,} | n 是一个非负整数。至少匹配n 次。 例如,o{2,} 不能匹配 Bob 中的 o ,但能匹配 foooood 中的所有 oooooo 。‘o{1,}’ 等价于 ‘o+’。‘o{0,}’ 则等价于 ‘o*’。 |
{n,m} | m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。 例如,o{1,3} 将匹配 fooooood 中的前三个 ooo 。‘o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。 |
( ) | 标记一个子表达式的开始和结束位置。即分组匹配。 |
[ ] | 符合 [ ]中的任一字符就匹配。例如,a[bcd]f 可以匹配acf , 但不能匹配agf |
2. 预定义字符
字符 | 描述 | 规则 | 结果 |
---|---|---|---|
\d | \d匹配任何十进制数,它相当于类[0-9] \d+如果需要匹配一位或者多位数的数字时用 | a\dc | a1c |
\D | \D匹配任何非数字字符,它相当于类[ ^0-9 ] | a\Dc | abc |
\s | \s匹配任何空白字符,它相当于类[\t\n\r\f\v] | a\sc | a c |
\S | \S匹配任何非空白字符,它相当于类[ ^\t\n\r\f\v ] | a\Sc | abc |
\w | \w匹配包括下划线在内任何字母数字字符,它相当于类[a-zA-Z0-9_] | a\wc | abc |
\W | \W匹配非任何字母数字字符包括下划线在内,它相当于类[ ^a-zA-Z0-9_ ] | a\Wc | a c |
3. 常用函数
re.findall() 匹配所有符合规则的字符串
语法:
findall(pattern, string, flags=0)
# pattern: 正则模型
# string : 要匹配的字符串
# falgs : 匹配模式
re.match() 从起始位置开始匹配,匹配成功返回一个对象,未匹配成功返回None
语法:
match(pattern, string, flags=0)
# pattern: 正则模型
# string : 要匹配的字符串
# falgs : 匹配模式
re.search() 浏览全部字符串,直到匹配出第一个符合规则的字符串
语法:
search(pattern, string, flags=0)
# pattern: 正则模型
# string : 要匹配的字符串
# falgs : 匹配模式
注意:search()和match()的区别
match() 从第一个字符开始匹配, 如果第一个字符不匹配就返回None, 不再继续匹配。
search() 在整个字符串查找,直到找到一个匹配的字符。
re.split() 根据正则匹配分割字符串,返回分割后的一个列表
语法:
split(pattern, string, maxsplit=0, flags=0)
# pattern: 正则模型
# string : 要匹配的字符串
# maxsplit:指定分割个数
# flags : 匹配模式
re.sub() 替换匹配成功的指定位置字符串
语法:
sub(pattern, repl, string, count=0, flags=0)
# pattern: 正则模型
# repl : 要替换的字符串
# string : 要匹配的字符串
# count : 指定匹配个数
# flags : 匹配模式
问题:正则表达式匹配中,(.*)和(.*?)匹配区别?
(.*)是贪婪匹配,会把满足正则的尽可能多的往后匹配
(.*?)是非贪婪匹配,会把满足正则的尽可能少匹配
import re
s = "<a>哈哈</a><a>呵呵呵</a>"
res1 = re.findall("<a>(.\*)</a>",s)
print("贪婪匹配", res1)
res2 = re.findall("<a>(.\*?)</a>",s)
print("非贪婪匹配", res2)
运行结果:贪婪匹配 ['哈哈</a><a>呵呵呵']
非贪婪匹配 ['哈哈', '呵呵呵']
七. 常见问题
1. 时间复杂度
如何推导出时间复杂度呢?有如下几个原则:
- 如果运行时间是常数量级,用常数1表示;
- 只保留时间函数中的最高阶项;
- 如果最高阶项存在,则省去最高阶项前面的系数。
场景1:T(n) = 3n,执行次数是线性的,T(n) = O(n)
void eat1(int n){
for(int i=0; i<n; i++){;
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("吃一寸面包");
}
}
场景2:T(n) = 5logn,执行次数是对数的,T(n) = O(log n)
void eat2(int n)
for(int i=1; i<n; i\*=2){
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("吃一半面包");
}
}
场景3:T(n) = 2,执行次数是常量的, T(n) = O(1)
void eat3(int n){
System.out.println("等待一天");
System.out.println("吃一个鸡腿");
}
场景4:T(n) = 0.5n^2 + 0.5n,执行次数是一个多项式,T(n) = O(n^2)
void eat4(int n){
for(int i=0; i<n; i++){
for(int j=0; j<i; j++){
System.out.println("等待一天");
}
System.out.println("吃一寸面包");
}
}
这四种时间复杂度究竟谁用时更长,谁节省时间呢?
O(1)< O(logn)< O(n)< O(n^2)
2. python2 和 python3 的 range(100)的区别
python2返回列表,python3返回迭代器,节约内存