קורס Python 3 שיעור תרגול תחביר המחלקות


זה נושא דיון מלווה לערך המקורי ב- https://www.tocode.co.il/bundles/python/lessons/21-class-syntax-lab

למה אין פתרונות לשאלות?

הי @gizmo

המטרה של הקורס והשיעורים והדוגמאות זה להראות את החומר,
המטרה של התרגילים היא לתת לך מקום להתנסות בו - דווקא בתוך סיטואציה שאין תשובה נכונה ואין פיתרון בית ספר אלא מתוך מטרה כמה שיותר לדמות את העולם האמיתי.

אני כאן ואם אתה נתקע על אחד התרגילים אשמח לעזור להגיע לתשובה הנכונה - פשוט פרסם על מה הסתכלת ומה ניסית לעשות ונמשיך יחד.

בנוסף אם אתה רוצה פידבק על פיתרון שכתבת ועובד גם מוזמן להעלות לכאן ואני שמח להסתכל ולהגיב

היי @ynonp

אני מנסה לפתור את התרגיל הראשון לא כזה הולך לי…

אני רוצה להבין משהו יש לי את הקוד הזה:

 class Summer:
    total = 0
    def __init__(self, ):


    def print_total(self):
        total =

    def add(self):

s = Summer()
t = Summer()

s.add(10, 20)
t.add(50)
s.add(30)

# should print 60
s.print_total()

# should print 50
t.print_total()

אני לא יודע כמה משתנים לשים בפונקציה ADD וכמה ב Print total.
אם אני שם 2 משתנים בפונקציה add לדוג :

def add(self,num1,num2):

אז יש לי בעיה פה

t.add(50)
s.add(30)

אני מקבל מpycharm : parameter num2 unfilded

איו לי מושג איך להמשיך :frowning:

תוכל לעזור לי ?

נסה לראות את השיעור הזה ותראה אם זה נותן לך רעיון:

https://www.tocode.co.il/bundles/advanced-python3/lessons/variadic-functions

הצעת הפתרון שלי לארבעת התרגילים:

# Ex1
print('\n ----- Ex1 -----')

class Summer:
    def __init__(self):
        self._total = 0

    def add(self, *numbers: int) -> None:
        self._total += sum(numbers)

    def print_total(self):
        print(self._total)

s = Summer()
t = Summer()
s.add(10, 20)
t.add(50)
s.add(30)
s.print_total()  # should print 60
t.print_total()  # should print 50


# Ex2
print('\n ----- Ex2 -----')

class MyCounter:
    count = 0

    def __init__(self):
        MyCounter.count += 1

for _ in range(10):
    c1 = MyCounter()

print(MyCounter.count)  # should print 10


# Ex3
print('\n ----- Ex3 -----')

class Widget:
    def __init__(self, name):
        self.name = name
        self._dependencies = []
        self.built = False

    def add_dependency(self, *dependencies):
        self._dependencies.extend(dependencies)

    def build(self):
        self.built = True
        for d in self._dependencies:
            if not d.built:
                d.build()
                print(d.name)


luke = Widget("Luke")
hansolo = Widget("Han Solo")
leia = Widget("Leia")
yoda = Widget("Yoda")
padme = Widget("Padme Amidala")
anakin = Widget("Anakin Skywalker")
obi = Widget("Obi-Wan")
darth = Widget("Darth Vader")
_all = Widget("All")

luke.add_dependency(hansolo, leia, yoda)
leia.add_dependency(padme, anakin)
obi.add_dependency(yoda)
darth.add_dependency(anakin)

_all.add_dependency(luke, hansolo, leia, yoda, padme, anakin, obi, darth)
_all.build()
# code should print: Han Solo, Padme Amidala, Anakin Skywalker, Leia, Yoda, Luke, Obi-Wan, Darth Vader
# (can print with newlines in between modules)


# Ex4
print('\n ----- Ex4 -----')
from collections import deque

debug = False

class Queue:
    def __init__(self, clerk):
        self.id = clerk
        self.queue = deque()
        if debug: print(f'__init__ Queue for {clerk}')

    def queue_len(self):
        return len(self.queue)

    def add_to_queue(self, customer):
        self.queue.append(customer)

    def serve_next(self):
        return self.queue.popleft()


class QueueSystem:
    no_valid_command_msg = """No valid command was given.
Please use one of these formats:
wait <clerk-name> <customer-name>
next <clerk-name>
"""

    def __init__(self):
        self.clerks = {}
        if debug: print('__init__ QueueSystem')

    def accept_commands(self):
        while True:
            command = input(': ')
            try:
                (action, clerk_customer) = command.split(' ', 1)
            except ValueError:
                if 'exit' == command:
                    print('bye')
                    return None
                print(QueueSystem.no_valid_command_msg)
                continue
            if 'wait' == action:
                try:
                    (clerk, customer) = clerk_customer.split(' ', 1)
                except ValueError:
                    print(QueueSystem.no_valid_command_msg)
                    continue
                self.wait(clerk, customer)
            elif 'next' == action:
                self.next(clerk_customer)
            else:
                print(QueueSystem.no_valid_command_msg)

            if debug: self.show_queues()

    def wait(self, clerk, customer):
        if debug: print(f'customer: {customer} will wait in clerk {clerk} queue.')
        if clerk in self.clerks:
            self.clerks[clerk].add_to_queue(customer)
        else:
            new_clerk = Queue(clerk)
            new_clerk.add_to_queue(customer)
            self.clerks[clerk] = new_clerk

    def next(self, clerk):
        if clerk not in self.clerks:
            new_clerk = Queue(clerk)
            self.clerks[clerk] = new_clerk
        try:
            customer = self.clerks[clerk].serve_next()
        except IndexError:  # No customers in given clerk's queue
            clerks_queues_by_len = {self.clerks[c].queue_len(): c for c in self.clerks}  # OK to squash same size queues.
            busiest_queue_len = max(clerks_queues_by_len.keys())
            if 0 < busiest_queue_len:
                customer = self.clerks[clerks_queues_by_len[busiest_queue_len]].serve_next()
        try:
            print(customer)
        except UnboundLocalError:
            print('There are no more customers to serve.')

    def show_queues(self):
        for cur_clerk in self.clerks:
            print(cur_clerk, self.clerks[cur_clerk].queue)

qs = QueueSystem()
qs.accept_commands()

הפלט (כולל הקלט שהכנסתי עבור תרגיל 4):

 ----- Ex1 -----
60
50

 ----- Ex2 -----
10

 ----- Ex3 -----
Han Solo
Padme Amidala
Anakin Skywalker
Leia
Yoda
Luke
Obi-Wan
Darth Vader

 ----- Ex4 -----
: wait dave mika
: wait dave jane
: wait dave bill
: wait michael eddy
: wait michael jim
: next dave
mika
: next dave
jane
: next michael
eddy
: next michael
jim
: next michael
bill
: exit
bye

Process finished with exit code 0
לייק 1

לגבי האחרון - נדמה לי שאפשר לשפר את הקוד אם מחלקים לקלאסים קצת אחרת:

  1. קלאס עבור ״פקודה״ שמקבל שורת טקסט ומגדיר לעצמו מאפיינים (כמו סוג הפקודה, לאיזה פקיד היא יכולה להישלח, מה הפרמטרים)

  2. קלאס עבור ״פקיד״ שמקבל פקודה ומבצע אותה

מה דעתך? רוצה לנסות?

אתה מתכוון שמערכת התורים לא תהיה מחלקה, אלא התוכנית עצמה והיא תעזר במחלקה (ספריית כלים) עבור הפקודות ובמחלקה (“אמיתית”) עבור הפקידים?
משהו כזה?:

from collections import deque

debug = False


class Clerk:
    def __init__(self, clerk_name):
        self.name = clerk_name
        self.queue = deque()
        if debug: print(f'__init__ Clerk for {clerk_name}')

    def wait(self, customer):
        self.queue.append(customer)

    def next(self):
        return self.queue.popleft()

    def has_customers_in_queue(self):
        return True if 1 <= len(self.queue) else False


class CommandTools:
    valid_actions = ('wait', 'next')
    customer_actions = ('wait')
    no_valid_command_msg = """
Please use one of these formats:
wait <clerk-name> <customer-name>
next <clerk-name>
"""

    @staticmethod
    def process_command(command):
        command_words = command.split(' ')
        if 2 > len(command_words) or 4 <= len(command_words):
            raise UserWarning(f'Wrong number of words ({len(command_words)}).')
        command_attribs = {}
        if command_words[0] not in CommandTools.valid_actions:
            raise UserWarning(f'Invalid command action ({command_words[0]}).')
        command_attribs['action'] = command_words[0]
        command_attribs['clerk'] = command_words[1]
        if command_attribs['action'] in CommandTools.customer_actions:
            if 3 > len(command_words):
                raise UserWarning(f"Missing mandatory customer on a {command_attribs['action']} action.")
            command_attribs['customer'] = command_words[2]
        return command_attribs


clarks = {}

while True:
    this_command = input(': ')
    try:
        this_command_attributes = CommandTools.process_command(this_command)
    except UserWarning as e:
        print(f'No valid command was given: {e}', CommandTools.no_valid_command_msg)
        continue

    if this_command_attributes['clerk'] not in clarks:
        new_clerk = Clerk(this_command_attributes['clerk'])
        clarks[this_command_attributes['clerk']] = new_clerk

    if 'wait' == this_command_attributes['action']:
        clarks[this_command_attributes['clerk']].wait(this_command_attributes['customer'])
    elif 'next' == this_command_attributes['action']:
        next_customer = None
        if clarks[this_command_attributes['clerk']].has_customers_in_queue():
            next_customer = clarks[this_command_attributes['clerk']].next()
        else:
            for clark in clarks:
                if clark == this_command_attributes['clerk']:
                    continue
                if clarks[clark].has_customers_in_queue():
                    next_customer = clarks[clark].next()
        if next_customer is not None:
            print(next_customer)
        else:
            print('There are no more customers to serve.')

    if debug:
        for c in clarks:
            print(f'{c}\t{clarks[c].queue}')

יותר טוב אבל אני חושב שאפשר לקחת את זה עוד כמה צעדים קדימה. שים לב איך עכשיו המחלקה של Clrek נראית נקיה עם פונקציות קטנות שברור מה כל אחת עושה, ולעומתה שאר הקוד נראה מאוד עמוס.

בוא נתחיל עם הרעיון של Dispatching כלומר תוכן לולאת ה while True שיש לך שקוראת פקודות ושולחת אותן לפקידים. בדרך כלל מבנה של לולאה כזאת נראה ככה:

while True:
  next_command = read_next_command()
  next_clerk = get_handling_clerk(next_command)
  next_clerk.handle(next_command)

עם הפרדה כזאת לפונקציות כל הלולאה תהיה הרבה יותר ברורה. בגלל שהפונקציה get_handling_clerk צריכה לעבוד על אוסף הפקידים וכל פעם למצוא פקיד פנוי, יהיה נחמד לשים אותה במחלקה שתוכל לנהל את כל אוסף הפקידים, ליצור חדש אם צריך ולמצוא פקיד פנוי אם אפשר. במילים אחרות קוד יותר מדויק יראה כך:

dispatcher = Dispatcher()
while True:
  cmd = read_command()
  clerk = dispatcher.get_clerk(cmd)
  clerk.handle(cmd)

נסה לכתוב את המימוש של Dispatcher ושל הפונקציות כאן כדי שהלולאה הראשית תהיה קצרה יותר ונמשיך משם.

הוספתי מחלקה של Dispatcher והתאמתי את שאר הקוד:

from collections import deque


class Clerk:
    def __init__(self, clerk_name):
        self.name = clerk_name
        self._queue = deque()

    def handle(self, cmd_atr):
        if 'wait' == cmd_atr['action']:
            self.wait(cmd_atr['customer'])
        elif 'next' == cmd_atr['action']:
            self.next()

    def wait(self, customer):
        self._queue.append(customer)

    def next(self):
        try:
            print(self._queue.popleft())
        except IndexError:
            print('There are no more customers to serve.')

    def has_customers_in_queue(self):
        return True if 1 <= len(self._queue) else False


class CommandTools:
    valid_actions = ('wait', 'next')
    customer_actions = ('wait')
    no_valid_command_msg = """
Please use one of these formats:
wait <clerk-name> <customer-name>
next <clerk-name>
"""

    @staticmethod
    def read_command():
        while True:
            cmd_input = input(': ')
            try:
                return CommandTools.parse_command(cmd_input)
            except UserWarning as e:
                print(f'No valid command was given: {e}', CommandTools.no_valid_command_msg)
                continue

    @staticmethod
    def parse_command(command):
        command_words = command.split(' ')
        if 2 > len(command_words) or 4 <= len(command_words):
            raise UserWarning(f'Wrong number of words ({len(command_words)}).')
        command_attribs = {}
        if command_words[0] not in CommandTools.valid_actions:
            raise UserWarning(f'Invalid command action ({command_words[0]}).')
        command_attribs['action'] = command_words[0]
        command_attribs['clerk'] = command_words[1]
        if command_attribs['action'] in CommandTools.customer_actions:
            if 3 > len(command_words):
                raise UserWarning(f"Missing mandatory customer on a {command_attribs['action']} action.")
            command_attribs['customer'] = command_words[2]
        return command_attribs


class Dispatcher:
    def __init__(self):
        self._clerks = {}

    def get_clerk(self, cmd_atr):
        clerk_on_call = cmd_atr['clerk']
        if clerk_on_call not in self._clerks:
            new_clerk = Clerk(clerk_on_call)
            self._clerks[clerk_on_call] = new_clerk

        if 'next' == cmd_atr['action'] and not self._clerks[clerk_on_call].has_customers_in_queue():
            for alt_clerk in self._clerks:
                if self._clerks[alt_clerk].has_customers_in_queue():
                    clerk_on_call = alt_clerk
                    break

        return self._clerks[clerk_on_call]


dispatcher = Dispatcher()
while True:
    cmd = CommandTools.read_command()
    clerk = dispatcher.get_clerk(cmd)
    clerk.handle(cmd)

כבר נראה הרבה יותר טוב לא?

הפונקציות קצרות יותר, ומסודרות בצורה יותר מאוזנת בין המחלקות כך שיותר קל להתמצא. עכשיו אפשר להסתכל על הקוד ברזולוציה יותר גבוהה ולדבר על קטעים ספציפיים.

בוא נתחיל עם הפונקציה get_clerk. איזה פעולות היא עושה שאפשר היה להוציא לפונקציות נפרדות כך שיהיה יותר קל להבין אותה? אם היית צריך לתאר את הלוגיקה שלה באמצעות פונקציות עזר איך היית כותב אותה? (תחשוב על התהליך שעשיתי עם הלולאה הראשית)

גרסה חדשה מותאמת להערה שלך:

from collections import deque


class Clerk:
    def __init__(self, clerk_name):
        self.name = clerk_name
        self._queue = deque()

    def handle(self, cmd_atr):
        if 'wait' == cmd_atr['action']:
            self.wait(cmd_atr['customer'])
        elif 'next' == cmd_atr['action']:
            self.next()

    def wait(self, customer):
        self._queue.append(customer)

    def next(self):
        try:
            print(self._queue.popleft())
        except IndexError:
            print('There are no more customers to serve.')

    def has_customers_in_queue(self):
        return True if 1 <= len(self._queue) else False


class CommandTools:
    valid_actions = ('wait', 'next')
    customer_actions = ('wait')
    no_valid_command_msg = """
Please use one of these formats:
wait <clerk-name> <customer-name>
next <clerk-name>
"""

    @staticmethod
    def read_command():
        while True:
            cmd_input = input(': ')
            try:
                return CommandTools.parse_command(cmd_input)
            except UserWarning as e:
                print(f'No valid command was given: {e}', CommandTools.no_valid_command_msg)
                continue

    @staticmethod
    def parse_command(command):
        command_words = command.split(' ')
        if 2 > len(command_words) or 4 <= len(command_words):
            raise UserWarning(f'Wrong number of words ({len(command_words)}).')

        command_attribs = {}
        if command_words[0] not in CommandTools.valid_actions:
            raise UserWarning(f'Invalid command action ({command_words[0]}).')

        command_attribs['action'] = command_words[0]
        command_attribs['clerk'] = command_words[1]
        if command_attribs['action'] in CommandTools.customer_actions:
            if 3 > len(command_words):
                raise UserWarning(f"Missing mandatory customer on a {command_attribs['action']} action.")

            command_attribs['customer'] = command_words[2]

        return command_attribs


class Dispatcher:
    def __init__(self):
        self._clerks = {}

    def get_clerk(self, cmd_atr):
        clerk_on_call = cmd_atr['clerk']
        self.add_clerk_if_not_exist(clerk_on_call)

        if 'next' == cmd_atr['action']:
            clerk_on_call = self.get_clerk_with_customers(clerk_on_call)

        return self._clerks[clerk_on_call]

    def add_clerk_if_not_exist(self, name):
        if name not in self._clerks:
            clerk_obj = Clerk(name)
            self._clerks[name] = clerk_obj

    def get_clerk_with_customers(self, requested_clerk):
        if self._clerks[requested_clerk].has_customers_in_queue():
            return requested_clerk

        for c in self._clerks:
            if c != requested_clerk and self._clerks[c].has_customers_in_queue():
                return c

        return requested_clerk


dispatcher = Dispatcher()
while True:
    cmd = CommandTools.read_command()
    clerk = dispatcher.get_clerk(cmd)
    clerk.handle(cmd)

האמת שלי עדיין קשה לקרוא את זה. כשאני מסתכל על הקוד בתוך get_clerk שלך קשה להבין ממנו מה המהות של הפונקציה - מה היא צריכה לעשות, ומה האילוצים שלה.

הייתי מתחיל ממשהו כזה:

def get_clerk(self, cmd):
  clerk_on_call = self.find_or_create_clerk(cmd.clerk_name)
  if clerk_on_call.is_busy and cmd.allows_other_clerks:
    clerk_on_call = self.find_free_clerk()

  clerk_on_call.handle(cmd)

כלומר לתאר במילים את הדבר שאתה רוצה שהפונקציה תעשה באמצעות האוביקטים שיש לה גישה אליהם, ולהפריד כל מנגנון שיטפל בחלקים שרלוונטים אליו.

מה דעתך?

קוד מעולה יהיה פשוט (ולכן לרוב קצר) יעיל ומסביר את עצמו ללא צורך בתיעוד.
זה לא קוד מעולה :slight_smile:
לא בטוח אם כל אחד, לכל משימה, יכול להפיק קוד מעולה.

עדיין, הקוד כנראה טוב ולא מסובך במיוחד, כך שהוספת תיעוד (שבמסגרת תרגיל זה לא עשיתי) יכולה להשלים את ולעזור לקוד להיות קריא וברור.
במיוחד כשהקורא לא יודע את מטרת התכנית בכללותה ומטרות הפונקציות בפרט, קשה יותר לקוד לבדו “להסביר את עצמו”. יש סיכוי שגם אתה קצת התרחקת מהמטרות של החלקים השונים - לדוגמה הצעת עכשיו:

  • לבצע handle לפקודה בתוך get_clerk כשהמקום הראוי לזה (כפי שאתה הצעת שתי תגובות קודם) הוא בלולאה הראשית.
  • לבדוק אם פקיד “עסוק” - קונספט לא רלוונטי באופן ישיר, כשבאופן עקיף רלוונטי רק לפקודות מסוימות.

אני רואה את מטרת פונקציית get_clerk במחלקת Dispatcher כך:
לקבל את מאפייני הפקודה לטיפול ולהחזיר אובייקט של פקיד המתאים לטפל בה.

מאפייני הפקודה הם: הפעולה לביצוע, שם הפקיד המבוקש, שם לקוח (רלוונטי רק לפעולת “המתנה”).
לכן לוגיקת הפונקציה הזו היא:

  1. אם הפקיד המבוקש עדיין לא במאגר הפקידים של ה-Dispatcher, צור וצרף אתו.
  2. אם הפעולה לביצוע דורשת שיהיו לפקיד לקוחות בתור, קבל שם של פקיד כזה (הפקיד המבוקש אם יש לו לקוחות, אחרת פקיד אחר עם לקוחות אם יש כזה)
  3. החזר את אובייקט הפקיד

שיפור מסוים לקריאות של הקוד עשוי להיות אם אשתמש ב CommandTools.customer_actions כחלק מהתנאי בזיהוי סוג הפעולה לביצוע. אבל אם יודעים את מטרת הפונקציה ומבנה האובייקטים Dispatcher ו-Clerk הקוד די ברור לדעתי.

    def get_clerk(self, cmd_atr):
        clerk_on_call = cmd_atr['clerk']
        self.add_clerk_if_not_exist(clerk_on_call)

        if cmd_atr['action'] in CommandTools.customer_actions:
            clerk_on_call = self.get_clerk_with_customers(clerk_on_call)

        return self._clerks[clerk_on_call]
לייק 1

אוי כן אני רואה - הפקודה handle לא צריכה להיות שם. הייתי הולך על משהו כזה:

def get_clerk(self, cmd):
  clerk_on_call = self.find_or_create_clerk(cmd.clerk_name)
  if clerk_on_call.is_busy and cmd.allows_other_clerks:
    clerk_on_call = self.find_free_clerk()

  return clerk_on_call

אני חושב על ה Dispatcher הזה כמו סדרן בתחנת מוניות - אתה מתקשר לתחנה מבקש מונית, מספר את האילוצים שלך והעדפות שלך והסדרן צריך למצוא לך מונית מתאימה.

האילוצים וההעדפות נשמרים בתוך האוביקט cmd, ומאפייני המונית בתוך clerk. בגלל זה הבחירה להוסיף את busy ל clerk ואת allows_other_clerks ל cmd.

אבל בכל מקרה אפשר להסכים שהגישות שלנו שונות לגבי הפונקציה הזאת ובכל מקרה אני חושב ששתי הגישות טובות.

הפונקציה הבאה שהייתי ניגש לטפל בה היא הפונקציה parse_command של CommandTools.

שים לב איך הפונקציה הזאת צריכה להכיר את כל סוגי הפקודות וכמה פרמטרים כל פקודה צריכה לקבל ובדיוק איפה כל פרמטר שייך לכל פקודה. זה המון לוגיקה שככל שהשפה תאפשר יותר פקודות הפונקציה הזאת הולכת לגדול ולהיות קשה לתחזוקה.

בפיתוח מונחה עצמים אפשר להתמודד עם מצבים כאלה באמצעות חלוקה למחלקות - נסה להגדיר לכל סוג פקודה מחלקה משלה, לשנות את parse_command כך שהיא רק תצטרך לזהות איזה סוג פקודה היא קיבלה, ותתן למחלקת הפקודה המתאימה לשבור את הראש איך לפענח את כל הפרמטרים. זה יבטל לך את כל ה if-ים בפונקציה ואני חושב שיעשה אותה קצת יותר פשוטה

אני לא חושב שהגישות שלנו שונות ומתחבר מאוד לדוגמה סדרן המוניות.
אולי ההבדל נובע מהסתכלות שלך על הפונקציה הספציפית במנותק מהמחלקה שלה ומכלל התכנית:

  • למחלקת “סדרן המוניות” שלנו יש “מאגר” פקידים ב-dict עם שמות הפקידים כמפתחות המצביעים על האובייקט שלהם.
    בסוף הפונקציה פשוט מחזירים את האובייקט לפי מפתח שם הפקיד. ללא “חיפוש”, אלא בעזרת שם הפקיד שהתקבל ממאפייני הפקודה שניתנו לנו (או ממקרה מיוחד, אליו אתייחס בסעיף הבא).
    לכן הפעולה הראשונה בפונקציה היא רק לייצר ולהוסיף את הפקיד אם הוא לא קיים. לא “לחפש” ולהחזיר כבר בשלב זה את האובייקט של הפקיד.
  • בהגדרת הבונוס של התרגיל התייחסת לפקודת next כך שיטופל לקוח מתור של פקיד אחר אם אין לקוחות לפקיד שבפקודה.
    הלוגיקה הנובעת מזה היא: בפקודת next; אם הפקיד המבוקש פנוי (אין לקוחות בתור); יש להשתמש בפקיד עם לקוחות.
    לכן יצרתי את get_clerk_with_customers שמופעלת אם מדובר על פקודת next והיא “דואגת” לזה - תחזיר לנו שם של פקיד בהתאם ללוגיקה הזו (כולל הפקיד “המקורי” אם יש לו לקוחות בתור).
    [בפקודה get_clerk_with_customers יש שימוש בפקודה המקבילה ל-is_busy (שהיא has_customers_in_queue).]

האם אנחנו כבר גולשים לירושה? כי זה היה תרגיל אחרי פרק התחביר בלבד… צריך להשאיר משהו לתרגיל המסכם של כלל פרקי תכנות מונחה עצמים :wink:

אגב, אשמח לקבל ממך משוב גם על מה שכתבתי בעקבות הפרק הבא:

[וגם על פרק מוקדם יותר]

למה ירושה? אנחנו ב Python כאן לא ב Java :slight_smile: אין צורך בירושה בשביל להתיחס בצורה דומה למספר מחלקות. במקרה שלנו לא צריך שהפקודות השונות ירשו מאיזה מחלקת בסיס

לכאורה אמורות להיות להם מטודות זהות מבחינת ממשק, כשבפרק 24 המלצת להשתמש בירושה במקרים כאלו, למרות שבשימוש הנוכחי (המאוד מצומצם) באמת לא נראה שיהיה בזה תועלת (אלא רק אם יהיה משהו שבאמת אפשר לשתף בינהן).

from collections import deque


class Clerk:
    def __init__(self, clerk_name):
        self.name = clerk_name
        self._queue = deque()

    def handle(self, cmd_atr):
        if 'wait' == cmd_atr['action']:
            self.wait(cmd_atr['customer'])
        elif 'next' == cmd_atr['action']:
            self.next()

    def wait(self, customer):
        self._queue.append(customer)

    def next(self):
        try:
            print(self._queue.popleft())
        except IndexError:
            print('There are no more customers to serve.')

    def has_customers_in_queue(self):
        return True if 1 <= len(self._queue) else False


class CommandTools:
    valid_actions = ('wait', 'next')
    customer_actions = ('wait')
    no_valid_command_msg = """
Please use one of these formats:
wait <clerk-name> <customer-name>
next <clerk-name>
"""

    @staticmethod
    def read_command():
        while True:
            cmd_input = input(': ')
            try:
                return CommandTools.parse_command(cmd_input)
            except UserWarning as e:
                print(f'No valid command was given: {e}', CommandTools.no_valid_command_msg)
                continue

    @staticmethod
    def parse_command(command):
        command_words = command.split(' ')
        # if command_words[0] not in CommandTools.valid_actions:
        #   raise UserWarning(f'Invalid command action ({command_words[0]}).')
        try:
            if 'wait' == command_words[0]:
                return WaitCommand.parse(command)
            elif 'next' == command_words[0]:
                return NextCommand.parse(command)
            else:
                raise UserWarning(f'Invalid command action ({command_words[0]}).')
        except ValueError:
            raise UserWarning(f"Invalid number of words for {command_words[0]} action.")


class WaitCommand:
    @staticmethod
    def parse(command):
        com_atr = {}
        (com_atr['action'], com_atr['clerk'], com_atr['customer']) = command.split(' ')
        return com_atr


class NextCommand:
    @staticmethod
    def parse(command):
        com_atr = {}
        (com_atr['action'], com_atr['clerk']) = command.split(' ')
        return com_atr


class Dispatcher:
    def __init__(self):
        self._clerks = {}

    def get_clerk(self, cmd_atr):
        clerk_on_call = cmd_atr['clerk']
        self.add_clerk_if_not_exist(clerk_on_call)

        if 'next' == cmd_atr['action']:
            clerk_on_call = self.get_clerk_with_customers(clerk_on_call)

        return self._clerks[clerk_on_call]

    def add_clerk_if_not_exist(self, name):
        if name not in self._clerks:
            clerk_obj = Clerk(name)
            self._clerks[name] = clerk_obj

    def get_clerk_with_customers(self, requested_clerk):
        if self._clerks[requested_clerk].has_customers_in_queue():
            return requested_clerk

        for c in self._clerks:
            if c != requested_clerk and self._clerks[c].has_customers_in_queue():
                return c

        return requested_clerk

    def print_queues(self):
        for c in self._clerks:
            print(f'{c}\t{self._clerks[c]._queue}')


dispatcher = Dispatcher()
while True:
    cmd = CommandTools.read_command()
    clerk = dispatcher.get_clerk(cmd)
    clerk.handle(cmd)