programowanieSenior Python Developer

Czym są deskryptory w Pythonie? Jak się je implementuje, do czego są używane, jaka jest różnica między nimi a property i jakie są typowe błędy przy ich używaniu?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

Deskryptor to obiekt, który implementuje dowolną z metod get, set i delete. Pozwala on kontrolować dostęp do atrybutów klasy.

Standardowe metody deskryptora:

  • __get__(self, instance, owner)
  • __set__(self, instance, value)
  • __delete__(self, instance)

Deskryptory dzielą się na:

  • Klasyczne (data descriptors) — implementują set, mają pierwszeństwo przed zwykłymi atrybutami;
  • Nie-klasyczne (non-data descriptors) — implementują tylko get, ustępują zwykłemu atrybutowi.

Przykład użycia:

class OnlyPositive: def __init__(self): self._name = '_value' def __get__(self, instance, owner): return instance.__dict__[self._name] def __set__(self, instance, value): if value < 0: raise ValueError('Wartość musi być >= 0') instance.__dict__[self._name] = value class Account: value = OnlyPositive() def __init__(self, value): self.value = value acc = Account(10) acc.value = -1 # ValueError!

property to tylko syntaktyczny cukier do tworzenia deskryptorów na poziomie klasy.

Pytanie z pułapką

Co się stanie, jeśli utworzymy deskryptor jako atrybut instancji, a nie klasy?

Wielu sądzi, że deskryptor będzie działać, ale to nieprawda.

Prawidłowa odpowiedź:

Deskryptor działa tylko, jeśli jest bezpośrednio zdefiniowany jako atrybut klasy. Jeśli deskryptor jest zdefiniowany jako atrybut instancji, metody get/set nie zostaną wywołane — działa zwykła logika dostępu do atrybutu.

Przykłady rzeczywistych błędów z braku znajomości niuansów tematu


Historia

Użycie property zamiast pełnoprawnego deskryptora

Do walidacji i innych powiązanych atrybutów programista użył prostego property, co prowadziło do częstego powielania logiki i braku możliwości ponownego wykorzystania kodu pomiędzy różnymi klasami.


Historia

Niezachowanie niezmienności przy ponownym użyciu magazynowania

Deskryptor przechowywał dane w wewnętrznym atrybucie swojej klasy (self.x), a nie w wewnętrznym magazynie instancji, przez co atrybut stawał się "wspólny" i różne instancje klasy nadpisywały sobie nawzajem wartości — "przeciekały" dane między obiektami.


Historia

Pomyłka między deskryptorem danych a deskryptorem nie-danych

W skomplikowanej hierarchii pominięto implementację set, przez co zwykły atrybut instancji przysłonił deskryptor, łamiąc cały mechanizm walidacji — błąd występował nie zawsze, był trudny do debugowania.