>>> h = b'Content-Type: 30'
>>> h.lower()
b'content-type: 30'
>>> h
b'Content-Type: 30'
>>> h.split(b' ')
[b'Content-Type:', b'30']
>>> h
b'Content-Type: 30'
>>> h
b'Content-Type: 30'
>>> h[7] = b' '
Traceback (most recent call last):
...
TypeError: 'bytes' object does not
support item assignment
bytearray
Note
Problem: "based on"!
Q: Why?
>>> s = 'Hello world!'
>>> len(s)
12
>>> s.split()
['Hello', 'world!']
>>> s = b'Hello world!'
>>> len(s)
12
>>> s.split()
[b'Hello', b'world!']
>>> s = 'Hello world!'
>>> s[3:10]
'lo worl'
>>> s = b'Hello world!'
>>> s[3:10]
b'lo worl'
>>> s = 'abc'
>>> len(s)
3
>>> for item in s:
... print repr(item)
'a'
'b'
'c'
>>> for item in b'abc':
... print repr(item)
?
Note
It gives you "SyntaxError, you failed to pay the Python 3 paren tax"
>>> for item in b'abc':
... print repr(item)
Traceback (most recent call last):
...
SyntaxError: you failed to pay the
Python 3 paren tax
Note
Like an old text-based adventure game throwing an extra obstacle in your way.
> cross bridge
A BURLY TROLL STANDS BY THE BRIDGE AND
INSISTS YOU THROW HIM TWO PARENTHESES
BEFORE YOU MAY CROSS.
Note
Ruby
18,000 parenthesis characters per year
Note
I have to go into paren debt just to write any Emacs LISP. But, what's another two parens when I've already used so many?
>>> for item in b'abc':
... print(repr(item))
Updated total: 18,002 parenthesis
Note
We'll just move on, not mention it.
>>> for item in b'abc':
... print(repr(item))
97
98
99
>>> s = b'Hello world!'
>>> s[0] + s[1:]
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +:
'int' and 'bytes'
>>> s = b'Hello world!'
>>> s[0:1] + s[1:]
b'Hello world!'
bits = 2048
mask = bits - 1
a = bytearray(bits >> 3)
def set(n):
a[n >> 3] |= 1 << (n & 7)
for word in words:
for h in hashes(word):
set(h & mask)
def get(n):
return a[n >> 3] & (1 << (n & 7))
def test(word):
return all(get(h & mask)
for h in hashes(word))
list of ints
a = [0] * (bits >> 3)
Slightly slower, but 8× less space
$ time cat < gigabyte.data > /dev/null
0.00s user 0.12s system 99% cpu 0.117 total
$ dd if=gigabyte.data of=/dev/null
2097152+0 records in
2097152+0 records out
1073741824 bytes (1.1 GB) copied,
0.698802 s, 1.5 GB/s
Block size!
$ strace cat < gigabyte.data > /dev/null
read(0, ""..., 131072) = 131072
write(1, ""..., 131072) = 131072
$ strace dd if=gigabyte.data of=/dev/null
read(0, ""..., 512) = 512
write(1, ""..., 512) = 512
$ dd if=gigabyte.data of=/dev/null bs=131072
8192+0 records in
8192+0 records out
1073741824 bytes (1.1 GB) copied,
0.117653 s, 9.1 GB/s
Normal Python
while True:
data = i.read(blocksize)
if not data:
break
o.write(data)
Broken attempt at readinto()
data = bytearray(blocksize)
while True:
length = i.readinto(data)
if not length:
break
o.write(data)
Fix that incurs a copy
data = bytearray(blocksize)
while True:
length = i.readinto(data)
if not length:
break
o.write(data[:length])
What if we want to achieve zero-copy?
memoryview!
>>> s = bytearray(b'_________')
>>> m = memoryview(s)
>>> v = m[3:6]
>>> v[0] = 65 # A
>>> v[1] = 66 # B
>>> v[2] = 67 # C
>>> s
bytearray(b'___ABC___')
Zero-copy version of fix
data = bytearray(blocksize)
view = memoryview(data)
while True:
length = i.readinto(data)
if not length:
break
o.write(view[:length])
dd 0.112 s **********
cat 0.113 s **********
read() 0.122 s ************
readinto() 0.117 s ***********
+bytearray[] 0.183 s ******************
+memoryview[] 0.119 s ***********
512B
What will slicing so often do?
data = bytearray(blocksize)
view = memoryview(data)
while True:
length = i.readinto(data)
if not length:
break
o.write(view[:length])
dd 0.64 *************
read() 1.04 *********************
readinto() 0.96 *******************
+bytearray[] 1.33 **************************
+memoryview[] 1.25 *************************
20% slowdown
I thought of something
data = bytearray(blocksize)
view = memoryview(data)
while True:
length = i.readinto(data)
if not length:
break
elif length == blocksize:
o.write(data)
else:
o.write(view[:length])
dd 0.64 *************
read() 1.04 *********************
readinto() 0.96 *******************
+bytearray[] 1.33 **************************
+memoryview[] 1.25 *************************
+hybrid 1.00 ********************
bytearray gives 4% speedup
Note
Easy to write code that looks good, but is slower. Functional programmer's heart sing, versus side-effecty.
Note
Could be a single packet; could be several; could be one stray byte from the most recent packet
Note
Using min() here is a 10% performance penalty
How does a recv() solution perform?
data = b''
while len(data) < content_length:
n = content_length - len(data)
more = s.recv(n if n < 1500 else 1500)
if not more:
raise EOFError()
data += more
How long does the += approach take?
∞
blocks = []
n = content_length
while n:
more = s.recv(n if n < 1500 else 1500)
if not more:
raise EOFError()
blocks.append(more)
n -= len(more)
data = b''.join(blocks)
recv()
1.08 s
incoming blocks
┌─┬─┬─┬─┐
└─┴─┴─┴─┘
┌─┬─┬─┐
└─┴─┴─┘
┌─┬─┬─┬─┐
└─┴─┴─┴─┘
↓
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴
bytearray
incoming blocks
┌─┬─┬─┬─┐
└─┴─┴─┴─┘
┌─┬─┬─┐
└─┴─┴─┘
┌─┬─┬─┬─┐
└─┴─┴─┴─┘
↓ ↓ ↓
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴
bytearray
>>> s = bytearray(b'_________')
>>> m = memoryview(s)
>>> v = m[3:6]
>>> v[0] = 65 # A
>>> v[1] = 66 # B
>>> v[2] = 67 # C
>>> s
bytearray(b'___ABC___')
┌─┬─┬─┬─┐ First block
└─┴─┴─┴─┘
↓
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴
bytearray
┌─┬─┬─┐ Second block
└─┴─┴─┘
↓ memoryview
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴
bytearray
┌─┬─┬─┬─┐
└─┴─┴─┴─┘
↓ memoryview
┌─┬─┬─┬─┬─┬─┬─┬
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴
bytearray
data = bytearray(content_length)
view = memoryview(data)
n = content_length
while n:
limit = n if n < 1500 else 1500
more = s.recv_into(view[-n:], limit)
if not more:
raise EOFError()
n -= more
data = bytearray(0)
n = content_length
while n:
more = s.recv(n if n < 1500 else 1500)
if not more:
raise EOFError()
data.extend(more)
n -= len(more)
0.81 s
Turns out?
call next(i)
integer = address of int object
INCREF(integer)
return integer
ask the integer its value
insert that value into bytearray
DECREF(integer)
INCREF()
reads 8 bytes from RAM
writes 8 bytes to RAM
“return”
copies 8 byte address
DECREF()
reads 8 bytes from RAM
writes 8 bytes to RAM
__________________________
40 bytes of RAM bandwidth
just to communicate 8 bits
Yes!
Note
Now think about it: where would you put the real append?
+=
Note
The one they taught us to avoid!
data = bytearray(0)
n = content_length
while n:
more = s.recv(n if n < 1500 else 1500)
if not more:
raise EOFError()
data += more
n -= len(more)
0.73 s
data += more ∞
recv(…) + join(…) 1.09 s ***********
recv_into(…) 0.80 s ********
data.extend(…) 0.81 s ********
data += more 0.73 s *******
bytearray gives 33% speedup
data = bytearray(0)
n = content_length
while n:
more = s.recv(n if n < 1500 else 1500)
if not more:
raise EOFError()
data += more
n -= len(more)
>>> b = bytearray(b'Hello, world!')
>>> b.upper()
bytearray(b'HELLO, WORLD!')
>>> b
bytearray(b'Hello, world!')
>>> b
bytearray(b'Hello, world!')
>>> b[5] = 32
>>> b[7:12] = b'PyCon'
>>> b
bytearray(b'Hello PyCon!')
>>> b.clear()
>>> b
bytearray(b'')
>>> b = bytearray(b'ALL YOUR BASE')
>>> bslice = b[4:8]
>>> l = bslice.lower()
>>> b[4:8] = l
>>> b
bytearray(b'ALL your BASE')
Can the memoryview save us?
No.
>>> b = bytearray(b'ALL YOUR BASE')
>>> v = memoryview(b)
>>> bslice = v[4:8]
>>> l = bslice.lower()
Traceback (most recent call last):
...
AttributeError: 'memoryview' object has
no attribute 'lower'
Do you remember indexes?
i = s.find(': ')
j = i + 2
name = s[:i].lower()
value = s[j:]
a[6] = 65
a[10:16] = 'Python'
>>> import re
>>> s = bytearray(b'Big Ben tower')
>>> match = re.search(b'Be.', s)
>>> match.span()
(4, 7)
bytearray
bytearray