Get Mystery Box with random crypto!

Python etc

Logo of telegram channel pythonetc — Python etc P
Logo of telegram channel pythonetc — Python etc
Channel address: @pythonetc
Categories: Technologies
Language: English
Subscribers: 6.28K
Description from channel

Regular tips about Python and programming in general
Owner — @pushtaev
The current season is run by @orsinium
Tips are appreciated: https://ko-fi.com/pythonetc / https://sobe.ru/na/pythonetc
© CC BY-SA 4.0 — mention if repost

Ratings & Reviews

4.00

2 reviews

Reviews can be left only by registered users. All reviews are moderated by admins.

5 stars

1

4 stars

0

3 stars

1

2 stars

0

1 stars

0


The latest Messages 5

2021-03-11 18:00:10 Suppose, you have 10 lists:

lists = [list(range(10_000)) for _ in range(10)]

What's the fastest way to join them into one? To have a baseline, let's just + everything together:

s = lists
%timeit s[0] + s[1] + s[2] + s[3] + s[4] + s[5] + s[6] + s[7] + s[8] + s[9]
# 1.65 ms ± 25.1 µs per loop

Now, let's try to use functools.reduce. It should be about the same but cleaner and doesn't require to know in advance how many lists we have:

from functools import reduce
from operator import add
%timeit reduce(add, lists)
# 1.65 ms ± 27.2 µs per loop

Good, about the same speed. However, reduce is not "pythonic" anymore, this is why it was moved from built-ins into functools. The more beautiful way to do it is using sum:

%timeit sum(lists, start=[])
# 1.64 ms ± 83.8 µs per loop

Short and simple. Now, can we make it faster? What if we itertools.chain everything together?

from itertools import chain
%timeit list(chain(*lists))
# 599 µs ± 20.4 µs per loop

Wow, this is about 3 times faster. Can we do better? Let's try something more straightforward:

%%timeit
r = []
for lst in lists:
r.extend(lst)
# 250 µs ± 5.96 µs per loop

Turned out, the most straightforward and simple solution is the fastest one.
2.0K views15:00
Open / Comment
2021-03-09 18:00:06 Types str and bytes are immutable. As we learned in previous posts, + is optimized for str but sometimes you need a fairly mutable type. For such cases, there is bytearray type. It is a "hybrid" of bytes and list:

b = bytearray(b'hello, ')
b.extend(b'@pythonetc')
b
# bytearray(b'hello, @pythonetc')

b.upper()
# bytearray(b'HELLO, @PYTHONETC')

The type bytearray has all methods of both bytes and list except sort:

set(dir(bytearray)) ^ (set(dir(bytes)) | set(dir(list)))
# {'__alloc__', '__class_getitem__', '__getnewargs__', '__reversed__', 'sort'}

If you're looking for reasons why there is no bytearray.sort, there is the only answer we found: stackoverflow.com/a/22783330/8704691.
2.0K views15:00
Open / Comment
2021-03-04 18:00:15 Let's learn a bit more about strings performance. What if instead of an unknown amount of strings we have only a few known variables?

s1 = 'hello, '
s2 = '@pythonetc'

%timeit s1+s2
# 56.7 ns ± 6.17 ns per loop

%timeit ''.join([s1, s2])
# 110 ns ± 6.09 ns per loop

%timeit '{}{}'.format(s1, s2)
# 63.3 ns ± 6.69 ns per loop

%timeit f'{s1}{s2}'
# 57 ns ± 5.43 ns per loop

No surprises here, + and f-strings are equally good, str.format is quite close. But what if we have numbers instead?

n1 = 123
n2 = 456
%timeit str(n1)+str(n2)
# 374 ns ± 7.09 ns per loop

%timeit '{}{}'.format(n1, n2)
# 249 ns ± 4.73 ns per loop

%timeit f'{n1}{n2}'
# 208 ns ± 3.49 ns per loop

In this case, formatting is faster because it doesn't create intermediate strings. However, there is something else about f-strings. Let's measure how long it takes just to convert an int into an str:

%timeit str(n1)
# 138 ns ± 4.86 ns per loop

%timeit '{}'.format(n1)
# 148 ns ± 3.49 ns per loop

%timeit format(n1, '')
# 91.8 ns ± 6.12 ns per loop

%timeit f'{n1}'
# 63.8 ns ± 6.13 ns per loop

Wow, f-strings are twice faster than just str! This is because f-strings are part of the grammar but str is just a function that requires function-lookup machinery:

import dis
dis.dis("f'{n1}'")
1 0 LOAD_NAME 0 (n1)
2 FORMAT_VALUE 0
4 RETURN_VALUE

dis.dis("str(n1)")
1 0 LOAD_NAME 0 (str)
2 LOAD_NAME 1 (n1)
4 CALL_FUNCTION 1
6 RETURN_VALUE

And once more, disclaimer: readability is more important than performance until proven otherwise. Use your knowledge with caution :)
509 views15:00
Open / Comment
2021-03-02 18:00:07 Any enclosing variable can be shadowed in the local scope without affecting the global one:

v = 'global'
def f():
v = 'local'
print(f'f {v=}')
f()
# f v='local'

print(f'{v=}')
# v='global'

And if you try to use a variable and then shadow it, the code will fail at runtime:

v = 'global'
def f():
print(v)
v = 'local'
f()
# UnboundLocalError: local variable 'v' referenced before assignment

If you want to re-define the global variable instead of locally shadowing it, it can be achieved using global and nonlocal statements:

v = 'global'
def f():
global v
v = 'local'
print(f'f {v=}')
f()
# f v='local'
print(f'g {v=}')
# g v='local'

def f1():
v = 'non-local'
def f2():
nonlocal v
v = 'local'
print(f'f2 {v=}')
f2()
print(f'f1 {v=}')
f1()
# f2 v='local'
# f1 v='local'

Also, global can be used to skip non-local definitions:

v = 'global'
def f1():
v = 'non-local'
def f2():
global v
print(f'f2 {v=}')
f2()
f1()
# f2 v='global'

To be said, using global and nonlocal is considered a bad practice that complicates the code testing and usage. If you want a global state, think if it can be achieved in another way. If you desperately need a global state, consider using singleton pattern which is a little bit better.
257 views15:00
Open / Comment
2021-02-25 18:00:14 Let's talk a bit more about scopes.

Any class and function can implicitly use variables from the global scope:

v = 'global'
def f():
print(f'{v=}')
f()
# v='global'

Or from any other enclosing scope, even if it is defined after the fucntion definition:

def f():
v1 = 'local1'
def f2():
def f3():
print(f'{v1=}')
print(f'{v2=}')
v2 = 'local2'
f3()
f2()
f()
# v1='local1'
# v2='local2'

Class body is a tricky case. It is not considered an enclosing scope for functions defined inside of it:

v = 'global'
class A:
v = 'local'
print(f'A {v=}')
def f():
print(f'f {v=}')
# A v='local'

A.f()
# f v='global'
1.2K views15:00
Open / Comment
2021-02-24 18:20:43 It was a long break but tomorrow we start again. We have plenty of ideas for posts but don't always have time to write them. So, this is how you can help us:

+ If you have something to tell about Python (syntax, stdlib, PEPs), check if it already was posted. If not, write a post, send it to us, and we will publish it. It will include your name (if you want to), we don't steal content ;)

+ If you don't have an idea, just contact us, we have plenty of them! And if you like it, the algorithm is the same as above: write a post, send it, we publish it with your name.

+ If you don't have time to write posts but still want to help, consider donating a bit of money, links are in the channel description. If we get enough, we can take a one-day vacation and invest it exclusively into writing posts.

+ If you see a bug or typo in a post, please, let us know!

And speaking about bugs, there are few in recent posts that our lovely subscribers have reported:

+ post #641, reported by @recursing. functools.cache isn't faster than functools.lru_cache(maxsize=None), it is exactly the same. The confusion comes from the documentation which says "this is smaller and faster than lru_cache() WITH A SIZE LIMIT".

+ post #644, reported by @el71Gato. It should be 10**8 instead of 10*8. We've re-run benchmarks with these values, relative numbers are the same, so all conclusions are still correct.

Welcome into season 2.5 :)
1.3K viewsedited  15:20
Open / Comment
2021-01-27 19:01:01 Today Guido van Rossum posted a Python riddle:

x = 0
y = 0
def f():
x = 1
y = 1
class C:
print(x, y) # What does this print?
x = 2
f()

The answer is 0 1.

The first tip is if you replace the class with a function, it will fail:

x = 0
y = 0
def f():
x = 1
y = 1
def f2():
print(x, y)
x = 2
f2()
f()
# UnboundLocalError: local variable 'x' referenced before assignment

Why so? The answer can be found in the documentation (see Execution model):

> If a variable is used in a code block but not defined there, it is a free variable.

So, x is a free variable but y isn't, this is why behavior for them is different. And when you try to use a free variable, the code fails at runtime because you haven't defined it yet in the current scope but will define it later.

Let's disassemble the snippet above:

import dis
dis.dis("""[insert here the previous snippet]""")

It outputs a lot of different things, this is the part we're interested in:

8 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (x)
4 LOAD_DEREF 0 (y)
6 CALL_FUNCTION 2
8 POP_TOP

Indeed, x and y have different instructions, and they're different at bytecode-compilation time. Now, what's different for a class scope?

import dis
dis.dis("""[insert here the first code snippet]""")

This is the same dis part for the class:

8 8 LOAD_NAME 3 (print)
10 LOAD_NAME 4 (x)
12 LOAD_CLASSDEREF 0 (y)
14 CALL_FUNCTION 2
16 POP_TOP

So, the class scope behaves differently. x and y loaded with LOAD_FAST and LOAD_DEREF for a function and with LOAD_NAME and LOAD_CLASSDEREF for a class.

The same documentation page answers how this behavior is different:

> Class definition blocks and arguments to exec() and eval() are special in the context of name resolution. A class definition is an executable statement that may use and define names. These references follow the normal rules for name resolution with an exception that unbound local variables are looked up in the global namespace.

In other words, if a variable in the class definition is unbound, it is looked up in the global namespace skipping enclosing nonlocal scope.
4.1K views16:01
Open / Comment
2020-12-31 18:00:02 from base64 import b64decode
from random import choice

CELLS = '~' * 12 + '¢•*@&.;,"'

def tree(max_width):
yield '/⁂\\'.center(max_width)

for width in range(3, max_width - 1, 2):
row = '/'
for _ in range(width):
row += choice(CELLS)
row += '\\'
yield row.center(max_width)

yield "'| |'".center(max_width)
yield " | | ".center(max_width)
yield '-' * max_width
title = b'SGFwcHkgTmV3IFllYXIsIEBweXRob25ldGMh'
yield b64decode(title).decode().center(max_width)

for row in tree(40):
print(row)
5.2K views15:00
Open / Comment
2020-12-29 18:00:03 What is the fastest way to build a string from many substrings in a loop? In other words, how to concatenate fast when we don't know in advance how many strings we have? There are many discussions about it, and the common advice is that strings are immutable, so it's better to use a list and then str.join it. Let's not trust anyone and just check it.

The straightforward solution:

%%timeit
s = ''
for _ in range(10*8):
s += 'a'
# 4.04 µs ± 256 ns per loop

Using lists:

%%timeit
a = []
for _ in range(10*8):
a.append('a')
''.join(a)
# 4.06 µs ± 144 ns per loop

So, it's about the same. But we can go deeper. What about generator expressions?

%%timeit
''.join('a' for _ in range(10*8))
# 3.56 µs ± 95.9 ns per loop

A bit faster. What if we use list comprehensions instead?

%%timeit
''.join(['a' for _ in range(10*8)])
# 2.52 µs ± 42.1 ns per loop

Wow, this is 1.6x faster than what we had before. Can you make it faster?

And there should be a disclaimer:

1. Avoid premature optimization, value readability over performance when using a bit slower operation is tolerable.

2. If you think that something is slow, prove it first. It can be different in your case.
5.2K views15:00
Open / Comment
2020-12-24 18:00:04 The issue with a beautiful number #12345 proposed to add the following constant into stdlib:

tau = 2*math.pi

It was a controversial proposal since apparently it's not hard to recreate this constant on your own which will be more explicit, since more people are familiar with π rather than τ. However, the proposal was accepted and tau landed in math module in Python 3.6 (PEP-628):

import math
math.tau
# 6.283185307179586

There is a long story behind τ which you can read at tauday.com. Especially good this numberphile video.
5.1K views15:00
Open / Comment