Go最新Python 面试问题总结_pytorch面试(2),进阶学习

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

import time
import threading

class MyThread(threading.Thread):
    def \_\_init\_\_(self, name, event):
        super().__init__()
        self.name = name
        self.event = event

    def run(self):
        print('Thread: {} start at {}'.format(self.name, time.ctime(time.time())))
        # 等待event.set()后,才能往下执行
        self.event.wait()
        print('Thread: {} finish at {}'.format(self.name, time.ctime(time.time())))

threads = []
event = threading.Event()

# 定义三个线程,使用event事件
[threads.append(MyThread(str(i), event)) for i in range(1, 4)]

# 重置event,使得event.wait()起到阻塞作用
event.clear()

# 启动所有线程
[t.start() for t in threads]

print('等待5s...')
time.sleep(5)

print('唤醒所有线程...')
event.set()     

执行结果:Thread: 1 start at Fri Jul 12 15:05:50 2019
		Thread: 2 start at Fri Jul 12 15:05:50 2019
		Thread: 3 start at Fri Jul 12 15:05:50 2019
		等待5s...
		唤醒所有线程...
		Thread: 3 finish at Fri Jul 12 15:05:55 2019
		Thread: 2 finish at Fri Jul 12 15:05:55 2019
		Thread: 1 finish at Fri Jul 12 15:05:55 2019

可见在所有线程都启动 start() 后,并不会执行完,而是都在self.event.wait()阻塞了,需要通过event.set()来给所有线程发送执行指令才能往下执行。

方式二:threading.Condition条件

Condition和Event 是类似的,并没有多大区别。

Condition需要掌握几个函数:

cond = threading.Condition() # 创建一个cond条件 , 默认锁为 Rlock.

  • cond.acquire() # 类似lock.acquire()
  • cond.release() # 类似lock.release()
  • cond.wait() # 等待指定触发,同时会释放锁,直到被notify才重新占有琐。
  • cond.notify() # 发送指定,触发执行一个线程
  • cond.notifyAll() # 发送指定,触发执行所有线程

举个生产消费的例子:

import threading
import time
from random import randint

class Producer(threading.Thread):
    def run(self):
        global L
        while True:
            val=randint(0,100)
            print('生产者',self.name,":Append"+str(val), L)
            if lock_con.acquire():
                L.append(val)
                lock_con.notify()
                lock_con.release()
            time.sleep(3)
            
class Consumer(threading.Thread):
    def run(self):
        global L
        while True:
                lock_con.acquire()
                if len(L)==0:
                    lock_con.wait()
                print('消费者',self.name,":Delete"+str(L[0]),L)
                del L[0]
                lock_con.release()
                time.sleep(0.25)

if __name__=="\_\_main\_\_":

    L=[]
    lock_con=threading.Condition()
    threads=[]
    for i in range(5):
        threads.append(Producer())
    threads.append(Consumer())
    for t in threads:
        t.start()
    for t in threads:
        t.join()

执行结果:生产者 Thread-1 :Append48 []
		生产者 Thread-2 :Append9 [48]
		生产者 Thread-3 :Append73 [48, 9]
		生产者 Thread-4 :Append11 [48, 9, 73]
		生产者 Thread-5 :Append94 [48, 9, 73, 11]
		消费者 Thread-6 :Delete48 [48, 9, 73, 11, 94]
		消费者 Thread-6 :Delete9 [9, 73, 11, 94]
		消费者 Thread-6 :Delete73 [73, 11, 94]
		消费者 Thread-6 :Delete11 [11, 94]
		消费者 Thread-6 :Delete94 [94]

可见通过cond来通信,阻塞自己,并使对方执行。

方式三:queue.Queue队列

从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。

创建一个被多个线程共享的 Queue 对象,这些线程通过使用put()get() 操作来向队列中添加或者删除元素。

队列需要掌握的函数:

q = Queue(maxsize=0) # maxsize默认为0,不受限。

  • q.get() # 阻塞程序,等待队列消息。
  • q.get(timeout=5.0) # 获取消息,设置超时时间。
  • q.put() # 发送消息。
  • q.join() # 等待所有的消息都被消费完。

举一个老师点名的例子:

from threading import Thread
from queue import Queue
import time

class Student(Thread):
    def \_\_init\_\_(self, name, queue):
        super().__init__()
        self.name = name
        self.queue = queue

    def run(self):
        while True:
            # 阻塞程序,时刻监听老师,接收消息
            msg = self.queue.get()
            # 一旦发现点到自己名字,就赶紧答到
            if msg == self.name:
                print("{}:到!".format(self.name))

class Teacher:
    def \_\_init\_\_(self, queue):
        self.queue = queue

    def call(self, student_name):
        print("老师:{}来了没?".format(student_name))
        # 发送消息,要点谁的名
        self.queue.put(student_name)

queue = Queue()
teacher = Teacher(queue=queue)
s1 = Student(name="小明", queue=queue)
s2 = Student(name="小亮", queue=queue)
s1.start()
s2.start()

print('开始点名~')
teacher.call('小明')
time.sleep(1)
teacher.call('小亮')

总结:

学习了以上三种通信方法,我们很容易就能发现EventCondition 是threading模块原生提供的模块,原理简单,功能单一,它能发送 TrueFalse 的指令,所以只能适用于某些简单的场景中。

Queue则是比较高级的模块,它可能发送任何类型的消息,包括字符串、字典等。其内部实现其实也引用了Condition模块(譬如putget函数的阻塞),正是其对Condition进行了功能扩展,所以功能更加丰富,更能满足实际应用。

6. 进程之间如何通信?

方式一:Queue队列,用于多个进程间实现通信

一个进程向 Queue 中放入数据,另一个进程从 Queue 中读取数据。

multiprocessing 模块下的 Queue 和 queue 模块下的 Queue 基本类似,它们都提供了 qsize()、empty()、full()、put()、put_nowait()、get()、get_nowait() 等方法。

区别只是 multiprocessing 模块下的 Queue 为进程提供服务,而 queue 模块下的 Queue 为线程提供服务。

from queue import Queue    # 为线程提供服务
from multiprocessing import Queue   # 为进程提供服务

举个例子:

import multiprocessing

def fun(que):
    print('(%s) 进程开始放入数据...' % multiprocessing.current_process().pid)
    que.put('Python')    # 向 Queue 中放入数据

if __name__ == '\_\_main\_\_':

    que = multiprocessing.Queue()    # 创建进程通信的Queue
    p = multiprocessing.Process(target=fun, args=(que,))  # 创建子进程
    p.start()  # 启动子进程
    print('(%s) 进程开始取出数据...' % multiprocessing.current_process().pid)   # 先走主进程
    print(que.get())  # 从 Queue 中读取数据

执行结果:(6364) 进程开始取出数据...
		(12716) 进程开始放入数据...
		Python

方式二:Pipe管道,用于两个进程的通信

使用 Pipe 实现进程通信,程序会调用 multiprocessing.Pipe() 函数来创建一个管道,该函数会返回两个 PipeConnection 对象,代表管道的两个连接端,用于连接通信的两个进程。

PipeConnection 对象包含如下常用方法:

  • send(obj):发送一个 obj 给管道的另一端,另一端使用 recv() 方法接收。
  • recv():接收另一端通过 send() 方法发送过来的数据。
  • fileno():关于连接所使用的文件描述器。
  • close():关闭连接。
  • poll([timeout]):返回连接中是否还有数据可以读取。
  • send_bytes(buffer[, offset[, size]]):发送字节数据。
  • recv_bytes([maxlength]):接收通过 send_bytes() 方法发迭的数据,maxlength 指定最多接收的字节数。
  • recv_bytes_into(buffer[, offset]):功能与 recv_bytes() 方法类似,只是该方法将接收到的数据放在 buffer 中。

举一个例子:

import multiprocessing

def f(conn):
    print('(%s) 进程开始发送数据...' % multiprocessing.current_process().pid)
    # 使用conn发送数据
    conn.send('Python')

if __name__ == '\_\_main\_\_':

    parent_conn, child_conn = multiprocessing.Pipe()    # 创建Pipe,该函数返回两个PipeConnection对象
    p = multiprocessing.Process(target=f, args=(child_conn, ))    # 创建子进程
    p.start()
    print('(%s) 进程开始接收数据...' % multiprocessing.current_process().pid)
    # 通过conn读取数据
    print(parent_conn.recv())  # Python
    p.join()

执行结果:(13796) 进程开始接收数据...
		(14792) 进程开始发送数据...
		Python

7. 什么时候用多线程?什么时候用多进程?
  • 多线程使用场景:IO 密集型
  • 多进程使用场景:CPU 密集型
8. python 的 GIL

全局解释器锁,Guido van Rossum(吉多·范罗苏姆)创建python时就只考虑到单核cpu,解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁, 于是有了GIL这把超级大锁。因为cpython解析只允许拥有GIL全局解析器锁才能运行程序,这样就保证了保证同一个时刻只允许一个线程可以使用cpu。也就是说多线程并不是真正意义上的同时执行。

如何解决 GIL 锁的问题呢?

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)?

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。

单例模式优点:

  1. 由于单例模式要求全局内只有一个实例,所以可以节省很多内存空间
  2. 全局只有一个接入点,可以更好的进行数据同步,避免多重占用
  3. 单例可以常驻内存,减少系统开销

单例模式应用:

  1. 生成唯一序列号
  2. 访问全局复用的唯一资源,如磁盘,总线等
  3. 数据库连接池
  4. 网站计数器

实现单例的方式?

  • 全局变量:不直接调用 Config() ,而使用同一个全局变量
  • 重写 __new__ 方法:重写 __new__来保证每次调用 Config() 都会返回同一个对象, __new__ 为对象分配空间,返回对象的引用
  • 使用metaclass:metaclass重写__call__ 方法来保证每次调用 Config() 都会返回同一个对象
  • 使用装饰器:使用装饰器来保证每次调用 Config() 都会返回同一个对象

原理:在真正调用 Config() 之前进行一些拦截操作,来保证返回的对象都是同一个:

具体实现:

  1. 全局变量
# 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 表达式的作用:

  1. python 写一些执行脚本时,使用 lambda 就可以省下定义函数过程,比如说我们只是需要写个简单的脚本来管理服务器时间,我们就不需要专门定义一个函数然后再写调用,使用 lambda 就可以使得代码更加精简。
  2. 对于一些比较抽象并且整个程序执行下来只需要调用一两次的函数,有时候给函数起个名字也是比较头疼的问题,使用 lambda 就不需要考虑命名的问题了。
  3. 简化代码的可读性,由于普通的函数阅读经常要跳到开头 def 定义部分,使用 lambda 函数可以省去这样的步骤。

四. 底层问题

1. 垃圾回收机制

python 垃圾回收主要以引用计数为主,标记-清除和分代清除为辅的机制,其中标记-清除和分代回收主要是为了处理循环引用的难题。

引用计数

引用计数算法

当有1个变量保存了对象的引用时,此对象的引用计数就会加1。
当使用del删除变量指向的对象时,如果对象的引用计数不为1,比如3,那么此时只会让这个引用计数减1,即变为2,当再次调用del时,变为1,如果再调用1次del,此时会真的把对象进行删除。

引用计数+1的四种情况:

  1. 对象被创建  a=14
  2. 对象被引用  b=a
  3. 对象被作为参数,传到函数中   func(a)
  4. 对象作为一个元素,存储在容器中   List={a,”a”,”b”,2}

对应的引用计数-1的四种情况:

  1. 当该对象的别名被显式销毁时  del a
  2. 当该对象的引别名被赋予新的对象,   a=26
  3. 一个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
  4. 将该元素从容器中删除时,或者容器被销毁时。

优点: 简单实时,一旦没有引用,内存就直接释放了。处理回收内存的时间分摊到了平时。
缺点: 维护引用计数消耗资源,会造成循环引用导致无法回收,造成内存泄露,比如:

list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
'''
 list1与list2相互引用,即使不存在其他对象对它们的引用,list1与list2的引用计数也
 仍然为1,所占用的内存永远无法被回收,这将是致命的。
'''

标记清除(Mark—Sweep)

算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。
它分为两个阶段:

  • 第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,
  • 第二阶段是把那些没有标记的对象『非活动对象』进行回收。

那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

  1. 对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。
  2. 从根对象(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默认的异常处理器,并在终端输出异常信息。

  1. try...except...finally语句:当try语句执行时发生异常,回到try语句层,寻找后面是否有except语句。找到except语句后,会调用这个自定义的异常处理器。except将异常处理完毕后,程序继续往下执行。finally语句表示,无论异常发生与否,finally中的语句都要执行。
  2. assert语句:判断assert后面紧跟的语句是True还是False,如果是True则继续执行print,如果是False则中断程序,调用默认的异常处理器,同时输出assert语句逗号后面的提示信息。
  3. with语句:如果with语句或语句块中发生异常,会调用默认的异常处理器处理,但文件还是会正常关闭。
11. python 中断言方法举例

assert()方法,断言成功,则程序继续执行,断言失败,则程序报错

a = 3
assert (a > 1)
print("断言成功,程序继续往下执行")
b = 4
assert (b > 7)
print("断言失败,程序报错")

12. 如何提高 python 的运行效率?
  1. 关键代码使用外部功能包(Cython,pylnlne,pypy,pyrex)
  2. 针对循环的优化–尽量避免在循环中访问变量的属性
  3. 使用较新版本的python
  4. 尝试多种编码方法
  5. 交叉编译应用程序

五. 列表

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()函数恢复生成器。

两种创建方式

  1. (x*2 for x in range(5,100)) # 此处注意区分列表生成式
  2. 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'



![img](https://img-blog.csdnimg.cn/img_convert/dadb1952040e51a126c0641a444ea3d7.png)
![img](https://img-blog.csdnimg.cn/img_convert/881d2d18f24e4e963f349b3eb24b8c57.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

thon3中,就是不断调用next() 方法实现for循环。



x = [1,2,3]
for i in x:
print(i)


实际执行情况:  
 ![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/e98d6a1322b780c8c405ef5b0d3b5337.png)  
 等价于:



首先获得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’

[外链图片转存中…(img-u6mXvVap-1715518054563)]
[外链图片转存中…(img-sPYhrs27-1715518054564)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值