Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the wp-pagenavi domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /var/www/blog.zhujinhui.net/wp-includes/functions.php on line 6114

Notice: 函数 _load_textdomain_just_in_time 的调用方法不正确twentyseventeen 域的翻译加载触发过早。这通常表示插件或主题中的某些代码运行过早。翻译应在 init 操作或之后加载。 请查阅调试 WordPress来获取更多信息。 (这个消息是在 6.7.0 版本添加的。) in /var/www/blog.zhujinhui.net/wp-includes/functions.php on line 6114
煅魂-JeffreyChu的修炼屋 – 第 5 页 – 做一个胡思乱想的程序员

面试官问:说说Python的is和==区别

下面有段代码(以下简称测试例子),你能在不看答案情况下知道运行结果吗?

a = 1
b = 1
print(a is b) # True
print(a == b) # True
a = 10000
b = 10000
print(a is b) # 有时True 有时False
print(a == b) # True
a = -6
b = -6
print(a is b) # 有时True 有时False
print(a == b) # True
a = [1, 5]
b = [1, 5]
c = a
print(a is b) # False
print(a == b) # True
print(c is a) # True
print(c == a) # True
print(c is b) # False
print(c == b) # True

概念

在 Python 中,is== 都用于比较,是分别根据对象的内存地址和对象的值来判断,所以用途上也不一样。

用图书馆的书籍可以打个值和内存地址形象的比喻:

  • 对象的值,就像书籍内容:
  • 每本书都有它的内容,例如文字、图片等,这代表了书的“值”。
  • 不同的书可能有相同的内容(例如两本相同的书),但它们仍然是不同的实体。
  • 对象的内存地址,就好比书籍在图书馆中的位置:
  • 每本书在图书馆中都有一个特定的位置,比如在某个书架的某一层某一排。
  • 即使两本书的内容完全相同,如果它们在不同的位置上,它们就是两个不同的对象。
is身份比较

is 用于比较两个对象的身份,即比较两个对象在内存中的地址(或者叫引用,内存地址可以用Python内建函数id()查看,它会返回一个整数)是否相同。

换句话说,is检查的是两个对象是否是同一个对象。回到开头测试例子里:

# 注意:以下id函数计算出的内存地址整数不一定是代码注释里的那些整数
a = 1
b = 1
print(id(a), id(b)) # 输出:2398302306544 2398302306544
print(a is b) # True,因为a和b的id内存地址相同,所以a和b是同一个对象

a = [1, 5]
b = [1, 5]
print(id(a), id(b)) # 输出:2398357892288 2398357883648
print(a is b)  # 输出: False,因为a和b的id内存地址不同,所以a和b是不同的对象,即使它们的值相同

c = a
print(id(a), id(c)) # 输出:2398357892288 2398357892288
print(a is c)  # 输出: True,因为c和a的id内存地址相同,所以c和a是同一个对象
==值比较

== 用于比较两个对象的值(真实数据内容)是否相等,而不是对象本身是否相同(虽然拗口,但事实如此)。

这意味着即使是两个不同的对象,只要它们的值相同,==比较的结果就是 True

a = 1
b = 1
print(a == b)  # 输出: True,因为 a 和 b 的值相同

a = [1, 5]
b = [1, 5]
c = a
print(a == b)  # 输出: True,因为 a 和 b 的值相同
print(c == b)  # 输出: True,因为 c 和 b 的值相同

== 还可以通过重载 __eq__ 方法来自定义值比较行为。

再举回书籍的例子,我们用书籍内容content来表示对象的值,location表示对象的内存地址:

class Book:
    def __init__(self, location, content):
        self.location = location
        self.content = content

    def __eq__(self, other):
        if isinstance(other, Book):
            return self.content == other.content
        return False

    def __repr__(self):
        return f"Book(content='{self.content}')"

# 创建两个 Book 对象
book1 = Book(location="A", content="Hello")
book2 = Book(location="B", content="Hello")
book3 = Book(location="C", content="Hi")

# 比较对象
print(book1 == book2)  # 输出: True,因为 content 相同
print(book1 == book3)  # 输出: False,因为 content 不同

# 查看对象内存地址,都不同的对象
print(id(book1)) # 输出: 1907430631760
print(id(book2)) # 输出: 1907430632240
print(id(book3)) # 输出: 1907430632336

注意

重用缓存对象

你也许发现了is在某些情况下判断并不稳定,比如测试例子中ab都等于10000(或-6)时,a is b有时候会等于False,有时候会等于True

这是因为Python对于一些小整数(通常在-5256之间,范围并不是固定)和一些字符串进行了缓存,所以这些对象在内存中会被重用

这会导致当比较这些小整数和字符串时,is 比较时可能返回 True。因此涉及到这些对象进行比较时,一般不用is,而是==

单例模式

在单例模式下,所有构造出来的对象实际上都是同一个实例,因此使用 is== 进行比较时,结果都会是 True

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class MyClass(metaclass=SingletonMeta):
    pass


a = MyClass()
b = MyClass()
print(a is b)  # 输出:True
print(a == b)  # 输出:True

单例对象一般都是用is来判断, 这是因为使用 is 来判断更加直观和高效, is比较本质是两个内存地址的整数值比较,而 == 在某些值比较时需要进行额外计算消耗资源,比如字符串/可变对象值比较都比整数值比较更耗性能。

这也是为什么在与NoneTrueFalse等特殊的单例对象比较时,都建议使用 is,而不是 ==

应用场景

  • is主要用于判定是不是同一个对象,在与单例对象比较时,一般都是有is来判断。
  • ==通常是用于两个对象确切的值的比较,比如字符串比较,数值比较、数组比较等。

总结

  • ==:比较两个对象的是否相等。
  • is:比较两个对象的内存地址是否相同。

如果你觉得本文有帮助到你,非常期待得到你的支持和鼓励;如果有其他问题或补充,欢迎评论区留言交流。

collections,一个让Python开发更高效的标准库

介绍

collections 模块是 Python 标准库的一部分,提供了多种增强的数据类型,包括 namedtupledequeCounterOrderedDictdefaultdictChainMap

这些数据类型比内置类型更灵活高效,适用于特定场景。掌握和合理使用这些数据结构,你会发现日常开发效率会非常高。

常见数据结构

namedtuple

namedtuple 是 Python collections 模块中的一个工厂函数,用于创建具有命名字段的不可变序列。它提供了类似于元组的性能和内存效率,同时可以通过名称访问其元素,从而提高代码的可读性可维护性

下面通过一段关于操作 地理坐标(Geolocation) 的代码来详细说明:如何创建 namedtuple、访问其元素,以及 namedtuple 的方法和属性。

# 导入
from collections import namedtuple

# 定义 namedtuple 类型 Geo
Geo = namedtuple('Geo', ['latitude', 'longitude'])

# 创建几个 Geo 实例
a_geo = Geo(39.9042, 116.4074)
b_geo = Geo(31.2304, 121.4737)
c_geo = Geo(23.1291, 113.2644)

# 打印地理坐标
print(f"a_geo: {a_geo.latitude}, {a_geo.longitude}")  # 输出: a_geo: 39.9042, 116.4074
print(f"b_geo: {b_geo.latitude}, {b_geo.longitude}")  # 输出: b_geo: 31.2304, 121.4737
print(f"c_geo: {c_geo.latitude}, {c_geo.longitude}")  # 输出: c_geo: 23.1291, 113.2644

# 使用 _make() 方法创建实例
data = [22.5431, 114.0579]
d_geo = Geo._make(data)
print(f"d_geo: {d_geo.latitude}, {d_geo.longitude}")  # 输出: d_geo: 22.5431, 114.0579

# 使用 _asdict() 方法将 namedtuple 转换为字典
a_dict = a_geo._asdict()
print(a_dict)  # 输出: OrderedDict([('latitude', 39.9042), ('longitude', 116.4074)])

# 使用 _replace() 方法更新实例
updated_a = a_geo._replace(latitude=39.913818)
print(updated_a)  # 输出: Geo(latitude=39.913818, longitude=116.4074)

# 查看字段名称
print(Geo._fields)  # 输出: ('latitude', 'longitude')

应用场景

  • 数据记录:用于表示数据库查询结果或日志记录。
  • 配置项:用于保存配置信息,例如服务器配置或应用程序设置。
  • 数据传输:用于定义消息格式,方便序列化和传输数据。

deque

deque 是一个双端队列,它支持从两端进行高效的添加和删除操作。

基本使用

deque 支持从两端进行快速的添加和删除操作。

from collections import deque

# 创建一个空的 deque
my_deque = deque()

# 创建包含元素的 deque
my_deque_with_data = deque([1, 2, 3, 4, 5])

# 添加元素到 deque 的右端
my_deque.append(6)

# 添加元素到 deque 的左端
my_deque.appendleft(0)

# 从 deque 的右端删除元素
right_element = my_deque.pop()

# 从 deque 的左端删除元素
left_element = my_deque.popleft()

deque 的方法和属性

  • append(item): 将元素添加到 deque 的右端。
  • appendleft(item): 将元素添加到 deque 的左端。
  • pop(): 从 deque 的右端删除并返回一个元素。
  • popleft(): 从 deque 的左端删除并返回一个元素。
  • extend(iterable): 将可迭代对象中的元素添加到 deque 的右端。
  • extendleft(iterable): 将可迭代对象中的元素添加到 deque 的左端。
  • rotate(n): 将 deque 向右循环移动 n 步(如果 n 是负数,则向左移动)。
  • clear(): 清空 deque 中的所有元素。
  • count(item): 返回 deque 中等于 item 的元素个数。
  • reverse(): 将 deque 中的元素逆序排列。
  • copy(): 返回 deque 的浅拷贝。

我们来用这些方法实现一个基于LRU策略(最近最少使用)的缓存类来慢慢感受下deque的高级玩法:

from collections import deque

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = deque(maxlen=capacity)  # 使用 deque 作为缓存
        self.cache_map = {}  # 用于存储键值对的映射关系

    def get(self, key):
        if key in self.cache_map:
            # 如果键存在于缓存中,则将其移到缓存队列的右端(最近使用)
            self.cache.remove(key)
            self.cache.append(key)
            return self.cache_map[key]
        else:
            return -1

    def put(self, key, value):
        if key in self.cache_map:
            # 如果键已存在于缓存中,则更新值并将其移到缓存队列的右端(最近使用)
            self.cache.remove(key)
            self.cache.append(key)
            self.cache_map[key] = value
        else:
            if len(self.cache) == self.capacity:
                # 如果缓存已满,则移除最左端(最久未使用)的键值对
                removed_key = self.cache.popleft()
                del self.cache_map[removed_key]
            # 将新的键值对添加到缓存队列的右端(最近使用)
            self.cache.append(key)
            self.cache_map[key] = value

    def clear(self):
        # 清空缓存队列和映射关系
        self.cache.clear()
        self.cache_map.clear()

    def extend(self, iterable):
        # 将可迭代对象中的元素添加到缓存队列的右端(最近使用)
        self.cache.extend(iterable)

    def __repr__(self):
        return str(self.cache)


# 测试示例
cache = LRUCache(3)
cache.put(1, 'a')
cache.put(2, 'b')
cache.put(3, 'c')
print(cache)  # 输出: deque([1, 2, 3], maxlen=3)

cache.put(4, 'd')
print(cache)  
# 输出: deque([2, 3, 4], maxlen=3)
# 因为元素超过了3个,所以淘汰了元素 1,添加保留了元素 4

cache.clear()
print(cache)  # 输出: deque([], maxlen=3)

cache.extend([4, 5, 6])
print(cache)  # 输出: deque([4, 5, 6], maxlen=3)

应用场景

  • 队列和栈:用于实现队列(FIFO)和栈(LIFO)等数据结构。
  • 缓存:用于实现LRU(Least Recently Used)缓存算法,保留最近访问的元素,丢弃最旧的元素。
  • 任务调度:用于实现异步任务调度器,管理任务队列并支持快速的入队和出队操作。

Counter

常用于计数可哈希对象。

基本使用

from collections import Counter

# 创建一个 Counter 对象
my_counter = Counter([1, 1, 2, 3, 3, 3, 4, 4, 5])
# 获取元素的计数
count_of_3 = my_counter[3]  # 输出: 3

# 更新计数
my_counter[3] += 1

# 添加新元素
my_counter[6] = 1

方法和属性

  • elements(): 返回一个迭代器,包含 Counter 对象中的所有元素,重复次数与计数相等。
  • most_common(n): 返回前 n 个最常见的元素及其计数,以列表形式返回。
  • subtract(iterable): 从 Counter 对象中减去可迭代对象中的元素的计数。
  • update(iterable): 将可迭代对象中的元素添加到 Counter 对象中。
  • clear(): 清空 Counter 对象中的所有元素。
  • copy(): 返回 Counter 对象的浅拷贝。
  • items(): 返回 Counter 对象的键值对。
  • keys(): 返回 Counter 对象的键。
  • values(): 返回 Counter 对象的值。
  • most_common(n): 返回前 n 个最常见的元素及其计数。

你会发现Counter有部分类似dict字段的方法,这是因为Counter类继承了dict

通过实现一个打工人出生地的统计计数器来感受下Counter类的使用:

from collections import Counter

# 模拟一堆大学生的出生地数据
birthplaces = ['Beijing', 'Shanghai', 'Guangzhou', 'Beijing', 'Shanghai']
# 创建 Counter 对象统计出生地
birthplace_counter = Counter(birthplaces)
# 获取出生地为上海的学生人数
shanghai_students = birthplace_counter['Shanghai']
# 更新计数,添加更多学生的出生地数据
more_birthplaces = ['Beijing', 'Shanghai', 'Chongqing', 'Shanghai', 'Shenzhen']
birthplace_counter.update(more_birthplaces)
# 获取出生地为北京的学生人数
beijing_students = birthplace_counter['Beijing']
# 统计有哪些地区
uniq_birthplaces = list(birthplace_counter.keys())
# 获取计数最多的 2 个出生地及其计数
top_birthplaces = birthplace_counter.most_common(2)
# 从统计中减去一部分出生地数据
subtract_birthplaces = ['Beijing', 'Shanghai']
birthplace_counter.subtract(subtract_birthplaces)
# 获取计数最少的 1 个出生地及其计数
bottom_birthplace = birthplace_counter.most_common()[-1]
# 清空计数器
birthplace_counter.clear()

# 输出结果
print("出生地为上海的学生人数:", shanghai_students)  # 出生地为上海的学生人数: 2
print("出生地为北京的学生人数:", beijing_students)  # 出生地为北京的学生人数: 3
print("有哪些出生地:", uniq_birthplaces)
# 有哪些出生地: ['Beijing', 'Shanghai', 'Guangzhou', 'Chongqing', 'Shenzhen']
print("计数最多的 2 个出生地:", top_birthplaces)  
# 计数最多的 2 个出生地: [('Shanghai', 4), ('Beijing', 3)]
print("计数最少的 1 个出生地:", bottom_birthplace)  # 计数最少的 1 个出生地: ('Shenzhen', 1)
print("清空后的计数器:", birthplace_counter)  # 清空后的计数器: Counter()

应用场景

  • 文本处理:用于统计单词出现次数或字符出现次数。
  • 数据分析:用于统计数据集中各个元素的出现频率。
  • 词频统计:用于生成词云、绘制频率分布图等。

OrderedDict

OrderedDict 是一个有序字典,它可以记住元素的添加顺序。虽然 Python3.7 以后版本的dict也改成了有序字典,但基于可读性和兼容性,依然使用推荐OrderedDict

基本使用

from collections import OrderedDict

# 创建一个空的 OrderedDict
empty_ordered_dict = OrderedDict()
# 创建包含键值对的 OrderedDict
ordered_dict = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
# 添加键值对到 OrderedDict
ordered_dict['d'] = 4
# 删除键值对
del ordered_dict['a']
# 获取键对应的值
value_of_b = ordered_dict['b']

OrderedDict 的方法和属性

  • move_to_end(key, last=True): 将指定键移动到有序字典的最后或最开始,默认移到最后。
  • popitem(last=True): 弹出有序字典中的最后一个键值对或第一个键值对,默认弹出最后一个。
  • clear(): 清空有序字典中的所有键值对。
  • copy(): 返回有序字典的浅拷贝。
  • keys(): 返回有序字典中的所有键。
  • values(): 返回有序字典中的所有值。
  • items(): 返回有序字典中的所有键值对。

OrderedDictCounter一样,也是继承了dict一样

应用场景

  • 配置文件:用于保存配置信息,并保持配置项的顺序与文件中的顺序一致。
  • 历史记录:用于记录用户操作历史,并保持操作的顺序。
  • 命令行参数:用于保存命令行参数,并保持参数的顺序与输入顺序一致。

defaultdict

defaultdict 是一种字典的子类,它允许给每个键一个默认值,从而避免了在访问不存在的键时引发 KeyError 异常,这在实际开发中非常有用

基本使用

from collections import defaultdict

# 创建一个默认字典,指定默认值为 int 类型的 0
default_dict = defaultdict(int)
# 添加键值对到 defaultdict
default_dict['a'] = 1
# 获取键对应的值,如果键不存在,则返回默认值, int 默认返回0
value_of_b = default_dict['b']
print(value_of_b)  # 输出:0
# 删除键值对
# 当然删除不存在的键值对还是会触发 KeyError 的
del default_dict['a']

# 创建一个默认字典,指定默认值为 list 类型的空列表
default_dict_list = defaultdict(list)
print(default_dict_list["a"])  # 输出:[]

defaultdict 的方法和属性

  • default_factory: 默认工厂函数,用于生成默认值。
  • copy(): 返回 defaultdict 对象的浅拷贝。
  • keys(): 返回 defaultdict 对象中的所有键。
  • values(): 返回 defaultdict 对象中的所有值。
  • items(): 返回 defaultdict 对象中的所有键值对。

defaultdict也是继承了dict,所以很多方法都很类似,区别在于defaultdict允许给每个键一个默认值

重点说说defualtdict中的default_factory:

default_factory除了设置为某种类型(如 intlist 等)外,还可以将其设置为函数,以便在需要时动态生成默认值(任何对象:数值、字符串、类实例等),从而实现更灵活的功能。

下面则是使用这种方法构造默认采集配置:

from collections import defaultdict


# 定义默认的爬虫配置参数
def default_config():
    return {
        'user_agent': 'Mozilla/5.0 ***',
        'timeout': 10,
        'retry': 3,
        'headers': {'Accept': 'text/html,application/json'},
        'proxies': None
    }

# 创建一个 defaultdict,并指定默认值的生成函数为 default_config
crawler_config = defaultdict(default_config)

print(crawler_config["baidu"]["timeout"])  # 输出:10
print(crawler_config["weixin"]["timeout"])  # 输出:10

应用场景

  • 计数器初始化:用于创建计数器字典,并设置默认计数值为0。
  • 数据分组:用于分组数据集,并将缺失的组初始化为空列表或其他默认值。
  • 统计分析:用于统计数据集中各类别的频率,并将缺失的类别初始化为0。

ChainMap

用于将多个字典或映射组合在一起形成单个视图,适用于需要处理多个映射的情况。

方法与属性

  • new_child(m=None): 创建一个新的 ChainMap 对象,将参数 m(字典或映射)添加到链的开头。
  • parents: 返回一个包含所有父映射的新 ChainMap 对象。
  • maps: 返回一个包含所有映射的列表。
  • copy(): 返回 ChainMap 对象的浅拷贝。

用法

下面举个配置合并的例子来说明下用法:

from collections import ChainMap

# 定义三个模块的配置信息
module1_config = {'timeout': 10, 'retry': 3}
module2_config = {'user_agent': 'Mozilla/5.0', 'proxies': 1}
module3_config = {'headers': {'Accept': 'text/html'}}

# 使用 ChainMap 合并多个模块的配置
combined_config = ChainMap(module1_config, module2_config, module3_config)

# 获取所有映射
print("所有映射:")
print(combined_config.maps)

# 获取父映射(去除最后一个映射)
parent_maps = combined_config.parents
print("\n父映射:")
for parent_map in parent_maps:
    print(parent_map)

# 获取指定键的值
print("\n获取指定键的值:")
print("timeout:", combined_config['timeout'])
print("user_agent:", combined_config['user_agent'])

# 添加新的映射
new_module_config = {'timeout': 20, 'retry': 5}
combined_config = combined_config.new_child(new_module_config)
print("\n添加新的映射后的配置信息:")
print(combined_config)

print(combined_config['proxies'])

应用场景

  • 配置管理:用于合并多个配置源(如全局配置、用户配置、默认配置)并提供统一的配置视图。
  • 命名空间:用于将多个命名空间(如全局命名空间、模块命名空间、局部命名空间)组合在一起,形成单个命名空间。
  • 上下文管理器:用于管理多个上下文,并提供统一的上下文视图,使得上下文的嵌套和覆盖更加灵活。

使用优势

  • 更具表达性collections中的数据结构通常比内置的数据结构更具表达性,能够更清晰地表达程序的意图,代码也会简单很多。
  • 更高级的功能collections中的数据结构提供了一些额外的功能,这些功能在特定的使用场景下非常有用。
  • 性能优势:在某些特定的操作中,collections中的数据结构可能比内置数据结构性能更好。
  • 可扩展性:如果你的程序需要更多的功能,collections模块提供了一些非常有用的数据结构,可以满足更广泛的需求。
  • 标准化:使用collections中的数据结构能够让你的代码更加标准化和易于理解。

如果你觉得本文有帮助到你,非常期待得到你的支持和鼓励;
如果有其他问题或补充,欢迎评论区留言交流。