Adăugați în mod dinamic metode la o clasă în Python 3.0

Încerc să scriu un strat de abstracție a bazei de date în Python care vă permite să construiți instrucțiuni SQL folosind apeluri de funcții înlănțuite, cum ar fi:

results = db.search("book")
          .author("J. K. Rowling")
          .price("<40.00")
          .title("Harry")
          .execute()

dar întâmpin probleme când încerc să adaug în mod dinamic metodele necesare la clasa db.

Iată părțile importante ale codului meu:

import inspect

def myName():
    return inspect.stack()[1][3]

class Search():

    def __init__(self, family):
        self.family = family
        self.options = ['price', 'name', 'author', 'genre']
        #self.options is generated based on family, but this is an example
        for opt in self.options:
            self.__dict__[opt] = self.__Set__
        self.conditions = {}

    def __Set__(self, value):
        self.conditions[myName()] = value
        return self

    def execute(self):
        return self.conditions

Cu toate acestea, când rulez exemplul, cum ar fi:

print(db.search("book").price(">4.00").execute())

iesiri:

{'__Set__': 'harry'}

Mă duc la asta în mod greșit? Există o modalitate mai bună de a obține numele funcției apelate sau de a face cumva o „copie pe hârtie” a funcției?


person Codahk    schedule 26.11.2011    source sursă
comment
Pot să vă întreb de ce încercați să reinventați SQLAlchemy?   -  person Lennart Regebro    schedule 26.11.2011
comment
De fapt, destul de obișnuit, încerc să-mi codific propriile biblioteci care le reproduc pe cele care există deja, astfel încât să pot afla mai multe despre limbă și să mă simt mai bine cum se reunesc părțile mai avansate :)   -  person Codahk    schedule 26.11.2011
comment
OK, un exercițiu de învățare, acesta este un motiv întemeiat. Deși în acest caz cred că citirea codului sursă SQLAlchemy va fi un început bun. ORM-urile sunt complexe și magice.   -  person Lennart Regebro    schedule 26.11.2011


Răspunsuri (3)


Puteți adăuga pur și simplu funcțiile (metodele) de căutare după ce clasa este creată:

class Search:  # The class does not include the search methods, at first
    def __init__(self):
        self.conditions = {}

def make_set_condition(option):  # Factory function that generates a "condition setter" for "option"
    def set_cond(self, value):
        self.conditions[option] = value
        return self
    return set_cond

for option in ('price', 'name'):  # The class is extended with additional condition setters
    setattr(Search, option, make_set_condition(option))

Search().name("Nice name").price('$3').conditions  # Example
{'price': '$3', 'name': 'Nice name'}

PS: această clasă are o metodă __init__() care nu are parametrul family (setatorii de condiții sunt adăugați dinamic în timpul execuției, dar sunt adăugați la clasă, nu la fiecare instanță separat). Dacă trebuie create Search obiecte cu stabilitori de condiții diferiți, atunci următoarea variație a metodei de mai sus funcționează (metoda __init__() are un parametru family):

import types

class Search:  # The class does not include the search methods, at first

    def __init__(self, family):
        self.conditions = {}
        for option in family:  # The class is extended with additional condition setters
            # The new 'option' attributes must be methods, not regular functions:
            setattr(self, option, types.MethodType(make_set_condition(option), self))

def make_set_condition(option):  # Factory function that generates a "condition setter" for "option"
    def set_cond(self, value):
        self.conditions[option] = value
        return self
    return set_cond

>>> o0 = Search(('price', 'name'))  # Example
>>> o0.name("Nice name").price('$3').conditions
{'price': '$3', 'name': 'Nice name'}
>>> dir(o0)  # Each Search object has its own condition setters (here: name and price)
['__doc__', '__init__', '__module__', 'conditions', 'name', 'price']

>>> o1 = Search(('director', 'style'))
>>> o1.director("Louis L").conditions  # New method name
{'director': 'Louis L'}
>>> dir(o1)  # Each Search object has its own condition setters (here: director and style)
['__doc__', '__init__', '__module__', 'conditions', 'director', 'style']

Referință: http://docs.python.org/howto/descriptor.html#functions-and-methods


Dacă într-adevăr aveți nevoie de metode de căutare care știu despre numele atributului în care sunt stocate, îl puteți seta pur și simplu în make_set_condition() cu

       set_cond.__name__ = option  # Sets the function name

(chiar înainte de return set_cond). Înainte de a face acest lucru, metoda Search.name are următorul nume:

>>> Search.price
<function set_cond at 0x107f832f8>

după ce setați atributul __name__, obțineți un alt nume:

>>> Search.price
<function price at 0x107f83490>

Setarea numelui metodei în acest fel face posibile mesaje de eroare care implică metoda mai ușor de înțeles.

person Eric O Lebigot    schedule 26.11.2011
comment
+1. Ai dreptate. Eram prea inteligent. Nu există nimic special în aceste metode de stabilire a condițiilor care necesită crearea lor pe fiecare instanță. - person Eryk Sun; 26.11.2011
comment
Presupuneam că aceste metode trebuie să fie setate automat din schemă, poate că presupuneam prea multe. :-) - person Lennart Regebro; 26.11.2011
comment
Conform descrierii problemei, numele metodei vor varia în funcție de familie, astfel încât setarea numelor în clasă va duce fie la lipsă nume pentru unele familii, fie la toate numele pentru fiecare familie. - person Ethan Furman; 29.11.2011
comment
@EthanFurman: Ai dreptate. Am ghicit (poate incorect) că familia nu era de fapt un parametru __init__() adecvat, deoarece __init__() era întotdeauna apelat cu aceeași familie, într-un anumit program. Am adăugat un PS care subliniază acest punct și oferă o soluție care oferă fiecărei instanțe Search propria familie de posibile căutări de opțiuni. - person Eric O Lebigot; 30.11.2011

În primul rând, nu adăugați nimic la clasă, o adăugați la instanță.

În al doilea rând, nu trebuie să accesați dict. self.__dict__[opt] = self.__Set__ este mai bine făcut cu setattr(self, opt, self.__Set__).

În al treilea rând, nu folosiți __xxx__ ca nume de atribute. Acestea sunt rezervate pentru uz intern Python.

În al patrulea rând, după cum ați observat, Python nu este ușor de păcălit. Numele intern al metodei pe care o apelați este în continuare __Set__, chiar dacă îl accesați sub un alt nume. :-) Numele este setat atunci când definiți metoda ca parte a instrucțiunii def.

Probabil doriți să creați și să setați metodele de opțiuni cu o metaclasă. De asemenea, poate doriți să creați acele metode în loc să încercați să utilizați o singură metodă pentru toate. Dacă într-adevăr doriți să utilizați doar un singur __getattr__, este calea, dar poate fi puțin greoi, vă recomand în general să nu faceți acest lucru. Lambda sau alte metode generate dinamic sunt probabil mai bune.

person Lennart Regebro    schedule 26.11.2011
comment
Iată referința despre __*__ nume: docs.python.org/ referință/ - person Eric O Lebigot; 26.11.2011
comment
@EOL M-am gândit întotdeauna la numele „xxx” ca proprietăți „private” sau metode ale unei clase care nu/nu pot fi apelate din afara acesteia? Există vreo convenție pe care ar trebui să o utilizați pentru acestea? - person Codahk; 26.11.2011
comment
@Ben: Da, există o convenție, care este foarte asemănătoare cu notația rezervată de Python: este __xxx (fără dunder final) - aceeași referință ca mai sus. - person Eric O Lebigot; 26.11.2011
comment
@EOL: Nu, asta nu este o convenție, asta declanșează distrugerea numelui. Diferit. - person Lennart Regebro; 26.11.2011
comment

probabil singura modalitate de a face acest lucru este dacă textWidth este mai lungă decât lățimea, să parcurgeți textul literă cu literă fie adăugând, fie eliminând o literă la un moment dat și adăugând punctele de suspensie și verificând lățimea până când este aproape de a umple întregul câmp de text.

- person Lennart Regebro; 26.11.2011
comment
@LennartRegebro: Nu văd problema cu referirea la __xxx ca convenție a lui Python pentru metodele „private” sau convenție pentru declanșarea mângării de nume. Într-adevăr, văd o diferență, dar este între convenția pentru metodele „private” (__xxx) și modificarea numelui (care este adăugată peste convenție). Întrebarea lui Ben a fost despre convenție, dar este într-adevăr interesant de observat că Python îi conferă o putere mai expresivă prin mângâierea numelor. Este, de asemenea, interesant să observăm că există o noțiune de nume de metodă semi-privată, neschimbată (_xxx). - person Eric O Lebigot; 26.11.2011
comment
@EOL: Ideea este că dubla subliniere nu este deloc o convenție. Declanșează deformarea numelor pentru a evita ciocnirile de nume. De fapt face ceva și nu este o convenție. Convenția pentru semnalarea faptului că ceva este privat/intern/folosit pe propriul risc este un punct de subliniere important. - person Lennart Regebro; 26.11.2011

Iată un cod de lucru pentru a începe (nu întregul program pe care încercai să-l scrii, ci ceva care arată cum se pot potrivi părțile):

class Assign:

    def __init__(self, searchobj, key):
        self.searchobj = searchobj
        self.key = key

    def __call__(self, value):
        self.searchobj.conditions[self.key] = value
        return self.searchobj

class Book():

    def __init__(self, family):
        self.family = family
        self.options = ['price', 'name', 'author', 'genre']
        self.conditions = {}

    def __getattr__(self, key):
        if key in self.options:
            return Assign(self, key)
        raise RuntimeError('There is no option for: %s' % key)

    def execute(self):
        # XXX do something with the conditions.
        return self.conditions

b = Book('book')
print(b.price(">4.00").author('J. K. Rowling').execute())
person Raymond Hettinger    schedule 26.11.2011
comment
Fiecare căutare creează o instanță Assign, care este puțin timp și memorie grea. :) Există vreun avantaj al acestei abordări în comparație cu metodele de adăugare directă după metoda de creare a clasei prezentate în răspunsul meu? - person Eric O Lebigot; 26.11.2011
comment
@EOL Această abordare este modul în care funcționează Python în sine și este o expresie comună. De exemplu, Python creează o nouă instanță BoundMethod de fiecare dată când efectuați un apel de metodă, cum ar fi a.m(x). OP încearcă să învețe cum funcționează Python și este potrivit să predea __call__ pentru apeluri și __getattr__ pentru căutarea punctată dinamică. Pentru asta sunt. - person Raymond Hettinger; 26.11.2011
comment
Mulțumiri. Da, __getattr__ și __call__ sunt într-adevăr importante de știut când vine vorba de căutare dinamică. Totuși, nu sunt sigur de asta are nevoie OP: înțeleg că el vrea să definească atribute de clasă non-dinamice, în loc de atribute dinamice ale instanței. Nu? (Cred că asta rezumă aproape cele două abordări diferite adoptate în răspunsurile noastre.) - person Eric O Lebigot; 26.11.2011