Podklasa: Czy można zastąpić właściwość za pomocą konwencjonalnego atrybutu?

14

Załóżmy, że chcemy stworzyć rodzinę klas, które są różnymi implementacjami lub specjalizacjami nadrzędnej koncepcji. Załóżmy, że istnieje pewna domyślna implementacja niektórych właściwości pochodnych. Chcielibyśmy umieścić to w klasie podstawowej

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

Tak więc podklasa automatycznie będzie mogła policzyć swoje elementy w tym dość głupim przykładzie

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

Concrete_Math_Set(1,2,3).size
# 3

Ale co jeśli podklasa nie chce używać tej wartości domyślnej? To nie działa:

import math

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

Square_Integers_Below(7)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 3, in __init__
# AttributeError: can't set attribute

Zdaję sobie sprawę, że istnieją sposoby na zastąpienie właściwości za pomocą właściwości, ale chciałbym tego uniknąć. Ponieważ celem klasy podstawowej jest ułatwienie życia użytkownikowi, aby nie dodawać wzdęć poprzez narzucenie (z wąskiego punktu widzenia podklasy) zwiniętej i zbędnej metody dostępu.

Czy da się to zrobić? Jeśli nie, jakie jest następne najlepsze rozwiązanie?

Paul Panzer
źródło

Odpowiedzi:

6

Właściwość to deskryptor danych, który ma pierwszeństwo przed atrybutem instancji o tej samej nazwie. Możesz zdefiniować deskryptor niebędący danymi za pomocą unikalnej __get__()metody: atrybut instancji ma pierwszeństwo przed deskryptorem niebędącym danymi o tej samej nazwie, zobacz dokumentację . Problem polega na tym, że non_data_propertyzdefiniowana poniżej funkcja służy wyłącznie do obliczeń (nie można zdefiniować setera ani separatora), ale wydaje się, że tak jest w twoim przykładzie.

import math

class non_data_property:
    def __init__(self, fget):
        self.__doc__ = fget.__doc__
        self.fget = fget

    def __get__(self, obj, cls):
        if obj is None:
            return self
        return self.fget(obj)

class Math_Set_Base:
    @non_data_property
    def size(self, *elements):
        return len(self.elements)

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1, 2, 3).size) # 3
print(Square_Integers_Below(1).size) # 1
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below(9).size) # 3

Zakłada się jednak, że masz dostęp do klasy podstawowej, aby dokonać tych zmian.

Arkelis
źródło
To jest prawie idealne. Oznacza to, że poperty, nawet jeśli zdefiniujesz tylko getter, domyślnie doda setter, który nie robi nic oprócz blokowania przypisania? Ciekawy. Prawdopodobnie zaakceptuję tę odpowiedź.
Paul Panzer
Tak, zgodnie z dokumentacją, właściwość zawsze definiuje wszystkie trzy metody deskryptora ( __get__(), __set__()i __delete__()) i wywołują one, AttributeErrorjeśli nie zapewnisz dla nich żadnej funkcji. Zobacz odpowiednik implementacji właściwości w języku Python .
Arkelis
Dopóki nie obchodzi dla setterlub __set__od nieruchomości, będzie to działać. Ostrzegam jednak, że oznacza to również, że możesz łatwo zastąpić właściwość nie tylko na poziomie klasy ( self.size = ...), ale nawet na poziomie instancji (np. Concrete_Math_Set(1, 2, 3).size = 10Byłoby równie ważne). Jedzenie dla myśli :)
r.ook
I Square_Integers_Below(9).size = 1jest również ważna, ponieważ jest to atrybut prosty. W tym szczególnym przypadku użycia „rozmiar” może to wydawać się trafne (zamiast tego należy użyć właściwości), ale w ogólnym przypadku „łatwo zastąpić obliczoną rekwizyt w podklasie”, w niektórych przypadkach może być dobre. Możesz także kontrolować dostęp do atrybutów za pomocą, __setattr__()ale może to być przytłaczające.
Arkelis
1
Nie zgadzam się, że może istnieć uzasadniony przypadek użycia tego, chciałem tylko wspomnieć o zastrzeżeniu, ponieważ może to skomplikować próby debugowania w przyszłości. Tak długo, jak ty i OP jesteście świadomi konsekwencji, wszystko jest w porządku.
r.ook
10

To będzie długa, wyczerpująca odpowiedź, która może być tylko komplementarna ... ale twoje pytanie zabrało mnie na przejażdżkę w dół króliczej nory, więc chciałbym również podzielić się swoimi odkryciami (i bólem).

Ostatecznie może się okazać, że ta odpowiedź nie jest pomocna w rzeczywistym problemie. W rzeczywistości doszedłem do wniosku, że wcale tego nie zrobiłbym. To powiedziawszy, tło do tego wniosku może cię trochę zabawić, ponieważ szukasz więcej szczegółów.


Rozwiązanie niektórych nieporozumień

Pierwsza odpowiedź, choć poprawna w większości przypadków, nie zawsze tak jest. Rozważmy na przykład tę klasę:

class Foo:
    def __init__(self):
        self.name = 'Foo!'
        @property
        def inst_prop():
            return f'Retrieving {self.name}'
        self.inst_prop = inst_prop

inst_prop, będąc jednocześnie property , jest nieodwołalnie atrybutem instancji:

>>> Foo.inst_prop
Traceback (most recent call last):
  File "<pyshell#60>", line 1, in <module>
    Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'

Wszystko zależy od tego, gdzie Twoje propertyjest zdefiniowane. Jeśli twoje @propertyjest zdefiniowane w ramach klasy „scope” (lub tak naprawdę tonamespace ), staje się atrybutem klasy. W moim przykładzie sama klasa nie zdaje sobie z tego sprawy, inst_propdopóki nie zostanie utworzona . Oczywiście tutaj wcale nie jest bardzo przydatna.


Ale najpierw zajmijmy się twoim komentarzem na temat rozwiązania dziedziczenia ...

Więc w jaki sposób dziedziczenie uwzględnia ten problem? W tym artykule omówiono nieco temat i kolejność rozwiązywania metod jest w pewnym stopniu powiązana, chociaż omawia ona głównie szerokość dziedziczenia zamiast głębokości.

W połączeniu z naszym ustaleniem, biorąc pod uwagę poniższe ustawienia:

@property
def some_prop(self):
    return "Family property"

class Grandparent:
    culture = some_prop
    world_view = some_prop

class Parent(Grandparent):
    world_view = "Parent's new world_view"

class Child(Parent):
    def __init__(self):
        try:
            self.world_view = "Child's new world_view"
            self.culture = "Child's new culture"
        except AttributeError as exc:
            print(exc)
            self.__dict__['culture'] = "Child's desired new culture"

Wyobraź sobie, co się dzieje, gdy te wiersze są wykonywane:

print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

Wynik jest zatem następujący:

Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>

Zauważ jak:

  1. self.world_view był w stanie być zastosowany, podczas gdy self.culture nie udało się
  2. culturenie istnieje w Child.__dict__(mappingproxy klasie, nie należy mylić z instancją __dict__)
  3. Mimo że cultureistnieje c.__dict__, nie jest przywoływany.

Być może potrafisz zgadnąć, dlaczego - world_viewzostał nadpisany przez Parentklasę jako nie-własność, więc również Childbył w stanie go zastąpić. Tymczasem, ponieważ culturejest dziedziczona, to istnieje tylko w obrębie mappingproxyodGrandparent :

Grandparent.__dict__ is: {
    '__module__': '__main__', 
    'culture': <property object at 0x00694C00>, 
    'world_view': <property object at 0x00694C00>, 
    ...
}

W rzeczywistości, jeśli spróbujesz usunąć Parent.culture:

>>> del Parent.culture
Traceback (most recent call last):
  File "<pyshell#67>", line 1, in <module>
    del Parent.culture
AttributeError: culture

Zauważysz, że nawet nie istnieje Parent. Ponieważ obiekt bezpośrednio odwołuje się doGrandparent.culture .


A co z nakazem rozstrzygnięcia?

Dlatego jesteśmy zainteresowani obserwowaniem rzeczywistego zlecenia rozdzielczości, spróbujmy usunąć Parent.world_view:

del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Zastanawiam się, jaki jest wynik?

c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>

Wrócił do Dziadka world_view property, mimo że udało nam się wcześniej przypisać self.world_view! Ale co, jeśli zmienimy siłą world_viewna poziomie klasy, podobnie jak inna odpowiedź? Co jeśli go usuniemy? Co jeśli przypisamy bieżący atrybut klasy jako właściwość?

Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Wynik to:

# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view

# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view

# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>

Jest to interesujące, ponieważ c.world_viewjest przywracane do atrybutu wystąpienia, podczas gdy Child.world_viewjest to ten, który przypisaliśmy. Po usunięciu atrybutu instancji następuje powrót do atrybutu klasy. Po ponownym przypisaniu Child.world_viewdo właściwości natychmiast tracimy dostęp do atrybutu instancji.

Dlatego możemy założyć następującą kolejność rozwiązywania :

  1. Jeśli istnieje atrybut klasy i jest to property, pobierz jego wartość za pomocą getterlubfget (więcej na ten temat później). Bieżąca klasa od pierwszej do podstawowej jako ostatnia.
  2. W przeciwnym razie, jeśli istnieje atrybut instancji, pobierz wartość atrybutu instancji.
  3. W przeciwnym razie pobierz propertyatrybut nieklasowy. Bieżąca klasa pierwsza do klasy podstawowej ostatnia.

W takim przypadku usuńmy root property:

del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

Co daje:

c.culture is: Child's desired new culture
Traceback (most recent call last):
  File "<pyshell#74>", line 1, in <module>
    print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'

Ta dah! Childteraz ma swoją własną cultureopartą na silnym wstawianiu do c.__dict__. Child.culturenie istnieje oczywiście, ponieważ nigdy nie został zdefiniowany w atrybucie Parentlub Childclass, i Grandparentzostał usunięty.


Czy to jest podstawowa przyczyna mojego problemu?

Właściwie nie . Występujący błąd, który wciąż obserwujemy podczas przypisywania self.culture, jest zupełnie inny . Ale kolejność dziedziczenia stanowi tło dla odpowiedzi - która jestproperty samą sobą.

Oprócz poprzednio wspomnianej gettermetody, propertymasz również kilka zgrabnych sztuczek w rękawach. W tym przypadku najbardziej istotna jest metoda setterlub fset, która jest uruchamiana przez self.culture = ...linię. Ponieważ propertynie zaimplementowałeś żadnej setterani fgetfunkcji, python nie wie, co robić, i AttributeErrorzamiast tego rzuca (tj can't set attribute.).

Jeśli jednak zastosowałeś settermetodę:

@property
def some_prop(self):
    return "Family property"

@some_prop.setter
def some_prop(self, val):
    print(f"property setter is called!")
    # do something else...

Podczas tworzenia Childklasy otrzymasz:

Instantiating Child class...
property setter is called!

Zamiast odbierać AttributeError, aktualnie wywołujesz some_prop.settermetodę. Co daje ci większą kontrolę nad twoim obiektem ... dzięki naszym wcześniejszym ustaleniom wiemy, że musimy nadpisać atrybut klasy, zanim dotrze on do właściwości. Można to zaimplementować w klasie bazowej jako wyzwalacz. Oto nowy przykład:

class Grandparent:
    @property
    def culture(self):
        return "Family property"

    # add a setter method
    @culture.setter
    def culture(self, val):
        print('Fine, have your own culture')
        # overwrite the child class attribute
        type(self).culture = None
        self.culture = val

class Parent(Grandparent):
    pass

class Child(Parent):
    def __init__(self):
        self.culture = "I'm a millennial!"

c = Child()
print(c.culture)

Co skutkuje w:

Fine, have your own culture
I'm a millennial!

TA DAH! Możesz teraz zastąpić własny atrybut instancji dziedziczoną właściwością!


Więc problem rozwiązany?

... Nie całkiem. Problem z tym podejściem polega na tym, że nie możesz mieć właściwej settermetody. Są przypadki, w których chcesz ustawić swoje wartości property. Ale teraz, gdy ustawisz self.culture = ...to będzie zawsze zastąpić cokolwiek działać zdefiniowano w getter(który w tym przypadku naprawdę jest po prostu @propertyowinięta część. Ty możesz dodać w środkach bardziej zróżnicowany, ale tak czy inaczej będzie to zawsze obejmować więcej niż tylko self.culture = .... na przykład:

class Grandparent:
    # ...
    @culture.setter
    def culture(self, val):
        if isinstance(val, tuple):
            if val[1]:
                print('Fine, have your own culture')
                type(self).culture = None
                self.culture = val[0]
        else:
            raise AttributeError("Oh no you don't")

# ...

class Child(Parent):
    def __init__(self):
        try:
            # Usual setter
            self.culture = "I'm a Gen X!"
        except AttributeError:
            # Trigger the overwrite condition
            self.culture = "I'm a Boomer!", True

To waaaaay bardziej skomplikowane niż w innych odpowiedzi, size = Nonena poziomie klasy.

Możesz również rozważyć napisanie własnego deskryptora zamiast obsługi metod __get__i __set__lub dodatkowych metod. Ale pod koniec dnia, gdy self.culturejest przywoływane, __get__zawsze będzie uruchamiane jako pierwsze, a kiedy self.culture = ...jest przywoływane, __set__zawsze będzie uruchamiane jako pierwsze. O ile próbowałem, nie można tego obejść.


Sedno problemu, IMO

Problem, który tu widzę, polega na tym, że nie możesz mieć ciasta i też jesz. propertyoznacza deskryptor z wygodnym dostępem z metod takich jak getattrlub setattr. Jeśli chcesz, aby te metody osiągnęły inny cel, po prostu prosisz o kłopoty. Być może przemyślę to podejście:

  1. Czy naprawdę potrzebuję property do tego?
  2. Czy metoda może mi służyć inaczej?
  3. Jeśli potrzebuję property, czy jest jakiś powód, dla którego musiałbym go zastąpić?
  4. Czy podklasa naprawdę należy do tej samej rodziny, jeśli propertynie mają one zastosowania?
  5. Jeśli muszę zastąpić dowolne / wszystkie propertys, czy osobna metoda byłaby dla mnie lepsza niż zwykłe ponowne przypisanie, ponieważ ponowne przypisanie może przypadkowo unieważnić te property?

W przypadku punktu 5 moim podejściem byłaby overwrite_prop()metoda w klasie bazowej, która nadpisze aktualny atrybut klasy, aby propertynie był już wyzwalany:

class Grandparent:
    # ...
    def overwrite_props(self):
        # reassign class attributes
        type(self).size = None
        type(self).len = None
        # other properties, if necessary

# ...

# Usage
class Child(Parent):
    def __init__(self):
        self.overwrite_props()
        self.size = 5
        self.len = 10

Jak widać, choć wciąż nieco wymyślony, jest co najmniej bardziej jawny niż tajemniczy size = None. To powiedziawszy, ostatecznie nie zastąpiłbym w ogóle właściwości i ponownie rozważyłbym mój projekt od podstaw.

Jeśli dotarłeś tak daleko - dziękuję za podróż ze mną. To było zabawne, małe ćwiczenie.

wieża
źródło
2
Wow, wielkie dzięki! Zajmie mi to trochę czasu, aby to przyswoić, ale przypuszczam, że o to poprosiłem.
Paul Panzer
6

A @propertyjest zdefiniowane na poziomie klasy. Dokumentacja zawiera szczegółowe informacje na temat tego, jak to działa, ale wystarczy powiedzieć, że ustawienie lub zmusza właściwość do wywołania określonej metody. Jednakżeproperty obiekt zarządzający tym procesem jest definiowany za pomocą własnej definicji klasy. Oznacza to, że jest zdefiniowany jako zmienna klasy, ale zachowuje się jak zmienna instancji.

Jedną z konsekwencji tego jest to, że możesz swobodnie zmienić przypisanie na poziomie klasy :

print(Math_Set_Base.size)
# <property object at 0x10776d6d0>

Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4

I tak jak każda inna nazwa na poziomie klasy (np. Metody), możesz zastąpić ją w podklasie, po prostu jawnie definiując ją inaczej:

class Square_Integers_Below(Math_Set_Base):
    # explicitly define size at the class level to be literally anything other than a @property
    size = None

    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Square_Integers_Below(4).size)  # 2
print(Square_Integers_Below.size)     # None

Kiedy tworzymy rzeczywistą instancję, zmienna instancji po prostu zacienia zmienną klasy o tej samej nazwie. propertyPrzedmiot zazwyczaj używa niektórych shenanigans manipulować ten proces (tj stosujące pobierające i ustawiające metody), ale kiedy nazwa klasy poziomie nie jest zdefiniowana jako właściwość, nic szczególnego się nie dzieje, a więc działa jak można oczekiwać od każdej innej zmiennej.

Facet z zielonego płaszcza
źródło
Dzięki, to jest odświeżająco proste. Czy to oznacza, że ​​nawet jeśli w mojej klasie i jej przodkach nigdy nie było żadnych właściwości, to najpierw przeszuka całe drzewo spadkowe w celu znalezienia nazwy na poziomie klasy, zanim w ogóle zajmie się nią __dict__? Czy istnieją też sposoby na zautomatyzowanie tego? Tak, to tylko jedna linijka, ale jest to coś bardzo tajemniczego, jeśli nie jesteś zaznajomiony z krwawymi szczegółami własności itp.
Paul Panzer
@PaulPanzer Nie przyjrzałem się procedurom, więc nie mogłem sam dać zadowalającej odpowiedzi. Jeśli chcesz, możesz spróbować rozwiązać zagadkę z kodu źródłowego cpython . Jeśli chodzi o automatyzację procesu, nie sądzę, że istnieje dobry sposób, aby to zrobić inaczej niż nie czyniąc go w ogóle własnością, lub po prostu dodając komentarz / dokumentację, aby osoby czytające Twój kod wiedziały, co robisz . Coś w stylu# declare size to not be a @property
facet Green Cloak
4

W ogóle nie potrzebujesz przypisania size. sizejest właściwością w klasie podstawowej, więc możesz zastąpić tę właściwość w klasie potomnej:

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

    # size = property(lambda self: self.elements)


class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._cap = cap

    @property
    def size(self):
        return int(math.sqrt(self._cap))

    # size = property(lambda self: int(math.sqrt(self._cap)))

Możesz (mikro) zoptymalizować to, obliczając pierwiastek kwadratowy:

class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._size = int(math.sqrt(self._cap))

    @property
    def size(self):
        return self._size
chepner
źródło
To świetny punkt, po prostu zastąp właściwość nadrzędną właściwością podrzędną! +1
r.ook
Jest to w pewien sposób prosta rzecz, ale byłem zainteresowany i poprosiłem konkretnie o sposoby, w jakie można zezwolić na obejście własności innej niż własność, aby nie nakładać niedogodności związanych z koniecznością napisania właściwości zapamiętywania, w przypadku której proste przypisanie praca.
Paul Panzer
Nie jestem pewien, dlaczego miałbyś tak komplikować swój kod. Za niewielką cenę rozpoznania, że sizejest to właściwość, a nie atrybut instancji, nie musisz robić nic wymyślnego.
chepner
Mój przypadek użycia to wiele klas potomnych z mnóstwem atrybutów; nie trzeba pisać 5 wierszy zamiast jednego dla wszystkich, co uzasadnia pewną dozę szaleństwa, IMO.
Paul Panzer
2

Wygląda na to, że chcesz zdefiniować sizew swojej klasie:

class Square_Integers_Below(Math_Set_Base):
    size = None

    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

Inną opcją jest przechowywanie capw klasie i obliczanie jej za pomocą sizezdefiniowanej właściwości (która zastępuje właściwość klasy bazowej size).

Uri
źródło
2

Sugeruję dodanie takiego setera:

class Math_Set_Base:
    @property
    def size(self):
        try:
            return self._size
        except:
            return len(self.elements)

    @size.setter
    def size(self, value):
        self._size = value

W ten sposób możesz zastąpić domyślną .sizewłaściwość w następujący sposób:

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1,2,3).size) # 3
print(Square_Integers_Below(7).size) # 2
Amir
źródło
1

Możesz także zrobić następną rzecz

class Math_Set_Base:
    _size = None

    def _size_call(self):
       return len(self.elements)

    @property
    def size(self):
        return  self._size if self._size is not None else self._size_call()

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self._size = int(math.sqrt(cap))
Ryabchenko Alexander
źródło
To fajnie, ale tak naprawdę nie potrzebujesz _sizeani _size_calltam. Mógłbyś wypalić wywołanie funkcji sizejako warunek i użyć try... except... do przetestowania _sizezamiast brać dodatkowe odwołanie do klasy, które nie zostanie użyte. Niezależnie od tego uważam, że jest to jeszcze bardziej tajemnicze niż zwykłe size = Nonenadpisywanie.
r.ook