├── requirements.txt
├── .gitignore
├── .devcontainer
├── startup.sh
├── devcontainer.json
└── Dockerfile
├── .github
├── CODEOWNERS
├── PULL_REQUEST_TEMPLATE.md
├── workflows
│ └── main.yml
└── ISSUE_TEMPLATE.md
├── Ch02
├── 02_04
│ ├── error_cost.png
│ ├── plugin.py
│ └── plugin_abc.py
├── 02_02
│ └── vm.py
├── 02_05
│ └── writer.py
├── challenge
│ └── ingest.py
├── 02_03
│ └── logger.py
├── 02_01
│ └── user.py
└── solution
│ └── ingest.py
├── Ch04
├── challenge
│ └── singleton.py
├── solution
│ └── singleton.py
├── 04_01
│ └── robot.py
├── 04_02
│ └── robot.py
├── 04_03
│ └── robot.py
└── 04_04
│ └── robot.py
├── Ch01
├── challenge
│ └── colors.py
├── 01_01
│ ├── game.py
│ └── attr.py
├── 01_03
│ └── proxy.py
├── solution
│ └── colors.py
├── 01_04
│ ├── car.py
│ └── compact_car.py
├── 01_05
│ ├── worker.py
│ └── worker_mangled.py
├── 01_02
│ └── line_item.py
└── 01_06
│ └── validators.py
├── Ch03
├── 03_05
│ └── norm.py
├── 03_03
│ ├── traces.py
│ └── headers.py
├── 03_04
│ └── duration.py
├── 03_01
│ └── currency.py
├── 03_02
│ └── stack.py
├── challenge
│ └── kv.py
└── solution
│ └── kv.py
├── Ch05
├── challenge
│ └── vm.py
├── 05_04
│ └── event.py
├── 05_02
│ └── game.py
├── 05_03
│ └── bookmark.py
├── solution
│ └── vm.py
└── 05_01
│ └── serialize.py
├── CONTRIBUTING.md
├── NOTICE
├── .vscode
└── settings.json
├── README.md
└── LICENSE
/requirements.txt:
--------------------------------------------------------------------------------
1 | mypy ~= 1.5
2 |
3 | jupyter ~= 1.0 # For VSCode
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | .tmp
4 | npm-debug.log
5 | .mypy_cache
6 |
--------------------------------------------------------------------------------
/.devcontainer/startup.sh:
--------------------------------------------------------------------------------
1 | if [ -f requirements.txt ]; then
2 | pip install --user -r requirements.txt
3 | fi
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Codeowners for these exercise files:
2 | # * (asterisk) denotes "all files and folders"
3 | # Example: * @producer @instructor
4 |
--------------------------------------------------------------------------------
/Ch02/02_04/error_cost.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinkedInLearning/advanced-python-object-oriented-programming-4510177/main/Ch02/02_04/error_cost.png
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Ch04/challenge/singleton.py:
--------------------------------------------------------------------------------
1 | # %% Singleton
2 | class Singleton:
3 | # TODO
4 | pass
5 |
6 |
7 | # %% Test
8 |
9 | class Driver(Singleton):
10 | pass
11 |
12 | d1 = Driver()
13 | d2 = Driver()
14 | print(d1 is d2)
15 |
--------------------------------------------------------------------------------
/Ch01/challenge/colors.py:
--------------------------------------------------------------------------------
1 | # %% Color database
2 | color_db = {
3 | 'red': 0xFF0000,
4 | 'green': 0x00FF00,
5 | 'blue': 0x0000FF,
6 | }
7 |
8 |
9 | class Colors:
10 | """Dynamically get color from color_db"""
11 | # FIXME
12 |
13 |
14 | # %% Test
15 | colors = Colors()
16 |
17 | val = colors.green
18 | print(f'green: {val:06X}') # 00FF00
19 |
--------------------------------------------------------------------------------
/Ch01/01_01/game.py:
--------------------------------------------------------------------------------
1 | # %% Player
2 | class Player:
3 | num_players = 0 # Class attribute
4 |
5 | def __init__(self, name):
6 | self.name = name # Instance attribute
7 | self.mana = 100
8 | self.num_players += 1
9 | print('self:', self.num_players)
10 |
11 |
12 | # %% Test
13 | p1 = Player('Parzival')
14 | print('Player:', Player.num_players)
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Copy To Branches
2 | on:
3 | workflow_dispatch:
4 | jobs:
5 | copy-to-branches:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v2
9 | with:
10 | fetch-depth: 0
11 | - name: Copy To Branches Action
12 | uses: planetoftheweb/copy-to-branches@v1.2
13 | env:
14 | key: main
15 |
--------------------------------------------------------------------------------
/Ch01/01_03/proxy.py:
--------------------------------------------------------------------------------
1 | # %% Proxy
2 | class Proxy:
3 | def __init__(self, obj):
4 | self._obj = obj
5 |
6 | def __getattr__(self, attr):
7 | value = getattr(self._obj, attr)
8 | print(f'{attr} -> {value!r}')
9 | return value
10 |
11 | # %% Test
12 | import sqlite3
13 |
14 | conn = sqlite3.connect(':memory:')
15 | proxy = Proxy(conn)
16 | n = proxy.total_changes
17 | print(n)
18 |
19 | # %% Method
20 | proxy.close()
--------------------------------------------------------------------------------
/Ch02/02_02/vm.py:
--------------------------------------------------------------------------------
1 | # %% VM
2 | from random import choice
3 |
4 | adjectives = ['cool', 'funny', 'strong']
5 | names = ['bruce', 'carol', 'natasha']
6 |
7 |
8 | class VM:
9 | def __init__(self):
10 | self.name = VM.random_name()
11 |
12 |
13 | @staticmethod
14 | def random_name():
15 | adjective, name = choice(adjectives), choice(names)
16 | return f'{adjective}_{name}'
17 |
18 |
19 | # %% Test
20 | vm = VM()
21 | print(vm.name)
22 |
--------------------------------------------------------------------------------
/Ch03/03_05/norm.py:
--------------------------------------------------------------------------------
1 | # %% normalize
2 | def normalize(value):
3 | return value * .9
4 |
5 |
6 | # %% Norm
7 | class Norm:
8 | def __init__(self, factor):
9 | self.factor = factor
10 |
11 | def __call__(self, value):
12 | return self.factor * value
13 |
14 |
15 | # %% Test
16 | n93 = Norm(.93)
17 | print(n93(100))
18 |
19 | # %%
20 | def make_norm(factor):
21 | return lambda value: factor * value
22 |
23 | n93 = make_norm(.93)
24 | print(n93(100))
25 |
--------------------------------------------------------------------------------
/Ch04/solution/singleton.py:
--------------------------------------------------------------------------------
1 | # %% Singleton
2 | class SingletonMeta(type):
3 | def __call__(cls, *args, **kw):
4 | inst = getattr(cls, '_instance', None)
5 | if inst is None:
6 | inst = cls._instance = type.__call__(cls, *args, **kw)
7 | return inst
8 |
9 |
10 | class Singleton(metaclass=SingletonMeta):
11 | pass
12 |
13 |
14 | # %% Test
15 |
16 | class Driver(Singleton):
17 | pass
18 |
19 | d1 = Driver()
20 | d2 = Driver()
21 | print(d1 is d2)
22 |
--------------------------------------------------------------------------------
/Ch01/solution/colors.py:
--------------------------------------------------------------------------------
1 | # %% Color database
2 | color_db = {
3 | 'red': 0xFF0000,
4 | 'green': 0x00FF00,
5 | 'blue': 0x0000FF,
6 | }
7 |
8 |
9 | class Colors:
10 | """Dynamically get color from color_db"""
11 | def __getattr__(self, attr):
12 | val = color_db.get(attr)
13 | if val is None:
14 | raise AttributeError(attr)
15 | return val
16 |
17 | # %% Test
18 | colors = Colors()
19 |
20 | val = colors.green
21 | print(f'green: {val:06X}') # 00FF00
22 |
--------------------------------------------------------------------------------
/Ch05/challenge/vm.py:
--------------------------------------------------------------------------------
1 | """
2 | Use a dataclass, write a VM class that has the following fields:
3 | - id: str, no default
4 | - cpus: int, default to 2
5 | - memory: int, default to 512 (in MB)
6 | - state: one of 'starting', 'running', 'stopped'
7 | - tags: list of str, default to empty
8 |
9 | After the VM is created, check that
10 | - id is not empty
11 | - cpus >= 1
12 | - memory >= 256
13 | """
14 |
15 | # %% test
16 | vm = VM(
17 | id='i-0af01c0123456789a',
18 | cpus=4,
19 | memory=2048,
20 | tags=['db', 'env:qa']
21 | )
22 | print(vm)
--------------------------------------------------------------------------------
/Ch05/05_04/event.py:
--------------------------------------------------------------------------------
1 | # %% Event
2 | from dataclasses import dataclass
3 |
4 | @dataclass
5 | class Event:
6 | uri: str
7 | action: str
8 |
9 |
10 | events = [
11 | Event(uri='file:///etc/passwd', action='READ'),
12 | Event(uri='file:///etc/passwd', action='WRITE'),
13 | Event(uri='file:///var/log/httpd', action='WRITE'),
14 | Event(uri='file:///etc/passwd', action='READ'),
15 | ]
16 |
17 | # %% count
18 | from collections import Counter
19 |
20 | counts = Counter()
21 | for evt in events:
22 | counts[evt] += 1
23 | print(counts)
--------------------------------------------------------------------------------
/Ch05/05_02/game.py:
--------------------------------------------------------------------------------
1 | # %% Room
2 | from collections import namedtuple
3 |
4 | Room = namedtuple('Room', 'x y')
5 |
6 | r1 = Room(1, 2)
7 | print(r1)
8 |
9 | # %% Attributes
10 | print('len:', len(r1))
11 | print('x:', r1.x)
12 | print('[0]:', r1[0])
13 |
14 | # %% attributes
15 | from collections import defaultdict
16 |
17 | players = defaultdict(list)
18 | players[r1].append('amy')
19 | print(players)
20 |
21 | # %% replace
22 | r2 = r1._replace(x=3)
23 | print(r2)
24 |
25 | # %% asdict
26 | print(r2._asdict())
27 |
28 | # %% compare
29 | Range = namedtuple('Range', 'low high')
30 |
31 | rng = Range(1, 2)
32 | print(rng == r1)
33 |
--------------------------------------------------------------------------------
/Ch01/01_04/car.py:
--------------------------------------------------------------------------------
1 | # %% Car
2 | class Car:
3 | def __init__(self, id, lat, lng):
4 | self.id = id
5 | self.lat = lat
6 | self.lng = lng
7 |
8 |
9 | # %% Memory
10 | import tracemalloc
11 | from random import random
12 |
13 | def rand_lat():
14 | return random()*180 - 90
15 |
16 | def rand_lng():
17 | return random()*360 - 180
18 |
19 | def mb(bytes):
20 | return bytes/1_000_000
21 |
22 | size = 100_000
23 |
24 | tracemalloc.start()
25 | cars = [Car(i, rand_lat(), rand_lng()) for i in range(size)]
26 | current, peak = tracemalloc.get_traced_memory()
27 | print('current:', mb(current), 'peak:', mb(peak))
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | Contribution Agreement
3 | ======================
4 |
5 | This repository does not accept pull requests (PRs). All pull requests will be closed.
6 |
7 | However, if any contributions (through pull requests, issues, feedback or otherwise) are provided, as a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code (or otherwise providing feedback), you (and, if applicable, your employer) are licensing the submitted code (and/or feedback) to LinkedIn and the open source community subject to the BSD 2-Clause license.
8 |
--------------------------------------------------------------------------------
/Ch02/02_05/writer.py:
--------------------------------------------------------------------------------
1 | # %% Writer
2 | from typing import Protocol
3 |
4 |
5 | class Writer(Protocol):
6 | def write(self, data: bytes) -> None:
7 | ...
8 |
9 |
10 | # %% Store
11 | import json
12 |
13 | def store_json(w: Writer, obj: dict) -> None:
14 | data = json.dumps(obj).encode('utf-8')
15 | w.write(data)
16 |
17 |
18 | # %% S3File
19 | class S3File:
20 | def write(self, data: str) -> None:
21 | # TODO
22 | print(f's3: write: {data!r}')
23 |
24 |
25 | # %% Test
26 | out = S3File()
27 | obj = {
28 | 'id': '007',
29 | 'lat': 51.4871871,
30 | 'lng': -0.1270605,
31 | }
32 | store_json(out, obj)
33 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2024 LinkedIn Corporation
2 | All Rights Reserved.
3 |
4 | Licensed under the LinkedIn Learning Exercise File License (the "License").
5 | See LICENSE in the project root for license information.
6 |
7 | Please note, this project may automatically load third party code from external
8 | repositories (for example, NPM modules, Composer packages, or other dependencies).
9 | If so, such third party code may be subject to other license terms than as set
10 | forth above. In addition, such third party code may also depend on and load
11 | multiple tiers of dependencies. Please review the applicable licenses of the
12 | additional dependencies.
13 |
--------------------------------------------------------------------------------
/Ch01/01_04/compact_car.py:
--------------------------------------------------------------------------------
1 | # %% Car
2 | class Car:
3 | __slots__ = ['id', 'lat', 'lng']
4 |
5 | def __init__(self, id, lat, lng):
6 | self.id = id
7 | self.lat = lat
8 | self.lng = lng
9 |
10 |
11 | # %% Memory
12 | import tracemalloc
13 | from random import random
14 |
15 | def rand_lat():
16 | return random()*180 - 90
17 |
18 | def rand_lng():
19 | return random()*360 - 180
20 |
21 | def mb(bytes):
22 | return bytes/1_000_000
23 |
24 | size = 100_000
25 |
26 | tracemalloc.start()
27 | cars = [Car(i, rand_lat(), rand_lng()) for i in range(size)]
28 | current, peak = tracemalloc.get_traced_memory()
29 | print('current:', mb(current), 'peak:', mb(peak))
--------------------------------------------------------------------------------
/Ch05/05_03/bookmark.py:
--------------------------------------------------------------------------------
1 | # %% Bookmark
2 | from dataclasses import dataclass, field
3 | from datetime import datetime, UTC
4 |
5 | @dataclass
6 | class Bookmark:
7 | url: str
8 | title: str = ''
9 | tags: list[str] = field(default_factory=list)
10 | created: datetime = None
11 |
12 | def __post_init__(self):
13 | if self.created is None:
14 | self.created = datetime.now(tz=UTC)
15 |
16 | b1 = Bookmark(
17 | url='https://fastapi.tiangolo.com/',
18 | title='FastAPI web framework',
19 | tags=['python', 'web', 'server'],
20 | )
21 | print(b1)
22 |
23 | # %%
24 | b2 = Bookmark(
25 | url='https://python.org',
26 | )
27 | b2.title = 'Python programming language'
28 | print(b2)
29 |
--------------------------------------------------------------------------------
/Ch01/01_05/worker.py:
--------------------------------------------------------------------------------
1 | # %% Worker
2 | from datetime import datetime, timedelta
3 | from time import time, sleep
4 |
5 |
6 | class Worker:
7 | def __init__(self, id):
8 | self.id = id
9 | self._started = time()
10 |
11 | def uptime(self):
12 | return time() - self._started
13 |
14 |
15 | # %% SpotWorker
16 | class SpotWorker(Worker):
17 | def __init__(self, id):
18 | super().__init__(id)
19 | self._started = datetime.now()
20 |
21 | def cost(self):
22 | duration = datetime.now() - self._started
23 | return duration / timedelta(seconds=60) * 0.02
24 |
25 | # %% Test
26 | worker = SpotWorker('769f984')
27 | sleep(0.123)
28 | print(f'uptime: {worker.uptime():.2f}')
29 |
--------------------------------------------------------------------------------
/Ch01/01_02/line_item.py:
--------------------------------------------------------------------------------
1 | # %% LineItem
2 | class LineItem:
3 | def __init__(self, sku: str, price: float, amount: int):
4 | self.sku = sku
5 | self.price = price
6 | self.amount = amount
7 |
8 | @property # Computed property.
9 | def value(self):
10 | return self.price * self.amount
11 |
12 | @property # Getter.
13 | def sku(self):
14 | return self._sku
15 |
16 | @sku.setter # Setter.
17 | def sku(self, value):
18 | value = value.strip()
19 | if not value:
20 | raise ValueError(f'empty sku: {value!r}')
21 | self._sku = value
22 |
23 |
24 | # %% Test
25 | li = LineItem('esp32', 1.34, 10)
26 | print(li.value)
27 |
28 | # %% Invalid SKU
29 | li.sku = ' '
--------------------------------------------------------------------------------
/Ch04/04_01/robot.py:
--------------------------------------------------------------------------------
1 | # %% Robot
2 | class Robot:
3 | manufacture = 'BnL'
4 |
5 | def move(self, x, y):
6 | print(f'{self} moving to {x}/{y}')
7 |
8 |
9 | walle = Robot()
10 | walle.move(100, 200)
11 |
12 |
13 | # %% What "class" keyword does
14 | from textwrap import dedent
15 |
16 | class_body = '''
17 | manufacture = 'BnL'
18 |
19 | def move(self, x, y):
20 | print(f'{self} moving to {x}/{y}')
21 | '''
22 |
23 | cls_dict = {}
24 | exec(dedent(class_body), None, cls_dict)
25 | print(cls_dict)
26 | move = cls_dict['move']
27 | print(move.__code__.co_varnames)
28 | move(walle, 10, 20)
29 |
30 | # %% Using type
31 | Robot = type(
32 | 'Robot',
33 | (object,),
34 | cls_dict,
35 | )
36 | walle = Robot()
37 | walle.move(100, 200)
38 |
--------------------------------------------------------------------------------
/Ch01/01_05/worker_mangled.py:
--------------------------------------------------------------------------------
1 | # %% Worker
2 | from datetime import datetime, timedelta
3 | from time import time, sleep
4 |
5 |
6 | class Worker:
7 | def __init__(self, id):
8 | self.id = id
9 | self.__started = time()
10 |
11 | def uptime(self):
12 | return time() - self.__started
13 |
14 |
15 | # %% SpotWorker
16 | class SpotWorker(Worker):
17 | def __init__(self, id):
18 | super().__init__(id)
19 | self._started = datetime.now()
20 |
21 | def cost(self):
22 | duration = datetime.now() - self._started
23 | return duration / timedelta(seconds=60) * 0.02
24 |
25 | # %% Test
26 | worker = SpotWorker('769f984')
27 | sleep(0.123)
28 | print(f'uptime: {worker.uptime():.2f}')
29 |
30 | # %%
31 | print(worker.__dict__)
32 |
--------------------------------------------------------------------------------
/Ch02/challenge/ingest.py:
--------------------------------------------------------------------------------
1 | # %% Events
2 | class LoginEvent:
3 | def __init__(self, login):
4 | self.login = login
5 |
6 | def notify_loaded(self):
7 | print('LoginEvent loaded')
8 |
9 |
10 | class AccessEvent:
11 | def __init__(self, login, uri):
12 | self.login = login
13 | self.uri = uri
14 |
15 | def notify_loaded(self):
16 | print(f'AccessEvent loaded (uri={self.uri!r})')
17 |
18 |
19 | # Add an ability to load events from JSON data (str)
20 | # Use a Mixin class
21 | # Make sure that Events have notify_loaded method (ABC)
22 |
23 |
24 | # %% Test
25 | # LoginEvent
26 | login_data = '{"login": "elliot"}'
27 | # AccessEvent
28 | access_data = '''
29 | {
30 | "login": "elliot",
31 | "uri": "file:///etc/passwd"
32 | }
33 | '''
34 |
--------------------------------------------------------------------------------
/Ch02/02_03/logger.py:
--------------------------------------------------------------------------------
1 | # %% Logger
2 | import logging
3 |
4 |
5 | logging.basicConfig(
6 | level=logging.INFO,
7 | format='%(levelname)s: %(message)s',
8 | )
9 |
10 | class LoggerMixin:
11 | def log_id(self):
12 | logging.info('%s with id %r', self.name, self.id)
13 |
14 |
15 | # %% User
16 | class User:
17 | def __init__(self, name, id):
18 | self.name = name
19 | self.id = id
20 |
21 | # %% VM
22 | class VM:
23 | def __init__(self, name, id):
24 | self.name = name
25 | self.id = id
26 |
27 | # %% Mixing
28 | class LoggedUser(LoggerMixin, User):
29 | pass
30 |
31 | class LoggedVM(LoggerMixin, VM):
32 | pass
33 |
34 |
35 | # %% Test
36 | user = LoggedUser('root', 1)
37 | user.log_id()
38 |
39 | vm = LoggedVM('m1', '4922a77')
40 | vm.log_id()
41 |
--------------------------------------------------------------------------------
/Ch03/03_03/traces.py:
--------------------------------------------------------------------------------
1 | # %% Traces
2 | class TraceIDs(dict):
3 | def __init__(self, *args, **kw):
4 | super().__init__(*args, **kw)
5 | self._counter = 1
6 |
7 | def __missing__(self, key):
8 | val = self._counter
9 | self._counter += 1
10 | self[key] = val
11 | return val
12 |
13 |
14 | # %% Test
15 | trace_ids = TraceIDs()
16 | print('calls ID:', trace_ids['http.calls'])
17 | print('calls ID:', trace_ids['http.calls'])
18 | print('errors ID:', trace_ids['http.errors'])
19 |
20 |
21 | # %%
22 | from collections import defaultdict
23 | from itertools import count
24 |
25 | trace_ids = defaultdict(count(1).__next__)
26 | print('calls ID:', trace_ids['http.calls'])
27 | print('calls ID:', trace_ids['http.calls'])
28 | print('errors ID:', trace_ids['http.errors'])
29 |
--------------------------------------------------------------------------------
/Ch02/02_01/user.py:
--------------------------------------------------------------------------------
1 | # %% Auth
2 | class Auth:
3 | def __init__(self, db):
4 | self.db = db
5 |
6 | def from_token(self, token):
7 | return self.db.get(token)
8 |
9 | auth = Auth({
10 | 'b92d877': 'carly',
11 | '18317ac': 'elliot',
12 | })
13 |
14 |
15 | # %% User
16 | class User:
17 | def __init__(self, login):
18 | self.login = login
19 | # TODO: More fields
20 |
21 | @classmethod
22 | def from_token(cls, token):
23 | login = auth.from_token(token)
24 | return cls(login)
25 |
26 | # %% Admin
27 | class Admin(User):
28 | ... # TODO
29 |
30 |
31 | # %% Test
32 | u = User('carly')
33 | print(u.login, type(u))
34 |
35 | u = User.from_token('b92d877')
36 | print(u.login, type(u))
37 |
38 | a = Admin.from_token('18317ac')
39 | print(a.login, type(a))
40 |
--------------------------------------------------------------------------------
/Ch03/03_04/duration.py:
--------------------------------------------------------------------------------
1 | # %% Duration
2 |
3 | class Duration:
4 | unit_values = {
5 | 'ns': 1,
6 | 'us': 1000,
7 | 'ms': 1_000_000,
8 | }
9 |
10 | def __init__(self, value: float, unit: str):
11 | if value < 0 or unit not in Duration.unit_values:
12 | raise ValueError(f'invalid duration: {value=}, {unit=}')
13 |
14 | self.value = value
15 | self.unit = unit
16 |
17 | def __repr__(self):
18 | return f'{self.value}{self.unit}'
19 |
20 | def __add__(self, other):
21 | v1 = self.value * Duration.unit_values[self.unit]
22 | v2 = other.value * Duration.unit_values[other.unit]
23 | value = (v1 + v2) / Duration.unit_values[self.unit]
24 | return Duration(value, self.unit)
25 |
26 |
27 | # %% Test
28 | u1 = Duration(317, 'us')
29 | u2 = Duration(2.7, 'ms')
30 | print(u1 + u2)
31 |
--------------------------------------------------------------------------------
/Ch03/03_03/headers.py:
--------------------------------------------------------------------------------
1 | # %% Headers
2 | from collections.abc import Mapping
3 |
4 | class Headers(Mapping):
5 | def __init__(self, headers: dict):
6 | self._headers = {
7 | key.lower(): value
8 | for key, value in headers.items()
9 | }
10 |
11 | def __len__(self):
12 | return len(self._headers)
13 |
14 | def __getitem__(self, key):
15 | key = key.lower()
16 | return self._headers[key]
17 |
18 | def __iter__(self):
19 | return iter(self._headers)
20 |
21 |
22 | # %% Test
23 | headers = Headers({
24 | 'Content-Type': 'application/json; charset=utf-8',
25 | 'Content-Length': '1366',
26 | 'Accept-Ranges': 'bytes',
27 | })
28 | print(len(headers), 'headers')
29 | print('Content Type:', headers['content-type'])
30 | for key in headers:
31 | print('key:', key)
32 | for key, value in headers.items():
33 | print(key, '->', value)
34 |
--------------------------------------------------------------------------------
/Ch02/02_04/plugin.py:
--------------------------------------------------------------------------------
1 | # %% Plugins
2 |
3 | class LoggingPlugin:
4 | def notify(self, event):
5 | print(f'got {event}')
6 |
7 | def shutdown(self):
8 | print('logger shutting down')
9 |
10 |
11 | class SecurityPlugin:
12 | def notify(self, event):
13 | if event.action == 'login' and event.user == 'elliot':
14 | print(f'WARNING: {event.user} has logged in')
15 |
16 | def shutdwon(self):
17 | print('security shutting down')
18 |
19 |
20 | def notify(plugins, event):
21 | for plugin in plugins:
22 | plugin.notify(event)
23 |
24 |
25 | def shutdown(plugins):
26 | for plugin in plugins:
27 | plugin.shutdown()
28 |
29 |
30 |
31 | # %% Test
32 | class Event:
33 | def __init__(self, user, action):
34 | self.user = user
35 | self.action = action
36 |
37 | plugins = [LoggingPlugin(), SecurityPlugin()]
38 | event = Event('elliot', 'login')
39 | notify(plugins, event)
40 | shutdown(plugins)
41 |
--------------------------------------------------------------------------------
/Ch03/03_01/currency.py:
--------------------------------------------------------------------------------
1 | # %% Payment
2 | import re
3 |
4 |
5 | class Payment:
6 | def __init__(self, amount, currency):
7 | self.amount = amount
8 | self.currency = currency
9 |
10 | def __str__(self):
11 | return f'{self.currency}{self.amount:.2f}'
12 |
13 | def __repr__(self):
14 | name = self.__class__.__name__ # Support inheritance.
15 | return f'{name}({self.amount!r}, {self.currency!r})'
16 |
17 | def _replace(self, match):
18 | if match[1] == 'a':
19 | return f'{self.amount:.2f}'
20 | if match[1] == 'c':
21 | return self.currency
22 | raise ValueError(f'unknown format: {match.group()}')
23 |
24 | def __format__(self, spec):
25 | if not spec:
26 | return str(self)
27 | return re.sub(r'(? 10:
18 | raise ValueError(f'{name} has too many methods ({count})')
19 | print(f'[checker] {name} with {count} methods')
20 | return type.__init__(cls, name, bases, mapping)
21 |
22 |
23 | # %% Checker
24 | class Checker(metaclass=CheckerMeta):
25 | pass
26 |
27 | # %% Robot
28 | class Robot(Checker):
29 | manufacture = 'BnL'
30 |
31 | def move(self, x, y):
32 | print(f'{self} moving to {x}/{y}')
33 |
34 |
35 | walle = Robot()
36 | walle.move(100, 200)
37 |
--------------------------------------------------------------------------------
/Ch05/solution/vm.py:
--------------------------------------------------------------------------------
1 | """
2 | Use a dataclass, write a VM class that has the following fields:
3 | - id: str, no default
4 | - cpus: int, default to 2
5 | - memory: int, default to 512 (in MB)
6 | - state: one of 'starting', 'running', 'stopped'
7 | - tags: list of str, default to empty
8 |
9 | After the VM is created, check that
10 | - id is not empty
11 | - cpus >= 1
12 | - memory >= 256
13 | """
14 | # %% VM
15 | from dataclasses import dataclass, field
16 |
17 | @dataclass
18 | class VM:
19 | id: str
20 | cpus: int = 2
21 | memory: int = 512 # MB
22 | state: str = 'starting'
23 | tags: list[str] = field(default_factory=list)
24 |
25 | def __post_init__(self):
26 | if not self.id:
27 | raise ValueError('empty ID')
28 | if self.cpus < 1:
29 | raise ValueError(f'cpus ({self.cpus}) < 1')
30 | if self.memory < 256:
31 | raise ValueError(f'memory ({self.memory}) < 256')
32 |
33 | # %% test
34 | vm = VM(
35 | id='i-0af01c0123456789a',
36 | cpus=4,
37 | memory=2048,
38 | tags=['db', 'env:qa']
39 | )
40 | print(vm)
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
7 |
8 | ## Issue Overview
9 |
10 |
11 | ## Describe your environment
12 |
13 |
14 | ## Steps to Reproduce
15 |
16 | 1.
17 | 2.
18 | 3.
19 | 4.
20 |
21 | ## Expected Behavior
22 |
23 |
24 | ## Current Behavior
25 |
26 |
27 | ## Possible Solution
28 |
29 |
30 | ## Screenshots / Video
31 |
32 |
33 | ## Related Issues
34 |
35 |
--------------------------------------------------------------------------------
/Ch03/03_02/stack.py:
--------------------------------------------------------------------------------
1 | # %% Stack
2 | from collections.abc import Sequence
3 |
4 | class Node:
5 | def __init__(self, value, next):
6 | self.value = value
7 | self.next = next
8 |
9 |
10 | class Stack(Sequence):
11 | def __init__(self):
12 | self._head = None
13 |
14 | def push(self, value):
15 | self._head = Node(value, self._head)
16 |
17 | def pop(self):
18 | if self._head is None:
19 | raise ValueError('pop from empty stack')
20 |
21 | value = self._head.value
22 | self._head = self._head.next
23 | return value
24 |
25 | def __len__(self):
26 | count = 0
27 | node = self._head
28 | while node:
29 | count += 1
30 | node = node.next
31 | return count
32 |
33 | def __getitem__(self, index):
34 | node = self._head
35 | while index > 0 and node:
36 | index -= 1
37 | node = node.next
38 | if not node:
39 | raise IndexError(index)
40 | return node.value
41 |
42 |
43 |
44 | # %% Test
45 | s = Stack()
46 | for c in 'Python':
47 | s.push(c)
48 | print('len:', len(s))
49 | print('s[2]:', s[2])
50 | print('t' in s)
51 |
--------------------------------------------------------------------------------
/Ch02/02_04/plugin_abc.py:
--------------------------------------------------------------------------------
1 | # %% Plugin
2 | from abc import ABC, abstractmethod
3 |
4 |
5 | class Plugin(ABC):
6 | @abstractmethod
7 | def notify(self, event):
8 | pass
9 |
10 | @abstractmethod
11 | def shutdown(self):
12 | pass
13 |
14 | # %% Plugins
15 |
16 | class LoggingPlugin(Plugin):
17 | def notify(self, event):
18 | print(f'got {event}')
19 |
20 | def shutdown(self):
21 | print('logger shutting down')
22 |
23 |
24 | class SecurityPlugin(Plugin):
25 | def notify(self, event):
26 | if event.action == 'login' and event.user == 'elliot':
27 | print(f'WARNING: {event.user} has logged in')
28 |
29 | def shutdwon(self):
30 | print('security shutting down')
31 |
32 |
33 | def notify(plugins, event):
34 | for plugin in plugins:
35 | plugin.notify(event)
36 |
37 |
38 | def shutdown(plugins):
39 | for plugin in plugins:
40 | plugin.shutdown()
41 |
42 |
43 |
44 | # %% Test
45 | class Event:
46 | def __init__(self, user, action):
47 | self.user = user
48 | self.action = action
49 |
50 | plugins = [LoggingPlugin(), SecurityPlugin()]
51 | event = Event('elliot', 'login')
52 | notify(plugins, event)
53 | shutdown(plugins)
54 |
--------------------------------------------------------------------------------
/Ch01/01_01/attr.py:
--------------------------------------------------------------------------------
1 | # %% Emulate build-in "getattr"
2 | def find_attribute(obj, attr):
3 | if attr in obj.__dict__:
4 | print(f'found {attr} in instance')
5 | return obj.__dict__[attr]
6 |
7 | if attr in obj.__class__.__dict__:
8 | print(f'found {attr} in class')
9 | return obj.__class__.__dict__[attr]
10 |
11 | for cls in obj.__class__.__mro__:
12 | if attr not in cls.__dict__:
13 | continue
14 | print(f'found {attr} in parent {cls.__name__!r}')
15 | return cls.__dict__[attr]
16 |
17 | # TODO: __getattr__, descriptors ...
18 |
19 | raise AttributeError(attr)
20 |
21 |
22 | # %% VM
23 | class VM:
24 | version = '1.2.3' # Class attribute.
25 |
26 |
27 | class A1(VM):
28 | cpu_family = 'arm64'
29 |
30 | def __init__(self, id):
31 | self.id = id # Instance attribute.
32 | self.state = 'running'
33 |
34 | def shutdown(self):
35 | # TODO
36 | self.state = 'stopped'
37 |
38 |
39 | a1 = A1('9e99929')
40 |
41 | #%% a1.id
42 | print(find_attribute(a1, 'id'))
43 |
44 | #%% a1.cpu_family
45 | print(find_attribute(a1, 'cpu_family'))
46 |
47 | #%% a1.version
48 | print(find_attribute(a1, 'version'))
49 |
50 | #%% a1.nic
51 | print(find_attribute(a1, 'nic'))
--------------------------------------------------------------------------------
/Ch04/04_03/robot.py:
--------------------------------------------------------------------------------
1 | # %%
2 | from datetime import datetime
3 |
4 |
5 | class CheckerMeta(type):
6 | """
7 | If you have a procedure with 10 parameters, you probably missed some.
8 | - Alan J. Perlis
9 | """
10 | def __new__(mclass, name, bases, mapping):
11 | print(f'[checker] Creating class {name} with {bases}')
12 | mapping['created'] = datetime.now()
13 | return type.__new__(mclass, name, bases, mapping)
14 |
15 | def __init__(cls, name, bases, mapping):
16 | count = sum(1 for v in mapping.values() if callable(v))
17 | if count > 10:
18 | raise ValueError(f'{name} has too many methods ({count})')
19 | print(f'[checker] {name} with {count} methods')
20 | return type.__init__(cls, name, bases, mapping)
21 |
22 | def __call__(cls, *args, **kw):
23 | print(f'[checker] instance of {cls.__name__} created')
24 | if (count := len(args) + len(kw) > 10):
25 | name = cls.__name__
26 | raise ValueError(f'{name} instance with too many arguments ({count})')
27 | return type.__call__(cls, *args, **kw)
28 |
29 |
30 | class Checker(metaclass=CheckerMeta):
31 | pass
32 |
33 |
34 | class Robot(Checker):
35 | manufacture = 'BnL'
36 |
37 | def move(self, x, y):
38 | print(f'{self} moving to {x}/{y}')
39 |
40 |
41 | walle = Robot()
42 | walle.move(100, 200)
43 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Python 3",
3 | "build": {
4 | "dockerfile": "Dockerfile",
5 | "context": "..",
6 | "args": {
7 | "VARIANT": "3.11", // Set Python version here
8 | "NODE_VERSION": "lts/*"
9 | }
10 | },
11 | "settings": {
12 | "python.defaultInterpreterPath": "/usr/local/bin/python",
13 | "python.linting.enabled": true,
14 | "python.linting.pylintEnabled": false,
15 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
16 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
17 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
18 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
19 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
20 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
21 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
22 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
23 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
24 | "python.linting.pylintArgs": ["--disable=C0111"]
25 | },
26 | "extensions": [
27 | "ms-python.python",
28 | "ms-python.vscode-pylance",
29 | "ms-toolsai.jupyter"
30 | ],
31 | "remoteUser": "vscode",
32 | "onCreateCommand": "echo PS1='\"$ \"' >> ~/.bashrc", //Set Terminal Prompt to $
33 | "postCreateCommand": "sh .devcontainer/startup.sh"
34 | }
35 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3/.devcontainer/base.Dockerfile
2 |
3 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
4 | ARG VARIANT="3.11"
5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
6 |
7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
8 | ARG NODE_VERSION="none"
9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
10 |
11 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
12 | # COPY requirements.txt /tmp/pip-tmp/
13 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
14 | # && rm -rf /tmp/pip-tmp
15 |
16 | # [Optional] Uncomment this section to install additional OS packages.
17 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
18 | # && apt-get -y install --no-install-recommends
The best way to learn a language is to use it in practice. That’s why this course is integrated with GitHub Codespaces, an instant cloud developer environment that offers all the functionality of your favorite IDE without the need for any local machine setup. With GitHub Codespaces, you can get hands-on practice from any machine, at any time—all while using a tool that you’ll likely encounter in the workplace. Check out the “Using GitHub Codespaces with this course” video to learn how to get started.
9 |
10 | ### Instructor
11 |
12 | Miki Tebeka
13 |
14 | CEO at 353Solutions
15 |
16 |
17 | Check out my other courses on [LinkedIn Learning](https://www.linkedin.com/learning/instructors/miki-tebeka?u=104).
18 |
19 | [0]: # (Replace these placeholder URLs with actual course URLs)
20 |
21 | [lil-course-url]: https://www.linkedin.com/learning/advanced-python-object-oriented-programming
22 | [lil-thumbnail-url]: https://media.licdn.com/dms/image/D560DAQGj3Mv2DFnQJA/learning-public-crop_675_1200/0/1710976165627?e=2147483647&v=beta&t=dUWSvC-unURI4wEjOIUB7v8zet7idWRUteDo6druIY0
23 |
24 |
--------------------------------------------------------------------------------
/Ch03/challenge/kv.py:
--------------------------------------------------------------------------------
1 | # %%
2 | import sqlite3
3 | from collections.abc import MutableMapping
4 |
5 | # %% Create
6 | schema_sql = '''
7 | CREATE TABLE IF NOT EXISTS kv (
8 | key TEXT PRIMARY KEY,
9 | value TEXT
10 | );
11 | '''
12 |
13 | conn = sqlite3.connect(':memory:')
14 | conn.executescript(schema_sql)
15 |
16 | # %% Set
17 | set_sql = '''
18 | INSERT INTO kv
19 | (key, value)
20 | VALUES
21 | (:key, :value)
22 | ON CONFLICT (key) DO
23 | UPDATE SET key=:key
24 | ;
25 | '''
26 |
27 | entry = {'key': 'fish', 'value': 'water'}
28 | with conn:
29 | conn.execute(set_sql, entry)
30 |
31 | # %% Get
32 | get_sql = '''
33 | SELECT value
34 | FROM kv
35 | WHERE key = :key
36 | '''
37 |
38 | cur = conn.execute(get_sql, {'key': 'fish'})
39 | print(cur.fetchone())
40 |
41 | # %% Delete
42 | del_sql = '''
43 | DELETE FROM kv
44 | WHERE key = :key
45 | '''
46 |
47 | with conn:
48 | conn.execute(del_sql, {'key': 'fish'})
49 | cur = conn.execute(get_sql, {'key': 'fish'})
50 | print(cur.fetchone())
51 |
52 | # %% Keys
53 | keys_sql = '''
54 | SELECT key FROM kv
55 | '''
56 |
57 | with conn:
58 | entry = {'key': 'fish', 'value': 'water'}
59 | conn.execute(set_sql, entry)
60 | entry = {'key': 'horse', 'value': 'land'}
61 | conn.execute(set_sql, entry)
62 | for row in conn.execute(keys_sql):
63 | print('key:', row[0])
64 |
65 | # %% Len
66 | len_sql = '''
67 | SELECT COUNT(key) FROM kv
68 | '''
69 |
70 | cur = conn.execute(len_sql)
71 | row = cur.fetchone()
72 | print('len:', row[0])
73 |
74 | # %% DB
75 | class DB(MutableMapping):
76 | """sqlite3 backed mapping"""
77 | def __init__(self, db_file):
78 | # TODO
79 | ...
80 |
81 | # %% Test
82 | db = DB('/tmp/animals.db')
83 | db['fish'] = 'water'
84 | db['horse'] = 'land'
85 | print('