Last modified: June 30, 2022

This article is written in: 🇵🇱

Kod bajtowy

Kod bajtowy (ang. bytecode) w Pythonie to pośrednia, niskopoziomowa reprezentacja kodu źródłowego, która jest zrozumiała dla wirtualnej maszyny Pythona (Python Virtual Machine, PVM). Kiedy uruchamiamy skrypt Pythona, interpreter nie wykonuje bezpośrednio kodu źródłowego; zamiast tego, najpierw kompiluje go do kodu bajtowego. Ten proces kompilacji jest automatyczny i zazwyczaj niewidoczny dla programisty.

Kod bajtowy jest zoptymalizowaną wersją kodu, która może być szybciej interpretowana przez PVM. Dzięki temu wykonanie programu jest bardziej efektywne, ponieważ PVM operuje na kodzie bajtowym zamiast na kodzie źródłowym wysokiego poziomu. Kod bajtowy jest przenośny między różnymi platformami, co oznacza, że ten sam kod bajtowy może być uruchamiany na różnych systemach operacyjnych z zainstalowanym interpreterem Pythona.

Proces kompilacji w Pythonie

  1. Kiedy uruchamiasz program w Pythonie, interpreter najpierw kompiluje kod źródłowy do kodu bajtowego. Jest to reprezentacja wewnętrzna programu, która jest bardziej zwięzła i szybciej przetwarzana przez PVM.
  2. Skompilowany kod bajtowy może być zapisywany w plikach z rozszerzeniem .pyc w katalogu __pycache__. Dzięki temu przy ponownym uruchomieniu programu Python może użyć już skompilowanego kodu bajtowego, co przyspiesza uruchamianie aplikacji.
  3. Kod bajtowy jest następnie interpretowany przez wirtualną maszynę Pythona, która wykonuje instrukcje zawarte w kodzie bajtowym.

Zalety używania kodu bajtowego

Struktura kodu bajtowego

Kod bajtowy składa się z sekwencji instrukcji, z których każda reprezentuje podstawową operację, taką jak:

Przykład kodu bajtowego

Rozważmy prosty kod:

def powitaj(imie):
    print(f"Cześć, {imie}!")

Kompilując tę funkcję do kodu bajtowego i używając modułu dis, otrzymamy:

2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Cześć, {}!')
              4 LOAD_FAST                0 (imie)
              6 FORMAT_VALUE             0
              8 BUILD_STRING             2
             10 CALL_FUNCTION            1
             12 POP_TOP
             14 LOAD_CONST               0 (None)
             16 RETURN_VALUE

Każda z tych instrukcji odpowiada konkretnemu działaniu w funkcji, takim jak ładowanie wartości, wywołanie funkcji czy zwrócenie wartości.

Analiza funkcji z użyciem modułu dis

Moduł dis (disassembler) jest narzędziem wbudowanym w Pythona, które pozwala na dezasemblację kodu bajtowego. Umożliwia on analizę, w jaki sposób interpreter Pythona przekształca kod źródłowy na instrukcje kodu bajtowego.

Użycie modułu dis

Aby użyć modułu dis, należy go zaimportować:

from dis import dis

Następnie można przekazać funkcję lub inny obiekt do funkcji dis(), aby wyświetlić jego kod bajtowy.

Przykład analizy funkcji

Rozważmy funkcję sumującą dwie liczby:

def suma(a, b):
    return a + b

dis(suma)

Wynik dezasemblacji:

2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE

Analiza złożonej funkcji

Weźmy bardziej złożony przykład z użyciem pętli i warunków:

def licz_parzyste(n):
    suma = 0
    for i in range(n):
        if i % 2 == 0:
            suma += i
    return suma

dis(licz_parzyste)

Wynik dezasemblacji będzie dłuższy i zawierać będzie instrukcje obsługujące pętle (SETUP_LOOP, FOR_ITER), warunki (POP_JUMP_IF_FALSE), operacje arytmetyczne (INPLACE_ADD), itp.

Analizując ten kod bajtowy, możemy zrozumieć, jak Python implementuje pętle i warunki na niskim poziomie.

Korzyści z analizy kodu bajtowego

Analiza bloków kodu

Moduł dis pozwala również na analizę dowolnych bloków kodu, nie tylko funkcji. Możemy skompilować fragment kodu za pomocą funkcji compile(), a następnie zdezasemblować go.

Użycie funkcji compile()

Funkcja compile() przekształca ciąg znaków zawierający kod źródłowy Pythona w obiekt kodu bajtowego.

code = compile(source, filename, mode)

Przykład analizy bloku kodu

from dis import dis

code = compile("""
x = 10
y = 20
z = x + y
print(z)
""", "<string>", "exec")

dis(code)

Wynik dezasemblacji pokaże, jakie instrukcje są wykonywane podczas inicjalizacji zmiennych, wykonywania operacji arytmetycznych i wywoływania funkcji print.

Wyjaśnienie instrukcji

Analiza kodu z zewnętrznych bibliotek za pomocą modułu inspect

Moduł inspect w Pythonie dostarcza potężnych narzędzi do introspekcji, czyli badania obiektów w czasie wykonywania programu. Pozwala na analizę modułów, klas, funkcji, metod, ramek stosu i kodu źródłowego. Jest to szczególnie przydatne przy pracy z zewnętrznymi bibliotekami, gdy chcemy zrozumieć ich wewnętrzne działanie lub znaleźć odpowiedzi na pytania, których nie ma w dokumentacji.

Kluczowe funkcje dostępne w module inspect

Oto poprawiona tabela w formacie Markdown, gdzie ostatni wiersz został odpowiednio dostosowany:

Funkcja Opis
inspect.getsource(object) Zwraca kod źródłowy obiektu jako string. Umożliwia zobaczenie implementacji funkcji, klasy czy metody.
inspect.getdoc(object) Zwraca dokumentację obiektu (docstring). Pozwala na szybki dostęp do informacji o tym, co robi dany obiekt.
inspect.getcomments(object) Zwraca komentarze poprzedzające definicję obiektu.
inspect.signature(object) Zwraca obiekt Signature, który reprezentuje podpis funkcji lub metody, w tym informacje o argumentach i wartościach domyślnych.
inspect.getmodule(object) Zwraca moduł, w którym obiekt został zdefiniowany.
inspect.getmembers(object, predicate=None) Zwraca listę krotek (name, value) dla wszystkich atrybutów obiektu. Opcjonalnie można podać predykat filtrujący wyniki.
inspect.isclass(object), inspect.isfunction(object), inspect.ismethod(object) Funkcje kontrolne sprawdzające, czy obiekt jest klasą, funkcją lub metodą.

Przykładowe użycie

Przeanalizujmy klasę Tk z modułu tkinter:

import inspect
import tkinter

# Pobranie kodu źródłowego klasy Tk
source = inspect.getsource(tkinter.Tk)
print("Kod źródłowy klasy Tk:")
print(source)

# Pobranie dokumentacji klasy Tk
doc = inspect.getdoc(tkinter.Tk)
print("\nDokumentacja klasy Tk:")
print(doc)

# Pobranie podpisu konstruktora klasy Tk
signature = inspect.signature(tkinter.Tk)
print("\nPodpis konstruktora klasy Tk:")
print(signature)

# Sprawdzenie, czy Tk jest klasą
is_class = inspect.isclass(tkinter.Tk)
print(f"\nCzy tkinter.Tk jest klasą? {is_class}")

# Pobranie członków klasy Tk
members = inspect.getmembers(tkinter.Tk)
print("\nCzłonkowie klasy Tk:")
for name, value in members:
    print(f"{name}: {value}")

Praktyczne zastosowania

Ograniczenia i uwagi

Przykład problemu z brakiem kodu źródłowego

Jeśli spróbujemy użyć inspect.getsource() na wbudowanej funkcji, np.:

import inspect

print(inspect.getsource(len))

Otrzymamy błąd OSError, ponieważ funkcja len jest zaimplementowana w C i nie ma dostępnego kodu źródłowego w Pythonie.