I need the ability to generate random MAC addresses, so I wrote up a little function that does that:
>>> random_mac()
'7C:93:B7:AF:BA:AE'
>>> random_mac()
'D8:D8:A0:D4:A5:3F'
>>> random_mac(unicast=False, universal=True)
'55:47:C6:EE:C6:2B'
>>> random_mac(unicast=True, universal=False)
'FE:A1:4B:98:76:B6'
I decided to allow the user to pick if it's unicast/multicast and universally/locally administered; even though I'll only need unicast/universal. This caused me headaches though because I'm still not great at dealing with bits. The LSB of the first octet indicates uni/multicast, and the second LSB of the octet indicates universal/local, so these bits can't be random.
After playing around with a few failed ideas (generating all random bits, then "fixing" the two bits later), I finally decided to generate a random number between 0 and 63 inclusive, left shift it twice, than add the two bits on after. It works, but it's ugly and looks suboptimal.
It's not a lot of code, but I'd like a few things reviewed:
- Is there a better approach? It feels hacky generating it as two pieces then adding them together. I tried explicitly setting the bits, but the code to decide between
|
, and&
and~
got messier than what I have now, so I went with this way. - The number constants are bugging me too. The numbers kind of sit on a border of self-explanatory and magic, so I decided to name them to be safe.
LAST_SIX_BITS_VALUE
feels off though. - Is treating a boolean value as a number during bitwise operation idiomatic? Is it clear as I have it now?
- Attaching the first octet to the rest is suboptimal as well. Speed isn't a huge concern, but I'm curious if there's a cleaner way that I'm missing.
from random import randint, randrange
N_MAC_OCTETS = 6
OCTET_VALUE = 256
LAST_SIX_BITS_VALUE = 63
def random_mac(unicast: bool = True, universal: bool = True) -> str:
least_two_bits = (not unicast) + ((not universal) << 1)
first_octet = least_two_bits + (randint(0, LAST_SIX_BITS_VALUE) << 2)
octets = [first_octet] + [randrange(OCTET_VALUE) for _ in range(N_MAC_OCTETS - 1)]
return ":".join(f"{octet:02X}" for octet in octets)
Examples of the bits for the first octet for different inputs:
def display(mac):
print(mac, f"{int(mac.split(':')[0], 16):08b}")
# Unicast, Universal
>>> display(random_mac(True, True))
04:27:DE:9A:1B:D7 00000100 # Ends with 0,0
# Unicast, Local
>>> display(random_mac(True, False))
72:FB:49:43:D5:F2 01110010 # 1,0
# Multicast, Universal
>>> display(random_mac(False, True))
7D:BF:03:4E:E5:2A 01111101 # 0,1
# Multicast, Local
>>> display(random_mac(False, False))
2F:73:52:12:8C:50 00101111 # 1,1
randmac
does it. randmac Python 3 \$\endgroup\$