按位操作符
某知名计算机教授提出了这样一个看法: 按位操作符是在底层语言中才有必要使用的,实际上,对于高级语言来说,不应该保留这个操作。因此,例如python应该删除按位操作符。即使因为种种原因如兼容性等,只能保留该操作符,那么也不应当去使用它。
当然啦,这个观点是较为大胆的,自然有很多争议,不过,按位操作符在python的大多数应用中,很少使用,甚至很多人根本不知道按位操作符到底要怎么样发挥作用,也是一个事实。下面,我们就来讨论一下按位操作符的使用,评估一下其究竟是否有用。
按位操作符介绍
按位操作符用于处理整数的二进制位,也就是说这些操作符直接操作数的二进制形式,常用于底层编程任务,常见的按位操作符有几种:
- 按位与操作符
&:对于两个操作数的对应位,只有当两个位都为1时,结果位才为1。 - 按位或操作符
|:对于两个操作数的对应位,只要有一个位为1,结果位就为1。 - 按位异或操作符
^:对于两个操作数的对应位,当两个位不相同时,结果位为1,相同时为0。 - 按位取反操作符
~:将1变为0,将0变为1。 - 左移操作符
<<:二进制向左移动n位(右边补0)。 - 右移操作符
>>:二进制表示向右移动n位。根据操作数的类型,左边补的位可能是0或者保留符号位。
按位操作符学生管理
假设我们的班级有很多学生,我们需要记录每个人是否出席,我们可以使用二进制的形式来进行记录。用1表示这个学生出席了,用0表示这个学生没有出席。
首先,我们将所有的学生设置为没有出席:
attendance = 0 # 初始时,所有学生都标记为缺席
如果我们希望记录学生是否出席(将学生根据学号进行标记,且学号从1开始),在这个过程中,记录学生出席需要使用按位或和左移:
# 例如第5位学生出席了,我们就使用按位或操作10000
# 由于按位或只需要有一个为1则为1,那么一定会将5的出席状态记录为1
def mark_attendance(attendance, student_id):
return attendance | (1 << (student_id - 1))
如果第五位学生出席了,那么我们这样使用函数:
attendance = mark_attendance(attendance, 5) # 结果为16,也就是二进制的10000
如果我们希望记录学生缺席,需要使用按位与,左移和按位取反:
# 例如第5位学生缺席了,那么我们先通过左移得到10000
# 然后通过取反得到01111
# 然后通过按位与标记缺席,因为按位与是两个操作数都为1才是1,由于第5位学生已经是0了,因此,按位与后一定是0
# 由于其他学生被取反标记为1,因此按位与以后不会改变原本的结果
def unmark_attendance(attendance, student_id):
return attendance & ~(1 << (student_id - 1))
如果第五位学生缺席了,那么我们这样使用函数:
attendance = unmark_attendance(attendance, 5)
如果我们需要检查学生是否出席了,需要使用按位与,左移:
# 例如检查第5位学生是否出席,那么我们先通过左移得到10000
# 然后通过按位与只保留下第5位学生(因为此时其他学生都是0,按位与以后都变成0不影响结果)
# 此时,只需要比较这个结果是否为0即可,因为这个结果如果是0,那么就代表缺席,否则就是出席
def check_attendance(attendance, student_id):
return (attendance & (1 << (student_id - 1))) != 0
此时,如果我们需要检查学生5是否出席,就可以这样使用函数:
student_id = 5
is_present = check_attendance(attendance, 5)
print(f"学生{student_id}{'出席' if is_present else '缺席'}了")
现代python学生管理
如果我们不使用这个按位操作符的形式,使用现代python来实现的话,那么我们有很多种实现的方法,比如说,我们可以使用列表来完成这个实现。
class StudentManager:
def __init__(self, student_num):
self.attendance_list = [False] * student_num
def mark_attendance(self, student_id):
self.attendance_list[student_id - 1] = True
def unmark_attendance(self, student_id):
self.attendance_list[student_id - 1] = False
def check_attendance(self, student_id):
return self.attendance_list[student_id - 1]
def check_attendance_with_print_info(self, student_id):
is_present = self.check_attendance(student_id)
print(f"学生{student_id}{'出席' if is_present else '缺席'}了")
sm = StudentManager(32)
sm.mark_attendance(5)
sm.check_attendance_with_print_info(5)
sm.check_attendance_with_print_info(6)
我们只需要对比(attendance & (1 << (student_id - 1))) != 0和attendance_list[student_id - 1],这两种实现,就可以非常明显的看出,改为现代python的列表实现以后,代码的可读性有了非常高的提升,这也有助于减少实现出现错误。
对于大多数不理解按位操作符的人来说,他们可能根本就读不懂,或者要花很长时间去思考,才能明白按位操作符是如何实现的。但是对于掌握了现代python的人来说,可以轻松一眼读懂列表实现。从这个角度来说,即使按位操作符可以实现某些功能,而且有一定的性能提升,也不选择使用按位操作符是有道理的。
实际性能差异(运行时间)
我们总说按位操作的实现性能会更好,那么到底能快多少呢?我们可以通过编写一段测试程序,来尝试了解一下他们之间的实际差距。
import timeit
class StudentManagerBit:
def __init__(self):
self.attendance = 0
def mark_attendance(self, student_id):
self.attendance = self.attendance | (1 << (student_id - 1))
def unmark_attendance(self, student_id):
self.attendance = self.attendance & ~(1 << (student_id - 1))
def check_attendance(self, student_id):
return (self.attendance & (1 << (student_id - 1))) != 0
class StudentManager:
def __init__(self, student_num):
self.attendance_list = [False] * student_num
def mark_attendance(self, student_id):
self.attendance_list[student_id - 1] = True
def unmark_attendance(self, student_id):
self.attendance_list[student_id - 1] = False
def check_attendance(self, student_id):
return self.attendance_list[student_id - 1]
num_students = 1000
sm_bit = StudentManagerBit()
sm_list = StudentManager(num_students)
def test_mark_bit():
for i in range(1, num_students + 1):
sm_bit.mark_attendance(i)
def test_unmark_bit():
for i in range(1, num_students + 1):
sm_bit.unmark_attendance(i)
def test_check_bit():
for i in range(1, num_students + 1):
sm_bit.check_attendance(i)
def test_mark_list():
for i in range(1, num_students + 1):
sm_list.mark_attendance(i)
def test_unmark_list():
for i in range(1, num_students + 1):
sm_list.unmark_attendance(i)
def test_check_list():
for i in range(1, num_students + 1):
sm_list.check_attendance(i)
print("测试按位操作的学生管理...")
bit_mark_time = timeit.timeit(test_mark_bit, number=10)
bit_unmark_time = timeit.timeit(test_unmark_bit, number=10)
bit_check_time = timeit.timeit(test_check_bit, number=10)
print("测试列表的学生管理...")
list_mark_time = timeit.timeit(test_mark_list, number=10)
list_unmark_time = timeit.timeit(test_unmark_list, number=10)
list_check_time = timeit.timeit(test_check_list, number=10)
print(f"按位出席用时: {bit_mark_time} 秒")
print(f"按位缺席用时: {bit_unmark_time} 秒")
print(f"按位检查用时: {bit_check_time} 秒")
print(f"列表出席用时: {list_mark_time} 秒")
print(f"列表缺席用时: {list_unmark_time} 秒")
print(f"列表检查用时: {list_check_time} 秒")
那么最终结果如何呢?(注意:以下结果仅代表当前测试的设备的运行结果,不同设备,同一设备不同时间都会有所差距,因此不能代表普遍情况,如果你感兴趣可以自己进行测试!)
测试按位操作的学生管理...
测试列表的学生管理...
按位出席用时: 0.004425639000146475 秒
按位缺席用时: 0.0032289840000885306 秒
按位检查用时: 0.0022537570002896246 秒
列表出席用时: 0.0020566879993566545 秒
列表缺席用时: 0.0015577690001009614 秒
列表检查用时: 0.0020706559998870944 秒
结果让我们大吃一惊!怎么会这样呢?我们一般认为按位操作要明显好于列表操作,在性能上无论是内存占用还是运行时间都应该优于列表的实现。但是实际结果不仅没有更好,反而表现的更差,这是怎么回事?
有几点可能:
- python的优化和数据类型的干扰:python针对于列表操作,是有自己的优化的,而python中的整型并非真正的整型,而是python自己的一种数据类型,这可能影响了按位操作的效率。
- 测试数据集的大小和代码的复杂程度:在测试中,我们选择的测试集(1000个学生)可能不够大,而学生出席管理的代码也相对简单,这可能让按位操作的优势没能很好发挥。
- 代码实现的正确:我们的代码实现可能不够好,在使用按位操作的时候引入了不必要的复杂性,这也可能影响到了按位操作的发挥。
当然,这些结果也提醒了我们,尽管按位操作理论上应当更加高效,但实际性能还取决于多种因素,包括数据类型,操作的复杂性,数据集大小以及运行时环境等等。
总结
按位操作符的使用在某些特定的用途中可能非常有效,如进行底层硬件操作,实现特定的算法或者需要大规模内存操作时,在性能和存储空间很重要的时候,按位操作符可能会发挥很大作用。
尽管按位操作符可能在性能上有所优势,但是不可否认的时,其实现会变得更加复杂,代码会变得更加难以维护和理解,对于现代项目尤其是团队协作中,代码的简洁和可理解可能是更重要的。
根据之前的性能测试,也告诉我们,虽然理论上按位操作符的性能会更好,但是在一些具体例子的使用中,实际情况可能并非如此,应该进行实际的测试,并判断其是否真的带来了性能提升。
1848

被折叠的 条评论
为什么被折叠?



