Callables in Elternklasse

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Folgendes Problem:

ich verwende ein Webframework (Tornado) und habe eine Elternklasse "BaseHandler" geschrieben in denen einige Methoden und Attribute vorhanden sind, die ich in mehreren Subklassen (bestimmte Ressourcen-Handler) vererben möchte.

Das ganze funktioniert problemlos, jedoch stört mich eine Sache und ich weiß nicht ob mein Ansatz irgendwas besser machen würde außer der Lesbarkeit. In jedem Handler verwende ich ein Objekt von "Client()", daher wollte ich das über BaseHandler allen erbenden Klassen verfügbar machen, so dass dann via self.client automagisch darauf zugegriffen werden kann.

Naiver Ansatz:

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import datetime
import logging.config
import sys
from typing import ClassVar, Callable

import yaml

from dateutil import tz
from tornado import web, ioloop

from app import Client()

SERVICE_NAME = 'Example Service'
LOG = logging.getLogger(SERVICE_NAME)
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%f%z"


class BaseHandler(web.RequestHandler):
    
    web_client: ClassVar[Callable]
    
    def write_error(self, status_code, **kwargs):
        self.finish({
            "code": status_code,
            "message": self._reason,
            "ts": datetime.datetime.now(
                tz=tz.tzlocal()).strftime(TIMESTAMP_FORMAT),
        })

    def finish(self, chunk=None):
        self.set_header("Server", SERVICE_NAME)
        super().finish(chunk=chunk)


class Handler1(BaseHandler):

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)

    def initialize(self, name):
        self.name = name


class Handler2(BaseHandler):

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)

    def initialize(self, name):
        self.name = name


class Handler3(BaseHandler):

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)

    def initialize(self, name):
        self.name = name


def main():
    with open(sys.argv[1], 'r') as config_file:
        config = yaml.load(config_file, Loader=yaml.FullLoader)

    var1 = config['bla']['a']
    var2 = config['bla']['b']
    var3 = config['bla']['c']
    
    client = Client()
    
    BaseHandler.web_client = client

    application = web.Application([(
        "/rest/v1/store", Handler1, {'name': var1},
        "/rest/v1/search", Handler2, {'name': var2},
        "/rest/v1/index", Handler3, {'name': var3}
    )])
    application.listen(config['service']['port'])
    ioloop.IOLoop.current().start()


if __name__ == '__main__':
    main()
Der Code ist zur vereinfacht. Beim Schreiben fiel mir wieder "Explicit is better than implicit" ein, mir fehlt hier schlicht die Erfahrung weshalb ich mich frage ob es hier noch andere Möglichkeiten gibt als jeder Subklasse direkt eine Instanz von Client() zu übergeben.
When we say computer, we mean the electronic computer.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Mir fällt eine Antwort schwer, weil ich die Klassen sinnlos finde. Nichts was in HandlerX steht könnte nicht auch in BsseHandler stehen.

Und was den client angeht: ich nehme mal an, die Routen werden auf callables gemappt, aus denen Tornado dann Objekte zum abarbeiten eines requests baut? Dann würde ich eher mit Partial arbeiten, und den Client damit als argument an die Konstruktoren currien.
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Was ist denn `Client`? Warum gibt es davon nur ein Exemplar?
Es gibt ja anscheinend schon die Möglichkeit, Parameter an `initialize` übergeben. Warum nicht auch `client`?
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Client sollte nur ein Dummy-Objekt darstellen, ich verwende smtplib-Instanzen, ich schreibe das nächstes Mal einfach gleich.

Die ursprüngliche Idee der Basisklasse war so:

Code: Alles auswählen

class BaseHandler(web.RequestHandler):

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)
        self.smtp_session = smtplib.SMTP(self.hostname, self.port)


Mir ist jetzt aber klar geworden, dass die Parameter `hostname` und `port` ja bei den Subklassen sowieso wieder zugeführt werden müssen und ich dann auch gleich die smtp_session in der main()-Funktion instantiieren und als Parameter übergeben kann wie ich es vor der Idee mit den Klassenvariablen immer gemacht habe.

Was ich damit gewinnen wollte war, dass ich nicht jedes Mal ein Objekt jedem Handler übergeben muss, da ich das z.B. für Elasticsearch, NSQ, SMTP, Redis uvm. machen muss, sondern in jeder Subklasse all diese Instanzen bereits aus der Elternklasse bezogen werden können. Diese Instanzen sind für jeden Handler gleich, via `initialize` wollte ich dann nur noch die für den jeweiligen Handler individuellen Parameter übergeben.

@__deets__: ich habe für jede Ressource einen eigenen Handler, da jeder Handler eine andere get / post-Methode implementiert, und ich so keine komplexen Fallentscheidungen machen muss.

EDIT: vielleicht ist so eher klar, was ich vermeiden möchte:

Code: Alles auswählen

class BaseHandler(web.RequestHandler):

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)


class Handler1(BaseHandler):

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)

    def initialize(self, name, smtp_session, elastic_client, 
                   redis_client, nsq_client, store_client):
        self.name = name
        self.smtp_session = smtp_session
        self.elastic_client = elastic_client
        self.redis_client = redis_client
        self.nsq_client = nsq_client
        self.store_client = store_client
        
    def get(self):
        ...


class Handler2(BaseHandler):

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)

    def initialize(self, name, smtp_session, elastic_client,
                   redis_client, nsq_client, store_client):
        self.name = name
        self.smtp_session = smtp_session
        self.elastic_client = elastic_client
        self.redis_client = redis_client
        self.nsq_client = nsq_client
        self.store_client = store_client


    def get(self):
        ...


class Handler3(BaseHandler):

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)

    def initialize(self, name, smtp_session, elastic_client,
                   redis_client, nsq_client, store_client):
        self.name = name
        self.smtp_session = smtp_session
        self.elastic_client = elastic_client
        self.redis_client = redis_client
        self.nsq_client = nsq_client
        self.store_client = store_client


    def get(self):
        ...


def main():
    with open(sys.argv[1], 'r') as config_file:
        config = yaml.load(config_file, Loader=yaml.FullLoader)

    smtp_session = smtplib.SMTP()
    elastic_client = elasticsearch.Elasticsearch()
    redis_client = redis.Redis()
    nsq_client = nsq.Reader()
    store_client = app.store.Store()
    
    application = web.Application([(
        "/rest/v1/store", Handler1, {
            'name': 'bla', 'smtp_session': smtp_session, 
            'elastic_client': elastic_client, 
            'redis_client': redis_client, 
            'nsq_client': nsq_client, 
            'store_client': store_client},
        "/rest/v1/index", Handler2, {
            'name': 'bla', 'smtp_session': smtp_session,
            'elastic_client': elastic_client,
            'redis_client': redis_client,
            'nsq_client': nsq_client,
            'store_client': store_client},
        "/rest/v1/search", Handler3, {
            'name': 'bla', 'smtp_session': smtp_session,
            'elastic_client': elastic_client,
            'redis_client': redis_client,
            'nsq_client': nsq_client,
            'store_client': store_client},
    )])
    application.listen(config['service']['port'])
    ioloop.IOLoop.current().start()


if __name__ == '__main__':
    main()
When we say computer, we mean the electronic computer.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Wenn in initialize() überall das Gleiche passiert, dann schreib das doch einmalig in BaseHandler(). Die Methode wird ja dann weitervererbt.

Übrigens: Eine __init__(), die nur super().__init__() enthält und die gleiche Signatur wie die Elternklasse hat, ist überflüssig. Wenn da auch im tatsächlichen Code nicht mehr passiert, dann sollte sie weggelassen werden, da sie keinen Mehrwert hat. Denn es wird dann automatisch die __init__() der Elternklasse für die Initialisierung genommen.

Ich habe ehrlich gesagt den Eindruck, dass du Vererbung noch nicht so ganz verstanden hast. Befass dich vielleicht nochmal genauer damit, dass immer die Attribute der Elternklasse gelten (d.h. auch deren Methoden), solange sie nicht in der erbenden Klasse überschrieben werden.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

@snafu: du hast recht, __init__ ist in jeder der o.g. Klassen überflüssig, dafür ist initialize als Subclass-Hook vorgesehen. Tatsächlich passiert aber in initialize für jede Klasse etwas anderes. Der Vorschlag initialize im BaseHandler zu definieren ist ein guter Ansatz, problematisch an diesem Design ist allerdings dass ich dann in jedem Ressourcen-Handler-Attribute-Tupel eine ganze Ladung an Parametern übergeben muss.

Umso mehr ich da herumprobiere komme ich zu dem Ergebnis dass der sauberste Weg ist, jedem initialize die entsprechende Instanz als ganz normalen Parameter zu übergeben, alles andere ist quatsch.

Danke für eure Hilfe.
When we say computer, we mean the electronic computer.
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Die ganze Idee, Webseiten durch Klassen zu repräsentieren, krankt einfach daran, dass Webseiten normalerweise statuslos sind, und das dem Klassenkonzept diametral widerspricht.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

sls hat geschrieben: Mittwoch 10. April 2019, 17:01 Umso mehr ich da herumprobiere komme ich zu dem Ergebnis dass der sauberste Weg ist, jedem initialize die entsprechende Instanz als ganz normalen Parameter zu übergeben, alles andere ist quatsch.
Das hört sich noch am vernünftigsten an. Klassen sollten grundsätzlich zwar möglichst wenig voneinander wissen, aber hier ist es möglicherweise die bessere Lösung, wenn der Handler einfach weiß, was er mit dem übergebenen Objekt tun kann.
Antworten