Python大礼包:《2024年最新Python全套学习资料包】免费领取
标题001: 在Python中怎样实现单例模式。
点评:单例模式是指让一个类只能创建出唯一的实例,这个标题在口试中出现的频率极高,由于它观察的不但仅是单例模式,更是对Python语言到底掌握到何种程度,发起大家用装饰器和元类这两种方式来实现单例模式,由于这两种方式的通用性最强,而且也可以趁便展示本身对装饰器和元类中两个关键知识点的理解。
方法一:使用装饰器实现单例模式。
- from functools import wraps
-
-
- def singleton(cls):
- """单例类装饰器"""
- instances = {}
-
- @wraps(cls)
- def wrapper(*args, **kwargs):
- if cls not in instances:
- instances[cls] = cls(*args, **kwargs)
- return instances[cls]
-
- return wrapper
-
-
- @singleton
- class President:
- pass
复制代码 扩展:装饰器是Python中非常有特色的语法,用一个函数去装饰另一个函数或类,为其添加额外的能力。通常通过装饰来实现的功能都属横切关注功能,也就是跟正常的业务逻辑没有一定联系,可以动态添加或移除的功能。装饰器可以为代码提供缓存、署理、上下文环境等服务,它是对设计模式中署理模式的践行。在写装饰器的时候,带装饰功能的函数(上面代码中的wrapper函数)通常都会用functools模块中的wraps再加以装饰,这个装饰器最重要的作用是给被装饰的类或函数动态添加一个__wrapped__属性,这个属性会将被装饰之前的类或函数保留下来,这样在我们不须要装饰功能的时候,可以通过它来取消装饰器,比方可以使用President = President.__wrapped__来取消对President类做的单例处理。须要提醒大家的是:上面的单例并不是线程安全的,假如要做到线程安全,须要对创建对象的代码进行加锁的处理。在Python中可以使用threading模块的RLock对象来提供锁,可以使用锁对象的acquire和release方法来实现加锁息争锁的操作。固然,更为简便的做法是使用锁对象的with上下文语法来进行隐式的加锁息争锁操作。
方法二:使用元类实现单例模式。
- class SingletonMeta(type):
- """自定义单例元类"""
-
- def __init__(cls, *args, **kwargs):
- cls.__instance = None
- super().__init__(*args, **kwargs)
-
- def __call__(cls, *args, **kwargs):
- if cls.__instance is None:
- cls.__instance = super().__call__(*args, **kwargs)
- return cls.__instance
-
-
- class President(metaclass=SingletonMeta):
- pass
复制代码 扩展:Python是面向对象的编程语言,在面向对象的世界中,统统皆为对象。对象是通过类来创建的,而类本身也是对象,类这样的对象是通过元类来创建的。我们在界说类时,假如没有给一个类指定父类,那么默认的父类是object,假如没有给一个类指定元类,那么默认的元类是type。通过自界说的元类,我们可以改变一个类默认的行为,就如同上面的代码中,我们通过元类的__call__魔术方法,改变了President类的构造器那样。
增补:关于单例模式,在口试中另有大概被问到它的应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,比方项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上制止了重复创建对象造成的时间和空间上的开销,也制止了对资源的多重占用。再举个例子,项目中的日记操作通常也会使用单例模式,这是由于共享的日记文件不停处于打开状态,只能有一个实例去操作它,否则在写入日记的时候会产生混乱。
标题002:不使用中间变量,交换两个变量`a`和`b`的值。
点评:典型的送人头的标题,通常交换两个变量须要借助一个中间变量,假如不答应使用中间变量,在其他编程语言中可以使用异或运算的方式来实现交换两个变量的值,但是Python中有更为简单明了的做法。
方法一:
- a = a ^ b
- b = a ^ b
- a = a ^ b
复制代码 方法二:
扩展:须要留意,a, b = b, a这种做法其实并不是元组解包,虽然很多人都这样认为。Python字节码指令中有ROT_TWO指令来支持这个操作,类似的另有ROT_THREE,对于3个以上的元素,如a, b, c, d = b, c, d, a,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用Python标准库中dis模块的dis函数来反汇编你的Python代码。
标题003:写一个删除列表中重复元素的函数,要求去重后元素相对位置保持稳定。
点评:这个标题在初中级Python岗位口试的时候经常出现,标题源于《Python Cookbook》这本书第一章的第10个标题,有很多口试题其实都是这本书上的原题,以是发起大家有时间好好研读一下这本书。
- def dedup(items):
- no_dup_items = []
- seen = set()
- for item in items:
- if item not in seen:
- no_dup_items.append(item)
- seen.add(item)
- return no_dup_items
复制代码 假如愿意也可以把上面的函数改造成一个天生器,代码如下所示。
- def dedup(items):
- seen = set()
- for item in items:
- if item not in seen:
- yield item
- seen.add(item)
复制代码 扩展:由于Python中的集合底层使用哈希存储,以是集合的in和not in成员运算在性能上远远优于列表,以是上面的代码我们使用了集合来保存已经出现过的元素。集合中的元素必须是hashable对象,因此上面的代码在列表元素不是hashable对象时会失效,要办理这个标题可以给函数增加一个参数,该参数可以设计为返回哈希码或hashable对象的函数。
标题004:假设你使用的是官方的CPython,说出下面代码的运行效果。
点评:下面的程序对实际开辟并没有什么意义,但却是CPython中的一个大坑,这道题旨在观察口试者对官方的Python表明器到底相识到什么程度。
- a, b, c, d = 1, 1, 1000, 1000
- print(a is b, c is d)
-
- def foo():
- e = 1000
- f = 1000
- print(e is f, e is d)
- g = 1
- print(g is a)
-
- foo()
复制代码 运行效果:
- True False
- True False
- True
复制代码 上面代码中a is b的效果是True但c is d的效果是False,这一点的确让人费解。CPython表明器出于性能优化的考虑,把频仍使用的整数对象用一个叫small_ints的对象池缓存起来造成的。small_ints缓存的整数值被设定为[-5, 256]这个区间,也就是说,在任何引用这些整数的地方,都不须要重新创建int对象,而是直接引用缓存池中的对象。假如整数不在该范围内,那么即便两个整数的值类似,它们也是差别的对象。
CPython底层为了进一步提升性能还做了另一个设定,对于同一个代码块中值不在small_ints缓存范围内的整数,假如同一个代码块中已经存在一个值与其类似的整数对象,那么就直接引用该对象,否则创建新的int对象。须要大家留意的是,这条规则对数值型适用,但对字符串则须要考虑字符串的长度,这一点大家可以自行证明。
扩展:假如你用PyPy(另一种Python表明器实现,支持JIT,对CPython的缺点进行了改良,在性能上优于CPython,但对三方库的支持略差)来运行上面的代码,你会发现所有的输出都是True。
标题005:Lambda函数是什么,举例说明的它的应用场景。
点评:这个标题重要想观察的是Lambda函数的应用场景,潜台词是问你在项目中有没有使用过Lambda函数,具体在什么场景下会用到Lambda函数,借此来判断你写代码的能力。由于Lambda函数通常用在高阶函数中,重要的作用是通过向函数传入函数或让函数返回函数终极实现代码的解耦合。
Lambda函数也叫匿名函数,它是功能简单用一行代码就能实现的小型函数。Python中的Lambda函数只能写一个表达式,这个表达式的实行效果就是函数的返回值,不消写return关键字。Lambda函数由于没著名字,以是也不会跟其他函数发生命名冲突的标题。
扩展:口试的时候有大概还会考你用Lambda函数来实现一些功能,也就是用一行代码来实现标题要求的功能,比方:用一行代码实现求阶乘的函数,用一行代码实现求最大公约数的函数等。
- fac = lambda x: __import__('functools').reduce(int.__mul__, range(1, x + 1), 1)
- gcd = lambda x, y: y % x and gcd(y % x, x) or x
复制代码 Lambda函数其实最为重要的用途是把一个函数传入另一个高阶函数(如Python内置的filter、map等)中来为函数做解耦合,增强函数的机动性和通用性。下面的例子通过使用filter和map函数,实现了从列表中筛选出奇数并求平方构成新列表的操作,由于用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过别的一个函数传入的,因此这filter和map函数没有跟特定的过滤和映射数据的规则耦合在一起。
- items = [12, 5, 7, 10, 8, 19]
- items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items)))
- print(items) # [25, 49, 361]
复制代码 扩展:用列表的天生式来实现上面的代码会更加简单明了,代码如下所示。
- items = [12, 5, 7, 10, 8, 19]
- items = [x ** 2 for x in items if x % 2]
- print(items) # [25, 49, 361]
复制代码 标题006:说说Python中的浅拷贝和深拷贝。
点评:这个标题本身出现的频率非常高,但是就题论题而言没有什么技术含量。对于这种口试题,在回答的时候一定要让你的答案能够超出口试官的预期,这样才能获得更好的印象分。以是回答这个标题的要点不但仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候大概遇到的两大标题,还要说出Python标准库对浅拷贝和深拷贝的支持,然后可以说说列表、字典怎样实现拷贝操作以及怎样通过序列化和反序列的方式实现深拷贝,末了还可以提到设计模式中的原型模式以及它在项目中的应用。
浅拷贝通常只复制对象本身,而深拷贝不但会复制对象,还会递归的复制对象所关联的对象。深拷贝大概会遇到两个标题:一是一个对象假如直接或间接的引用了自身,会导致无休止的递归拷贝;二是深拷贝大概对原本设计为多个对象共享的数据也进行拷贝。Python通过copy模块中的copy和deepcopy函数来实现浅拷贝和深拷贝操作,其中deepcopy可以通过memo字典来保存已经拷贝过的对象,从而制止刚才所说的自引用递归标题;别的,可以通过copyreg模块的pickle函数来定制指定类型对象的拷贝行为。
deepcopy函数的本质其实就是对象的一次序列化和一次返回序列化,口试题中还考过用自界说函数实现对象的深拷贝操作,显然我们可以使用pickle模块的dumps和loads来做到,代码如下所示。
- import pickle
-
- my_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))
复制代码 列表的切片操作[:]相称于实现了列表对象的浅拷贝,而字典的copy方法可以实现字典对象的浅拷贝。对象拷贝其实是更为快捷的创建对象的方式。在Python中,通过构造器创建对象属于两阶段构造,起首是分配内存空间,然后是初始化。在创建对象时,我们也可以基于“原型”对象来创建新对象,通过对原型对象的拷贝(复制内存)就完成了对象的创建和初始化,这种做法更加高效,这也就是设计模式中的原型模式。在Python中,我们可以通过元类的方式来实现原型模式,代码如下所示。
- import copy
-
-
- class PrototypeMeta(type):
- """实现原型模式的元类"""
-
- def __init__(cls, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # 为对象绑定clone方法来实现对象拷贝
- cls.clone = lambda self, is_deep=True: \
- copy.deepcopy(self) if is_deep else copy.copy(self)
-
-
- class Person(metaclass=PrototypeMeta):
- pass
-
-
- p1 = Person()
- p2 = p1.clone() # 深拷贝
- p3 = p1.clone(is_deep=False) # 浅拷贝
复制代码 标题007:Python是怎样实现内存管理的?
点评:当口试官问到这个标题的时候,一个展示本身的机会就摆在面前了。你要先反问口试官:“你说的是官方的CPython表明器吗?”。这个反问可以展示出你相识过Python表明器的差别的实现版本,而且你也知道口试官想问的是CPython。固然,很多口试官对差别的Python表明器底层实现到底有什么差别也没有概念。以是,千万不要以为口试官一定比你强,怀揣着这份自负可以让你更好的完成口试。
Python提供了主动化的内存管理,也就是说内存空间的分配与开释都是由Python表明器在运行时主动进行的,主动管理内存功能极大的减轻程序员的工作负担,也能够帮助程序员在一定程度上办理内存泄漏的标题。以CPython表明器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。
引用计数:对于CPython表明器来说,Python中的每一个对象其实就是PyObject结构体,它的内部有一个名为ob_refcnt 的引用计数器成员变量。程序在运行的过程中ob_refcnt的值会被更新并藉此来反映引用有多少个变量引用到该对象。当对象的引用计数值为0时,它的内存就会被开释掉。
- typedef struct _object {
- _PyObject_HEAD_EXTRA
- Py_ssize_t ob_refcnt;
- struct _typeobject *ob_type;
- } PyObject;
复制代码 以下环境会导致引用计数加1:
- 对象被创建
- 对象被引用
- 对象作为参数传入到一个函数中
- 对象作为元素存储到一个容器中
以下环境会导致引用计数减1:
- 用del语句显示删除对象引用
- 对象引用被重新赋值其他对象
- 一个对象离开它所在的作用域
- 持有该对象的容器自身被销毁
- 持有该对象的容器删除该对象
可以通过sys模块的getrefcount函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤,因此须要其他的垃圾回收算法对其进行增补。
标记清理:CPython使用了“标记-清理”(Mark and Sweep)算法办理容器类型大概产生的循环引用标题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,假如对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,假如发现某个对象没有标记为可达,则就将其回收。CPython底层维护了两个双端链表,一个链表存放着须要被扫描的容器对象(姑且称之为链表A),另一个链表存放着临时不可达对象(姑且称之为链表B)。为了实现“标记-清理”算法,链表中的每个节点除了有记录当前引用计数的ref_count变量外,另有一个gc_ref变量,这个gc_ref是ref_count的一个副本,以是初始值为ref_count的大小。实行垃圾回收时,起首遍历链表A中的节点,而且将当前对象所引用的所有对象的gc_ref减1,这一步重要作用是解除循环引用对引用计数的影响。再次遍历链表A中的节点,假如节点的gc_ref值为0,那么这个对象就被标记为“暂时不可达”(GC_TENTATIVELY_UNREACHABLE)并被移动到链表B中;假如节点的gc_ref不为0,那么这个对象就会被标记为“可达“(GC_REACHABLE),对于”可达“对象,还要递归的将该节点可以到达的节点标记为”可达“;链表B中被标记为”可达“的节点要重新放回到链表A中。在两次遍历之后,链表B中的节点就是须要开释内存的节点。
分代回收:在循环引用对象的回收中,整个应用程序会被停息,为了减少应用程序停息的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收服从。分代回收的基本头脑是:对象存在的时间越长,是垃圾的大概性就越小,应该尽量不对这样的对象进行垃圾回收。CPython将对象分为三种世代分别记为0、1、2,每一个新生对象都在第0代中,假如该对象在一轮垃圾回收扫描中存活下来,那么它将被移到第1代中,存在于第1代的对象将较少的被垃圾回收扫描到;假如在对第1代进行垃圾回收扫描时,这个对象又存活下来,那么它将被移至第2代中,在那里它被垃圾回收扫描的次数将会更少。分代回收扫描的门限值可以通过gc模块的get_threshold函数来获得,该函数返回一个三元组,分别体现多少次内存分配操作后会实行0代垃圾回收,多少次0代垃圾回收后会实行1代垃圾回收,多少次1代垃圾回收后会实行2代垃圾回收。须要说明的是,假如实行一次2代垃圾回收,那么比它年轻的代都要实行垃圾回收。假如想修改这几个门限值,可以通过gc模块的set_threshold函数来做到。
标题008:说一下你对Python中迭代器和天生器的理解。
点评:很多人口试者都会写迭代器和天生器,但是却无法正确的表明什么是迭代器和天生器。假如你也有同样的困惑,可以参考下面的回答。
迭代器是实现了迭代器协议的对象。跟其他编程语言不通,Python中没有用于界说协议或体现约定的关键字,像interface、protocol这些单词并不在Python语言的关键字列表中。Python语言通过邪术方法来体现约定,也就是我们所说的协议,而__next__和__iter__这两个邪术方法就代表了迭代器协议。可以通过for-in循环从迭代器对象中取出值,也可以使用next函数取出迭代器对象中的下一个值。天生器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。
扩展:口试中经常让写天生斐波那契数列的迭代器,大家可以参考下面的代码。
- class Fib(object):
-
- def __init__(self, num):
- self.num = num
- self.a, self.b = 0, 1
- self.idx = 0
-
- def __iter__(self):
- return self
-
- def __next__(self):
- if self.idx < self.num:
- self.a, self.b = self.b, self.a + self.b
- self.idx += 1
- return self.a
- raise StopIteration()
复制代码 假如用天生器的语法来改写上面的代码,代码会简单优雅很多。
- def fib(num):
- a, b = 0, 1
- for _ in range(num):
- a, b = b, a + b
- yield a
复制代码 标题009:正则表达式的match方法和search方法有什么区别?
点评:正则表达式是字符串处理的重要工具,以是也是口试中经常观察的知识点。在Python中,使用正则表达式有两种方式,一种是直接调用re模块中的函数,传入正则表达式和须要处理的字符串;一种是先通过re模块的compile函数创建正则表达式对象,然后再通过对象调用方法并传入须要处理的字符串。假如一个正则表达式被频仍的使用,我们保举用re.compile函数创建正则表达式对象,这样会减少频仍编译同一个正则表达式所造成的开销。
match方法是从字符串的起始位置进行正则表达式匹配,返回Match对象或None。search方法会扫描整个字符串来找寻匹配的模式,同样也是返回Match对象或None。
标题010:下面这段代码的实行效果是什么。
- def multiply():
- return [lambda x: i * x for i in range(4)]
-
- print([m(100) for m in multiply()])
复制代码 运行效果:
上面代码的运行效果很容易被误判为[0, 100, 200, 300]。起首须要留意的是multiply函数用天生式语法返回了一个列表,列表中保存了4个Lambda函数,这4个Lambda函数会返回传入的参数乘以i的效果。须要留意的是这里有闭包(closure)征象,multiply函数中的局部变量i的生命周期被延展了,由于i终极的值是3,以是通过m(100)调列表中的Lambda函数时会返回300,而且4个调用都是如此。
假如想得到[0, 100, 200, 300]这个效果,可以按照下面几种方式来修改multiply函数。
方法一:使用天生器,让函数获得i的当前值。
- def multiply():
- return (lambda x: i * x for i in range(4))
-
- print([m(100) for m in multiply()])
复制代码 或者
- def multiply():
- for i in range(4):
- yield lambda x: x * i
-
- print([m(100) for m in multiply()])
复制代码 方法二:使用偏函数,彻底避开闭包。
- from functools import partial
- from operator import __mul__
-
- def multiply():
- return [partial(__mul__, i) for i in range(4)]
-
- print([m(100) for m in multiply()])
复制代码 标题011:Python中为什么没有函数重载?
点评:C++、Java、C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有差别的参数列表(参数个数差别或参数类型差别或二者皆差别),可以相互区分。重载也是一种多态性,由于通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,以是也被称为编译时多态性或者叫前绑定。这个标题的潜台词其实是问口试者是否有其他编程语言的履历,是否理解Python是动态类型语言,是否知道Python中函数的可变参数、关键字参数这些概念。
起首Python是表明型语言,函数重载征象通常出如今编译型语言中。其次Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者Python中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生差别的行为。
标题012:用Python代码实现Python内置函数max。
点评:这个标题看似简单,但实际上还是比较观察口试者的功底。由于Python内置的max函数既可以传入可迭代对象找出最大,又可以传入两个或多个参数找出最大;最为关键的是还可以通过命名关键字参数key来指定一个用于元素比较的函数,还可以通过default命名关键字参数来指定当可迭代对象为空时返回的默认值。
下面的代码仅供参考:
- def my_max(*args, key=None, default=None):
- """
- 获取可迭代对象中最大的元素或两个及以上实参中最大的元素
- :param args: 一个可迭代对象或多个元素
- :param key: 提取用于元素比较的特征值的函数,默认为None
- :param default: 如果可迭代对象为空则返回该默认值,如果没有给默认值则引发ValueError异常
- :return: 返回可迭代对象或多个元素中的最大元素
- """
- if len(args) == 1 and len(args[0]) == 0:
- if default:
- return default
- else:
- raise ValueError('max() arg is an empty sequence')
- items = args[0] if len(args) == 1 else args
- max_elem, max_value = items[0], items[0]
- if key:
- max_value = key(max_value)
- for item in items:
- value = item
- if key:
- value = key(item)
- if value > max_value:
- max_elem, max_value = item, value
- return max_elem
复制代码 标题013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。
点评:送人头的标题,不表明。
- def count_letters(items):
- result = {}
- for item in items:
- if isinstance(item, (int, float)):
- result[item] = result.get(item, 0) + 1
- return result
复制代码 也可以直接使用Python标准库中collections模块的Counter类来办理这个标题,Counter是dict的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。
- from collections import Counter
-
- def count_letters(items):
- counter = Counter(items)
- return {key: value for key, value in counter.items() \
- if isinstance(key, (int, float))}
复制代码 标题014:使用Python代码实现遍历一个文件夹的操作。
点评:基本也是送人头的标题,只要用过os模块就应该知道怎么做。
Python标准库os模块的walk函数提供了遍历一个文件夹的功能,它返回一个天生器。
- import os
-
- g = os.walk('/Users/Hao/Downloads/')
- for path, dir_list, file_list in g:
- for dir_name in dir_list:
- print(os.path.join(path, dir_name))
- for file_name in file_list:
- print(os.path.join(path, file_name))
复制代码 说明:os.path模块提供了很多进行路径操作的工具函数,在项目开辟中也是经常会用到的。假如标题明确要求不能使用os.walk函数,那么可以使用os.listdir函数来获取指定目录下的文件和文件夹,然后再通过循环遍历用os.isdir函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。
标题015:现有2元、3元、5元共三种面额的货币,假如须要找零99元,一共有多少种找零的方式?
点评:另有一个非常类似的标题:“一个小朋友走楼梯,一次可以走1个台阶、2个台阶或3个台阶,问走完10个台阶一共有多少种走法?”,这两个标题的思路是一样,假如用递归函数来写的话非常简单。
- from functools import lru_cache
-
-
- @lru_cache()
- def change_money(total):
- if total == 0:
- return 1
- if total < 0:
- return 0
- return change_money(total - 2) + change_money(total - 3) + \
- change_money(total - 5)
复制代码 说明:在上面的代码中,我们用lru_cache装饰器装饰了递归函数change_money,假如不做这个优化,上面代码的渐近时间复杂度将会是,而假如参数total的值是99,这个运算量是非常巨大的。lru_cache装饰器会缓存函数的实行效果,这样就可以减少重复运算所造成的开销,这是空间换时间的计谋,也是动态规划的编程头脑。
标题016:写一个函数,给定矩阵的阶数`n`,输出一个螺旋式数字矩阵。
比方:n = 2,返回:
比方:n = 3,返回:
这个标题本身并不复杂,下面的代码仅供参考。
- def show_spiral_matrix(n):
- matrix = [[0] * n for _ in range(n)]
- row, col = 0, 0
- num, direction = 1, 0
- while num <= n ** 2:
- if matrix[row][col] == 0:
- matrix[row][col] = num
- num += 1
- if direction == 0:
- if col < n - 1 and matrix[row][col + 1] == 0:
- col += 1
- else:
- direction += 1
- elif direction == 1:
- if row < n - 1 and matrix[row + 1][col] == 0:
- row += 1
- else:
- direction += 1
- elif direction == 2:
- if col > 0 and matrix[row][col - 1] == 0:
- col -= 1
- else:
- direction += 1
- else:
- if row > 0 and matrix[row - 1][col] == 0:
- row -= 1
- else:
- direction += 1
- direction %= 4
- for x in matrix:
- for y in x:
- print(y, end='\t')
- print()
复制代码 标题017:阅读下面的代码,写出程序的运行效果。
- items = [1, 2, 3, 4]
- print([i for i in items if i > 2])
- print([i for i in items if i % 2])
- print([(x, y) for x, y in zip('abcd', (1, 2, 3, 4, 5))])
- print({x: f'item{x ** 2}' for x in (2, 4, 6)})
- print(len({x for x in 'hello world' if x not in 'abcdefg'}))
复制代码 点评:天生式(推导式)属于Python的特色语法之一,几乎是口试必考内容。Python中通过天生式字面量语法,可以创建出列表、集合、字典。
- [3, 4]
- [1, 3]
- [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
- {2: 'item4', 4: 'item16', 6: 'item36'}
- 6
复制代码 标题018:说出下面代码的运行效果。
- class Parent:
- x = 1
-
- class Child1(Parent):
- pass
-
- class Child2(Parent):
- pass
-
- print(Parent.x, Child1.x, Child2.x)
- Child1.x = 2
- print(Parent.x, Child1.x, Child2.x)
- Parent.x = 3
- print(Parent.x, Child1.x, Child2.x)
复制代码 点评:运行上面的代码起首输出1 1 1,这一点大家应该没有什么疑问。接下来,通过Child1.x = 2给类Child1重新绑定了属性x并赋值为2,以是Child1.x会输出2,而Parent和Child2并不受影响。实行Parent.x = 3会重新给Parent类的x属性赋值为3,由于Child2的x属性继承自Parent,以是Child2.x的值也是3;而之前我们为Child1重新绑定了x属性,那么它的x属性值不会受到Parent.x = 3的影响,还是之前的值2。
标题19:说说你用过Python标准库中的哪些模块。
点评:Python标准库中的模块非常多,发起大家根据本身过往的项目经向来先容你用过的标准库和三方库,由于这些是你最为熟悉的,经得起口试官深挖的。
模块名先容sys跟Python表明器相关的变量和函数,比方:sys.version、sys.exit()os和操作系统相关的功能,比方:os.listdir()、os.remove()re和正则表达式相关的功能,比方:re.compile()、re.search()math和数学运算相关的功能,比方:math.pi、math.e、math.coslogging和日记系统相关的类和函数,比方:logging.Logger、logging.Handlerjson / pickle实现对象序列化和反序列的模块,比方:json.loads、json.dumpshashlib封装了多种哈希摘要算法的模块,比方:hashlib.md5、hashlib.sha1urllib包含了和URL相关的子模块,比方:urllib.request、urllib.parseitertools提供各种迭代器的模块,比方:itertools.cycle、itertools.productfunctools函数相关工具模块,比方:functools.partial、functools.lru_cachecollections / heapq封装了常用数据结构和算法的模块,比方:collections.dequethreading / multiprocessing多线程/多进程相关类和函数的模块,比方:threading.Threadconcurrent.futures / asyncio并发编程/异步编程相关的类和函数的模块,比方:ThreadPoolExecutorbase64提供BASE-64编码相关函数的模块,比方:bas64.encodecsv和读写CSV文件相关的模块,比方:csv.reader、csv.writerprofile / cProfile / pstats和代码性能剖析相关的模块,比方:cProfile.run、pstats.Statsunittest和单元测试相关的模块,比方:unittest.TestCase 标题20:`init__`和`__new`方法有什么区别?
Python中调用构造器创建对象属于两阶段构造过程,起首实行__new__方法获得保存对象所需的内存空间,再通过__init__实行对内存空间数据的添补(对象属性的初始化)。__new__方法的返回值是创建好的Python对象(的引用),而__init__方法的第一个参数就是这个对象(的引用),以是在__init__中可以完成对对象的初始化操作。__new__是类方法,它的第一个参数是类,__init__是对象方法,它的第一个参数是对象。
标题21:输入年代日,判断这个日期是这一年的第几天。
方法一:不使用标准库中的模块和函数。
- def is_leap_year(year):
- """判断指定的年份是不是闰年,平年返回False,闰年返回True"""
- return year % 4 == 0 and year % 100 != 0 or year % 400 == 0
-
- def which_day(year, month, date):
- """计算传入的日期是这一年的第几天"""
- # 用嵌套的列表保存平年和闰年每个月的天数
- days_of_month = [
- [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
- [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
- ]
- days = days_of_month[is_leap_year(year)][:month - 1]
- return sum(days) + date
复制代码 方法二:使用标准库中的datetime模块。
- import datetime
-
- def which_day(year, month, date):
- end = datetime.date(year, month, date)
- start = datetime.date(year, 1, 1)
- return (end - start).days + 1
复制代码 标题22:平常工作中用什么工具进行静态代码分析。
点评:静态代码分析工具可以从代码中提炼出各种静态属性,这使得开辟者可以对代码的复杂性、可维护性和可读性有更好的相识,这里所说的静态属性包括:
- 代码是否符合编码规范,比方:PEP-8。
- 代码中潜在的标题,包括:语法错误、缩进标题、导入缺失、变量覆盖等。
- 代码中的坏味道。
- 代码的复杂度。
- 代码的逻辑标题。
工作中静态代码分析重要用到的是Pylint和Flake8。Pylint可以查抄出代码错误、坏味道、不规范的代码等标题,较新的版本中还提供了代码复杂度统计数据,可以天生查抄陈诉。Flake8封装了Pyflakes(查抄代码逻辑错误)、McCabe(查抄代码复杂性)和Pycodestyle(查抄代码是否符合PEP-8规范)工具,它可以实行这三个工具提供的查抄。
标题23:说一下你知道的Python中的魔术方法。
点评:魔术方法也称为邪术方法,是Python中的特色语法,也是口试中的高频标题。
魔术方法作用__new__、__init__、__del__创建和销毁对象相关__add__、__sub__、__mul__、__div__、__floordiv__、__mod__算术运算符相关__eq__、__ne__、__lt__、__gt__、__le__、__ge__关系运算符相关__pos__、__neg__、__invert__一元运算符相关__lshift__、__rshift__、__and__、__or__、__xor__位运算相关__enter__、__exit__上下文管理器协议__iter__、__next__、__reversed__迭代器协议__int__、__long__、__float__、__oct__、__hex__类型/进制转换相关__str__、__repr__、__hash__、__dir__对象表述相关__len__、__getitem__、__setitem__、__contains__、__missing__序列相关__copy__、__deepcopy__对象拷贝相关__call__、__setattr__、__getattr__、__delattr__其他魔术方法 标题24:函数参数`arg`和`*kwargs`分别代表什么?
Python中,函数的参数分为位置参数、可变参数、关键字参数、命名关键字参数。*args代表可变参数,可以接收0个或任意多个参数,当不确定调用者会传入多少个位置参数时,就可以使用可变参数,它会将传入的参数打包成一个元组。**kwargs代表关键字参数,可以接收用参数名=参数值的方式传入的参数,传入的参数的会打包成一个字典。界说函数时假如同时使用*args和**kwargs,那么函数可以接收任意参数。
标题25:写一个记录函数实行时间的装饰器。
点评:高频口试题,也是最简单的装饰器,口试者必须要掌握的内容。
方法一:用函数实现装饰器。
- from functools import wraps
- from time import time
-
-
- def record_time(func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- start = time()
- result = func(*args, **kwargs)
- print(f'{func.__name__}执行时间: {time() - start}秒')
- return result
-
- return wrapper
复制代码 方法二:用类实现装饰器。类有__call__魔术方法,该类对象就是可调用对象,可以当做装饰器来使用。
- from functools import wraps
- from time import time
-
-
- class Record:
-
- def __call__(self, func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- start = time()
- result = func(*args, **kwargs)
- print(f'{func.__name__}执行时间: {time() - start}秒')
- return result
-
- return wrapper
复制代码 说明:装饰器可以用来装饰类或函数,为其提供额外的能力,属于设计模式中的署理模式。
扩展:装饰器本身也可以参数化,比方上面的例子中,假如不希望在终端中显示函数的实行时间而是希望由调用者来决定怎样输出函数的实行时间,可以通过参数化装饰器的方式来做到,代码如下所示。
- from functools import wraps
- from time import time
-
-
- def record_time(output):
- """可以参数化的装饰器"""
-
- def decorate(func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- start = time()
- result = func(*args, **kwargs)
- output(func.__name__, time() - start)
- return result
-
- return wrapper
-
- return decorate
复制代码 标题26:什么是鸭子类型(duck typing)?
鸭子类型是动态类型语言判断一个对象是不是某种类型时使用的方法,也叫做鸭子判定法。简单的说,鸭子类型是指判断一只鸟是不是鸭子,我们只关心它游泳像不像鸭子、叫起来像不像鸭子、走路像不像鸭子就足够了。换言之,假如对象的行为跟我们的预期是一致的(能够接受某些消息),我们就认定它是某种类型的对象。
在Python语言中,有很多bytes-like对象(如:bytes、bytearray、array.array、memoryview)、file-like对象(如:StringIO、BytesIO、GzipFile、socket)、path-like对象(如:str、bytes),其中file-like对象都能支持read和write操作,可以像文件一样读写,这就是所谓的对象有鸭子的行为就可以判定为鸭子的判定方法。再比如Python中列表的extend方法,它须要的参数并不一定要是列表,只要是可迭代对象就没有标题。
说明:动态语言的鸭子类型使得设计模式的应用被大大简化。
标题27:说一下Python中变量的作用域。
Python中有四种作用域,分别是局部作用域(Local)、嵌套作用域(Embedded)、全局作用域(Global)、内置作用域(Built-in),搜刮一个标识符时,会按照LEGB的次序进行搜刮,假如所有的作用域中都没有找到这个标识符,就会引发NameError异常。
标题28:说一下你对闭包的理解。
闭包是支持一等函数的编程语言(Python、JavaScript等)中实现词法绑定的一种技术。当捕捉闭包的时候,它的自由变量(在函数外部界说但在函数内部使用的变量)会在捕捉时被确定,这样即便离开了捕捉时的上下文,它也能照常运行。简单的说,可以将闭包理解为能够读取其他函数内部变量的函数。正在环境下,函数的局部变量在函数调用结束之后就结束了生命周期,但是闭包使得局部变量的生命周期得到了延展。使用闭包的时候须要留意,闭包会使得函数中创建的对象不会被垃圾回收,大概会导致很大的内存开销,以是闭包一定不能滥用。
标题29:说一下Python中的多线程和多进程的应用场景和优缺点。
线程是操作系统分配CPU的基本单元,进程是操作系统分配内存的基本单元。通常我们运行的程序会包含一个或多个进程,而每个进程中又包含一个或多个线程。多线程的优点在于多个线程可以共享进程的内存空间,以是进程间的通信非常容易实现;但是假如使用官方的CPython表明器,多线程受制于GIL(全局表明器锁),并不能利用CPU的多核特性,这是一个很大的标题。使用多进程可以充实利用CPU的多核特性,但是进程间通信相对比较贫苦,须要使用IPC机制(管道、套接字等)。
多线程得当那些会耗费大量时间在I/O操作上,但没有太多并行计算需求且不需占用太多内存的I/O密集型应用。多进程得当实行计算密集型使命(如:视频编码解码、数据处理、科学计算等)、可以分解为多个并行子使命并能归并子使命实行效果的使命以及在内存使用方面没有任何限定且不强依赖于I/O操作的使命。
扩展:Python中实现并发编程通常有多线程、多进程和异步编程三种选择。异步编程实现了协作式并发,通过多个相互协作的子程序的用户态切换,实现对CPU的高效利用,这种方式也是非常得当I/O密集型应用的。
标题30:说一下Python 2和Python 3的区别。
点评:这种标题千万不要背所谓的参考答案,说一些本身最熟悉的就足够了。
- Python 2中的print和exec都是关键字,在Python 3中变成了函数。
- Python 3中没有long类型,整数都是int类型。
- Python 2中的不等号<>在Python 3中被废弃,统一使用!=。
- Python 2中的xrange函数在Python 3中被range函数取代。
- Python 3对Python 2中不安全的input函数做出了改进,废弃了raw_input函数。
- Python 2中的file函数被Python 3中的open函数取代。
- Python 2中的/运算对于int类型是整除,在Python 3中要用//来做整除除法。
- Python 3中改进了Python 2捕获异常的代码,很显着Python 3的写法更合理。
- Python 3天生式中循环变量的作用域得到了更好的控制,不会影响到天生式之外的同名变量。
- Python 3中的round函数可以返回int或float类型,Python 2中的round函数返回float类型。
- Python 3的str类型是Unicode字符串,Python 2的str类型是字节串,相称于Python 3中的bytes。
- Python 3中的比较运算符必须比较同类对象。
- Python 3中界说类的都是新式类,Python 2中界说的类有新式类(显式继承自object的类)和旧式类(经典类)之分,新式类和旧式类在MRO标题上有非常显著的区别,新式类可以使用**class__`属性获取自身类型,新式类可以使用`__slots**邪术。
- Python 3对代码缩进的要求更加严格,假如混用空格和制表键会引发TabError。
- Python 3中字典的keys、values、items方法都不再返回list对象,而是返回view object,内置的map、filter等函数也不再返回list对象,而是返回迭代器对象。
- Python 3标准库中某些模块的名字跟Python 2是有区别的;而在三方库方面,有些三方库只支持Python 2,有些只能支持Python 3。
标题31:谈谈你对“猴子补丁”(monkey patching)的理解。
“猴子补丁”是动态类型语言的一个特性,代码运行时在不修改源代码的前提下改变代码中的方法、属性、函数等以到达热补丁(hot patch)的效果。很多系统的安全补丁也是通过猴子补丁的方式来实现的,但实际开辟中应该制止对猴子补丁的使用,以免造成代码行为不一致的标题。
在使用gevent库的时候,我们会在代码开头的地方实行gevent.monkey.patch_all(),这行代码的作用是把标准库中的socket模块给替换掉,这样我们在使用socket的时候,不消修改任何代码就可以实现对代码的协程化,到达提升性能的目标,这就是对猴子补丁的应用。
别的,假如希望用ujson三方库替换掉标准库中的json,也可以使用猴子补丁的方式,代码如下所示。
- import json, ujson
-
- json.__name__ = 'ujson'
- json.dumps = ujson.dumps
- json.loads = ujson.loads
复制代码 单元测试中的Mock技术也是对猴子补丁的应用,Python中的unittest.mock模块就是办理单元测试中用Mock对象替换被测对象所依赖的对象的模块。
标题32:阅读下面的代码说出运行效果。
- class A:
- def who(self):
- print('A', end='')
-
- class B(A):
- def who(self):
- super(B, self).who()
- print('B', end='')
-
- class C(A):
- def who(self):
- super(C, self).who()
- print('C', end='')
-
- class D(B, C):
- def who(self):
- super(D, self).who()
- print('D', end='')
-
- item = D()
- item.who()
复制代码 点评:这道题考查到了两个知识点:
- Python中的MRO(方法解析次序)。在没有多重继承的环境下,向对象发出一个消息,假如对象没有对应的方法,那么向上(父类)搜刮的次序是非常清晰的。假如向上追溯到object类(所有类的父类)都没有找到对应的方法,那么将会引发AttributeError异常。但是有多重继承尤其是出现菱形继承(钻石继承)的时候,向上追溯到底应该找到那个方法就得确定MRO。Python 3中的类以及Python 2中的新式类使用C3算法来确定MRO,它是一种类似于广度优先搜刮的方法;Python 2中的旧式类(经典类)使用深度优先搜刮来确定MRO。在搞不清楚MRO的环境下,可以使用类的mro方法或**mro**属性来获得类的MRO列表。
- super()函数的使用。在使用super函数时,可以通过super(类型, 对象)来指定对哪个对象以哪个类为起点向上搜刮父类方法。以是上面B类代码中的super(B, self).who()体现以B类为起点,向上搜刮self(D类对象)的who方法,以是会找到C类中的who方法,由于D类对象的MRO列表是D --> B --> C --> A --> object。
标题33:编写一个函数实现对逆波兰表达式求值,不能使用Python的内置函数。
点评:逆波兰表达式也称为“后缀表达式”,相较于平常我们使用的“中缀表达式”,逆波兰表达式不须要括号来确定运算的优先级,比方5 * (2 + 3)对应的逆波兰表达式是5 2 3 + *。逆波兰表达式求值须要借助栈结构,扫描表达式遇到运算数就入栈,遇到运算符就出栈两个元素做运算,将运算效果入栈。表达式扫描结束后,栈中只有一个数,这个数就是终极的运算效果,直接出栈即可。
- import operator
-
-
- class Stack:
- """栈(FILO)"""
-
- def __init__(self):
- self.elems = []
-
- def push(self, elem):
- """入栈"""
- self.elems.append(elem)
-
- def pop(self):
- """出栈"""
- return self.elems.pop()
-
- @property
- def is_empty(self):
- """检查栈是否为空"""
- return len(self.elems) == 0
-
-
- def eval_suffix(expr):
- """逆波兰表达式求值"""
- operators = {
- '+': operator.add,
- '-': operator.sub,
- '*': operator.mul,
- '/': operator.truediv
- }
- stack = Stack()
- for item in expr.split():
- if item.isdigit():
- stack.push(float(item))
- else:
- num2 = stack.pop()
- num1 = stack.pop()
- stack.push(operators[item](num1, num2))
- return stack.pop()
复制代码 标题34:Python中怎样实现字符串替换操作?
Python中实现字符串替换大致有两类方法:字符串的replace方法和正则表达式的sub方法。
方法一:使用字符串的replace方法。
- message = 'hello, world!'
- print(message.replace('o', 'O').replace('l', 'L').replace('he', 'HE'))
复制代码 方法二:使用正则表达式的sub方法。
- import re
-
- message = 'hello, world!'
- pattern = re.compile('[aeiou]')
- print(pattern.sub('#', message))
复制代码 扩展:另有一个相关的口试题,对保存文件名的列表排序,要求文件名按照字母表和数字大小进行排序,比方对于列表filenames = ['a12.txt', 'a8.txt', 'b10.txt', 'b2.txt', 'b19.txt', 'a3.txt'],排序的效果是['a3.txt', 'a8.txt', 'a12.txt', 'b2.txt', 'b10.txt', 'b19.txt']。提示一下,可以通过字符串替换的方式为文件名补位,根据补位后的文件名用sorted函数来排序,大家可以思索下这个标题怎样办理。
标题35:怎样剖析Python代码的实行性能?
剖析代码性能可以使用Python标准库中的cProfile和pstats模块,cProfile的run函数可以实行代码并收集统计信息,创建出Stats对象并打印简单的剖析陈诉。Stats是pstats模块中的类,它是一个统计对象。固然,也可以使用三方工具line_profiler和memory_profiler来剖析每一行代码耗费的时间和内存,这两个三方工具都会用非常友爱的方式输出剖析结构。假如使用PyCharm,可以利用“Run”菜单的“Profile”菜单项对代码进行性能分析,PyCharm中可以用表格或者调用图(Call Graph)的方式来显示性能剖析的效果。
下面是使用cProfile剖析代码性能的例子。
example.py
- import cProfile
-
-
- def is_prime(num):
- for factor in range(2, int(num ** 0.5) + 1):
- if num % factor == 0:
- return False
- return True
-
-
- class PrimeIter:
-
- def __init__(self, total):
- self.counter = 0
- self.current = 1
- self.total = total
-
- def __iter__(self):
- return self
-
- def __next__(self):
- if self.counter < self.total:
- self.current += 1
- while not is_prime(self.current):
- self.current += 1
- self.counter += 1
- return self.current
- raise StopIteration()
-
-
- cProfile.run('list(PrimeIter(10000))')
复制代码 假如使用line_profiler三方工具,可以直接剖析is_prime函数每行代码的性能,须要给is_prime函数添加一个profiler装饰器,代码如下所示。
- @profiler
- def is_prime(num):
- for factor in range(2, int(num ** 0.5) + 1):
- if num % factor == 0:
- return False
- return True
复制代码 安装line_profiler。
- pip install line_profiler
复制代码 使用line_profiler。
运行效果如下所示。
- Line # Hits Time Per Hit % Time Line Contents
- ==============================================================
- 1 @profile
- 2 def is_prime(num):
- 3 86624 48420.0 0.6 50.5 for factor in range(2, int(num ** 0.5) + 1):
- 4 85624 44000.0 0.5 45.9 if num % factor == 0:
- 5 6918 3080.0 0.4 3.2 return False
- 6 1000 430.0 0.4 0.4 return True
复制代码 标题36:怎样使用`random`模块天生随机数、实现随机乱序和随机抽样?
点评:送人头的标题,由于Python标准库中的常用模块应该是Python开辟者都比较熟悉的内容,这个标题回假如答不上来,整个口试基本也就砸锅了。
- random.random()函数可以天生[0.0, 1.0)之间的随机浮点数。
- random.uniform(a, b)函数可以天生[a, b]或[b, a]之间的随机浮点数。
- random.randint(a, b)函数可以天生[a, b]或[b, a]之间的随机整数。
- random.shuffle(x)函数可以实现对序列x的原地随机乱序。
- random.choice(seq)函数可以从非空序列中取出一个随机元素。
- random.choices(population, weights=None, *, cum_weights=None, k=1)函数可以从总体中随机抽取(有放回抽样)出容量为k的样本并返回样本的列表,可以通过参数指定个体的权重,假如没有指定权重,个体被选中的概率均等。
- random.sample(population, k)函数可以从总体中随机抽取(无放回抽样)出容量为k的样本并返回样本的列表。
扩展:random模块提供的函数除了天生匀称分布的随机数外,还可以天生其他分布的随机数,比方random.gauss(mu, sigma)函数可以天生高斯分布(正态分布)的随机数;random.paretovariate(alpha)函数会天生帕累托分布的随机数;random.gammavariate(alpha, beta)函数会天生伽马分布的随机数。
标题37:表明一下线程池的工作原理。
点评:池化技术就是一种典型空间换时间的计谋,我们使用的数据库连接池、线程池等都是池化技术的应用,Python标准库currrent.futures模块的ThreadPoolExecutor就是线程池的实现,假如要弄清楚它的工作原理,可以参考下面的内容。
线程池是一种用于减少线程本身创建和销毁造成的开销的技术,属于典型的空间换时间操作。假如应用程序须要频仍的将使命派发到线程中实行,线程池就是必选项,由于创建和开释线程涉及到大量的系统底层操作,开销较大,假如能够在应用程序工作期间,将创建和开释线程的操作变成预创建和借还操作,将大大减少底层开销。线程池在应用程序启动后,立即创建一定数量的线程,放入空闲队列中。这些线程最开始都处于阻塞状态,不会消耗CPU资源,但会占用少量的内存空间。当使命到来后,从队列中取出一个空闲线程,把使命派发到这个线程中运行,并将该线程标记为已占用。当线程池中所有的线程都被占用后,可以选择主动创建一定数量的新线程,用于处理更多的使命,也可以选择让使命排队等待直到有空闲的线程可用。在使命实行完毕后,线程并不退出结束,而是继续保持在池中等待下一次的使命。当系统比较空闲时,大部分线程长时间处于闲置状态时,线程池可以主动销毁一部分线程,回收系统资源。基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的使命上,实行次数越多,每个使命所分担到的线程本身开销则越小。
一样平常线程池都必须具备下面几个组成部分:
- 线程池管理器:用于创建并管理线程池。
- 工作线程和线程队列:线程池中实际实行的线程以及保存这些线程的容器。
- 使命接口:将线程实行的使命抽象出来,形成使命接口,确保线程池与具体的使命无关。
- 使命队列:线程池中保存等待被实行的使命的容器。
标题38:举例说明什么环境下会出现`KeyError`、`TypeError`、`ValueError`。
举一个简单的例子,变量a是一个字典,实行int(a['x'])这个操作就有大概引发上述三种类型的异常。假如字典中没有键x,会引发KeyError;假如键x对应的值不是str、float、int、bool以及bytes-like类型,在调用int函数构造int类型的对象时,会引发TypeError;假如a[x]是一个字符串或者字节串,而对应的内容又无法处理成int时,将引发ValueError。
标题39:说出下面代码的运行效果。
- def extend_list(val, items=[]):
- items.append(val)
- return items
-
- list1 = extend_list(10)
- list2 = extend_list(123, [])
- list3 = extend_list('a')
- print(list1)
- print(list2)
- print(list3)
复制代码 点评:Python函数在界说的时候,默认参数items的值就被计算出来了,即[]。由于默认参数items引用了对象[],每次调用该函数,假如对items引用的列表进行了操作,下次调用时,默认参数还是引用之前的那个列表而不是重新赋值为[],以是列表中会有之前添加的元素。假如通过传参的方式为items重新赋值,那么items将引用到新的列表对象,而不再引用默认的那个列表对象。这个题在口试中经常被问到,通常不发起使用容器类型的默认参数,像PyLint这样的代码查抄工具也会对这种代码提出质疑和警告。
- [10, 'a']
- [123]
- [10, 'a']
复制代码 标题40:怎样读取大文件,比方内存只有4G,怎样读取一个大小为8G的文件?
很显然4G内存要一次性的加载大小为8G的文件是不现实的,遇到这种环境必须要考虑多次读取和分批次处理。在Python中读取文件可以先通过open函数获取文件对象,在读取文件时,可以通过read方法的size参数指定读取的大小,也可以通过seek方法的offset参数指定读取的位置,这样就可以控制单次读取数据的字节数和总字节数。除此之外,可以使用内置函数iter将文件对象处理成迭代器对象,每次只读取少量的数据进行处理,代码大致写法如下所示。
- with open('...', 'rb') as file:
- for data in iter(lambda: file.read(2097152), b''):
- pass
复制代码 在Linux系统上,可以通过split命令将大文件切割为小片,然后通过读取切割后的小文件对数据进行处理。比方下面的命令将名为filename的大文件切割为大小为512M的多个文件。
假如愿意, 也可以将名为filename的文件切割为10个文件,命令如下所示。
扩展:外部排序跟上述的环境非常类似,由于处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。“排序-归并算法”就是一种常用的外部排序计谋。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件,然后在归并阶段将这些临时文件组合为一个大的有序文件,这个大的有序文件就是排序的效果。
标题41:说一下你对Python中模块和包的理解。
每个Python文件就是一个模块,而保存这些文件的文件夹就是一个包,但是这个作为Python包的文件夹必须要有一个名为__init__.py的文件,否则无法导入这个包。通常一个文件夹下还可以有子文件夹,这也就意味着一个包下还可以有子包,子包中的__init__.py并不是必须的。模块和包办理了Python中命名冲突的标题,差别的包下可以有同名的模块,差别的模块下可以有同名的变量、函数或类。在Python中可以使用import或from ... import ...来导入包和模块,在导入的时候还可以使用as关键字对包、模块、类、函数、变量等进行别名,从而彻底办理编程中尤其是多人协作团队开辟时的命名冲突标题。
标题42:说一下你知道的Python编码规范。
点评:企业的Python编码规范基本上是参照PEP-8或谷歌开源项目风格指南来制定的,后者还提到了可以使用Lint工具来查抄代码的规范程度,口试的时候遇到这类标题,可以先说下这两个参照标准,然后挑重点说一下Python编码的留意事项。
- 使用空格来体现缩进而不要用制表符(Tab)。
- 和语法相关的每一层缩进都用4个空格来体现。
- 每行的字符数不要凌驾79个字符,假如表达式因太长而占据了多行,除了首行之外的别的各行都应该在正常的缩进宽度上再加上4个空格。
- 函数和类的界说,代码前后都要用两个空行进行分隔。
- 在同一个类中,各个方法之间应该用一个空行进行分隔。
- 二元运算符的左右两侧应该保留一个空格,而且只要一个空格就好。
- 变量、函数和属性应该使用小写字母来拼写,假如有多个单词就使用下划线进行连接。
- 类中受保护的实例属性,应该以一个下划线开头。
- 类中私有的实例属性,应该以两个下划线开头。
- 类和异常的命名,应该每个单词首字母大写。
- 模块级别的常量,应该采用全大写字母,假如有多个单词就用下划线进行连接。
- 类的实例方法,应该把第一个参数命名为self以体现对象自身。
- 类的类方法,应该把第一个参数命名为cls以体现该类自身。
- 采用内联形式的否定词,而不要把否定词放在整个表达式的前面。比方:if a is not b就比if not a is b更容易让人理解。
- 不要用查抄长度的方式来判断字符串、列表等是否为None或者没有元素,应该用if not x这样的写法来查抄它。
- 就算if分支、for循环、except异常捕获等中只有一行代码,也不要将代码和if、for、except等写在一起,分开写才会让代码更清晰。
- import语句总是放在文件开头的地方。
- 引入模块的时候,from math import sqrt比import math更好。
- 假如有多个import语句,应该将其分为三部分,从上到下分别是Python标准模块、第三方模块和自界说模块,每个部分内部应该按照模块名称的字母表次序来分列。
标题43:运行下面的代码是否会报错,假如报错请说明哪里有什么样的错,假如不报错请说出代码的实行效果。
- class A:
- def __init__(self, value):
- self.__value = value
-
- @property
- def value(self):
- return self.__value
-
- obj = A(1)
- obj.__value = 2
- print(obj.value)
- print(obj.__value)
复制代码 点评:这道题有两个观察点,一个观察点是对_和__开头的对象属性访问权限以及@property装饰器的相识,别的一个观察的点是对动态语言的理解,不须要过多的表明。
扩展:假如不希望代码运行时动态的给对象添加新属性,可以在界说类时使用__slots__邪术。比方,我们可以在上面的A中添加一行__slots__ = ('__value', ),再次运行上面的代码,将会在原来的第10行处产生AttributeError错误。
标题44:对下面给出的字典按值从大到小对键进行排序。
- prices = {
- 'AAPL': 191.88,
- 'GOOG': 1186.96,
- 'IBM': 149.24,
- 'ORCL': 48.44,
- 'ACN': 166.89,
- 'FB': 208.09,
- 'SYMC': 21.29
- }
复制代码 点评:sorted函数的高阶用法在口试的时候经常出现,key参数可以传入一个函数名或一个Lambda函数,该函数的返回值代表了在排序时比较元素的依据。
- sorted(prices, key=lambda x: prices[x], reverse=True)
复制代码 标题45:说一下`namedtuple`的用法和作用。
点评:Python标准库的collections模块提供了很多有用的数据结构,这些内容并不是每个开辟者都清楚,就比如标题问到的namedtuple,在我参加过的口试中,90%的口试者都不能正确的说出它的作用和应用场景。别的,deque也是一个非常有用但又经常被忽视的类,另有Counter、OrderedDict 、defaultdict 、UserDict等类,大家清楚它们的用法吗?
在使用面向对象编程语言的时候,界说类是最常见的一件变乱,有的时候,我们会用到只有属性没有方法的类,这种类的对象通常只用于组织数据,并不能接收消息,以是我们把这种类称为数据类或者退化的类,就像C语言中的结构体那样。我们并不发起使用这种退化的类,在Python中可以用namedtuple(命名元组)来替换这种类。
- from collections import namedtuple
-
- Card = namedtuple('Card', ('suite', 'face'))
- card1 = Card('红桃', 13)
- card2 = Card('草花', 5)
- print(f'{card1.suite}{card1.face}')
- print(f'{card2.suite}{card2.face}')
复制代码 命名元组与平凡元组一样是不可变容器,一旦将数据存储在namedtuple的顶层属性中,数据就不能再修改了,也就意味着对象上的所有属性都遵照“一次写入,多次读取”的原则。和平凡元组差别的是,命名元组中的数据有访问名称,可以通过名称而不是索引来获取保存的数据,不但在操作上更加简单,代码的可读性也会更好。
命名元组的本质就是一个类,以是它还可以作为父类创建子类。除此之外,命名元组内置了一系列的方法,比方,可以通过_asdict方法将命名元组处理成字典,也可以通过_replace方法创建命名元组对象的浅拷贝。
- class MyCard(Card):
-
- def show(self):
- faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
- return f'{self.suite}{faces[self.face]}'
-
-
- print(Card) # <class '__main__.Card'>
- card3 = MyCard('方块', 12)
- print(card3.show()) # 方块Q
- print(dict(card1._asdict())) # {'suite': '红桃', 'face': 13}
- print(card2._replace(suite='方块')) # Card(suite='方块', face=5)
复制代码 总而言之,命名元组能更好的组织数据结构,让代码更加清晰和可读,在很多场景下是元组、字典和数据类的替换品。在须要创建占用空间更少的不可变类时,命名元组就是很好的选择。
标题46:按照标题要求写出对应的函数。
要求:写一个函数,传入一个有若干个整数的列表,该列表中某个元素出现的次数凌驾了50%,返回这个元素。
- def more_than_half(items):
- temp, times = None, 0
- for item in items:
- if times == 0:
- temp = item
- times += 1
- else:
- if item == temp:
- times += 1
- else:
- times -= 1
- return temp
复制代码 点评:LeetCode上的标题,在Python口试中出现过,利用元素出现次数凌驾了50%这一特征,出现和temp类似的元素就将计数值加1,出现和temp差别的元素就将计数值减1。假如计数值为0,说明之前出现的元素已经对终极的效果没有影响,用temp记下当前元素并将计数值置为1。终极,出现次数凌驾了50%的这个元素一定会被赋值给变量temp。
标题47:按照标题要求写出对应的函数。
要求:写一个函数,传入的参数是一个列表(列表中的元素大概也是一个列表),返回该列表最大的嵌套深度。比方:列表[1, 2, 3]的嵌套深度为1,列表[[1], [2, [3]]]的嵌套深度为3。
- def list_depth(items):
- if isinstance(items, list):
- max_depth = 1
- for item in items:
- max_depth = max(list_depth(item) + 1, max_depth)
- return max_depth
- return 0
复制代码 点评:看到标题应该能够比较自然的想到使用递归的方式查抄列表中的每个元素。
标题48:按照标题要求写出对应的装饰器。
要求:有一个通过网络获取数据的函数(大概会由于网络原因出现异常),写一个装饰器让这个函数在出现指定异常时可以重试指定的次数,并在每次重试之前随机延迟一段时间,最长延迟时间可以通过参数进行控制。
方法一:
- from functools import wraps
- from random import random
- from time import sleep
-
-
- def retry(*, retry_times=3, max_wait_secs=5, errors=(Exception, )):
-
- def decorate(func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- for _ in range(retry_times):
- try:
- return func(*args, **kwargs)
- except errors:
- sleep(random() * max_wait_secs)
- return None
-
- return wrapper
-
- return decorate
复制代码 方法二:
- from functools import wraps
- from random import random
- from time import sleep
-
-
- class Retry(object):
-
- def __init__(self, *, retry_times=3, max_wait_secs=5, errors=(Exception, )):
- self.retry_times = retry_times
- self.max_wait_secs = max_wait_secs
- self.errors = errors
-
- def __call__(self, func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- for _ in range(self.retry_times):
- try:
- return func(*args, **kwargs)
- except self.errors:
- sleep(random() * self.max_wait_secs)
- return None
-
- return wrapper
复制代码 点评:我们不止一次强调过,装饰器几乎是Python口试必问内容,这个标题比之前的标题轻微复杂一些,它须要的是一个参数化的装饰器。
标题49:写一个函数实现字符串反转,尽大概写出你知道的所有方法。
点评:烂大街的标题,基本上算是送人头的标题。
方法一:反向切片
- def reverse_string(content):
- return content[::-1]
复制代码 方法二:反转拼接
- def reverse_string(content):
- return ''.join(reversed(content))
复制代码 方法三:递归调用
- def reverse_string(content):
- if len(content) <= 1:
- return content
- return reverse_string(content[1:]) + content[0]
复制代码 方法四:双端队列
- from collections import deque
-
- def reverse_string(content):
- q = deque()
- q.extendleft(content)
- return ''.join(q)
复制代码 方法五:反向组装
- from io import StringIO
-
- def reverse_string(content):
- buffer = StringIO()
- for i in range(len(content) - 1, -1, -1):
- buffer.write(content[i])
- return buffer.getvalue()
复制代码 方法六:反转拼接
- def reverse_string(content):
- return ''.join([content[i] for i in range(len(content) - 1, -1, -1)])
复制代码 方法七:半截交换
- def reverse_string(content):
- length, content= len(content), list(content)
- for i in range(length // 2):
- content[i], content[length - 1 - i] = content[length - 1 - i], content[i]
- return ''.join(content)
复制代码 方法八:对位交换
- def reverse_string(content):
- length, content= len(content), list(content)
- for i, j in zip(range(length // 2), range(length - 1, length // 2 - 1, -1)):
- content[i], content[j] = content[j], content[i]
- return ''.join(content)
复制代码 扩展:这些方法其实都是大同小异的,口试的时候能够给出几种有代表性的就足够了。给大家留一个思索题,上面这些方法,哪些做法的性能较好呢?我们之前提到过剖析代码性能的方法,大家可以用这些方法来检验下你给出的答案是否正确。
标题50:按照标题要求写出对应的函数。
要求:列表中有1000000个元素,取值范围是[1000, 10000),设计一个函数找出列表中的重复元素。
- def find_dup(items: list):
- dups = [0] * 9000
- for item in items:
- dups[item - 1000] += 1
- for idx, val in enumerate(dups):
- if val > 1:
- yield idx + 1000
复制代码 点评:这道题的解法和计数排序的原理一致,虽然元素的数量非常多,但是取值范围[1000, 10000)并不是很大,只有9000个大概的取值,以是可以用一个能够保存9000个元素的dups列表来记录每个元素出现的次数,dups列表所有元素的初始值都是0,通过对items列表中元素的遍历,当出现某个元素时,将dups列表对应位置的值加1,末了dups列表中值大于1的元素对应的就是items列表中重复出现过的元素。
标题001: 在Python中怎样实现单例模式。
点评:单例模式是指让一个类只能创建出唯一的实例,这个标题在口试中出现的频率极高,由于它观察的不但仅是单例模式,更是对Python语言到底掌握到何种程度,发起大家用装饰器和元类这两种方式来实现单例模式,由于这两种方式的通用性最强,而且也可以趁便展示本身对装饰器和元类中两个关键知识点的理解。
方法一:使用装饰器实现单例模式。
- from functools import wraps
-
-
- def singleton(cls):
- """单例类装饰器"""
- instances = {}
-
- @wraps(cls)
- def wrapper(*args, **kwargs):
- if cls not in instances:
- instances[cls] = cls(*args, **kwargs)
- return instances[cls]
-
- return wrapper
-
-
- @singleton
- class President:
- pass
复制代码 扩展:装饰器是Python中非常有特色的语法,用一个函数去装饰另一个函数或类,为其添加额外的能力。通常通过装饰来实现的功能都属横切关注功能,也就是跟正常的业务逻辑没有一定联系,可以动态添加或移除的功能。装饰器可以为代码提供缓存、署理、上下文环境等服务,它是对设计模式中署理模式的践行。在写装饰器的时候,带装饰功能的函数(上面代码中的wrapper函数)通常都会用functools模块中的wraps再加以装饰,这个装饰器最重要的作用是给被装饰的类或函数动态添加一个__wrapped__属性,这个属性会将被装饰之前的类或函数保留下来,这样在我们不须要装饰功能的时候,可以通过它来取消装饰器,比方可以使用President = President.__wrapped__来取消对President类做的单例处理。须要提醒大家的是:上面的单例并不是线程安全的,假如要做到线程安全,须要对创建对象的代码进行加锁的处理。在Python中可以使用threading模块的RLock对象来提供锁,可以使用锁对象的acquire和release方法来实现加锁息争锁的操作。固然,更为简便的做法是使用锁对象的with上下文语法来进行隐式的加锁息争锁操作。
方法二:使用元类实现单例模式。
- class SingletonMeta(type):
- """自定义单例元类"""
-
- def __init__(cls, *args, **kwargs):
- cls.__instance = None
- super().__init__(*args, **kwargs)
-
- def __call__(cls, *args, **kwargs):
- if cls.__instance is None:
- cls.__instance = super().__call__(*args, **kwargs)
- return cls.__instance
-
-
- class President(metaclass=SingletonMeta):
- pass
复制代码 扩展:Python是面向对象的编程语言,在面向对象的世界中,统统皆为对象。对象是通过类来创建的,而类本身也是对象,类这样的对象是通过元类来创建的。我们在界说类时,假如没有给一个类指定父类,那么默认的父类是object,假如没有给一个类指定元类,那么默认的元类是type。通过自界说的元类,我们可以改变一个类默认的行为,就如同上面的代码中,我们通过元类的__call__魔术方法,改变了President类的构造器那样。
增补:关于单例模式,在口试中另有大概被问到它的应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,比方项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上制止了重复创建对象造成的时间和空间上的开销,也制止了对资源的多重占用。再举个例子,项目中的日记操作通常也会使用单例模式,这是由于共享的日记文件不停处于打开状态,只能有一个实例去操作它,否则在写入日记的时候会产生混乱。
标题002:不使用中间变量,交换两个变量`a`和`b`的值。
点评:典型的送人头的标题,通常交换两个变量须要借助一个中间变量,假如不答应使用中间变量,在其他编程语言中可以使用异或运算的方式来实现交换两个变量的值,但是Python中有更为简单明了的做法。
方法一:
- a = a ^ b
- b = a ^ b
- a = a ^ b
复制代码 方法二:
扩展:须要留意,a, b = b, a这种做法其实并不是元组解包,虽然很多人都这样认为。Python字节码指令中有ROT_TWO指令来支持这个操作,类似的另有ROT_THREE,对于3个以上的元素,如a, b, c, d = b, c, d, a,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用Python标准库中dis模块的dis函数来反汇编你的Python代码。
标题003:写一个删除列表中重复元素的函数,要求去重后元素相对位置保持稳定。
点评:这个标题在初中级Python岗位口试的时候经常出现,标题源于《Python Cookbook》这本书第一章的第10个标题,有很多口试题其实都是这本书上的原题,以是发起大家有时间好好研读一下这本书。
- def dedup(items):
- no_dup_items = []
- seen = set()
- for item in items:
- if item not in seen:
- no_dup_items.append(item)
- seen.add(item)
- return no_dup_items
复制代码 假如愿意也可以把上面的函数改造成一个天生器,代码如下所示。
- def dedup(items):
- seen = set()
- for item in items:
- if item not in seen:
- yield item
- seen.add(item)
复制代码 扩展:由于Python中的集合底层使用哈希存储,以是集合的in和not in成员运算在性能上远远优于列表,以是上面的代码我们使用了集合来保存已经出现过的元素。集合中的元素必须是hashable对象,因此上面的代码在列表元素不是hashable对象时会失效,要办理这个标题可以给函数增加一个参数,该参数可以设计为返回哈希码或hashable对象的函数。
标题004:假设你使用的是官方的CPython,说出下面代码的运行效果。
点评:下面的程序对实际开辟并没有什么意义,但却是CPython中的一个大坑,这道题旨在观察口试者对官方的Python表明器到底相识到什么程度。
- a, b, c, d = 1, 1, 1000, 1000
- print(a is b, c is d)
-
- def foo():
- e = 1000
- f = 1000
- print(e is f, e is d)
- g = 1
- print(g is a)
-
- foo()
复制代码 运行效果:
- True False
- True False
- True
复制代码 上面代码中a is b的效果是True但c is d的效果是False,这一点的确让人费解。CPython表明器出于性能优化的考虑,把频仍使用的整数对象用一个叫small_ints的对象池缓存起来造成的。small_ints缓存的整数值被设定为[-5, 256]这个区间,也就是说,在任何引用这些整数的地方,都不须要重新创建int对象,而是直接引用缓存池中的对象。假如整数不在该范围内,那么即便两个整数的值类似,它们也是差别的对象。
CPython底层为了进一步提升性能还做了另一个设定,对于同一个代码块中值不在small_ints缓存范围内的整数,假如同一个代码块中已经存在一个值与其类似的整数对象,那么就直接引用该对象,否则创建新的int对象。须要大家留意的是,这条规则对数值型适用,但对字符串则须要考虑字符串的长度,这一点大家可以自行证明。
扩展:假如你用PyPy(另一种Python表明器实现,支持JIT,对CPython的缺点进行了改良,在性能上优于CPython,但对三方库的支持略差)来运行上面的代码,你会发现所有的输出都是True。
标题005:Lambda函数是什么,举例说明的它的应用场景。
点评:这个标题重要想观察的是Lambda函数的应用场景,潜台词是问你在项目中有没有使用过Lambda函数,具体在什么场景下会用到Lambda函数,借此来判断你写代码的能力。由于Lambda函数通常用在高阶函数中,重要的作用是通过向函数传入函数或让函数返回函数终极实现代码的解耦合。
Lambda函数也叫匿名函数,它是功能简单用一行代码就能实现的小型函数。Python中的Lambda函数只能写一个表达式,这个表达式的实行效果就是函数的返回值,不消写return关键字。Lambda函数由于没著名字,以是也不会跟其他函数发生命名冲突的标题。
扩展:口试的时候有大概还会考你用Lambda函数来实现一些功能,也就是用一行代码来实现标题要求的功能,比方:用一行代码实现求阶乘的函数,用一行代码实现求最大公约数的函数等。
- fac = lambda x: __import__('functools').reduce(int.__mul__, range(1, x + 1), 1)
- gcd = lambda x, y: y % x and gcd(y % x, x) or x
复制代码 Lambda函数其实最为重要的用途是把一个函数传入另一个高阶函数(如Python内置的filter、map等)中来为函数做解耦合,增强函数的机动性和通用性。下面的例子通过使用filter和map函数,实现了从列表中筛选出奇数并求平方构成新列表的操作,由于用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过别的一个函数传入的,因此这filter和map函数没有跟特定的过滤和映射数据的规则耦合在一起。
- items = [12, 5, 7, 10, 8, 19]
- items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items)))
- print(items) # [25, 49, 361]
复制代码 扩展:用列表的天生式来实现上面的代码会更加简单明了,代码如下所示。
- items = [12, 5, 7, 10, 8, 19]
- items = [x ** 2 for x in items if x % 2]
- print(items) # [25, 49, 361]
复制代码 标题006:说说Python中的浅拷贝和深拷贝。
点评:这个标题本身出现的频率非常高,但是就题论题而言没有什么技术含量。对于这种口试题,在回答的时候一定要让你的答案能够超出口试官的预期,这样才能获得更好的印象分。以是回答这个标题的要点不但仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候大概遇到的两大标题,还要说出Python标准库对浅拷贝和深拷贝的支持,然后可以说说列表、字典怎样实现拷贝操作以及怎样通过序列化和反序列的方式实现深拷贝,末了还可以提到设计模式中的原型模式以及它在项目中的应用。
浅拷贝通常只复制对象本身,而深拷贝不但会复制对象,还会递归的复制对象所关联的对象。深拷贝大概会遇到两个标题:一是一个对象假如直接或间接的引用了自身,会导致无休止的递归拷贝;二是深拷贝大概对原本设计为多个对象共享的数据也进行拷贝。Python通过copy模块中的copy和deepcopy函数来实现浅拷贝和深拷贝操作,其中deepcopy可以通过memo字典来保存已经拷贝过的对象,从而制止刚才所说的自引用递归标题;别的,可以通过copyreg模块的pickle函数来定制指定类型对象的拷贝行为。
deepcopy函数的本质其实就是对象的一次序列化和一次返回序列化,口试题中还考过用自界说函数实现对象的深拷贝操作,显然我们可以使用pickle模块的dumps和loads来做到,代码如下所示。
- import pickle
-
- my_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))
复制代码 列表的切片操作[:]相称于实现了列表对象的浅拷贝,而字典的copy方法可以实现字典对象的浅拷贝。对象拷贝其实是更为快捷的创建对象的方式。在Python中,通过构造器创建对象属于两阶段构造,起首是分配内存空间,然后是初始化。在创建对象时,我们也可以基于“原型”对象来创建新对象,通过对原型对象的拷贝(复制内存)就完成了对象的创建和初始化,这种做法更加高效,这也就是设计模式中的原型模式。在Python中,我们可以通过元类的方式来实现原型模式,代码如下所示。
- import copy
-
-
- class PrototypeMeta(type):
- """实现原型模式的元类"""
-
- def __init__(cls, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # 为对象绑定clone方法来实现对象拷贝
- cls.clone = lambda self, is_deep=True: \
- copy.deepcopy(self) if is_deep else copy.copy(self)
-
-
- class Person(metaclass=PrototypeMeta):
- pass
-
-
- p1 = Person()
- p2 = p1.clone() # 深拷贝
- p3 = p1.clone(is_deep=False) # 浅拷贝
复制代码 标题007:Python是怎样实现内存管理的?
点评:当口试官问到这个标题的时候,一个展示本身的机会就摆在面前了。你要先反问口试官:“你说的是官方的CPython表明器吗?”。这个反问可以展示出你相识过Python表明器的差别的实现版本,而且你也知道口试官想问的是CPython。固然,很多口试官对差别的Python表明器底层实现到底有什么差别也没有概念。以是,千万不要以为口试官一定比你强,怀揣着这份自负可以让你更好的完成口试。
Python提供了主动化的内存管理,也就是说内存空间的分配与开释都是由Python表明器在运行时主动进行的,主动管理内存功能极大的减轻程序员的工作负担,也能够帮助程序员在一定程度上办理内存泄漏的标题。以CPython表明器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。
引用计数:对于CPython表明器来说,Python中的每一个对象其实就是PyObject结构体,它的内部有一个名为ob_refcnt 的引用计数器成员变量。程序在运行的过程中ob_refcnt的值会被更新并藉此来反映引用有多少个变量引用到该对象。当对象的引用计数值为0时,它的内存就会被开释掉。
- typedef struct _object {
- _PyObject_HEAD_EXTRA
- Py_ssize_t ob_refcnt;
- struct _typeobject *ob_type;
- } PyObject;
复制代码 以下环境会导致引用计数加1:
- 对象被创建
- 对象被引用
- 对象作为参数传入到一个函数中
- 对象作为元素存储到一个容器中
以下环境会导致引用计数减1:
- 用del语句显示删除对象引用
- 对象引用被重新赋值其他对象
- 一个对象离开它所在的作用域
- 持有该对象的容器自身被销毁
- 持有该对象的容器删除该对象
可以通过sys模块的getrefcount函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤,因此须要其他的垃圾回收算法对其进行增补。
标记清理:CPython使用了“标记-清理”(Mark and Sweep)算法办理容器类型大概产生的循环引用标题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,假如对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,假如发现某个对象没有标记为可达,则就将其回收。CPython底层维护了两个双端链表,一个链表存放着须要被扫描的容器对象(姑且称之为链表A),另一个链表存放着临时不可达对象(姑且称之为链表B)。为了实现“标记-清理”算法,链表中的每个节点除了有记录当前引用计数的ref_count变量外,另有一个gc_ref变量,这个gc_ref是ref_count的一个副本,以是初始值为ref_count的大小。实行垃圾回收时,起首遍历链表A中的节点,而且将当前对象所引用的所有对象的gc_ref减1,这一步重要作用是解除循环引用对引用计数的影响。再次遍历链表A中的节点,假如节点的gc_ref值为0,那么这个对象就被标记为“暂时不可达”(GC_TENTATIVELY_UNREACHABLE)并被移动到链表B中;假如节点的gc_ref不为0,那么这个对象就会被标记为“可达“(GC_REACHABLE),对于”可达“对象,还要递归的将该节点可以到达的节点标记为”可达“;链表B中被标记为”可达“的节点要重新放回到链表A中。在两次遍历之后,链表B中的节点就是须要开释内存的节点。
分代回收:在循环引用对象的回收中,整个应用程序会被停息,为了减少应用程序停息的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收服从。分代回收的基本头脑是:对象存在的时间越长,是垃圾的大概性就越小,应该尽量不对这样的对象进行垃圾回收。CPython将对象分为三种世代分别记为0、1、2,每一个新生对象都在第0代中,假如该对象在一轮垃圾回收扫描中存活下来,那么它将被移到第1代中,存在于第1代的对象将较少的被垃圾回收扫描到;假如在对第1代进行垃圾回收扫描时,这个对象又存活下来,那么它将被移至第2代中,在那里它被垃圾回收扫描的次数将会更少。分代回收扫描的门限值可以通过gc模块的get_threshold函数来获得,该函数返回一个三元组,分别体现多少次内存分配操作后会实行0代垃圾回收,多少次0代垃圾回收后会实行1代垃圾回收,多少次1代垃圾回收后会实行2代垃圾回收。须要说明的是,假如实行一次2代垃圾回收,那么比它年轻的代都要实行垃圾回收。假如想修改这几个门限值,可以通过gc模块的set_threshold函数来做到。
标题008:说一下你对Python中迭代器和天生器的理解。
点评:很多人口试者都会写迭代器和天生器,但是却无法正确的表明什么是迭代器和天生器。假如你也有同样的困惑,可以参考下面的回答。
迭代器是实现了迭代器协议的对象。跟其他编程语言不通,Python中没有用于界说协议或体现约定的关键字,像interface、protocol这些单词并不在Python语言的关键字列表中。Python语言通过邪术方法来体现约定,也就是我们所说的协议,而__next__和__iter__这两个邪术方法就代表了迭代器协议。可以通过for-in循环从迭代器对象中取出值,也可以使用next函数取出迭代器对象中的下一个值。天生器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。
扩展:口试中经常让写天生斐波那契数列的迭代器,大家可以参考下面的代码。
- class Fib(object):
-
- def __init__(self, num):
- self.num = num
- self.a, self.b = 0, 1
- self.idx = 0
-
- def __iter__(self):
- return self
-
- def __next__(self):
- if self.idx < self.num:
- self.a, self.b = self.b, self.a + self.b
- self.idx += 1
- return self.a
- raise StopIteration()
复制代码 假如用天生器的语法来改写上面的代码,代码会简单优雅很多。
- def fib(num):
- a, b = 0, 1
- for _ in range(num):
- a, b = b, a + b
- yield a
复制代码 标题009:正则表达式的match方法和search方法有什么区别?
点评:正则表达式是字符串处理的重要工具,以是也是口试中经常观察的知识点。在Python中,使用正则表达式有两种方式,一种是直接调用re模块中的函数,传入正则表达式和须要处理的字符串;一种是先通过re模块的compile函数创建正则表达式对象,然后再通过对象调用方法并传入须要处理的字符串。假如一个正则表达式被频仍的使用,我们保举用re.compile函数创建正则表达式对象,这样会减少频仍编译同一个正则表达式所造成的开销。
match方法是从字符串的起始位置进行正则表达式匹配,返回Match对象或None。search方法会扫描整个字符串来找寻匹配的模式,同样也是返回Match对象或None。
标题010:下面这段代码的实行效果是什么。
- def multiply():
- return [lambda x: i * x for i in range(4)]
-
- print([m(100) for m in multiply()])
复制代码 运行效果:
上面代码的运行效果很容易被误判为[0, 100, 200, 300]。起首须要留意的是multiply函数用天生式语法返回了一个列表,列表中保存了4个Lambda函数,这4个Lambda函数会返回传入的参数乘以i的效果。须要留意的是这里有闭包(closure)征象,multiply函数中的局部变量i的生命周期被延展了,由于i终极的值是3,以是通过m(100)调列表中的Lambda函数时会返回300,而且4个调用都是如此。
假如想得到[0, 100, 200, 300]这个效果,可以按照下面几种方式来修改multiply函数。
方法一:使用天生器,让函数获得i的当前值。
- def multiply():
- return (lambda x: i * x for i in range(4))
-
- print([m(100) for m in multiply()])
复制代码 或者
- def multiply():
- for i in range(4):
- yield lambda x: x * i
-
- print([m(100) for m in multiply()])
复制代码 方法二:使用偏函数,彻底避开闭包。
- from functools import partial
- from operator import __mul__
-
- def multiply():
- return [partial(__mul__, i) for i in range(4)]
-
- print([m(100) for m in multiply()])
复制代码 标题011:Python中为什么没有函数重载?
点评:C++、Java、C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有差别的参数列表(参数个数差别或参数类型差别或二者皆差别),可以相互区分。重载也是一种多态性,由于通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,以是也被称为编译时多态性或者叫前绑定。这个标题的潜台词其实是问口试者是否有其他编程语言的履历,是否理解Python是动态类型语言,是否知道Python中函数的可变参数、关键字参数这些概念。
起首Python是表明型语言,函数重载征象通常出如今编译型语言中。其次Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者Python中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生差别的行为。
标题012:用Python代码实现Python内置函数max。
点评:这个标题看似简单,但实际上还是比较观察口试者的功底。由于Python内置的max函数既可以传入可迭代对象找出最大,又可以传入两个或多个参数找出最大;最为关键的是还可以通过命名关键字参数key来指定一个用于元素比较的函数,还可以通过default命名关键字参数来指定当可迭代对象为空时返回的默认值。
下面的代码仅供参考:
- def my_max(*args, key=None, default=None):
- """
- 获取可迭代对象中最大的元素或两个及以上实参中最大的元素
- :param args: 一个可迭代对象或多个元素
- :param key: 提取用于元素比较的特征值的函数,默认为None
- :param default: 如果可迭代对象为空则返回该默认值,如果没有给默认值则引发ValueError异常
- :return: 返回可迭代对象或多个元素中的最大元素
- """
- if len(args) == 1 and len(args[0]) == 0:
- if default:
- return default
- else:
- raise ValueError('max() arg is an empty sequence')
- items = args[0] if len(args) == 1 else args
- max_elem, max_value = items[0], items[0]
- if key:
- max_value = key(max_value)
- for item in items:
- value = item
- if key:
- value = key(item)
- if value > max_value:
- max_elem, max_value = item, value
- return max_elem
复制代码 标题013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。
点评:送人头的标题,不表明。
- def count_letters(items):
- result = {}
- for item in items:
- if isinstance(item, (int, float)):
- result[item] = result.get(item, 0) + 1
- return result
复制代码 也可以直接使用Python标准库中collections模块的Counter类来办理这个标题,Counter是dict的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。
- from collections import Counter
-
- def count_letters(items):
- counter = Counter(items)
- return {key: value for key, value in counter.items() \
- if isinstance(key, (int, float))}
复制代码 标题014:使用Python代码实现遍历一个文件夹的操作。
点评:基本也是送人头的标题,只要用过os模块就应该知道怎么做。
Python标准库os模块的walk函数提供了遍历一个文件夹的功能,它返回一个天生器。
- import os
-
- g = os.walk('/Users/Hao/Downloads/')
- for path, dir_list, file_list in g:
- for dir_name in dir_list:
- print(os.path.join(path, dir_name))
- for file_name in file_list:
- print(os.path.join(path, file_name))
复制代码 说明:os.path模块提供了很多进行路径操作的工具函数,在项目开辟中也是经常会用到的。假如标题明确要求不能使用os.walk函数,那么可以使用os.listdir函数来获取指定目录下的文件和文件夹,然后再通过循环遍历用os.isdir函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。
标题015:现有2元、3元、5元共三种面额的货币,假如须要找零99元,一共有多少种找零的方式?
点评:另有一个非常类似的标题:“一个小朋友走楼梯,一次可以走1个台阶、2个台阶或3个台阶,问走完10个台阶一共有多少种走法?”,这两个标题的思路是一样,假如用递归函数来写的话非常简单。
- from functools import lru_cache
-
-
- @lru_cache()
- def change_money(total):
- if total == 0:
- return 1
- if total < 0:
- return 0
- return change_money(total - 2) + change_money(total - 3) + \
- change_money(total - 5)
复制代码 说明:在上面的代码中,我们用lru_cache装饰器装饰了递归函数change_money,假如不做这个优化,上面代码的渐近时间复杂度将会是,而假如参数total的值是99,这个运算量是非常巨大的。lru_cache装饰器会缓存函数的实行效果,这样就可以减少重复运算所造成的开销,这是空间换时间的计谋,也是动态规划的编程头脑。
标题016:写一个函数,给定矩阵的阶数`n`,输出一个螺旋式数字矩阵。
比方:n = 2,返回:
比方:n = 3,返回:
这个标题本身并不复杂,下面的代码仅供参考。
- def show_spiral_matrix(n):
- matrix = [[0] * n for _ in range(n)]
- row, col = 0, 0
- num, direction = 1, 0
- while num <= n ** 2:
- if matrix[row][col] == 0:
- matrix[row][col] = num
- num += 1
- if direction == 0:
- if col < n - 1 and matrix[row][col + 1] == 0:
- col += 1
- else:
- direction += 1
- elif direction == 1:
- if row < n - 1 and matrix[row + 1][col] == 0:
- row += 1
- else:
- direction += 1
- elif direction == 2:
- if col > 0 and matrix[row][col - 1] == 0:
- col -= 1
- else:
- direction += 1
- else:
- if row > 0 and matrix[row - 1][col] == 0:
- row -= 1
- else:
- direction += 1
- direction %= 4
- for x in matrix:
- for y in x:
- print(y, end='\t')
- print()
复制代码 标题017:阅读下面的代码,写出程序的运行效果。
- items = [1, 2, 3, 4]
- print([i for i in items if i > 2])
- print([i for i in items if i % 2])
- print([(x, y) for x, y in zip('abcd', (1, 2, 3, 4, 5))])
- print({x: f'item{x ** 2}' for x in (2, 4, 6)})
- print(len({x for x in 'hello world' if x not in 'abcdefg'}))
复制代码 点评:天生式(推导式)属于Python的特色语法之一,几乎是口试必考内容。Python中通过天生式字面量语法,可以创建出列表、集合、字典。
- [3, 4]
- [1, 3]
- [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
- {2: 'item4', 4: 'item16', 6: 'item36'}
- 6
复制代码 标题018:说出下面代码的运行效果。
- class Parent:
- x = 1
-
- class Child1(Parent):
- pass
-
- class Child2(Parent):
- pass
-
- print(Parent.x, Child1.x, Child2.x)
- Child1.x = 2
- print(Parent.x, Child1.x, Child2.x)
- Parent.x = 3
- print(Parent.x, Child1.x, Child2.x)
复制代码 点评:运行上面的代码起首输出1 1 1,这一点大家应该没有什么疑问。接下来,通过Child1.x = 2给类Child1重新绑定了属性x并赋值为2,以是Child1.x会输出2,而Parent和Child2并不受影响。实行Parent.x = 3会重新给Parent类的x属性赋值为3,由于Child2的x属性继承自Parent,以是Child2.x的值也是3;而之前我们为Child1重新绑定了x属性,那么它的x属性值不会受到Parent.x = 3的影响,还是之前的值2。
标题19:说说你用过Python标准库中的哪些模块。
点评:Python标准库中的模块非常多,发起大家根据本身过往的项目经向来先容你用过的标准库和三方库,由于这些是你最为熟悉的,经得起口试官深挖的。
模块名先容sys跟Python表明器相关的变量和函数,比方:sys.version、sys.exit()os和操作系统相关的功能,比方:os.listdir()、os.remove()re和正则表达式相关的功能,比方:re.compile()、re.search()math和数学运算相关的功能,比方:math.pi、math.e、math.coslogging和日记系统相关的类和函数,比方:logging.Logger、logging.Handlerjson / pickle实现对象序列化和反序列的模块,比方:json.loads、json.dumpshashlib封装了多种哈希摘要算法的模块,比方:hashlib.md5、hashlib.sha1urllib包含了和URL相关的子模块,比方:urllib.request、urllib.parseitertools提供各种迭代器的模块,比方:itertools.cycle、itertools.productfunctools函数相关工具模块,比方:functools.partial、functools.lru_cachecollections / heapq封装了常用数据结构和算法的模块,比方:collections.dequethreading / multiprocessing多线程/多进程相关类和函数的模块,比方:threading.Threadconcurrent.futures / asyncio并发编程/异步编程相关的类和函数的模块,比方:ThreadPoolExecutorbase64提供BASE-64编码相关函数的模块,比方:bas64.encodecsv和读写CSV文件相关的模块,比方:csv.reader、csv.writerprofile / cProfile / pstats和代码性能剖析相关的模块,比方:cProfile.run、pstats.Statsunittest和单元测试相关的模块,比方:unittest.TestCase 标题20:`init__`和`__new`方法有什么区别?
Python中调用构造器创建对象属于两阶段构造过程,起首实行__new__方法获得保存对象所需的内存空间,再通过__init__实行对内存空间数据的添补(对象属性的初始化)。__new__方法的返回值是创建好的Python对象(的引用),而__init__方法的第一个参数就是这个对象(的引用),以是在__init__中可以完成对对象的初始化操作。__new__是类方法,它的第一个参数是类,__init__是对象方法,它的第一个参数是对象。
标题21:输入年代日,判断这个日期是这一年的第几天。
方法一:不使用标准库中的模块和函数。
- def is_leap_year(year):
- """判断指定的年份是不是闰年,平年返回False,闰年返回True"""
- return year % 4 == 0 and year % 100 != 0 or year % 400 == 0
-
- def which_day(year, month, date):
- """计算传入的日期是这一年的第几天"""
- # 用嵌套的列表保存平年和闰年每个月的天数
- days_of_month = [
- [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
- [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
- ]
- days = days_of_month[is_leap_year(year)][:month - 1]
- return sum(days) + date
复制代码 方法二:使用标准库中的datetime模块。
- import datetime
-
- def which_day(year, month, date):
- end = datetime.date(year, month, date)
- start = datetime.date(year, 1, 1)
- return (end - start).days + 1
复制代码 标题22:平常工作中用什么工具进行静态代码分析。
点评:静态代码分析工具可以从代码中提炼出各种静态属性,这使得开辟者可以对代码的复杂性、可维护性和可读性有更好的相识,这里所说的静态属性包括:
- 代码是否符合编码规范,比方:PEP-8。
- 代码中潜在的标题,包括:语法错误、缩进标题、导入缺失、变量覆盖等。
- 代码中的坏味道。
- 代码的复杂度。
- 代码的逻辑标题。
工作中静态代码分析重要用到的是Pylint和Flake8。Pylint可以查抄出代码错误、坏味道、不规范的代码等标题,较新的版本中还提供了代码复杂度统计数据,可以天生查抄陈诉。Flake8封装了Pyflakes(查抄代码逻辑错误)、McCabe(查抄代码复杂性)和Pycodestyle(查抄代码是否符合PEP-8规范)工具,它可以实行这三个工具提供的查抄。
标题23:说一下你知道的Python中的魔术方法。
点评:魔术方法也称为邪术方法,是Python中的特色语法,也是口试中的高频标题。
魔术方法作用__new__、__init__、__del__创建和销毁对象相关__add__、__sub__、__mul__、__div__、__floordiv__、__mod__算术运算符相关__eq__、__ne__、__lt__、__gt__、__le__、__ge__关系运算符相关__pos__、__neg__、__invert__一元运算符相关__lshift__、__rshift__、__and__、__or__、__xor__位运算相关__enter__、__exit__上下文管理器协议__iter__、__next__、__reversed__迭代器协议__int__、__long__、__float__、__oct__、__hex__类型/进制转换相关__str__、__repr__、__hash__、__dir__对象表述相关__len__、__getitem__、__setitem__、__contains__、__missing__序列相关__copy__、__deepcopy__对象拷贝相关__call__、__setattr__、__getattr__、__delattr__其他魔术方法 标题24:函数参数`arg`和`*kwargs`分别代表什么?
Python中,函数的参数分为位置参数、可变参数、关键字参数、命名关键字参数。*args代表可变参数,可以接收0个或任意多个参数,当不确定调用者会传入多少个位置参数时,就可以使用可变参数,它会将传入的参数打包成一个元组。**kwargs代表关键字参数,可以接收用参数名=参数值的方式传入的参数,传入的参数的会打包成一个字典。界说函数时假如同时使用*args和**kwargs,那么函数可以接收任意参数。
标题25:写一个记录函数实行时间的装饰器。
点评:高频口试题,也是最简单的装饰器,口试者必须要掌握的内容。
方法一:用函数实现装饰器。
- from functools import wraps
- from time import time
-
-
- def record_time(func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- start = time()
- result = func(*args, **kwargs)
- print(f'{func.__name__}执行时间: {time() - start}秒')
- return result
-
- return wrapper
复制代码 方法二:用类实现装饰器。类有__call__魔术方法,该类对象就是可调用对象,可以当做装饰器来使用。
- from functools import wraps
- from time import time
-
-
- class Record:
-
- def __call__(self, func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- start = time()
- result = func(*args, **kwargs)
- print(f'{func.__name__}执行时间: {time() - start}秒')
- return result
-
- return wrapper
复制代码 说明:装饰器可以用来装饰类或函数,为其提供额外的能力,属于设计模式中的署理模式。
扩展:装饰器本身也可以参数化,比方上面的例子中,假如不希望在终端中显示函数的实行时间而是希望由调用者来决定怎样输出函数的实行时间,可以通过参数化装饰器的方式来做到,代码如下所示。
- from functools import wraps
- from time import time
-
-
- def record_time(output):
- """可以参数化的装饰器"""
-
- def decorate(func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- start = time()
- result = func(*args, **kwargs)
- output(func.__name__, time() - start)
- return result
-
- return wrapper
-
- return decorate
复制代码 标题26:什么是鸭子类型(duck typing)?
鸭子类型是动态类型语言判断一个对象是不是某种类型时使用的方法,也叫做鸭子判定法。简单的说,鸭子类型是指判断一只鸟是不是鸭子,我们只关心它游泳像不像鸭子、叫起来像不像鸭子、走路像不像鸭子就足够了。换言之,假如对象的行为跟我们的预期是一致的(能够接受某些消息),我们就认定它是某种类型的对象。
在Python语言中,有很多bytes-like对象(如:bytes、bytearray、array.array、memoryview)、file-like对象(如:StringIO、BytesIO、GzipFile、socket)、path-like对象(如:str、bytes),其中file-like对象都能支持read和write操作,可以像文件一样读写,这就是所谓的对象有鸭子的行为就可以判定为鸭子的判定方法。再比如Python中列表的extend方法,它须要的参数并不一定要是列表,只要是可迭代对象就没有标题。
说明:动态语言的鸭子类型使得设计模式的应用被大大简化。
标题27:说一下Python中变量的作用域。
Python中有四种作用域,分别是局部作用域(Local)、嵌套作用域(Embedded)、全局作用域(Global)、内置作用域(Built-in),搜刮一个标识符时,会按照LEGB的次序进行搜刮,假如所有的作用域中都没有找到这个标识符,就会引发NameError异常。
标题28:说一下你对闭包的理解。
闭包是支持一等函数的编程语言(Python、JavaScript等)中实现词法绑定的一种技术。当捕捉闭包的时候,它的自由变量(在函数外部界说但在函数内部使用的变量)会在捕捉时被确定,这样即便离开了捕捉时的上下文,它也能照常运行。简单的说,可以将闭包理解为能够读取其他函数内部变量的函数。正在环境下,函数的局部变量在函数调用结束之后就结束了生命周期,但是闭包使得局部变量的生命周期得到了延展。使用闭包的时候须要留意,闭包会使得函数中创建的对象不会被垃圾回收,大概会导致很大的内存开销,以是闭包一定不能滥用。
标题29:说一下Python中的多线程和多进程的应用场景和优缺点。
线程是操作系统分配CPU的基本单元,进程是操作系统分配内存的基本单元。通常我们运行的程序会包含一个或多个进程,而每个进程中又包含一个或多个线程。多线程的优点在于多个线程可以共享进程的内存空间,以是进程间的通信非常容易实现;但是假如使用官方的CPython表明器,多线程受制于GIL(全局表明器锁),并不能利用CPU的多核特性,这是一个很大的标题。使用多进程可以充实利用CPU的多核特性,但是进程间通信相对比较贫苦,须要使用IPC机制(管道、套接字等)。
多线程得当那些会耗费大量时间在I/O操作上,但没有太多并行计算需求且不需占用太多内存的I/O密集型应用。多进程得当实行计算密集型使命(如:视频编码解码、数据处理、科学计算等)、可以分解为多个并行子使命并能归并子使命实行效果的使命以及在内存使用方面没有任何限定且不强依赖于I/O操作的使命。
扩展:Python中实现并发编程通常有多线程、多进程和异步编程三种选择。异步编程实现了协作式并发,通过多个相互协作的子程序的用户态切换,实现对CPU的高效利用,这种方式也是非常得当I/O密集型应用的。
标题30:说一下Python 2和Python 3的区别。
点评:这种标题千万不要背所谓的参考答案,说一些本身最熟悉的就足够了。
- Python 2中的print和exec都是关键字,在Python 3中变成了函数。
- Python 3中没有long类型,整数都是int类型。
- Python 2中的不等号<>在Python 3中被废弃,统一使用!=。
- Python 2中的xrange函数在Python 3中被range函数取代。
- Python 3对Python 2中不安全的input函数做出了改进,废弃了raw_input函数。
- Python 2中的file函数被Python 3中的open函数取代。
- Python 2中的/运算对于int类型是整除,在Python 3中要用//来做整除除法。
- Python 3中改进了Python 2捕获异常的代码,很显着Python 3的写法更合理。
- Python 3天生式中循环变量的作用域得到了更好的控制,不会影响到天生式之外的同名变量。
- Python 3中的round函数可以返回int或float类型,Python 2中的round函数返回float类型。
- Python 3的str类型是Unicode字符串,Python 2的str类型是字节串,相称于Python 3中的bytes。
- Python 3中的比较运算符必须比较同类对象。
- Python 3中界说类的都是新式类,Python 2中界说的类有新式类(显式继承自object的类)和旧式类(经典类)之分,新式类和旧式类在MRO标题上有非常显著的区别,新式类可以使用**class__`属性获取自身类型,新式类可以使用`__slots**邪术。
- Python 3对代码缩进的要求更加严格,假如混用空格和制表键会引发TabError。
- Python 3中字典的keys、values、items方法都不再返回list对象,而是返回view object,内置的map、filter等函数也不再返回list对象,而是返回迭代器对象。
- Python 3标准库中某些模块的名字跟Python 2是有区别的;而在三方库方面,有些三方库只支持Python 2,有些只能支持Python 3。
标题31:谈谈你对“猴子补丁”(monkey patching)的理解。
“猴子补丁”是动态类型语言的一个特性,代码运行时在不修改源代码的前提下改变代码中的方法、属性、函数等以到达热补丁(hot patch)的效果。很多系统的安全补丁也是通过猴子补丁的方式来实现的,但实际开辟中应该制止对猴子补丁的使用,以免造成代码行为不一致的标题。
在使用gevent库的时候,我们会在代码开头的地方实行gevent.monkey.patch_all(),这行代码的作用是把标准库中的socket模块给替换掉,这样我们在使用socket的时候,不消修改任何代码就可以实现对代码的协程化,到达提升性能的目标,这就是对猴子补丁的应用。
别的,假如希望用ujson三方库替换掉标准库中的json,也可以使用猴子补丁的方式,代码如下所示。
- import json, ujson
-
- json.__name__ = 'ujson'
- json.dumps = ujson.dumps
- json.loads = ujson.loads
复制代码 单元测试中的Mock技术也是对猴子补丁的应用,Python中的unittest.mock模块就是办理单元测试中用Mock对象替换被测对象所依赖的对象的模块。
标题32:阅读下面的代码说出运行效果。
- class A:
- def who(self):
- print('A', end='')
-
- class B(A):
- def who(self):
- super(B, self).who()
- print('B', end='')
-
- class C(A):
- def who(self):
- super(C, self).who()
- print('C', end='')
-
- class D(B, C):
- def who(self):
- super(D, self).who()
- print('D', end='')
-
- item = D()
- item.who()
复制代码 点评:这道题考查到了两个知识点:
- Python中的MRO(方法解析次序)。在没有多重继承的环境下,向对象发出一个消息,假如对象没有对应的方法,那么向上(父类)搜刮的次序是非常清晰的。假如向上追溯到object类(所有类的父类)都没有找到对应的方法,那么将会引发AttributeError异常。但是有多重继承尤其是出现菱形继承(钻石继承)的时候,向上追溯到底应该找到那个方法就得确定MRO。Python 3中的类以及Python 2中的新式类使用C3算法来确定MRO,它是一种类似于广度优先搜刮的方法;Python 2中的旧式类(经典类)使用深度优先搜刮来确定MRO。在搞不清楚MRO的环境下,可以使用类的mro方法或**mro**属性来获得类的MRO列表。
- super()函数的使用。在使用super函数时,可以通过super(类型, 对象)来指定对哪个对象以哪个类为起点向上搜刮父类方法。以是上面B类代码中的super(B, self).who()体现以B类为起点,向上搜刮self(D类对象)的who方法,以是会找到C类中的who方法,由于D类对象的MRO列表是D --> B --> C --> A --> object。
标题33:编写一个函数实现对逆波兰表达式求值,不能使用Python的内置函数。
点评:逆波兰表达式也称为“后缀表达式”,相较于平常我们使用的“中缀表达式”,逆波兰表达式不须要括号来确定运算的优先级,比方5 * (2 + 3)对应的逆波兰表达式是5 2 3 + *。逆波兰表达式求值须要借助栈结构,扫描表达式遇到运算数就入栈,遇到运算符就出栈两个元素做运算,将运算效果入栈。表达式扫描结束后,栈中只有一个数,这个数就是终极的运算效果,直接出栈即可。
- import operator
-
-
- class Stack:
- """栈(FILO)"""
-
- def __init__(self):
- self.elems = []
-
- def push(self, elem):
- """入栈"""
- self.elems.append(elem)
-
- def pop(self):
- """出栈"""
- return self.elems.pop()
-
- @property
- def is_empty(self):
- """检查栈是否为空"""
- return len(self.elems) == 0
-
-
- def eval_suffix(expr):
- """逆波兰表达式求值"""
- operators = {
- '+': operator.add,
- '-': operator.sub,
- '*': operator.mul,
- '/': operator.truediv
- }
- stack = Stack()
- for item in expr.split():
- if item.isdigit():
- stack.push(float(item))
- else:
- num2 = stack.pop()
- num1 = stack.pop()
- stack.push(operators[item](num1, num2))
- return stack.pop()
复制代码 标题34:Python中怎样实现字符串替换操作?
Python中实现字符串替换大致有两类方法:字符串的replace方法和正则表达式的sub方法。
方法一:使用字符串的replace方法。
- message = 'hello, world!'
- print(message.replace('o', 'O').replace('l', 'L').replace('he', 'HE'))
复制代码 方法二:使用正则表达式的sub方法。
- import re
-
- message = 'hello, world!'
- pattern = re.compile('[aeiou]')
- print(pattern.sub('#', message))
复制代码 扩展:另有一个相关的口试题,对保存文件名的列表排序,要求文件名按照字母表和数字大小进行排序,比方对于列表filenames = ['a12.txt', 'a8.txt', 'b10.txt', 'b2.txt', 'b19.txt', 'a3.txt'],排序的效果是['a3.txt', 'a8.txt', 'a12.txt', 'b2.txt', 'b10.txt', 'b19.txt']。提示一下,可以通过字符串替换的方式为文件名补位,根据补位后的文件名用sorted函数来排序,大家可以思索下这个标题怎样办理。
标题35:怎样剖析Python代码的实行性能?
剖析代码性能可以使用Python标准库中的cProfile和pstats模块,cProfile的run函数可以实行代码并收集统计信息,创建出Stats对象并打印简单的剖析陈诉。Stats是pstats模块中的类,它是一个统计对象。固然,也可以使用三方工具line_profiler和memory_profiler来剖析每一行代码耗费的时间和内存,这两个三方工具都会用非常友爱的方式输出剖析结构。假如使用PyCharm,可以利用“Run”菜单的“Profile”菜单项对代码进行性能分析,PyCharm中可以用表格或者调用图(Call Graph)的方式来显示性能剖析的效果。
下面是使用cProfile剖析代码性能的例子。
example.py
- import cProfile
-
-
- def is_prime(num):
- for factor in range(2, int(num ** 0.5) + 1):
- if num % factor == 0:
- return False
- return True
-
-
- class PrimeIter:
-
- def __init__(self, total):
- self.counter = 0
- self.current = 1
- self.total = total
-
- def __iter__(self):
- return self
-
- def __next__(self):
- if self.counter < self.total:
- self.current += 1
- while not is_prime(self.current):
- self.current += 1
- self.counter += 1
- return self.current
- raise StopIteration()
-
-
- cProfile.run('list(PrimeIter(10000))')
复制代码 假如使用line_profiler三方工具,可以直接剖析is_prime函数每行代码的性能,须要给is_prime函数添加一个profiler装饰器,代码如下所示。
- @profiler
- def is_prime(num):
- for factor in range(2, int(num ** 0.5) + 1):
- if num % factor == 0:
- return False
- return True
复制代码 安装line_profiler。
- pip install line_profiler
复制代码 使用line_profiler。
运行效果如下所示。
- Line # Hits Time Per Hit % Time Line Contents
- ==============================================================
- 1 @profile
- 2 def is_prime(num):
- 3 86624 48420.0 0.6 50.5 for factor in range(2, int(num ** 0.5) + 1):
- 4 85624 44000.0 0.5 45.9 if num % factor == 0:
- 5 6918 3080.0 0.4 3.2 return False
- 6 1000 430.0 0.4 0.4 return True
复制代码 标题36:怎样使用`random`模块天生随机数、实现随机乱序和随机抽样?
点评:送人头的标题,由于Python标准库中的常用模块应该是Python开辟者都比较熟悉的内容,这个标题回假如答不上来,整个口试基本也就砸锅了。
- random.random()函数可以天生[0.0, 1.0)之间的随机浮点数。
- random.uniform(a, b)函数可以天生[a, b]或[b, a]之间的随机浮点数。
- random.randint(a, b)函数可以天生[a, b]或[b, a]之间的随机整数。
- random.shuffle(x)函数可以实现对序列x的原地随机乱序。
- random.choice(seq)函数可以从非空序列中取出一个随机元素。
- random.choices(population, weights=None, *, cum_weights=None, k=1)函数可以从总体中随机抽取(有放回抽样)出容量为k的样本并返回样本的列表,可以通过参数指定个体的权重,假如没有指定权重,个体被选中的概率均等。
- random.sample(population, k)函数可以从总体中随机抽取(无放回抽样)出容量为k的样本并返回样本的列表。
扩展:random模块提供的函数除了天生匀称分布的随机数外,还可以天生其他分布的随机数,比方random.gauss(mu, sigma)函数可以天生高斯分布(正态分布)的随机数;random.paretovariate(alpha)函数会天生帕累托分布的随机数;random.gammavariate(alpha, beta)函数会天生伽马分布的随机数。
标题37:表明一下线程池的工作原理。
点评:池化技术就是一种典型空间换时间的计谋,我们使用的数据库连接池、线程池等都是池化技术的应用,Python标准库currrent.futures模块的ThreadPoolExecutor就是线程池的实现,假如要弄清楚它的工作原理,可以参考下面的内容。
线程池是一种用于减少线程本身创建和销毁造成的开销的技术,属于典型的空间换时间操作。假如应用程序须要频仍的将使命派发到线程中实行,线程池就是必选项,由于创建和开释线程涉及到大量的系统底层操作,开销较大,假如能够在应用程序工作期间,将创建和开释线程的操作变成预创建和借还操作,将大大减少底层开销。线程池在应用程序启动后,立即创建一定数量的线程,放入空闲队列中。这些线程最开始都处于阻塞状态,不会消耗CPU资源,但会占用少量的内存空间。当使命到来后,从队列中取出一个空闲线程,把使命派发到这个线程中运行,并将该线程标记为已占用。当线程池中所有的线程都被占用后,可以选择主动创建一定数量的新线程,用于处理更多的使命,也可以选择让使命排队等待直到有空闲的线程可用。在使命实行完毕后,线程并不退出结束,而是继续保持在池中等待下一次的使命。当系统比较空闲时,大部分线程长时间处于闲置状态时,线程池可以主动销毁一部分线程,回收系统资源。基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的使命上,实行次数越多,每个使命所分担到的线程本身开销则越小。
一样平常线程池都必须具备下面几个组成部分:
- 线程池管理器:用于创建并管理线程池。
- 工作线程和线程队列:线程池中实际实行的线程以及保存这些线程的容器。
- 使命接口:将线程实行的使命抽象出来,形成使命接口,确保线程池与具体的使命无关。
- 使命队列:线程池中保存等待被实行的使命的容器。
标题38:举例说明什么环境下会出现`KeyError`、`TypeError`、`ValueError`。
举一个简单的例子,变量a是一个字典,实行int(a['x'])这个操作就有大概引发上述三种类型的异常。假如字典中没有键x,会引发KeyError;假如键x对应的值不是str、float、int、bool以及bytes-like类型,在调用int函数构造int类型的对象时,会引发TypeError;假如a[x]是一个字符串或者字节串,而对应的内容又无法处理成int时,将引发ValueError。
标题39:说出下面代码的运行效果。
- def extend_list(val, items=[]):
- items.append(val)
- return items
-
- list1 = extend_list(10)
- list2 = extend_list(123, [])
- list3 = extend_list('a')
- print(list1)
- print(list2)
- print(list3)
复制代码 点评:Python函数在界说的时候,默认参数items的值就被计算出来了,即[]。由于默认参数items引用了对象[],每次调用该函数,假如对items引用的列表进行了操作,下次调用时,默认参数还是引用之前的那个列表而不是重新赋值为[],以是列表中会有之前添加的元素。假如通过传参的方式为items重新赋值,那么items将引用到新的列表对象,而不再引用默认的那个列表对象。这个题在口试中经常被问到,通常不发起使用容器类型的默认参数,像PyLint这样的代码查抄工具也会对这种代码提出质疑和警告。
- [10, 'a']
- [123]
- [10, 'a']
复制代码 标题40:怎样读取大文件,比方内存只有4G,怎样读取一个大小为8G的文件?
很显然4G内存要一次性的加载大小为8G的文件是不现实的,遇到这种环境必须要考虑多次读取和分批次处理。在Python中读取文件可以先通过open函数获取文件对象,在读取文件时,可以通过read方法的size参数指定读取的大小,也可以通过seek方法的offset参数指定读取的位置,这样就可以控制单次读取数据的字节数和总字节数。除此之外,可以使用内置函数iter将文件对象处理成迭代器对象,每次只读取少量的数据进行处理,代码大致写法如下所示。
- with open('...', 'rb') as file:
- for data in iter(lambda: file.read(2097152), b''):
- pass
复制代码 在Linux系统上,可以通过split命令将大文件切割为小片,然后通过读取切割后的小文件对数据进行处理。比方下面的命令将名为filename的大文件切割为大小为512M的多个文件。
假如愿意, 也可以将名为filename的文件切割为10个文件,命令如下所示。
扩展:外部排序跟上述的环境非常类似,由于处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。“排序-归并算法”就是一种常用的外部排序计谋。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件,然后在归并阶段将这些临时文件组合为一个大的有序文件,这个大的有序文件就是排序的效果。
标题41:说一下你对Python中模块和包的理解。
每个Python文件就是一个模块,而保存这些文件的文件夹就是一个包,但是这个作为Python包的文件夹必须要有一个名为__init__.py的文件,否则无法导入这个包。通常一个文件夹下还可以有子文件夹,这也就意味着一个包下还可以有子包,子包中的__init__.py并不是必须的。模块和包办理了Python中命名冲突的标题,差别的包下可以有同名的模块,差别的模块下可以有同名的变量、函数或类。在Python中可以使用import或from ... import ...来导入包和模块,在导入的时候还可以使用as关键字对包、模块、类、函数、变量等进行别名,从而彻底办理编程中尤其是多人协作团队开辟时的命名冲突标题。
标题42:说一下你知道的Python编码规范。
点评:企业的Python编码规范基本上是参照PEP-8或谷歌开源项目风格指南来制定的,后者还提到了可以使用Lint工具来查抄代码的规范程度,口试的时候遇到这类标题,可以先说下这两个参照标准,然后挑重点说一下Python编码的留意事项。
- 使用空格来体现缩进而不要用制表符(Tab)。
- 和语法相关的每一层缩进都用4个空格来体现。
- 每行的字符数不要凌驾79个字符,假如表达式因太长而占据了多行,除了首行之外的别的各行都应该在正常的缩进宽度上再加上4个空格。
- 函数和类的界说,代码前后都要用两个空行进行分隔。
- 在同一个类中,各个方法之间应该用一个空行进行分隔。
- 二元运算符的左右两侧应该保留一个空格,而且只要一个空格就好。
- 变量、函数和属性应该使用小写字母来拼写,假如有多个单词就使用下划线进行连接。
- 类中受保护的实例属性,应该以一个下划线开头。
- 类中私有的实例属性,应该以两个下划线开头。
- 类和异常的命名,应该每个单词首字母大写。
- 模块级别的常量,应该采用全大写字母,假如有多个单词就用下划线进行连接。
- 类的实例方法,应该把第一个参数命名为self以体现对象自身。
- 类的类方法,应该把第一个参数命名为cls以体现该类自身。
- 采用内联形式的否定词,而不要把否定词放在整个表达式的前面。比方:if a is not b就比if not a is b更容易让人理解。
- 不要用查抄长度的方式来判断字符串、列表等是否为None或者没有元素,应该用if not x这样的写法来查抄它。
- 就算if分支、for循环、except异常捕获等中只有一行代码,也不要将代码和if、for、except等写在一起,分开写才会让代码更清晰。
- import语句总是放在文件开头的地方。
- 引入模块的时候,from math import sqrt比import math更好。
- 假如有多个import语句,应该将其分为三部分,从上到下分别是Python标准模块、第三方模块和自界说模块,每个部分内部应该按照模块名称的字母表次序来分列。
标题43:运行下面的代码是否会报错,假如报错请说明哪里有什么样的错,假如不报错请说出代码的实行效果。
- class A:
- def __init__(self, value):
- self.__value = value
-
- @property
- def value(self):
- return self.__value
-
- obj = A(1)
- obj.__value = 2
- print(obj.value)
- print(obj.__value)
复制代码 点评:这道题有两个观察点,一个观察点是对_和__开头的对象属性访问权限以及@property装饰器的相识,别的一个观察的点是对动态语言的理解,不须要过多的表明。
扩展:假如不希望代码运行时动态的给对象添加新属性,可以在界说类时使用__slots__邪术。比方,我们可以在上面的A中添加一行__slots__ = ('__value', ),再次运行上面的代码,将会在原来的第10行处产生AttributeError错误。
标题44:对下面给出的字典按值从大到小对键进行排序。
- prices = {
- 'AAPL': 191.88,
- 'GOOG': 1186.96,
- 'IBM': 149.24,
- 'ORCL': 48.44,
- 'ACN': 166.89,
- 'FB': 208.09,
- 'SYMC': 21.29
- }
复制代码 点评:sorted函数的高阶用法在口试的时候经常出现,key参数可以传入一个函数名或一个Lambda函数,该函数的返回值代表了在排序时比较元素的依据。
- sorted(prices, key=lambda x: prices[x], reverse=True)
复制代码 标题45:说一下`namedtuple`的用法和作用。
点评:Python标准库的collections模块提供了很多有用的数据结构,这些内容并不是每个开辟者都清楚,就比如标题问到的namedtuple,在我参加过的口试中,90%的口试者都不能正确的说出它的作用和应用场景。别的,deque也是一个非常有用但又经常被忽视的类,另有Counter、OrderedDict 、defaultdict 、UserDict等类,大家清楚它们的用法吗?
在使用面向对象编程语言的时候,界说类是最常见的一件变乱,有的时候,我们会用到只有属性没有方法的类,这种类的对象通常只用于组织数据,并不能接收消息,以是我们把这种类称为数据类或者退化的类,就像C语言中的结构体那样。我们并不发起使用这种退化的类,在Python中可以用namedtuple(命名元组)来替换这种类。
- from collections import namedtuple
-
- Card = namedtuple('Card', ('suite', 'face'))
- card1 = Card('红桃', 13)
- card2 = Card('草花', 5)
- print(f'{card1.suite}{card1.face}')
- print(f'{card2.suite}{card2.face}')
复制代码 命名元组与平凡元组一样是不可变容器,一旦将数据存储在namedtuple的顶层属性中,数据就不能再修改了,也就意味着对象上的所有属性都遵照“一次写入,多次读取”的原则。和平凡元组差别的是,命名元组中的数据有访问名称,可以通过名称而不是索引来获取保存的数据,不但在操作上更加简单,代码的可读性也会更好。
命名元组的本质就是一个类,以是它还可以作为父类创建子类。除此之外,命名元组内置了一系列的方法,比方,可以通过_asdict方法将命名元组处理成字典,也可以通过_replace方法创建命名元组对象的浅拷贝。
- class MyCard(Card):
-
- def show(self):
- faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
- return f'{self.suite}{faces[self.face]}'
-
-
- print(Card) # <class '__main__.Card'>
- card3 = MyCard('方块', 12)
- print(card3.show()) # 方块Q
- print(dict(card1._asdict())) # {'suite': '红桃', 'face': 13}
- print(card2._replace(suite='方块')) # Card(suite='方块', face=5)
复制代码 总而言之,命名元组能更好的组织数据结构,让代码更加清晰和可读,在很多场景下是元组、字典和数据类的替换品。在须要创建占用空间更少的不可变类时,命名元组就是很好的选择。
标题46:按照标题要求写出对应的函数。
要求:写一个函数,传入一个有若干个整数的列表,该列表中某个元素出现的次数凌驾了50%,返回这个元素。
- def more_than_half(items):
- temp, times = None, 0
- for item in items:
- if times == 0:
- temp = item
- times += 1
- else:
- if item == temp:
- times += 1
- else:
- times -= 1
- return temp
复制代码 点评:LeetCode上的标题,在Python口试中出现过,利用元素出现次数凌驾了50%这一特征,出现和temp类似的元素就将计数值加1,出现和temp差别的元素就将计数值减1。假如计数值为0,说明之前出现的元素已经对终极的效果没有影响,用temp记下当前元素并将计数值置为1。终极,出现次数凌驾了50%的这个元素一定会被赋值给变量temp。
标题47:按照标题要求写出对应的函数。
要求:写一个函数,传入的参数是一个列表(列表中的元素大概也是一个列表),返回该列表最大的嵌套深度。比方:列表[1, 2, 3]的嵌套深度为1,列表[[1], [2, [3]]]的嵌套深度为3。
- def list_depth(items):
- if isinstance(items, list):
- max_depth = 1
- for item in items:
- max_depth = max(list_depth(item) + 1, max_depth)
- return max_depth
- return 0
复制代码 点评:看到标题应该能够比较自然的想到使用递归的方式查抄列表中的每个元素。
标题48:按照标题要求写出对应的装饰器。
要求:有一个通过网络获取数据的函数(大概会由于网络原因出现异常),写一个装饰器让这个函数在出现指定异常时可以重试指定的次数,并在每次重试之前随机延迟一段时间,最长延迟时间可以通过参数进行控制。
方法一:
- from functools import wraps
- from random import random
- from time import sleep
-
-
- def retry(*, retry_times=3, max_wait_secs=5, errors=(Exception, )):
-
- def decorate(func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- for _ in range(retry_times):
- try:
- return func(*args, **kwargs)
- except errors:
- sleep(random() * max_wait_secs)
- return None
-
- return wrapper
-
- return decorate
复制代码 方法二:
- from functools import wraps
- from random import random
- from time import sleep
-
-
- class Retry(object):
-
- def __init__(self, *, retry_times=3, max_wait_secs=5, errors=(Exception, )):
- self.retry_times = retry_times
- self.max_wait_secs = max_wait_secs
- self.errors = errors
-
- def __call__(self, func):
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- for _ in range(self.retry_times):
- try:
- return func(*args, **kwargs)
- except self.errors:
- sleep(random() * self.max_wait_secs)
- return None
-
- return wrapper
复制代码 点评:我们不止一次强调过,装饰器几乎是Python口试必问内容,这个标题比之前的标题轻微复杂一些,它须要的是一个参数化的装饰器。
标题49:写一个函数实现字符串反转,尽大概写出你知道的所有方法。
点评:烂大街的标题,基本上算是送人头的标题。
方法一:反向切片
- def reverse_string(content):
- return content[::-1]
复制代码 方法二:反转拼接
- def reverse_string(content):
- return ''.join(reversed(content))
复制代码 方法三:递归调用
- def reverse_string(content):
- if len(content) <= 1:
- return content
- return reverse_string(content[1:]) + content[0]
复制代码 方法四:双端队列
- from collections import deque
-
- def reverse_string(content):
- q = deque()
- q.extendleft(content)
- return ''.join(q)
复制代码 方法五:反向组装
- from io import StringIO
-
- def reverse_string(content):
- buffer = StringIO()
- for i in range(len(content) - 1, -1, -1):
- buffer.write(content[i])
- return buffer.getvalue()
复制代码 方法六:反转拼接
- def reverse_string(content):
- return ''.join([content[i] for i in range(len(content) - 1, -1, -1)])
复制代码 方法七:半截交换
- def reverse_string(content):
- length, content= len(content), list(content)
- for i in range(length // 2):
- content[i], content[length - 1 - i] = content[length - 1 - i], content[i]
- return ''.join(content)
复制代码 方法八:对位交换
- def reverse_string(content):
- length, content= len(content), list(content)
- for i, j in zip(range(length // 2), range(length - 1, length // 2 - 1, -1)):
- content[i], content[j] = content[j], content[i]
- return ''.join(content)
复制代码 扩展:这些方法其实都是大同小异的,口试的时候能够给出几种有代表性的就足够了。给大家留一个思索题,上面这些方法,哪些做法的性能较好呢?我们之前提到过剖析代码性能的方法,大家可以用这些方法来检验下你给出的答案是否正确。
标题50:按照标题要求写出对应的函数。
要求:列表中有1000000个元素,取值范围是[1000, 10000),设计一个函数找出列表中的重复元素。
- def find_dup(items: list):
- dups = [0] * 9000
- for item in items:
- dups[item - 1000] += 1
- for idx, val in enumerate(dups):
- if val > 1:
- yield idx + 1000
复制代码 点评:这道题的解法和计数排序的原理一致,虽然元素的数量非常多,但是取值范围[1000, 10000)并不是很大,只有9000个大概的取值,以是可以用一个能够保存9000个元素的dups列表来记录每个元素出现的次数,dups列表所有元素的初始值都是0,通过对items列表中元素的遍历,当出现某个元素时,将dups列表对应位置的值加1,末了dups列表中值大于1的元素对应的就是items列表中重复出现过的元素。
**---------------------------END---------------------------
**
感兴趣的小伙伴,赠送全套Python学习资料,包含口试题、简历资料等具体看下方。
|