Lecture 1 – Python Foundations
Clean Code, Modularity, Type Hints, Context Manager, Decorator
def square_positive(number: float): """Return square of number if number > 0, otherwise None.""" if number <= 0: return None return number * number def demo(): tests = [5, 0, -3, 2.5] for t in tests: print(f"Input={t:>4} -> Output={square_positive(t)}") if __name__ == "__main__": demo()
الكود ده مثال على Clean Code — يعني كتابة كود واضح وسهل القراءة.
- square_positive(number): دالة بتاخد رقم. لو الرقم صفر أو أقل → ترجع None. لو أكبر من صفر → ترجع مربعه.
- demo(): دالة بتجرب الدالة على أرقام مختلفة وتطبع النتيجة.
- استخدمنا type hint (float) عشان نوضح إن الدالة بتاخد عدد عشري.
- استخدمنا docstring لشرح الدالة — ده من أساسيات الكود النظيف.
from pathlib import Path def read_text(path: str) -> str: """Read all text from a file.""" return Path(path).read_text(encoding="utf-8") def clean_text(text: str) -> str: """Normalize whitespace and lowercase the text.""" normalized = " ".join(text.split()) return normalized.lower() def count_words(text: str) -> int: """Count words in cleaned text.""" return len(text.split()) def main(): sample_path = "lecture1_sample.txt" Path(sample_path).write_text(" Hello Python Students!\nWelcome to ELC 423. ", encoding="utf-8") raw = read_text(sample_path) cleaned = clean_text(raw) n_words = count_words(cleaned) print("RAW:", raw) print("CLEANED:", cleaned) print("WORD COUNT:", n_words) if __name__ == "__main__": main()
المثال ده بيطبق مبدأ Separation of Concerns — كل دالة مسؤولة عن حاجة واحدة بس.
- read_text(path): بتقرأ الملف وترجع محتواه كنص.
- clean_text(text): بتشيل المسافات الزيادة وتحوّل الكل لـ lowercase.
- count_words(text): بتعد الكلمات عن طريق split() ثم len().
- main(): بتنسق الكل — بتبني الملف → تقراه → تنظفه → تعد الكلمات → تطبع.
- استخدمنا pathlib بدل open() القديمة عشان أوضح وأنظف.
from typing import Optional def safe_divide(a: float, b: float) -> Optional[float]: """Return a/b if b != 0, otherwise None.""" if b == 0: return None return a / b def main(): tests = [(10, 2), (5, 0), (7.5, 2.5)] for a, b in tests: result = safe_divide(a, b) print(f"{a} / {b} = {result}")
Type Hints بتساعدك تعرف الدالة بتاخد إيه وبترجع إيه من غير ما تشغّل الكود.
- Optional[float]: معناه إن الدالة ممكن ترجع float أو None.
- الـ guard clause if b == 0 بيمنع ZeroDivisionError قبل ما يحصل.
- main() بتجرب 3 حالات: قسمة عادية، قسمة على صفر، وعدد عشري.
def write_students(path: str, students: list[str]) -> None: with open(path, "w", encoding="utf-8") as f: for name in students: f.write(name + "\n") def read_students(path: str) -> list[str]: with open(path, "r", encoding="utf-8") as f: return [line.strip() for line in f if line.strip()] def main(): file_path = "students.txt" students = ["Ahmed", "Mona", "Hany", "Sara"] write_students(file_path, students) loaded = read_students(file_path) print("Saved students:", students) print("Loaded students:", loaded)
الـ Context Manager (with) بيضمن إن الملف هيتقفل تلقائياً حتى لو في error.
- write_students(): بتفتح الملف للكتابة → تكتب كل اسم في سطر → الملف يتقفل تلقائي.
- read_students(): بتفتح الملف للقراءة → تقرا السطور → بتشيل المسافات الزيادة والسطور الفاضية.
- الـ list comprehension
[line.strip() for line in f if line.strip()]بتقرا وتنظف في خطوة واحدة.
import time from functools import wraps def timer(func): """Decorator to measure execution time.""" @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() value = func(*args, **kwargs) end = time.perf_counter() print(f"[TIMER] {func.__name__} took {(end - start):.6f} seconds") return value return wrapper @timer def sum_to_n(n: int) -> int: return sum(range(1, n + 1)) def main(): result = sum_to_n(5_000_000) print("Result:", result)
الـ Decorator ده بيضيف سلوك جديد (قياس الوقت) على أي دالة من غير ما تعدّلها.
- timer(func): decorator بياخد الدالة الأصلية كـ argument.
- wrapper(*args, **kwargs): الدالة الغلافة بتسجل الوقت قبل وبعد تنفيذ func.
- @wraps(func): بيحافظ على اسم الدالة الأصلية بدل ما يبقى "wrapper".
- @timer قبل sum_to_n: معناه إن sum_to_n = timer(sum_to_n) تلقائياً.
- perf_counter(): أدق من time.time() لقياس الأداء.
Lecture 2 – SOLID Principles
SRP, OCP, LSP, ISP, DIP
class StudentGrades: def __init__(self, student_name: str, grades: list[float]): self.student_name = student_name self.grades = grades def average(self) -> float: if not self.grades: return 0.0 return sum(self.grades) / len(self.grades) class ReportPrinter: def print_report(self, student: StudentGrades) -> None: avg = student.average() print(f"Student: {student.student_name}") print(f"Average: {avg:.2f}") def main(): student = StudentGrades("Ahmed", [90, 85, 92, 88, 95]) printer = ReportPrinter() printer.print_report(student)
SRP: كل class مسؤول عن حاجة واحدة بس — لو عندك سبب واحد للتغيير = تمام.
- StudentGrades: مسؤوليتها الوحيدة = تخزين الدرجات وحساب المتوسط. مش بتطبع حاجة.
- ReportPrinter: مسؤوليتها الوحيدة = طباعة التقرير. مش بتحسب حاجة.
- الفايدة: لو عايز تغير طريقة العرض → تعدل ReportPrinter بس. لو عايز تضيف GPA → تعدل StudentGrades بس.
from abc import ABC, abstractmethod class PaymentMethod(ABC): @abstractmethod def pay(self, amount: float) -> None: pass class CreditCardPayment(PaymentMethod): def pay(self, amount: float) -> None: print(f"[Credit Card] Paid {amount:.2f} EGP") class CashPayment(PaymentMethod): def pay(self, amount: float) -> None: print(f"[Cash] Paid {amount:.2f} EGP") class BankTransferPayment(PaymentMethod): # مضاف بدون تعديل PaymentProcessor def pay(self, amount: float) -> None: print(f"[Bank Transfer] Paid {amount:.2f} EGP") class PaymentProcessor: def __init__(self, method: PaymentMethod): self.method = method def process(self, amount: float) -> None: self.method.pay(amount) # Polymorphism — no if/else! def main(): for method in (CreditCardPayment(), CashPayment(), BankTransferPayment()): PaymentProcessor(method).process(250.0)
OCP: الكود مفتوح للإضافة (Open for extension) لكن مغلق للتعديل (Closed for modification).
- PaymentMethod (ABC): abstract class بتحدد العقد — كل طريقة دفع لازم عندها pay().
- PaymentProcessor: شغّالة مع أي PaymentMethod من غير if/else — ده الـ Polymorphism.
- عايز تضيف Bitcoin؟ → اعمل class جديد يورث من PaymentMethod بس. مش هتعدل PaymentProcessor خالص.
class Bird: def move(self) -> None: raise NotImplementedError class Sparrow(Bird): def move(self) -> None: print("Sparrow flies in the sky.") class Penguin(Bird): def move(self) -> None: print("Penguin swims in the water.") def make_bird_move(bird: Bird) -> None: bird.move() # works for ANY Bird subclass def main(): for b in [Sparrow(), Penguin()]: make_bird_move(b)
LSP: أي subclass لازم تقدر تحل محل الـ base class من غير ما البرنامج يتعطل.
- الخطأ الكلاسيكي: لو Bird كان فيها fly() → Penguin مش بتطير → هيكسر LSP.
- الحل: استخدمنا move() بدل fly() — كل bird بتتحرك بطريقتها.
- make_bird_move(bird: Bird): بتاخد أي Bird وتنادي move() بدون ما تعرف النوع.
- Sparrow → بتطير | Penguin → بتسبح. كلاهما يحل محل Bird بأمان.
from abc import ABC, abstractmethod class Printer(ABC): @abstractmethod def print_doc(self, text: str) -> None: pass class Scanner(ABC): @abstractmethod def scan_doc(self) -> str: pass class SimplePrinter(Printer): def print_doc(self, text: str) -> None: print(f"[SimplePrinter] Printing: {text}") class MultiFunctionDevice(Printer, Scanner): def print_doc(self, text: str) -> None: print(f"[MFD] Printing: {text}") def scan_doc(self) -> str: print("[MFD] Scanning document...") return "Scanned document content"
ISP: متجبرش أي class على implement وظائف مش محتاجها.
- لو عندنا interface واحد فيه print + scan + fax → SimplePrinter هيضطر يعمل scan وfax وهو مش بيدعمهم!
- الحل: Printer (interface للطباعة بس) + Scanner (interface للسكان بس).
- SimplePrinter: بتورث Printer بس → بتعمل print_doc بس.
- MultiFunctionDevice: بتورث Printer AND Scanner → بتعمل الاتنين.
from abc import ABC, abstractmethod class Notifier(ABC): @abstractmethod def send(self, message: str) -> None: pass class EmailNotifier(Notifier): def send(self, message: str) -> None: print(f"[EMAIL] {message}") class SMSNotifier(Notifier): def send(self, message: str) -> None: print(f"[SMS] {message}") class FakeNotifier(Notifier): # For testing def __init__(self): self.sent_messages: list[str] = [] def send(self, message: str) -> None: self.sent_messages.append(message) class AlertService: def __init__(self, notifier: Notifier): # Depends on abstraction! self.notifier = notifier def alert(self, level: str, details: str) -> None: self.notifier.send(f"ALERT [{level}]: {details}")
DIP: المستوى العالي (AlertService) لازم يعتمد على abstraction مش على تفاصيل concrete classes.
- AlertService.__init__(notifier: Notifier): بتاخد أي Notifier — ده الـ Dependency Injection.
- لو عايز تغير من Email لـ SMS → مش بتعدل AlertService خالص، بس بتبعت notifier مختلف.
- FakeNotifier: للـ testing — بدل ما تبعت emails حقيقية، بتخزن الرسائل في قايمة وتتأكد منها.
- ده بيخلي الكود سهل الاختبار (testable) وسهل التوسع (extensible).
Lecture 3 – Creational Design Patterns
Factory, Abstract Factory, Builder, Prototype, Singleton
from abc import ABC, abstractmethod import math class Shape(ABC): @abstractmethod def area(self) -> float: pass class Circle(Shape): def __init__(self, radius: float): self.radius = radius def area(self) -> float: return math.pi * self.radius ** 2 class Rectangle(Shape): def __init__(self, width: float, height: float): self.width = width; self.height = height def area(self) -> float: return self.width * self.height def create_shape(shape_type: str, **kwargs) -> Shape: if shape_type == "circle": return Circle(radius=kwargs["radius"]) if shape_type == "rectangle": return Rectangle(kwargs["width"], kwargs["height"]) raise ValueError(f"Unknown shape: {shape_type}") def main(): s1 = create_shape("circle", radius=5) s2 = create_shape("rectangle", width=4, height=6) print("Circle area:", round(s1.area(), 3)) print("Rectangle area:", round(s2.area(), 3))
الـ Factory Method بيفصل عملية الإنشاء عن الاستخدام — الـ client مش محتاج يعرف الـ class الحقيقية.
- create_shape(): هي الـ Factory — بتاخد string وترجع الـ object المناسب.
- الـ client (main) بيشتغل مع Shape interface بس.
- لو أضفت Triangle → بتضيف class + سطر في create_shape. الـ client ما بيتغيرش.
- **kwargs: بيسمح بتمرير parameters مختلفة لكل نوع shape.
class LightButton(Button): def render(self): print("[LightButton] White background") class DarkButton(Button): def render(self): print("[DarkButton] Dark background") class LightFactory(UIFactory): def create_button(self): return LightButton() def create_textbox(self): return LightTextBox() class DarkFactory(UIFactory): def create_button(self): return DarkButton() def create_textbox(self): return DarkTextBox() def build_screen(factory: UIFactory) -> None: btn = factory.create_button() tb = factory.create_textbox() btn.render(); tb.render() def main(): build_screen(LightFactory()) # Light theme build_screen(DarkFactory()) # Dark theme
الـ Abstract Factory بينشئ عائلة من الـ objects المترابطة — لو اخترت Light كل حاجة Light.
- UIFactory (ABC): الـ interface العام — create_button() و create_textbox().
- LightFactory / DarkFactory: كل واحدة بتنشئ عائلتها الكاملة.
- build_screen(factory): بتشتغل مع أي factory من غير ما تعرف النوع.
- الفرق عن Factory Method: هنا بننشئ عائلة من الـ objects مش object واحد.
from dataclasses import dataclass @dataclass class Computer: cpu: str | None = None ram: str | None = None storage: str | None = None gpu: str | None = None class ComputerBuilder: def __init__(self): self._computer = Computer() def cpu(self, cpu): self._computer.cpu = cpu; return self def ram(self, ram): self._computer.ram = ram; return self def storage(self, s): self._computer.storage = s; return self def gpu(self, gpu): self._computer.gpu = gpu; return self def build(self): built = self._computer self._computer = Computer() # reset for next build return built def main(): builder = ComputerBuilder() gaming = builder.cpu("Intel i7").ram("32GB").storage("1TB SSD").gpu("RTX 4070").build() office = builder.cpu("Intel i5").ram("16GB").storage("512GB SSD").build() print("Gaming PC:", gaming) print("Office PC:", office)
الـ Builder Pattern بيبني objects معقدة خطوة بخطوة باستخدام method chaining.
- Computer (@dataclass): الـ object النهائي — بياخد قطع اختيارية.
- ComputerBuilder: كل method بتضيف جزء وترجع self → يسمح بالـ chaining.
- Method Chaining: builder.cpu().ram().gpu().build() — واضح وسهل القراءة.
- build() بيرجع الـ Computer وبيـ reset الـ builder لـ build تاني.
- الفايدة: ممكن تعمل Computer بدون GPU (Office) أو بـ GPU (Gaming) بنفس الـ builder.
import copy from dataclasses import dataclass @dataclass class TrainingConfig: model_name: str learning_rate: float batch_size: int epochs: int def clone(self) -> "TrainingConfig": return copy.deepcopy(self) def main(): base = TrainingConfig("CNN", learning_rate=0.001, batch_size=32, epochs=20) variant_lr = base.clone(); variant_lr.learning_rate = 0.0005 variant_bs = base.clone(); variant_bs.batch_size = 64 print("Base :", base) print("Variant LR:", variant_lr) print("Variant BS:", variant_bs)
الـ Prototype Pattern بيسمح بإنشاء نسخة من object موجود بدل إنشاء واحد جديد من الصفر.
- clone(): بتستخدم copy.deepcopy() عشان كل attribute يتنسخ مستقل تماماً.
- deepcopy مهم جداً: لو استخدمت shallow copy، التعديل على الـ clone هيأثر على الأصل!
- الفايدة في ML: عندك base config → تعمل 10 experiments بتغيير parameter واحد كل مرة.
- النسخ الثلاثة مستقلة تماماً — أي تغيير في واحدة ما بيأثرش على التانية.
class LoggerSingleton: """Singleton logger — use carefully.""" _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._logs = [] return cls._instance def log(self, message: str) -> None: self._logs.append(message) def show_logs(self) -> None: for i, msg in enumerate(self._logs, 1): print(f"{i}. {msg}") def main(): logger1 = LoggerSingleton() logger2 = LoggerSingleton() print("Same object?", logger1 is logger2) # True logger1.log("System started") logger2.log("User logged in") logger1.show_logs()
الـ Singleton Pattern بيضمن إن class معينة عندها instance واحد بس في البرنامج كله.
- __new__(cls): ده اللي بيتنادى قبل __init__ لإنشاء الـ object. هنا بنتحكم فيه.
- لو _instance = None → اعمل instance جديد وخزّنه. لو لأ → رجّع نفس الـ instance القديم.
- logger1 is logger2 → True — نفس الـ object في الذاكرة.
- الـ logs اللي logger2 بيضيفها بتظهر مع logger1 لأنهم نفس الـ object.
- ⚠️ استخدمه بحذر: بيصعّب الـ testing وممكن يسبب مشاكل في multi-threading.