【编程之路】Python编程实例解析

Python编程实例解析

写一个删除列表中重复元素的函数,要求去重后元素相对位置保持不变。

def dedup(a):
    b = []
    c = set()
    for i in range(len(a)):
        if a[i] not in c:
            b.append(a[i])
            c.add(a[i])
    return b

print(dedup([2,6,4,5,2,6,8,7,9,2]))
# [2, 6, 4, 5, 8, 7, 9]

使用 Python 代码实现遍历一个文件夹的操作。

Python 标准库 os 模块的 walk 函数提供了遍历一个文件夹的功能,它返回一个生成器。每次会生成一个元组:(root,dirs,files)

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 函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。

假设你使用的是官方的 CPython,说出下面代码的运行结果。

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 对象。需要大家注意的是,这条规则对数值型适用,但对字符串则需要考虑字符串的长度,这一点大家可以自行证明。

下面这段代码的执行结果是什么?

def multiply():
    return [lambda x: i * x for i in range(4)]

print([m(100) for m in multiply()])

运行结果:

[300, 300, 300, 300]

上面代码的运行结果很容易被误判为 [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()])

方法二:使用偏函数,彻底避开闭包。

偏函数是将所要承载的函数作为 partial() 函数的第一个参数,原函数的各个参数依次作为 partial() 函数后续的参数,除非使用关键字参数。

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()])

用 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

写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。

def count_letters(items):
    result = {}
    for item in items:
        if isinstance(item, (int, float)):
            result[item] = result.get(item, 0) + 1
    return result
  • isinstance() 是一个内置函数,用于判断一个对象是否是一个已知的类型,类似 type()
  • dict.get(key[, value])key:字典中要查找的键。value:可选,如果指定键的值不存在时,返回该默认值。

也可以直接使用 Python 标准库中 collections 模块的 Counter 类来解决这个问题,Counterdict 的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。

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

现有 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)

print(change_money(99))

在这里插入图片描述

在上面的代码中,我们用 lru_cache 装饰器装饰了递归函数 change_money,如果不做这个优化,上面代码的渐近时间复杂度将会是 3 n 3^n 3n,而如果参数 total 的值是 99,这个运算量是非常巨大的。lru_cache 装饰器会缓存函数的执行结果,这样就可以减少重复运算所造成的开销,这是空间换时间的策略,也是动态规划的编程思想。lru_cache 缓存在应用进程的内存中,应用被关闭则被清空。

写一个函数,给定矩阵的阶数 n,输出一个螺旋式数字矩阵。

关键点是要设置一个螺旋的方向 direction,一圈过后进行取模操作。

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

show_spiral_matrix(6)

【编程之路】Python编程实例解析_第1张图片

阅读下面的代码,写出程序的运行结果。

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

说出下面代码的运行结果。

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,而 ParentChild2 并不受影响。

执行 Parent.x = 3 会重新给 Parent 类的 x 属性赋值为 3,由于 Child2x 属性继承自 Parent,所以 Child2.x 的值也是 3;而之前我们为 Child1 重新绑定了 x 属性,那么它的 x 属性值不会受到 Parent.x = 3 的影响,还是之前的值 2

输入年月日,判断这个日期是这一年的第几天。

不使用标准库中的模块和函数。

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

写一个记录函数执行时间的装饰器。

方法一:用函数实现装饰器。

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
@record_time
def sum_(b):
    a = 0
    for i in range(b):
        a = a + 1
    return a

sum_(10000000)

在这里插入图片描述

方法二:用类实现装饰器。类有 __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

注意 output 代表的是一个参数,如果不传参,则不会输出函数运行的时间。

@record_time(print)
def sum_(b):
    a = 0
    for i in range(b):
        a = a + 1
    return a

sum_(10000000)

在这里插入图片描述

阅读下面的代码说出运行结果。

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 中的 MROMethod Resolution Order,方法解析顺序)。在没有多重继承的情况下,向对象发出一个消息,如果对象没有对应的方法,那么向上(父类)搜索的顺序是非常清晰的。如果向上追溯到 object 类(所有类的父类)都没有找到对应的方法,那么将会引发 AttributeError 异常。但是有多重继承尤其是出现 菱形继承(钻石继承)的时候,向上追溯到底应该找到那个方法就得确定 MRO。Python3 中的类以及Python2 中的新式类使用 C3 算法来确定 MRO,它是一种类似于 广度优先搜索 的方法;Python2 中的旧式类(经典类)使用 深度优先搜索 来确定 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

编写一个函数实现对逆波兰表达式求值,不能使用 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()

Python中如何实现字符串替换操作?

  • 使用字符串的 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 函数来排序,大家可以思考下这个问题如何解决。

如何使用 random 模块生成随机数、实现随机乱序和随机抽样?

  • 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) 函数会生成伽马分布的随机数。

说出下面代码的运行结果。

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 这样的代码检查工具也会对这种代码提出质疑和警告。

运行下面的代码是否会报错,如果报错请说明哪里有什么样的错,如果不报错请说出代码的执行结果。

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 装饰器的了解,另外一个考察的点是对动态语言的理解,不需要过多的解释。

Python 内置了 3 种函数装饰器,分别是 @staticmethod@classmethod@property,其中 staticmethod()classmethod()property() 都是 Python 的内置函数。

property 函数字如其名,其在装饰器的主要应用在于,我们要实现

  • 保护类的封装特性
  • 让开发者可以使用 对象.属性 的方式操作操作类属性

除了使用 property() 函数,还可以使用 @property 装饰器。通过 @property 装饰器,可以直接通过方法名来访问方法,不需要在方法名后添加一对 () 小括号。

如果不希望代码运行时动态的给对象添加新属性,可以在定义类时使用 __slots__ 魔法。例如,我们可以在上面的 A 中添加一行 __slots__ = (‘__value’, ),再次运行上面的代码,将会在原来的第 10 行处产生 AttributeError 错误。

在这里插入图片描述

Python 中的 new-style class 要求继承 Python 中的一个内建类型, 一般继承 object,也可以继承 list 或者 dict 等其他的内建类型。在 Python 新式类中,可以定义一个变量 __slots__,它的作用是阻止在实例化类时为实例分配 dict,默认情况下每个类都会有一个 dict,通过 __dict__ 访问,这个 dict 维护了这个实例的所有属性。

由于每次实例化一个类都要分配一个新的 dict,因此存在空间的浪费,因此有了 __slots____slots__ 是一个元组,包括了当前能访问到的属性。当定义了 slots 后,slots 中定义的变量变成了类的描述符,相当于 Java,C++ 中的成员变量声明,类的实例只能拥有 slots 中定义的变量,不能再增加新的变量。注意:定义了 slots 后,就不再有 dict

对下面给出的字典按值从大到小对键进行排序。

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) 

在这里插入图片描述

写一个函数,传入的参数是一个列表(列表中的元素可能也是一个列表),返回该列表最大的嵌套深度。例如:列表 [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

有一个通过网络获取数据的函数(可能会因为网络原因出现异常),写一个装饰器让这个函数在出现指定异常时可以重试指定的次数,并在每次重试之前随机延迟一段时间,最长延迟时间可以通过参数进行控制。

方法一:函数实现

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

参考资料

【建议收藏】50 道硬核的 Python 面试题!

你可能感兴趣的:(编程之路,Python学习笔记,python,Python编程,装饰器)