[面试] 1. 关于Python的基础知识

本文章收录于:后端工程师面试题目总结(提供参考答案)

目录

1. 可变与不可变类型

2. 谈谈浅拷贝与深拷贝

3. __new__和__init__的区别

4. 谈谈设计模式

5. 列表推导式和生成器的优劣

6. 什么是装饰器,想在函数之后进行装饰,怎么做?

7. 使用装饰器的单例模式和其他方法(如new方法,或者单文件实现的单例)相比,有何区别?

8. 谈谈线程与进程的区别

9. 谈谈Python垃圾回收机制

10. Python中的@property有什么作用? 如何实现成员变量的只读属性?

11. *args and **kwargs如何使用?

12. with语句应用场景是?有什么好处?能否简述实现原理

【进阶题目】

13. 标准库线程安全的队列是哪一个?不安全的是哪一个?

14. python适合的场景有哪些?当遇到计算密集型任务怎么办?

15. python高并发解决方案?

 


 

注:代码均用python3实现,并测试通过!

 

  • 1. 可变与不可变类型

  •     1.1 解释两种类型:

        不可变对象 -- 该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,
        相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
        
        可变对象 -- 该对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值
        直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。
        

  •     1.2 两种类型分别有哪些:

        不可变:int,float,str,tuple
        可变: list,dict,set

  • 2. 谈谈浅拷贝与深拷贝

  •     2.1 python中有值类型与引用类型之分(对应不可变与可变对象)
  •     2.2 由于值类型的对象在内存中占固定内存空间,所以他们的值保存在栈,按值访问数据

         而引用类型的对象大小不固定,所以只将它们的内存地址(它是固定的)存放在栈,由于内存地址指向
         实际对象,所有引用类型是按引用访问

  •     2.3 浅拷贝实现:只复制某个对象的内存地址,新旧变量共享同一内存同一对象。

         深拷贝实现:直接复制对象本身,新的对象是独立的,拥有不一样的内存地址,对原对象的修改不影响新对象。

  • 3. __new__和__init__的区别

  •     3.1 new在init之前执行,前者可决定是否调用后者
  •     3.2 在OOP中,实例化遵循 创建x,初始化x,返回x (x=实例对象)这么个顺序,new是创建,init是初始化

    

  • 4. 谈谈设计模式

        *题外话*:设计模式详解 -> https://www.cnblogs.com/Liqiongyu/p/5916710.html

  •     4.1 解释一下什么是设计模式

        简述:是经过总结、优化后的可重用解决方案。它是一种代码设计思想,不同的模式解决不同的问题,
        跟编程语言不相关。

  •     4.2 你知道哪些设计模式,代码如何实现

        a. 工厂模式: 是一个在软件开发中用来创建对象的设计模式。(详解工厂模式:https://www.cnblogs.com/lizhitai/p/4471952.html)
        意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。
            适用性:
            当一个类不知道它所必须创建的对象的类的时候。
            当一个类希望由它的子类来指定它所创建的对象的时候。
            当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。
        
        代码实现:

class Person: #基类(超类):提供一个抽象的接口来创建一个特定类型的对象
	def __init__(self, name, gender):
		self.name = name
		self.gender = gender

	def getName(self):
		return self.name

	def getGender(self):
		return self.gender

class Male(Person): #子类1:继承自基类的子类
	def __init__(self, name, gender):
		super().__init__(name, gender)
		print(self.gender, name)

class Female(Person): #子类2:继承自基类的子类
	def __init__(self, name, gender):
		super().__init__(name, gender)
		print(self.gender, name)

class Factory: #工厂类(简易版可以只提供一个工厂函数)
	def getPerson(self, name, gender): #工厂方法:是创建类的入口,用于创建用户想要的类
		if gender == 'M':
			return Male(name, 'male')

		if gender == 'F':
			return Female(name, 'female')

factory = Factory()
male = factory.getPerson("Chetan", "M")
female = factory.getPerson('Charle', 'F')


            
        b. 单例模式:
            意图:保证程序运行时全局环境中只有一个该类的实例
            适用性:无论何时何处调用该类,都能使用同一个该类的实例对象的时候。
            代码实现:
                

class Singleton: #一个通用的单例超类,其他类继承即可(也可通过装饰器实现)
	def __new__(cls, *args, **kw):
		if not hasattr(cls, '_instance'):
			cls._instance = object.__new__(cls)
		return cls._instance

class SingleSpam(Singleton):
	def __init__(self, s):
		self.s = s

	def __str__(self):
		return self.s

s1 = SingleSpam('spam')
print(id(s1), s1)
s2 = SingleSpam('spa')
print(id(s2), s2)
print(id(s1), s1) #s1被s2替换,永远只有一个SingleSpam实例对象


                
        c. 装饰模式:
            意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
            适用性:
            1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
            2. 处理那些可以撤销的功能
            代码实现:
                

class foo(object): #原类
	def f1(self):
		print("original f1")

	def f2(self):
		print("original f2")

class foo_decorator(object): #装饰类
	def __init__(self, decorate):
		self._decorate = decorate

	def f1(self): #给原类方法添加功能
		print("decorated f1") #可以是logging 
		self._decorate.f1()

	def __getattr__(self, name):
		return getattr(self._decoratee, name)

u = foo()
v = foo_decorator(u) #通过将实例作为参数传入的方式,动态修改对象的方法
v.f1()
v.f2()


                
        d. 迭代器模式:
            意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
            适用性:
            1. 访问一个聚合对象的内容而无需暴露它的内部表示。 
            2. 支持对聚合对象的多种遍历。 
            3. 为遍历不同的聚合结构提供一个统一的接口(即, 支持多态迭代)。
            代码实现:
              

class concreIterator():
    def __init__(self, container):
        self._a = container
        self.index = -1

    def first(self):
        return self._a[0] if self._a else None

    def isDone(self):
        if self.index + 1 >= len(self._a):
            return True
        return False

    def next(self):
        try:
            ret = self._a[self.index+1]
            self.index += 1
            return ret
        except:
            return None
        
    def currItem(self):
        return self._a[self.index]

i = concreIterator([1,2,3,4,6])

while not i.isDone():
    print(i.next())

print(i.next()) # None

# 使用magic方法构建迭代器
class iterable2:
    def __init__(self,container):
        self._a = container
        self._index= -1
    def __iter__(self):
        return self
    def __next__(self):
        try:
            ret = self._a[self._index+1]
            self._index += 1
            return ret
        except:
            raise StopIteration

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


                    
        ...(共有二十多种,不必说完)
        

  • 5. 列表推导式和生成器的优劣

    推导式:它将所有元素一次性加载到内存中,适合数量小的元素集合表示
    生成器:只能遍历使用,每次返回一个元素,没有下一个元素时抛出异常。适合大数据量的处理。

  • 6. 什么是装饰器,想在函数之后进行装饰,怎么做?

    装饰器是一个函数,这个函数的主要作用是包装另一个函数或类,动态修改其行为。
    要在函数后进行装饰,只需要新增一个函数或类,将原函数作为参数传入新函数/类的方法中,
    在其中执行原函数,并在执行前或后添加想要的功能即可。

  • 7. 使用装饰器的单例模式和其他方法(如new方法,或者单文件实现的单例)相比,有何区别?

    使用装饰器实现不会重新初始化对象,它是直接返回之前的对象;
    而其他方法均会再执行一次init方法,这也是装饰器实现单例的好处。
        

  • 8. 谈谈线程与进程的区别

    从四个方面来分析:

    调度方不同:进程是操作系统调度的基本单位,而线程是cpu调度的基本单位。
    开销不同:创建一个进程需要系统单独分配新的地址空间,代价昂贵;而创建新的线程可以直接
                使用进程的内存空间,所以进程开销大于线程。
    通信方式不同:进程间通信一般通过HTTP或RPC方式;而同一进程下的不同线程间是共享全局变量的,通信更简便。
    健壮性不同:单个进程死掉不会影响其他进程,而单个线程死掉会导致整个进程死掉。
        

  • 9. 谈谈Python垃圾回收机制

    垃圾回收机制属于python内存管理机制中的一种。
    它包含三种机制:
        1. 引用计数
        2. 标记清除
        3. 分代回收

  •     9.1. 引用计数

        是一种直观,简单的垃圾收集技术。当内存中某个对象的引用计数为0时,
        说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。其缺点是
        不能处理一种特殊情况:循环引用,这种情况需要另一种机制,即【标记清除】
        

  •     9.2. 标记清除

        属于一种追踪式收集法,与引用计数不同,一般在【已申请内存空间达到阈值】时触发。
        若垃圾回收后仍然内存不足,就报错。
        原理:从位于内存中的一个根集开始,找到所有可达的对象集合,剩下不可达的对象都是待回收的垃圾。
        过程:1.从根集开始,标记所有可达对象(这一步将进行循环引用的的拆除,然后可以识别真正的垃圾对象)
              2.遍历所有对象,回收未标记对象(不可达对象)占用的内存。(不可达:用户无法调用,且引用计数不为0的对象)
              
        带来新问题:当内存中的对象较多时,而真正需要的回收的对象又很少的时候,对这些可达对象的检测是很耗时的,
        若生存周期较长的对象占比较大,那么每次垃圾回收就是低效率高耗时的操作,有较大优化空间,这时候又引入
        【分代回收】机制。
        

  •     9.3. 分代回收

        分代回收策略着重于提升垃圾回收的效率。
        当对象很多时,垃圾检测将耗费大量的时间而真的垃圾回收花不了多久。
        对于这种多对象程序,我们可以把一些进行垃圾回收频率相近的对象称为“同一代”的对象。
        垃圾检测的时候可以对频率较高的“代”多检测几次,反之,进行垃圾回收频率较低的“代”
        可以少检测几次。这样就可以提高垃圾回收的效率了。至于如何判断一个对象属于什么代,
        python中采取的方法是通过其生存时间来判断。如果在好几次垃圾检测中,该变量都是
        reachable的话,那就说明这个变量越不是垃圾,就要把这个变量往高的代移动,要减少
        对其进行垃圾检测的频率。
    代码演示python的垃圾回收(着重于循环引用问题的解决):
        

import sys,gc

gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) #开启gc调试
a = []
b = []
a.append(b)
print(sys.getrefcount(a)) # 2,但a真实的引用计数应该是2-1=1 ,这个函数获取的计数包含一个引用副本
b.append(a)
print(sys.getrefcount(a)) # 3
print(a) # [[[...]]]

del a,b #显式删除变量,对应变量引用计数应该清零,但由于循环引用,其引用计数仍然为一。

# print(sys.getrefcount(a))  #显式删除后无法再调用变量,此步异常

unreachable_count = gc.collect() #执行垃圾回收(用的就是标记-清除算法),得到所有的不可达对象(垃圾)

print(666, gc.garbage)  # [] 查看待回收的垃圾对象列表
print(unreachable_count) #检测此刻内存中的不可达对象的数量,所谓不可达:用户无法调用,且引用计数不为0,这也是内存泄露的原因之一

print(gc.collect()) # 0,即垃圾回收成功


        

  • 10. Python中的@property有什么作用? 如何实现成员变量的只读属性?

    @property装饰器就是负责把一个方法变成属性调用,通常用在属性的get方法和set方法,
    通过设置@property可以实现实例成员变量的直接访问,又保留了参数的检查。
    另外通过设置get方法而不定义set方法可以实现成员变量的只读属性。
    代码示例:
        

class student:
@property
def birth(self):
	return self._birth #使用一个内部变量来存储数据

@birth.setter  #这个装饰器使得age可以被修改,否则就是read_only
def birth(self,v):
	if v > 2019:
		raise ValueError('birth cannot greate than 2019!')
	self._birth = v

@property
def age(self):
	self._age = 2019-self._birth
	return self._age

s = student()
s.birth = 2011
print(s.age)
s.age = 22 #异常! 因age没有定义携带setter装饰器的方法,所以不能修改


        
        

  • 11. *args and **kwargs如何使用?

    *args代表位置参数,它会接收任意多个参数并把这些参数作为元组传递给函数。
    **kwargs代表的关键字参数,允许你使用没有事先定义的参数名,kwagrs是一个dict;
    另外,位置参数一定要放在关键字参数的前面。

    

  • 12. with语句应用场景是?有什么好处?能否简述实现原理

    场景:with语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,
    释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
    好处:不需要人为释放资源。
    原理:with 主要用到一个概念:上下文管理器。
        12.1. 程序运行时,将with后紧跟的对象(一般是个class示例)称作上下文管理器。这个class必须
        定义两个方法,__enter__()方法和__exit__(),前者的返回对象赋值给as后面变量。
        
        12.2. 运行中如果发生了异常,那么将会把异常的类型,值和追踪传递给__exit__()方法。如果__exit__()
        方法返回值为true,那么这个异常将会被抑制,否则这个异常将会被重新抛出。
        
        12.3. 如果没有发生异常,也会调用__exit__()方法,但是传入的参数为None, None, None。
        通常也是在这里放入代码进行如文件流/会话的关闭等操作。
        

【进阶题目】

  • 13. 标准库线程安全的队列是哪一个?不安全的是哪一个?

    标准库中的queue.Queue是线程安全的,基础类型list,dict,set,tuple都不是线程安全队列,因为没有锁机制。

    

  • 14. python适合的场景有哪些?当遇到计算密集型任务怎么办?

    适合场景:
        14.1. web开发,有非常优秀的web开发框架,如django,flask,aiohttp,且文档完善。
        14.2. linux运维。有完善的执行shell脚本的库。
        14.3. 网络爬虫。是最适合写爬虫的语言,没有之一;有丰富的访问网页和提取HTML文档数据的库。
        14.4. 对并发要求不高的场景。GIL使得python只适合应用于对IO密集型任务的并发处理,对计算密集型任务的并发无能为力。
    
    处理计算密集型任务:
        使用多进程来执行,进程之间相互独立,各自拥有系统分配的CPU资源和独立内存空间,实现并行
        (但CPU必须是多核或多颗CPU,现代计算机均符合此条件)。
    
    

  • 15. python高并发解决方案?

    高并发有两种任务场景:

  •     15.1. IO密集型任务

        这种任务场景直接使用多线程库threading即可解决。
        更高效:线程有上下文切换开销,使用协程更好,如asyncio、gevent库

  •     15.2. 计算密集型任务

        提高这种任务的执行效率只能使用并行,而并行只能通多进程(multiprocessing)来实现。
  

       以下是Gevent代码示例,更多关于gevent参考:https://blog.csdn.net/feixiaoxing/article/details/79056535:

import gevent
from gevent.lock import BoundedSemaphore
sem = BoundedSemaphore() #通过信号量来实现锁机制

def f1():
	for i in range(5):
		sem.acquire() #获得锁
		print('this is ' + str(i))
		sem.release()  # 释放锁
		gevent.sleep(2) #模拟IO事件操作,协程执行遇到IO事件时,将进行控制权转让

def f2():
	for i in range(5):
		sem.acquire()
		print('that is ' + str(i))
		# gevent.sleep(1)
		sem.release()

t1 = gevent.spawn(f1) #孵化任务
t2 = gevent.spawn(f2)
gevent.joinall([t1,t2]) #执行一系列协程任务
'''
总结:gevent是基于协程的网络库,通过事件循环机制来实现并发编程(区别于并行)。
	当某个协程遇到IO事件时,会将控制权转让给主线程,主线程再将控制权
	交给IO事件已经完成的协程,若都没完成,主线程将一直持有控制权。

需注意:不管是threading还是gevent都不能利用多核优势,都只适合处理IO密集型的任务。
'''


  
        

你可能感兴趣的:(Python,[Python],面试,后端)