5 функцій, які повинен знати кожен досвідчений розробник Python

Ви вже деякий час програмуєте на Python, створюєте сценарії та вирішуєте проблеми наліво і направо. Ви думаєте, що ви дуже хороший спеціаліст, чи не так?

Пропонуємо 5 функцій Python, які змусять вас сказати: «Я використовував це весь час!».

Навіть якщо ці речі для вас є новими, ви матимете чудовий контрольний список, який потрібно виконати, щоб вивести вміння на наступний рівень.

1. Області видимості

Важливим аспектом просунутого програмування на Python є глибоке знайомство з концепцією області видимості.

Область визначає порядок, у якому інтерпретатор Python шукає імена в програмі. Область Python відповідає правилу LEGB (локальна, охоплююча, глобальна та вбудована області). Відповідно до правила, коли ви отримуєте доступ до імені (воно може бути будь-яким,

змінна, функція або клас), інтерпретатор шукає його в локальній, охоплюючій, глобальній і вбудованій областях видимості по порядку.

Давайте розглянемо приклади, щоб краще зрозуміти кожен рівень.

Приклад 1 — Локальна область видимості

def func():     x = 10     print(x) func() # 10 print(x) # Raises NameError, x is only defined within the scope of func() 

Тут x визначено лише в області, локальній для func. Ось чому він недоступний більше ніде в сценарії.

Приклад 2 — Охоплююча область видимості

def outer_func():     x = 20     def inner_func():         print(x)     inner_func() outer_func() # 20

Охоплююча область є проміжною областю між локальною та глобальною областями. У наведеному вище прикладі x знаходиться в локальній області outer_func . З іншого боку, x знаходиться в охоплюючій області відносно вкладеної функції inner_func. Локальна область завжди має доступ лише для читання до охоплюючої області.

Приклад 3 — Глобальна область видимості

x = 30 def func():     print(x) func() # 30

Тут x і func визначені в глобальній області видимості, що означає, що їх можна читати з будь-якого місця поточного сценарію.

Щоб змінити їх на менших рівнях (локальних і охоплюючих), до них слід отримати доступ за допомогою ключового слова global:

 def func2():     global x     x = 40     print(x) func2() # 40 print(x) # 40 

Приклад 4 — Вбудовані області

Вбудована область включає всі вже визначені бібліотеки, класи, функції та змінні, які не потребують явних операторів імпорту. Деякі приклади вбудованих функцій і змінних у Python включають print, len, range, str, int, float тощо.

2. Замикання функцій 

Глибоке розуміння області видимості відкриває двері до ще одного важливого концепту – замикання функцій.

За замовчуванням, після завершення виконання функції, вона повертається до порожнього стану. Це означає, що пам’ять функції очищується від усіх її минулих аргументів.

def func(x):
    return x ** 2

func(3)
9
print(x) # NameError

У вищевказаному прикладі ми присвоїли значення 3 змінній x, але функція забула про це після виконання. Що, якщо ми не хочемо, щоб вона забула значення x?

Тут і виникає замикання функцій. Визначаючи змінну в оточуючій області видимості внутрішньої функції, ви можете зберегти її в пам’яті внутрішньої функції навіть після повернення функції.

Ось простий приклад функції, яка підраховує кількість разів її виконання:

def counter():
    count = 0
    def inner():
        nonlocal count
        count += 1
        return count
    return inner

# Return the inner function
counter = counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
1
2
3

 Згідно всіх правил Python, ми мали б втратити змінну count після першого виконання. Але, оскільки вона знаходиться у замиканні внутрішньої функції, вона залишиться там, поки ви не закриєте сеанс:

counter.__closure__[0].cell_contents
3

3. Декоратори 

Замикання функцій мають серйозніші застосування, ніж прості лічильники. Одне з них – створення декораторів. Декоратор – це вкладена функція, яку можна додати до інших функцій для покращення або навіть зміни їх поведінки.

Наприклад, нижче ми створюємо декоратор кешування, який запам’ятовує стан кожного позиційного та ключового аргумента функції.

def stateful_function(func):
    cache = {}
    def inner(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]
    return inner

Декоратор stateful_function тепер можна додати до функцій, що виконують обчислення, які можуть бути повторно використані з тими ж аргументами. Наприклад, рекурсивна функція Фібоначчі, яка повертає n-не число в послідовності:

%%time

@stateful_function
def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)
    
    
fibonacci(1000)
CPU times: user 1.53 ms, sys: 88 µs, total: 1.62 ms
Wall time: 1.62 ms

[OUT]:

43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875

Ми знайшли величезне 1000-те число в послідовності Фібоначчі за долі секунди. Ось скільки часу займе той самий процес без декоратора кешування:

%%time

def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)
    
fibonacci(40)
CPU times: user 21 s, sys: 0 ns, total: 21 s
Wall time: 21 s

[OUT]:

102334155

 На обчислення 40-го числа знадобилося 21 секунда. Без кешування обчислення 1000-го числа зайняло б дні.

4. Генератори

 Генератори – потужні конструкції в Python, які дозволяють ефективно обробляти великі обсяги даних.

Скажімо, у вас є 10 ГБ файл журналу після аварії деякого програмного забезпечення. Щоб з’ясувати, що пішло не так, вам потрібно ефективно просіювати його в Python.

Найгірший спосіб це зробити – прочитати весь файл, як показано нижче:

with open("logs.txt", "r") as f:
    contents = f.read()
    
    print(contents)

Оскільки ви проходитеся по журналах по одному рядку, вам не потрібно читати всі 10 ГБ, а лише невеликі частини кожного разу. Ось де можна використовувати генератори:

def read_large_file(filename):
    with open(filename) as f:
        while True:
            chunk = f.read(1024)
            if not chunk:
                break
            yield chunk # Generators are defined with `yield` instead of `return`

for chunk in read_large_file("logs.txt"):
    process(chunk)    # Process the chunk

Вище ми визначили генератор, який ітерується по рядках файлу журналу лише по 1024 рядки одночасно. В результаті цикл for вкінці є дуже ефективним. На кожній ітерації циклу в пам’яті знаходиться лише 1024 рядки файлу. Попередні частини видаляються, тоді як решта завантажується тільки за потребою.

Ще одна особливість генераторів – здатність повертати елемент по одному, навіть поза циклами, за допомогою функції next. Нижче ми визначаємо дуже швидку функцію, яка генерує послідовність Фібоначчі.

Щоб створити генератор, ви викликаєте функцію один раз і викликаєте next на отриманий об’єкт:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()

type(fib)
generator
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
print(next(fib)) # 3

5. Керування контекстом 

Ви, напевно, вже давно використовуєте керування контекстом. Воно дозволяє розробникам ефективно керувати ресурсами, такими як файли, бази даних та мережеві з’єднання. Вони автоматично відкривають і закривають ресурси, що призводить до чистого і безпомилкового коду.

Але є велика різниця між використанням керування контекстом і написанням власних. Правильне використання дозволяє абстрагувати велику кількість заготовочного коду поверх їх оригінальної функціональності.

Один з популярних прикладів власного керування контекстом – це таймер:

import time


class TimerContextManager:
    """
    Measure the time it takes to run
    a block of code.
    """
    def __enter__(self):
        self.start = time.time()

    def __exit__(self, type, value, traceback):
        end = time.time()
        print(f"The code took {end - self.start:.2f} seconds to execute.")

Вище ми визначили клас TimerContextManager, який буде використовуватися як наш майбутній керування контекстом. У його методі enter визначено, що відбувається при вході в контекст за допомогою ключового слова with. У цьому випадку ми запускаємо таймер.

У методі exit ми виходимо з контексту, зупиняємо таймер і повідомляємо про час, який пройшов.

with TimerContextManager():
    # This code is timed
    time.sleep(1)
The code took 1.00 seconds to execute.

 Виконання коду зайняло 1,00 секунди. Ось більш складний приклад, який дозволяє блокувати ресурси, щоб вони могли використовуватися одним процесом за один раз.

import threading

lock = threading.Lock()

class LockContextManager:
    def __enter__(self):
        lock.acquire()

    def __exit__(self, type, value, traceback):
        lock.release()

with LockContextManager():
    # This code is executed with the lock acquired
    # Only one process can be inside this block at a time

# The lock is automatically released when the with block ends, even if an error occurs

***

Скільки разів ви казали: “Я вже це знаю!”? Навіть якщо це не було так часто, зараз ви знаєте те, що потрібно вивчити, щоб стати більш кваліфікованим.

Не бійтеся незручностей, які супроводжують навчання нового. Просто пам’ятайте, що з великою силою приходять складніші помилки, які потрібно виправляти.