py 题型

115 阅读17分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情 题目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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 题目013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。 点评:送人头的题目,不解释。

def count_letters(items): result = {} for item in items: if isinstance(item, (int, float)): result[item] = result.get(item, 0) + 1 return result 1 2 3 4 5 6 也可以直接使用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))} 1 2 3 4 5 6 题目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)) 1 2 3 4 5 6 7 8 说明: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) 1 2 3 4 5 6 7 8 9 10 11 说明:在上面的代码中,我们用lru_cache装饰器装饰了递归函数change_money,如果不做这个优化,上面代码的渐近时间复杂度将会是O ( 3 N ) O(3^N)O(3 N ),而如果参数total的值是99,这个运算量是非常巨大的。lru_cache装饰器会缓存函数的执行结果,这样就可以减少重复运算所造成的开销,这是空间换时间的策略,也是动态规划的编程思想。

题目016:写一个函数,给定矩阵的阶数n,输出一个螺旋式数字矩阵。 例如:n = 2,返回:

1 2 4 3 1 2 例如:n = 3,返回:

1 2 3 8 9 4 7 6 5 1 2 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()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 题目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'})) 1 2 3 4 5 6 点评:生成式(推导式)属于Python的特色语法之一,几乎是面试必考内容。Python中通过生成式字面量语法,可以创建出列表、集合、字典。

[3, 4] [1, 3] [('a', 1), ('b', 2), ('c', 3), ('d', 4)] {2: 'item4', 4: 'item16', 6: 'item36'} 6 1 2 3 4 5 题目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 2 3 4 5 6 7 8 9 10 11 12 13 14 点评:运行上面的代码首先输出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。

1 1 1 1 2 1 3 2 3 1 2 3 题目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.cos logging 和日志系统相关的类和函数,例如:logging.Logger、logging.Handler json / pickle 实现对象序列化和反序列的模块,例如:json.loads、json.dumps hashlib 封装了多种哈希摘要算法的模块,例如:hashlib.md5、hashlib.sha1 urllib 包含了和URL相关的子模块,例如:urllib.request、urllib.parse itertools 提供各种迭代器的模块,例如:itertools.cycle、itertools.product functools 函数相关工具模块,例如:functools.partial、functools.lru_cache collections / heapq 封装了常用数据结构和算法的模块,例如:collections.deque threading / multiprocessing 多线程/多进程相关类和函数的模块,例如:threading.Thread concurrent.futures / asyncio 并发编程/异步编程相关的类和函数的模块,例如:ThreadPoolExecutor base64 提供BASE-64编码相关函数的模块,例如:bas64.encode csv 和读写CSV文件相关的模块,例如:csv.reader、csv.writer profile / cProfile / pstats 和代码性能剖析相关的模块,例如:cProfile.run、pstats.Stats unittest 和单元测试相关的模块,例如: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 1 2 3 4 5 6 7 8 9 10 11 12 13 方法二:使用标准库中的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 1 2 3 4 5 6 题目22:平常工作中用什么工具进行静态代码分析。 点评:静态代码分析工具可以从代码中提炼出各种静态属性,这使得开发者可以对代码的复杂性、可维护性和可读性有更好的了解,这里所说的静态属性包括:

代码是否符合编码规范,例如:PEP-8。 代码中潜在的问题,包括:语法错误、缩进问题、导入缺失、变量覆盖等。 代码中的坏味道。 代码的复杂度。 代码的逻辑问题。 工作中静态代码分析主要用到的是Pylint和Flake8。Pylint可以检查出代码错误、坏味道、不规范的代码等问题,较新的版本中还提供了代码复杂度统计数据,可以生成检查报告。Flake8封装了Pyflakes(检查代码逻辑错误)、McCabe(检查代码复杂性)和Pycodestyle(检查代码是否符合PEP-8规范)工具,它可以执行这三个工具提供的检查。

题目23:说一下你知道的Python中的魔术方法。 点评:魔术方法也称为魔法方法,是Python中的特色语法,也是面试中的高频问题。

魔术方法 作用 newinitdel 创建和销毁对象相关 addsubmuldivfloordivmod 算术运算符相关 eqneltgtlege 关系运算符相关 posneginvert 一元运算符相关 lshiftrshiftandorxor 位运算相关 enterexit 上下文管理器协议 iternextreversed 迭代器协议 intlongfloatocthex 类型/进制转换相关 strreprhashdir 对象表述相关 lengetitemsetitemcontainsmissing 序列相关 copydeepcopy 对象拷贝相关 callsetattrgetattrdelattr 其他魔术方法 题目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

1 2 3 4 5 6 7 8 9 13 14 15 16 17 18 19 题目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 1 2 3 4 5 单元测试中的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()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 点评:这道题考查到了两个知识点:

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。 ACBD 1 题目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()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 题目34:Python中如何实现字符串替换操作? Python中实现字符串替换大致有两类方法:字符串的replace方法和正则表达式的sub方法。

方法一:使用字符串的replace方法。

message = 'hello, world!' print(message.replace('o', 'O').replace('l', 'L').replace('he', 'HE')) 1 2 方法二:使用正则表达式的sub方法。

import re

message = 'hello, world!' pattern = re.compile('[aeiou]') print(pattern.sub('#', message)) 1 2 3 4 5 扩展:还有一个相关的面试题,对保存文件名的列表排序,要求文件名按照字母表和数字大小进行排序,例如对于列表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