Waarom werken de @staticmethoden van Python zo slecht samen met versierde klassen?

Onlangs heeft de StackOverflow-community me geholpen een vrij beknopte @memoize -decorator te ontwikkelen die niet alleen functies, maar ook methoden en klassen op een algemene manier kan versieren, dat wil zeggen, zonder enige voorkennis te hebben van wat voor soort dingen het zal worden versierd.

Een van de problemen die ik tegenkwam, was dat als je een klas met @memoize versierde en vervolgens probeerde een van zijn methoden te versieren met @staticmethod , dit niet zou werken zoals verwacht, dat wil zeggen dat je helemaal geen ClassName.thestaticmethod() zou kunnen aanroepen. De originele oplossing die ik bedacht zag er zo uit:

def memoize(obj):
    """General-purpose cache for classes, methods, and functions."""
    cache = obj.cache = {}

    def memoizer(*args, **kwargs):
        """Do cache lookups and populate the cache in the case of misses."""
        key = args[0] if len(args) is 1 else args
        if key not in cache:
            cache[key] = obj(*args, **kwargs)
        return cache[key]

    # Make the memoizer func masquerade as the object we are memoizing.
    # This makes class attributes and static methods behave as expected.
    for k, v in obj.__dict__.items():
        memoizer.__dict__[k] = v.__func__ if type(v) is staticmethod else v
    return memoizer

Maar toen leerde ik over functools.wraps , die bedoeld is om de decorateurfunctie te laten maskeren als de ingerichte functie op een veel schonere en meer complete manier, en inderdaad heb ik het zo aangenomen:

def memoize(obj):
    """General-purpose cache for class instantiations, methods, and functions."""
    cache = obj.cache = {}

    @functools.wraps(obj)
    def memoizer(*args, **kwargs):
        """Do cache lookups and populate the cache in the case of misses."""
        key = args[0] if len(args) is 1 else args
        if key not in cache:
            cache[key] = obj(*args, **kwargs)
        return cache[key]
    return memoizer

Hoewel dit er erg goed uitziet, biedt functools.wraps absoluut geen ondersteuning voor static method s of classmethod s. Als u bijvoorbeeld zoiets heeft geprobeerd:

@memoize
class Flub:
    def __init__(self, foo):
        """It is an error to have more than one instance per foo."""
        self.foo = foo

    @staticmethod
    def do_for_all():
        """Have some effect on all instances of Flub."""
        for flub in Flub.cache.values():
            print flub.foo
Flub('alpha') is Flub('alpha')  #=> True
Flub('beta') is Flub('beta')    #=> True
Flub.do_for_all()               #=> 'alpha'
                                #   'beta'

Dit zou werken met de eerste implementatie van @memoize die ik heb vermeld, maar zou TypeError ophalen: het statische methode-object kan niet worden opgevraagd met het tweede.

Ik wilde dit echt gewoon oplossen met functools.wraps zonder die __ dict __ lelijkheid terug te brengen, dus heb ik mijn eigen statische methode opnieuw geïmplementeerd in pure Python, die er zo uitzag:

class staticmethod(object):
    """Make @staticmethods play nice with @memoize."""

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        """Provide the expected behavior inside memoized classes."""
        return self.func(*args, **kwargs)

    def __get__(self, obj, objtype=None):
        """Re-implement the standard behavior for non-memoized classes."""
        return self.func

En dit werkt, voor zover ik weet, perfect naast de tweede @memoize -implementatie die ik hierboven opsomming.

Dus mijn vraag is: waarom gedraagt ​​de standaard ingebouwde statische methode zich niet goed en/of waarom anticipeert functools.wraps niet op deze situatie en lost het niet op voor mij?

Is dit een bug in Python? Of in functools.wraps ?

Wat zijn de voorbehouden voor het overschrijven van de ingebouwde statische methode ? Zoals ik al zei, lijkt het nu prima te werken, maar ik ben bang dat er een verborgen compilatie zou kunnen zijn tussen mijn implementatie en de ingebouwde implementatie, die later zou kunnen uitbarsten.

Bedankt.

Bewerken om te verduidelijken: in mijn applicatie heb ik een functie die een dure lookup doet, en die vaak wordt genoemd, dus ik heb het in memo vastgelegd. Dat is vrij eenvoudig. Daarnaast heb ik een aantal klassen die bestanden vertegenwoordigen en als meerdere instanties hetzelfde bestand in het bestandssysteem vertegenwoordigen, resulteert dit meestal in een inconsistente staat, dus het is belangrijk om slechts één instantie per bestandsnaam af te dwingen. Het is in wezen triviaal om de @memoize -decorator hiervoor aan te passen en toch zijn functionaliteit te behouden als een traditionele memoizer.

Echte voorbeelden van de drie verschillende toepassingen van @memoize zijn hier:

4
Lichte noot sleutel = args [0] als len (args) 1 else args is - is vergelijkt objectidentiteit en is implementatiespecifiek, en gebeurt hier te werken als kleine gehele getallen zijn gecached. Dit zou echt == 1 moeten zijn
toegevoegd de auteur Jon Clements, de bron

3 antwoord

Verschillende gedachten voor u:

  • De werking van een statische methode is volledig orthogonaal ten opzichte van de beheerder van klasse-decorateurs. Het maken van een functie in een statische methode heeft alleen invloed op wat er gebeurt tijdens attribuut opzoeken. Een klassendecorator is een compilatietransformatie in een klas.

  • Er is geen "bug" in functools.wraps . Het enige dat het doet is kopieerfunctie-attributen van de ene functie naar de andere.

  • Zoals momenteel geschreven, houdt uw memoize -tool geen rekening met de verschillende aanroephandtekeningen voor class methodes en statische methoden. Dat is een zwak punt in memoize , niet in de klassehulpmiddelen zelf.

Ik denk dat je je hebt voorgesteld dat gereedschappen als klassedecorsers, statische methodes, classmethods en functools een soort van wederzijds integreerbare intelligentie hadden. In plaats daarvan zijn al deze hulpmiddelen heel eenvoudig en moet de programmeur hun interacties bewust ontwerpen.

ISTM dat het onderliggende probleem is dat het gestelde doel enigszins ondergespecificeerd is: "decorateur die niet alleen functies, maar ook methoden en klassen op een algemene manier kan versieren, dat wil zeggen, zonder enige voorkennis te hebben van wat voor soort dingen het zal versieren. "

Het is niet helemaal duidelijk wat de semantiek van memoize in elk scenario zou zijn. En er is geen manier voor de eenvoudige componenten van Python om zichzelf automatisch te vormen op een manier die zou kunnen raden wat je echt wilde doen.

Mijn aanbeveling is dat je begint met een lijst met uitgewerkte voorbeelden van memoize-gebruik met een verscheidenheid aan objecten. Begin dan met het uitwerken van uw huidige oplossing zodat deze één voor één kunnen werken. Bij elke stap leer je waar je spec niet overeenkomt met wat meoize eigenlijk doet.

Een andere gedachte is dat functools.wraps en klasse-decorators niet strikt noodzakelijk zijn voor dit probleem. Beide kunnen handmatig worden geïmplementeerd. Begin met het bedraden van je gereedschap om te doen wat je wilt dat het doet. Als het eenmaal werkt, kijk dan naar het vervangen van stappen met wraps en decorateurs. Dat is beter dan het proberen om de gereedschappen naar je hand te zetten in situaties waar ze misschien geen goede pasvorm zijn.

Ik hoop dat dit helpt.

8
toegevoegd
Maakt mijn antwoord er een beetje triest uit - Goede post Raymond. Ik denk dat het probleem van het OP een klasarchitect vraagt ​​om op magische wijze methoden op een bepaalde manier te laten werken, terwijl er iets meer aan moet worden gedacht over het ontwerpen en decoreren van individuele methoden ... (moeilijk te zeggen echter)
toegevoegd de auteur Jon Clements, de bron
@Robru Het descriptorprotocol is geïmplementeerd in het object .__ getattribute__ en type .__ getattribute__. De memoize -wrapper retourneert een functie die deze verbergt voor gestippelde lookups.
toegevoegd de auteur Raymond Hettinger, de bron
Ik heb het Flub-voorbeeld uitgebreid, bekijk het alsjeblieft. De reden dat ik vroeg of dit een fout in functools was, is dat een functie standaard geen attributen heeft. Dus het lijkt erop dat wraps het nuttigst is voor iemand die een klas in een functie verpakt, zoals ik. Ben ik de eerste persoon die ooit een klas heeft ingericht die ook @staticmethod ingerichte methoden bevat? Het lijkt mij een voor de hand liggende en niet onredelijke situatie die de auteurs van functools mogelijk hadden verwacht. Maar nogmaals, waarom heeft Guido dit niet voorzien door objecten staticmethod in de eerste plaats opvraagbaar te maken?
toegevoegd de auteur robru, de bron
Eigenlijk komt het erop neer dat ik niet precies begrijp waarom mijn @memoize -klasse-decorateur static method s en classmethod s, en ik ben geïrriteerd dat ik de statische methode helemaal opnieuw moet uitvoeren in dit ogenschijnlijk voor de hand liggende en veel voorkomende scenario.
toegevoegd de auteur robru, de bron
Oh, en nog een laatste ding, sorry: het is me duidelijk wat de semantiek is van @memoize in elke situatie. Als ik er een klasse mee decoreer, betekent dit dat als ik de constructor tweemaal hetzelfde argument noem, ik de tweede keer een instantie in de cache moet ophalen (bijvoorbeeld dubbele exemplaren voorkomen), terwijl ik een functie of een methode ermee decoreer moet de resultaten in de cache worden geretourneerd (in het geval van methoden wordt self een deel van de cachesleutel en dus delen de verschillende exemplaren de cache niet per ontwerp). Mijn applicatie gebruikt dezelfde @memoize in alle drie situaties met succes.
toegevoegd de auteur robru, de bron
(Ik heb er op geslapen en net met frisse ogen wakker gemaakt) Ik denk dat een betere vraag zou zijn: "Waarom versieren een klasse het descriptorprotocol en voorkomen daardoor statische methode en classmethod -objecten van zich gedragen zoals verwacht? "
toegevoegd de auteur robru, de bron

Het aankleden van een klas wordt gebruikt om de constructie van een klas mogelijk te veranderen. Het is een soort handig maar niet helemaal hetzelfde als __ nieuw __ .

# Make the memoizer func masquerade as the object we are memoizing.
# This makes class attributes and static methods behave as expected.
for k, v in obj.__dict__.items():
    memoizer.__dict__[k] = v.__func__ if type(v) is staticmethod else v
return memoizer

De bovenstaande code dwingt je wrapper over methoden binnen je instantie.

class Flub:
    @memoize
    @staticmethod
    def do_things():
        print 'Do some things.'
Flub.do_things()

I believe this should be the code you should be using - bear in mind that if you receive no args, then args[0] is going to IndexError

2
toegevoegd
Nee, ik heb mezelf helemaal niet goed uitgelegd. Uw wijzigingen aan Flub gaan volledig voorbij aan het punt. @memoize moet de klasse specifiek versieren omdat ik niet meer dan één instantie per argument kan doorgeven aan Flub .__ init __ . Ik vind het niet onredelijk om staticmethods te willen alleen omdat de klasse op deze manier is ingericht. Ik heb het Flub-voorbeeld in de oorspronkelijke vraag uitgebreid in de hoop dit te verduidelijken.
toegevoegd de auteur robru, de bron
De statische methode zou zich ook als een statische methode moeten gedragen, het moet bijvoorbeeld volledig normaal worden uitgevoerd. Het is niet het ding dat ik probeer te onthouden.
toegevoegd de auteur robru, de bron

Het probleem is dat je binnenhuisarchitect een klasse accepteert (dat wil zeggen, een instantie van type ) en een functie retourneert. Dit is (vrijwel) het programmeerequivalent van een categoriefout ; klassen kunnen eruit zien als functies omdat ze kunnen worden aangeroepen (als een constructor), maar dat betekent niet dat een functie die een instantie retourneert, gelijk is aan de klasse van het type van die instantie. Het is bijvoorbeeld niet mogelijk dat exemplaarof het juiste resultaat oplevert, en bovendien kan uw gedecoreerde klasse niet langer worden onderklassificeerd (omdat het geen klasse meer is!)

Wat je moet doen, is je decorateur aan te passen om te detecteren wanneer deze in een klasse wordt aangeroepen en in dat geval een wrapperklasse te maken (met behulp van de class syntaxis, of via het type Constructor met 3 argumenten) die het vereiste gedrag vertoont. Ofwel, ofwel noteert u __ nieuw __ (hoewel u zich ervan bewust bent dat __ init __ wordt aangeroepen op de geretourneerde waarde van __ nieuw __ als deze van het juiste type is, zelfs als het een reeds bestaande instantie is).

2
toegevoegd
@Robru - goed, behalve dat de conclusie die je zou moeten trekken is dat je moet werken met het type systeem van met Python, niet ertegen. Dat betekent dat een binnenhuisarchitect die een klas accepteert, een andere klas moet teruggeven, geen functie.
toegevoegd de auteur ecatmur, de bron
Je hebt gelijk dat instanceof en subclassing beide gebroken zijn met mijn memoizer zoals ze zijn. Dat stoort me niet, want ik hou van de eenvoud ervan (en ik heb geen van beide dingen nodig). Ik ben meer aan het leren over het descriptorprotocol en het lijkt erop dat Python ze gewoon niet op deze manier zal laten werken, waardoor mijn aangepaste statische methode herimplementatie noodzakelijk is.
toegevoegd de auteur robru, de bron