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

האמת שלי עדיין קשה לקרוא את זה. כשאני מסתכל על הקוד בתוך 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)

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

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

def parse_command(command):
    commands = {
            'wait': WaitCommand,
            'next': NextCommand,
    }

    command_type, *_ = command.split(' ')

    commands[command_type].parse(command)
לייק 1

אז אני לומד מכאן 2 דברים:

  • אם רק האיבר הראשון רלוונטי, אפשר להשתמש בכוכבית-קו-תחתון לתפוס ולתעלם מאפס או יותר איברים נוספים?
  • [שמות של] מחלקות אפשר לשמור ולהשתמש מתוך מיני collections?

זה בהחלט שידרוג לקוד.

לייק 1

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

היי @ynonp רשמתי היום את השתי התרגילים הראשונים :slight_smile: :

אני ממשיך לשתיים הבאים :smile:

Targil 1

class Summer:

    def __init__(self):

        self.total = 0

 

    def add(self, *numbers):

        self.total += sum( i for i in numbers )

 

 

    def print_total(self):

        print(self.total)

 

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()

Targil 2

class MyCounter:

    count = 0

 

    def __init__(self):

        MyCounter.count += 1

 

for _ in range(10):

     c1 = MyCounter()

 

# should print 10

print(MyCounter.count)
לייק 1

משהו קטן לגבי החלק הזה בפתרון לתרגיל 1:

פקודת sum מקבלת רשימה (למעשה, כל סוג של iterable) ויצירת הלולאה הפנימית (List comprehensions) מיותרת - היא בעצם יוצרת בדיוק את אותה רשימה.
אז אפשר להסתפק ב:

self.total += sum(numbers)
לייק 1

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

אשמח לשמוע מה דעתכם…

import queue
from random import choice


def add(name):
    Clerks.clerks[name] = queue.Queue()
    print(Clerks.clerks)


class Clerks:
    clerks = {}

    def wait(self, clerk, client):
        self.clerks[clerk].put(client)

    def next(self, clerk):
        if not self.clerks[clerk].empty():
            print(f'Serviced from {clerk} queue')
            print(self.clerks[clerk].get())
        else:
            listOfClerks.next(choice(list(self.clerks.keys())))


listOfClerks = Clerks()
add("dave")
add("michael")

listOfClerks.wait("dave", "yossi")
listOfClerks.wait("dave", "shoshy")
listOfClerks.wait("dave", "alma")
listOfClerks.wait("michael", "abigale")
listOfClerks.wait("michael", "roni")

listOfClerks.next("dave")
listOfClerks.next("michael")
listOfClerks.next("michael")
listOfClerks.next("michael")
listOfClerks.next("michael")

לייק 1

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

חוץ מזה נראה טוב :ok_hand:

אני מניח שהייתי צריך לשים שם self… אבדוק בערב

החלק שמדאיג אותי בדרך כלל בתרגילים הללו הוא עד כמה אני בודק ומכין את המעטפת (בדיקת נכונות קלט, try-except וכד’…) - הפעם למדתי ששווה להכין פונקציות ו/או class’ים “שעושים את העבודה” ולטפל בנפרד בנכונות הקלט

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

class Clerk:
    def __init__(self):
        self.queue = []

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

    def next(self):
        if len(self.queue) > 0:
            return self.queue.pop(0)


class Queue:
    def __init__(self):
        self.clerks = {
            'dave': Clerk(),
            'michael': Clerk(),
        }

    def wait(self, clerk, name):
        self.clerks[clerk].wait(name)

    def next(self, name):
        name = self.clerks[name].next()
        i = 0
        for o in self.clerks.values():
            name = o.next()
            if name:
                print(name)
                return

    def get_clerk(self, clerk):
        if clerk not in self.clerks:
            self.clerks[clerk] = Clerk()
        return clerk


class Command:
    error_glob = "the input must begin with : and get next / wait after it"
    error_params = "you must give name of clerk and for wait must also give name for customer"

    def __init__(self):
        self.queue = Queue()

    def input_line(self):
        while True:
            _in = input()
            while not _in[0] == ":":
                if _in == "exit":
                    return True
                print(self.error_glob)
                _in = input()
            try:
                _, func, clerk, *name = _in.split()
            except ValueError:
                print(self.error_params)
                continue
            if func not in ["next", "wait"]:
                print(self.error_glob)
                continue
            clerk = self.queue.get_clerk(clerk)
            if func == "next":
                if name:
                    print(self.error_params)
                    continue
                self.queue.next(clerk)
            else:
                if not name:
                    print(self.error_params)
                    continue
                self.queue.wait(clerk, name[0])


c = Command()

while True:
    if c.input_line():
        break

הי,

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

תרגיל 4 נראה טוב

שלום,
מצורף פתרון לתרגיל
,
אשמח לשיפורים!

    class Clerck: 
            def __init__(self, name):
                self.name = name
                self.queue = list()

        class Command:
            active_clerks = {
                "dave" : Clerck("dave"),
                "michael" : Clerck("michael")}

            def __init__(self):
                pass

            def get_command(self):
                while True:
                    command = input("""Enter a valid command, for example:
                    \"wait <clerck name> <user name>\"
                    or: \"next <clerck name>\"""").split()
                    if command[0] == "exit":
                        break
                    if self.Verify_input(command):
                        self.command_parser(command)
                    else:
                        print("The input is incorrect, please follow the instructions")

            def Verify_input(self, command_to_valid):
                if command_to_valid:
                    if len(command_to_valid) >= 2:
                        if command_to_valid[0] == "wait" and len(command_to_valid) == 3 and command_to_valid[1] in self.active_clerks.keys():
                            return True
                        elif command_to_valid[0] == "next" and command_to_valid[1] in self.active_clerks.keys():
                            return True
                return False
                
            def command_parser(self, command_user_input):
                command_array = command_user_input
                if command_array[0] == "wait":
                    self.wait(command_array[1], command_array[2])
                else:
                    self.next(command_array[1])

            def wait(self, clerck_name, customer_name):
                self.active_clerks[clerck_name].queue.append(customer_name)
                print(self.active_clerks.get(clerck_name).queue)

            def next(self, clerck_name):
                if self.active_clerks.get(clerck_name).queue:
                    print(self.active_clerks.get(clerck_name).queue.pop())
                else: print("The queue is empty")

        class Queue:
            def __init__(self):
                self = list()
            
        c = Command()
    c.get_command()

אשמח לפידבק על התרגיל…

class Queue:
    clerks = []
    clients = []
    @staticmethod
    def print_queue():
        for clerk in Queue.clerks:
            print(f"waiting to {clerk.name}")
            for client in clerk.queue:
                print(f"> {client.name}")
    @staticmethod
    def get_client_by_name(client_name):
        exist = [
            client
            for client in Queue.clients
            if client.name == client_name
        ]
        if len(exist) == 0:
            return Client(client_name)
        return exist[0]
    @staticmethod
    def get_clerk_by_name(clerk_name):
        exist = [
            clerk
            for clerk in Queue.clerks
            if clerk.name == clerk_name
        ]
        if len(exist) == 0:
            return Clerk(clerk_name)
        return exist[0]
class Clerk:
    def __init__(self, name):
        self.name = name
        self.isFree = True
        self.appointment_with = None
        self.queue = []
        Queue.clerks.append(self)
    def next(self):
        if len(self.queue) > 0:
            next_client = self.queue.pop(0)
            self._call_to(next_client)
            if len(self.queue):
                self.queue[0].is_next = True
        else:
            print(
                f"no others clients waiting for {self.name} ... program will find if there are others waitings ...")
            find_waitings = [
                client
                for client in Queue.clients
                if client.is_next == True
            ]
            has_waitings = len(find_waitings) > 0
            if not has_waitings:
                return print(f"{self.name}, you can take a break , nobody is waiting")
            next_client = find_waitings[0]
            self._call_to(next_client)
            next_client.clerk.queue.remove(next_client)
    def _call_to(self, next_client):
        self.appointment_with = next_client
        next_client.is_next = False
        next_client.is_wait = False
        self.isFree = False
        print(f"{next_client.name} , please go to -> {self.name}")
    def get_break(self, client):
        self.queue.remove(client)
        self.appointment_with(client)
        self.isFree = False
    def finish_break(self):
        self.isFree = True
class Client:
    def __init__(self, name):
        self.name = name
        self.is_wait = False
        self.is_next = False
        Queue.clients.append(self)
    def remove_from_queue(self):
        pass
    def wait(self, clerk):
        clerk.queue.append(self)
        self.is_wait = True
        self.clerk = clerk
        self.is_next = self.clerk.queue[0] == self
        print(
            f"hi {self.name}... please wait for {clerk.name} , your number in queue is => {len(clerk.queue)}")
def prompt(msg):
    while True:
        try:
            cmd = input(msg)
            commands = cmd.split()
            if len(commands) != 2 and len(commands) != 3:
                raise Exception(
                    "Invalid number off words in command , command should either [wait clerk client_name] or [next clerk]")
            if(commands[0] != "wait" and commands[0] != "next"):
                raise Exception(
                    "command should be either [wait clerk client_name] or [next clerk] -> first word must be either wait or next")
            if(commands[0] == "wait"):
                if len(commands) == 2:
                    raise Exception(
                        f"command should be[wait clerk client_name] but got [wait {commands[1]}]")
                _, clerk_name , client_name = commands
                clerkObj = Queue.get_clerk_by_name(clerk_name)
                clientObj = Queue.get_client_by_name(client_name)
                clientObj.wait(clerkObj)
            if(commands[0] == "next"):
                if len(commands) == 3:
                    raise Exception(
                        f"command should be[next clerk] but got [wait {commands[1]} {commands[2]}]")
                _, clerk_name = commands
                clerkObj = Queue.get_clerk_by_name(clerk_name)
                clerkObj.next()
        except Exception as e:
            print(e)
while True:
    prompt(
        "please enter your command! hint -> [wait clerk_name client_name] | [next clerk] ")