Zrozumienie __get__ i __set__ oraz deskryptorów Pythona

310

Ja próbuje zrozumieć, co deskryptory Pythona są i jakie są przydatne do. Rozumiem, jak działają, ale oto moje wątpliwości. Rozważ następujący kod:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)


class Temperature(object):
    celsius = Celsius()
  1. Dlaczego potrzebuję klasy deskryptora?

  2. Co jest instancei ownertutaj? (w __get__). Jaki jest cel tych parametrów?

  3. Jak zadzwonić / skorzystać z tego przykładu?

Matt Bronson
źródło

Odpowiedzi:

147

Deskryptor określa sposób propertyimplementacji typu Python . Deskryptor prostu narzędzia __get__, __set__itp, a następnie dodaje się do innej klasy w jego definicji (jak to było wyżej z klasą temperatury). Na przykład:

temp=Temperature()
temp.celsius #calls celsius.__get__

Dostęp do właściwości, do której przypisałeś deskryptor ( celsiusw powyższym przykładzie) wywołuje odpowiednią metodę deskryptora.

instancein __get__jest instancją klasy (tak jak powyżej, __get__otrzyma temp, podczas gdy ownerjest klasą z deskryptorem (tak byłoby Temperature).

Musisz użyć klasy deskryptora do enkapsulacji logiki, która ją napędza. W ten sposób, jeśli deskryptor jest używany do buforowania niektórych kosztownych operacji (na przykład), może przechowywać wartość na sobie, a nie na swojej klasie.

Artykuł o deskryptorach można znaleźć tutaj .

EDYCJA: Jak jchl wskazał w komentarzach, jeśli po prostu spróbujesz Temperature.celsius, instancebędzie None.

li.davidm
źródło
6
Jaka jest różnica między selfi instance?
Lemma Prism
2
„instancja” może być instancją dowolnej klasy, jaźń będzie instancją tej samej klasy.
TheBeginner
3
@LemmaPrism selfjest instancją deskryptora, instancejest instancją klasy (jeśli jest utworzona instancja), w której znajduje się deskryptor ( instance.__class__ is owner).
Tcll,
Temperature.celsiuspodaje wartość 0.0zgodnie z kodem celsius = Celsius(). Wywoływany jest deskryptor Celsjusz, więc jego instancja ma wartość początkową 0.0przypisaną do atrybutu Klasa temperatury, Celsjusza.
Angel Salazar
109

Dlaczego potrzebuję klasy deskryptora?

Daje to dodatkową kontrolę nad działaniem atrybutów. Jeśli na przykład przyzwyczaiłeś się do programów pobierających i ustawiających w Javie, jest to sposób Pythona. Jedną z zalet jest to, że wygląda na użytkowników jak atrybut (nie ma zmiany w składni). Możesz więc zacząć od zwykłego atrybutu, a następnie, gdy musisz zrobić coś wymyślnego, przełącz się na deskryptor.

Atrybut to tylko zmienna wartość. Deskryptor umożliwia wykonanie dowolnego kodu podczas odczytu lub ustawiania (lub usuwania) wartości. Można więc sobie wyobrazić użycie go do mapowania atrybutu na pole w bazie danych, na przykład - rodzaj ORM.

Innym zastosowaniem może być odmowa zaakceptowania nowej wartości poprzez zgłoszenie wyjątku __set__- efektywnie czyniąc „atrybut” tylko do odczytu.

Co jest instancei ownertutaj? (w __get__). Jaki jest cel tych parametrów?

Jest to dość subtelne (i powód, dla którego piszę tutaj nową odpowiedź - znalazłem to pytanie, zastanawiając się nad tym samym i nie znalazłem istniejącej odpowiedzi tak wspaniale).

Deskryptor jest zdefiniowany w klasie, ale zwykle jest wywoływany z instancji. Kiedy to się nazywa z instancji zarówno instancei ownersą ustawione (i można pracować ownerz instancetak wydaje się trochę bez sensu). Ale kiedy jest wywoływany z klasy, ownerustawiany jest tylko - i dlatego tam jest.

Jest to potrzebne tylko __get__dlatego, że jako jedyny można wywołać klasę. Jeśli ustawisz wartość klasy, sam deskryptor. Podobnie do usunięcia. Dlatego ownernie jest tam potrzebny.

Jak zadzwonić / skorzystać z tego przykładu?

Oto fajna sztuczka wykorzystująca podobne klasy:

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(Używam Pythona 3; dla Pythona 2 musisz upewnić się, że te podziały to / 5.0i / 9.0). To daje:

100.0
32.0

Teraz istnieją inne, prawdopodobnie lepsze sposoby osiągnięcia tego samego efektu w pythonie (np. Gdyby celsius był właściwością, która jest tym samym podstawowym mechanizmem, ale umieszcza całe źródło w klasie temperaturowej), ale pokazuje to, co można zrobić ...

Andrzej Cooke
źródło
2
Konwersje są nieprawidłowe: powinny one wynosić C = 5 (F − 32) / 9, F = 32 + 9C / 5.
musiphil
1
Upewnij się, że masz jeden obiekt temperatury. Wykonanie następujących czynności psuje rzeczy. t1 = temperatura (190) drukuj t1. celsjusz t1.celsjusz = 100 drukuj t1.fahrenheit Teraz, gdy zaznaczysz t.celcius i t.fahrenheit, one również zostaną zmodyfikowane. t.celcius ma 115, a t.fahrenheit ma 32. co jest wyraźnie błędne. @Eric
Ishan Bhatt
1
@IshanBhatt: Myślę, że to z powodu błędu wskazanego przez musiphil powyżej. Również nie to nie jest moja odpowiedź
Eric
69

Próbuję zrozumieć, czym są deskryptory Pythona i do czego mogą być przydatne.

Deskryptory są atrybutami klas (takimi jak właściwości lub metody) za pomocą dowolnej z następujących metod specjalnych:

  • __get__ (metoda deskryptora innego niż dane, na przykład metoda / funkcja)
  • __set__ (metoda deskryptora danych, na przykład w instancji właściwości)
  • __delete__ (metoda deskryptora danych)

Te obiekty deskryptorów mogą być używane jako atrybuty w innych definicjach klas obiektów. (To znaczy, mieszkają w__dict__ obiekcie klasy).

Obiektów deskryptorów można używać do programowego zarządzania wynikami wyszukiwania z kropkami (np foo.descriptor ) W normalnym wyrażeniu, przypisaniu, a nawet usunięciu.

Funkcje / metody, metody powiązane, property , classmethod, istaticmethod wszystkie te szczególne zastosowanie metody do kontroli, jak są one dostępne za pośrednictwem przerywanej odnośnika.

ZA Deskryptor danych , jakproperty można pozwolić na leniwe oceny atrybutów w oparciu o prostszej stanu obiektu, dzięki czemu instancje użyć mniej pamięci niż gdybyś precomputed każdy możliwy atrybut.

Kolejny deskryptor danych, a member_descriptor, utworzony przez__slots__ , pozwala na oszczędność pamięci, umożliwiając klasie przechowywanie danych w zmiennej strukturze krotkowej podobnej do krotki zamiast bardziej elastycznej, ale zajmującej dużo miejsca __dict__.

Deskryptory niebędące danymi, zwykle instancja, klasa i metody statyczne, otrzymują swoje niejawne pierwsze argumenty (zwykle nazywane cls i self, odpowiednio) z ich non-danych metody deskryptora, __get__.

Większość użytkowników Pythona musi nauczyć się jedynie prostego użytkowania i nie musi dalej uczyć się ani rozumieć implementacji deskryptorów.

W głębi: czym są deskryptory?

Deskryptor to obiekt posiadający dowolną z następujących metod ( __get__, __set__lub __delete__), przeznaczony do użycia za pomocą wyszukiwania przerywanego, jakby był typowym atrybutem instancji. W przypadku obiektu właściciela obj_instance, z descriptorobiektem:

  • obj_instance.descriptorwywołuje
    descriptor.__get__(self, obj_instance, owner_class)zwracanie a value
    W ten sposób działają wszystkie metody i getwłaściwość.

  • obj_instance.descriptor = valuewywołuje
    descriptor.__set__(self, obj_instance, value)zwracanie None
    Oto jak setterdziała właściwość.

  • del obj_instance.descriptorwywołuje
    descriptor.__delete__(self, obj_instance)zwracanie None
    Oto jak deleterdziała właściwość.

obj_instancejest instancją, której klasa zawiera instancję obiektu deskryptora. selfjest instancją deskryptora (prawdopodobnie tylko jedną dla klasy obj_instance)

Aby zdefiniować to za pomocą kodu, obiekt jest deskryptorem, jeśli zestaw jego atrybutów przecina się z dowolnym z wymaganych atrybutów:

def has_descriptor_attrs(obj):
    return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))

def is_descriptor(obj):
    """obj can be instance of descriptor or the descriptor class"""
    return bool(has_descriptor_attrs(obj))

Danych deskryptorów ma __set__i / lub __delete__. Non-Data-deskryptorów ma ani ani .
__set____delete__

def has_data_descriptor_attrs(obj):
    return set(['__set__', '__delete__']) & set(dir(obj))

def is_data_descriptor(obj):
    return bool(has_data_descriptor_attrs(obj))

Przykłady wbudowanych deskryptorów:

  • classmethod
  • staticmethod
  • property
  • funkcje w ogólności

Deskryptory inne niż dane

Widzimy to classmethodi nie staticmethodjesteśmy deskryptorami danych:

>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)

Oba mają tylko __get__metodę:

>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))

Zauważ, że wszystkie funkcje są również nie deskryptorami danych:

>>> def foo(): pass
... 
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)

Deskryptor danych, property

Jest jednak propertydeskryptorem danych:

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])

Kropkowane polecenie wyszukiwania

Są to ważne rozróżnienia , ponieważ wpływają na kolejność wyszukiwania dla wyszukiwania przerywanego.

obj_instance.attribute
  1. Najpierw powyższe sprawdza, czy atrybut jest deskryptorem danych w klasie instancji,
  2. Jeśli nie, to sprawdza, czy atrybut jest w obj_instance„s __dict__, a następnie
  3. w końcu wraca do deskryptora nie-danych.

Konsekwencją tej kolejności wyszukiwania jest to, że nie-deskryptory danych, takie jak funkcje / metody, mogą zostać zastąpione przez instancje .

Podsumowanie i kolejne kroki

Dowiedzieliśmy się, że deskryptory są obiektami z dowolnego __get__, __set__lub __delete__. Te obiekty deskryptorów mogą być używane jako atrybuty w innych definicjach klas obiektów. Teraz przyjrzymy się, jak są one używane, wykorzystując Twój kod jako przykład.


Analiza kodu z pytania

Oto Twój kod, a następnie pytania i odpowiedzi na każde z nich:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)

class Temperature(object):
    celsius = Celsius()
  1. Dlaczego potrzebuję klasy deskryptora?

Twój deskryptor zapewnia, że ​​zawsze masz zmiennoprzecinkowe atrybuty tej klasy Temperaturei nie możesz użyć deldo usunięcia tego atrybutu:

>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

W przeciwnym razie deskryptory zignorują klasę właściciela i instancje właściciela, zapisując stan w deskryptorze. Równie łatwo możesz współdzielić stan we wszystkich instancjach za pomocą prostego atrybutu klasy (o ile zawsze ustawiasz go jako zmiennoprzecinkowy dla klasy i nigdy go nie usuwasz, lub czujesz się swobodnie z użytkownikami twojego kodu):

class Temperature(object):
    celsius = 0.0

To zapewnia dokładnie takie samo zachowanie jak w twoim przykładzie (patrz odpowiedź na pytanie 3 poniżej), ale używa wbudowanej wersji Pythona ( property) i byłoby uważane za bardziej idiomatyczne:

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)
  1. Co to jest tutaj instancja i właściciel? (w GET ). Jaki jest cel tych parametrów?

instancejest instancją właściciela, który wywołuje deskryptor. Właściciel jest klasą, w której obiekt deskryptora służy do zarządzania dostępem do punktu danych. Zobacz opisy specjalnych metod definiujących deskryptory obok pierwszego akapitu tej odpowiedzi, aby uzyskać bardziej opisowe nazwy zmiennych.

  1. Jak zadzwonić / skorzystać z tego przykładu?

Oto demonstracja:

>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>> 
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0

Nie możesz usunąć atrybutu:

>>> del t2.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

Nie można przypisać zmiennej, której nie można przekonwertować na zmiennoprzecinkową:

>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02

W przeciwnym razie masz tutaj stan globalny dla wszystkich instancji, którym zarządza się poprzez przypisanie do dowolnej instancji.

Oczekiwanym sposobem, w jaki najbardziej doświadczeni programiści Pythona osiągnęliby ten wynik, byłoby użycie propertydekoratora, który korzysta z tych samych deskryptorów pod maską, ale wprowadza zachowanie do implementacji klasy właściciela (ponownie, jak zdefiniowano powyżej):

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)

Który ma dokładnie takie samo oczekiwane zachowanie oryginalnego fragmentu kodu:

>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02

Wniosek

Omówiliśmy atrybuty definiujące deskryptory, różnicę między deskryptorami danych i nie-danych, wbudowane obiekty, które ich używają, oraz szczegółowe pytania dotyczące użycia.

Więc ponownie, jak użyłbyś przykładu pytania? Mam nadzieję, że nie. Mam nadzieję, że zaczniesz od mojej pierwszej sugestii (prosty atrybut klasy) i przejdziesz do drugiej sugestii (dekoratora nieruchomości), jeśli uważasz, że jest to konieczne.

Aaron Hall
źródło
1
Fajnie, nauczyłem się najwięcej z tej odpowiedzi (z pewnością także od innych). Pytanie o to stwierdzenie „Oczekiwany sposób, w jaki najbardziej doświadczeni programiści Pythona osiągnęliby ten wynik ...”. Klasa Temeperature zdefiniowana przed i po instrukcji jest identyczna. Czy tęskniłem za tym, co tutaj masz?
Yolo Voe
1
@ YoloVoe nie, zgadza się. Dodałem trochę nawiasów, aby podkreślić, że jest to powtórzenie powyższego.
Aaron Hall
1
To NIESAMOWITA odpowiedź. Muszę go przeczytać jeszcze kilka razy, ale wydaje mi się, że moje rozumienie Pythona po prostu podskoczyło o kilka stopni
Lucas Young,
20

Zanim przejdziemy do szczegółów deskryptorów, może być ważne, aby wiedzieć, jak działa wyszukiwanie atrybutów w Pythonie. Zakłada się, że klasa nie ma metaklasy i że używa domyślnej implementacji__getattribute__ (obie można wykorzystać do „dostosowania” zachowania).

Najlepszą ilustracją wyszukiwania atrybutów (w Pythonie 3.x lub klas nowego stylu w Pythonie 2.x) jest w tym przypadku Zrozumienie metaklas Pythona (dziennik kodów Jonela) . Obraz używa: jako substytutu dla „niestandardowego wyszukiwania atrybutów”.

Stanowi odnośnika atrybutu foobarna zasadzie instancez Class:

wprowadź opis zdjęcia tutaj

Ważne są tutaj dwa warunki:

  • Jeśli klasa instancema wpis dla nazwy atrybutu i ma__get__ i __set__.
  • Jeśli instancenie ma pozycji dla nazwy atrybutu, ale klasa ją ma i ma__get__ .

Właśnie tam wchodzą deskryptory:

  • Deskryptory danych, które mają zarówno __get__i__set__ .
  • Deskryptory inne niż dane, które mają tylko __get__.

W obu przypadkach zwracana wartość jest __get__wywoływana z instancją jako pierwszym argumentem, a klasą jako drugim argumentem.

Wyszukiwanie jest jeszcze bardziej skomplikowane w przypadku wyszukiwania atrybutów klasy (patrz na przykład wyszukiwanie atrybutów klasy (na wyżej wspomnianym blogu) ).

Przejdźmy do konkretnych pytań:

Dlaczego potrzebuję klasy deskryptora?

W większości przypadków nie musisz pisać klas deskryptorów! Jednak prawdopodobnie jesteś bardzo regularnym użytkownikiem końcowym. Na przykład funkcje. Funkcje są deskryptorami, dlatego funkcji można używać jako metod z selfdomyślnie przekazywanym jako pierwszy argument.

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

Jeśli spojrzysz test_methodna instancję, otrzymasz „powiązaną metodę”:

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>

Podobnie można również powiązać funkcję, __get__ręcznie wywołując jej metodę (niezbyt zalecane, tylko w celach ilustracyjnych):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

Możesz nawet nazwać tę „samodzielną metodą”:

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>

Zauważ, że nie podałem żadnych argumentów, a funkcja zwróciła instancję, którą związałem!

Funkcje to deskryptory inne niż dane !

Oto niektóre wbudowane przykłady deskryptora danych property. Zaniedbanie getter, setteri deskryptor jest (z deskryptora HowTo Guide "Properties" ):deleterproperty

class Property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

Ponieważ jest to dane deskryptor to jest wywoływana, gdy patrzysz w górę „” w imię propertyi po prostu deleguje do funkcji ozdobione @property, @name.setteroraz @name.deleter(jeśli występuje).

Istnieje kilka innych deskryptorów w bibliotece standardowej, na przykład staticmethod, classmethod.

Deskryptory są łatwe (choć rzadko ich potrzebujesz): abstrakcyjny wspólny kod dostępu do atrybutów. propertyjest abstrakcją na przykład dostępu do zmiennej, functionzapewnia abstrakcję dla metod, staticmethodzapewnia abstrakcję dla metod, które nie wymagają dostępu do instancji iclassmethod zapewnia abstrakcję dla metod wymagających dostępu do klasy, a nie dostępu do instancji (jest to nieco uproszczone).

Innym przykładem może być właściwość klasy .

Ciekawym przykładem (korzystanie __set_name__z Pythona 3.6) może być również właściwość, która zezwala tylko na określony typ:

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

Następnie możesz użyć deskryptora w klasie:

class Test(object):
    int_prop = TypedProperty(int)

I baw się z tym trochę:

>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>

Lub „leniwa nieruchomość”:

class LazyProperty(object):
    __slots__ = ('_fget', '_name')
    def __init__(self, fget):
        self._fget = fget

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        try:
            return instance.__dict__[self._name]
        except KeyError:
            value = self._fget(instance)
            instance.__dict__[self._name] = value
            return value

    def __set_name__(self, klass, name):
        self._name = name

class Test(object):
    @LazyProperty
    def lazy(self):
        print('calculating')
        return 10

>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10

Są to przypadki, w których przeniesienie logiki do wspólnego deskryptora może mieć sens, jednak można je również rozwiązać (ale być może poprzez powtórzenie kodu) innymi środkami.

Co jest instancei ownertutaj? (w __get__). Jaki jest cel tych parametrów?

To zależy od sposobu wyszukiwania atrybutu. Jeśli szukasz atrybutu na instancji, to:

  • drugim argumentem jest instancja, w której sprawdzasz atrybut
  • trzeci argument to klasa instancji

W przypadku wyszukiwania atrybutu w klasie (przy założeniu, że deskryptor jest zdefiniowany w klasie):

  • drugim argumentem jest None
  • trzeci argument to klasa, w której sprawdzasz atrybut

Więc w zasadzie trzeci argument jest konieczne, jeśli chcesz, aby dostosować zachowanie podczas wykonywania look-up na poziomie klasy (bo instancejestNone ).

Jak zadzwonić / skorzystać z tego przykładu?

Twój przykład jest w zasadzie właściwością, która pozwala tylko na wartości, które można przekonwertować floati które są współużytkowane przez wszystkie instancje klasy (i klasy - chociaż można użyć tylko dostępu do odczytu w klasie, w przeciwnym razie zastąpiłbyś instancję deskryptora ):

>>> t1 = Temperature()
>>> t2 = Temperature()

>>> t1.celsius = 20   # setting it on one instance
>>> t2.celsius        # looking it up on another instance
20.0

>>> Temperature.celsius  # looking it up on the class
20.0

Właśnie dlatego deskryptory zazwyczaj używają drugiego argumentu ( instance) do przechowywania wartości, aby uniknąć jej udostępniania. Jednak w niektórych przypadkach dzielenie wartości między instancjami może być pożądane (chociaż w tej chwili nie mogę wymyślić scenariusza). Jednak nie ma praktycznie żadnego sensu dla własności Celsjusza w klasie temperaturowej ... może z wyjątkiem czysto akademickich ćwiczeń.

MSeifert
źródło
nie jestem pewien, czy przezroczyste tło grafiki naprawdę cierpiące w trybie ciemnym powinno być zgłaszane jako błąd w przepełnieniu stosu.
Tshirtman
@ Tshirtman Myślę, że jest to problem z samym obrazem. Nie jest całkowicie przezroczysty ... Wziąłem go z postu na blogu i nie wiem, jak go odtworzyć z odpowiednim przezroczystym tłem. Szkoda, że ​​wygląda tak dziwnie na ciemnym tle :(
MSeifert
9

Dlaczego potrzebuję klasy deskryptora?

Zainspirowany przez Fluent Python przez Buciano Ramalho

Obrazowanie masz taką klasę

class LineItem:
     price = 10.9
     weight = 2.1
     def __init__(self, name, price, weight):
          self.name = name
          self.price = price
          self.weight = weight

item = LineItem("apple", 2.9, 2.1)
item.price = -0.9  # it's price is negative, you need to refund to your customer even you delivered the apple :(
item.weight = -0.8 # negative weight, it doesn't make sense

Powinniśmy sprawdzić wagę i cenę, aby uniknąć przypisania im liczby ujemnej, możemy napisać mniej kodu, jeśli użyjemy deskryptora jako proxy, ponieważ

class Quantity(object):
    __index = 0

    def __init__(self):
        self.__index = self.__class__.__index
        self._storage_name = "quantity#{}".format(self.__index)
        self.__class__.__index += 1

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self._storage_name, value)
        else:
           raise ValueError('value should >0')

   def __get__(self, instance, owner):
        return getattr(instance, self._storage_name)

następnie zdefiniuj klasę LineItem w następujący sposób:

class LineItem(object):
     weight = Quantity()
     price = Quantity()

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price

i możemy rozszerzyć klasę Quantity, aby przeprowadzić bardziej powszechne sprawdzanie poprawności

wllbll
źródło
1
Interesujący przypadek użycia, ponieważ pokazuje, jak używać deskryptora do interakcji z wieloma instancjami użytkowników. Początkowo nie rozumiałem ważnego punktu: Atrybut z deskryptorem musi zostać utworzony w przestrzeni nazw klasy (np. weight = Quantity()Ale wartości muszą być ustawione w przestrzeni nazw instancji tylko przy użyciu self(np. self.weight = 4), W przeciwnym razie atrybut zostałby powiązany z nową wartością a deskryptor zostałby odrzucony. Fajnie!
min
Nie jestem w stanie zrozumieć jednej rzeczy. Definiowania weight = Quantity()jako klasy zmiennej i jej __get__i __set__pracują na zmiennej instancji. W jaki sposób?
Technocrat
0

Próbowałem (z niewielkimi zmianami, jak sugerowano) kod z odpowiedzi Andrew Cooke. (Używam Pythona 2.7).

Kod:

#!/usr/bin/env python
class Celsius:
    def __get__(self, instance, owner): return 9 * (instance.fahrenheit + 32) / 5.0
    def __set__(self, instance, value): instance.fahrenheit = 32 + 5 * value / 9.0

class Temperature:
    def __init__(self, initial_f): self.fahrenheit = initial_f
    celsius = Celsius()

if __name__ == "__main__":

    t = Temperature(212)
    print(t.celsius)
    t.celsius = 0
    print(t.fahrenheit)

Wynik:

C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212

W przypadku Pythona wcześniejszego niż 3 upewnij się, że podklasujesz z obiektu, co sprawi, że deskryptor będzie działał poprawnie, ponieważ magia get nie działa dla klas starego stylu.

Gregory Kuhn
źródło
1
Deskryptory działają tylko z klasami nowego stylu. Dla Pythona 2.x oznacza to wyprowadzenie twojej klasy z „obiektu”, który jest domyślny w Pythonie 3.
Ivo van der Wijk
0

Zobaczysz https://docs.python.org/3/howto/descriptor.html#properties

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)
Yonks Somarl
źródło
1
To nie odpowiada na pytanie ani nie dostarcza żadnych użytecznych informacji.
Sebastian Nielsen