This commit is contained in:
adii1823 2021-10-28 17:38:47 +05:30
parent ef37ce0c4e
commit b6eb3ef8a7
32 changed files with 1063 additions and 0 deletions

View File

@ -0,0 +1,37 @@
# decorators/decorators.factory.py
from functools import wraps
def max_result(threshold):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if result > threshold:
print(
f'Result is too big ({result}). '
f'Max allowed is {threshold}.'
)
return result
return wrapper
return decorator
@max_result(75)
def cube(n):
return n ** 3
@max_result(100)
def square(n):
return n ** 2
@max_result(1000)
def multiply(a, b):
return a * b
print(cube(5))
"""
$ python decorators.factory.py
Result is too big (125). Max allowed is 75.
125
"""

40
ch06/decorators/syntax.py Normal file
View File

@ -0,0 +1,40 @@
# decorators/syntax.py
# This is not a valid Python module - Don't run it.
# ONE DECORATOR
def func(arg1, arg2, ...):
pass
func = decorator(func)
# is equivalent to the following:
@decorator
def func(arg1, arg2, ...):
pass
# TWO DECORATORS
def func(arg1, arg2, ...):
pass
func = deco1(deco2(func))
# is equivalent to the following:
@deco1
@deco2
def func(arg1, arg2, ...):
pass
# DECORATOR WITH ARGUMENTS
def func(arg1, arg2, ...):
pass
func = decoarg(arg_a, arg_b)(func)
# is equivalent to the following:
@decoarg(arg_a, arg_b)
def func(arg1, arg2, ...):
pass

View File

@ -0,0 +1,14 @@
# decorators/time.measure.arguments.py
from time import sleep, time
def f(sleep_time=0.1):
sleep(sleep_time)
def measure(func, *args, **kwargs):
t = time()
func(*args, **kwargs)
print(func.__name__, 'took:', time() - t)
measure(f, sleep_time=0.3) # f took: 0.30056095123291016
measure(f, 0.2) # f took: 0.2033553123474121

View File

@ -0,0 +1,18 @@
# decorators/time.measure.deco1.py
from time import sleep, time
def f(sleep_time=0.1):
sleep(sleep_time)
def measure(func):
def wrapper(*args, **kwargs):
t = time()
func(*args, **kwargs)
print(func.__name__, 'took:', time() - t)
return wrapper
f = measure(f) # decoration point
f(0.2) # f took: 0.20372915267944336
f(sleep_time=0.3) # f took: 0.30455899238586426
print(f.__name__) # wrapper <- ouch!

View File

@ -0,0 +1,22 @@
# decorators/time.measure.deco2.py
from time import sleep, time
from functools import wraps
def measure(func):
@wraps(func)
def wrapper(*args, **kwargs):
t = time()
func(*args, **kwargs)
print(func.__name__, 'took:', time() - t)
return wrapper
@measure
def f(sleep_time=0.1):
"""I'm a cat. I love to sleep! """
sleep(sleep_time)
f(sleep_time=0.3) # f took: 0.3010902404785156
print(f.__name__, ':', f.__doc__) # f : I'm a cat. I love to sleep!

View File

@ -0,0 +1,17 @@
# decorators/time.measure.dry.py
from time import sleep, time
def f():
sleep(.3)
def g():
sleep(.5)
def measure(func):
t = time()
func()
print(func.__name__, 'took:', time() - t)
measure(f) # f took: 0.30434322357177734
measure(g) # g took: 0.5048270225524902

View File

@ -0,0 +1,18 @@
# decorators/time.measure.start.py
from time import sleep, time
def f():
sleep(.3)
def g():
sleep(.5)
t = time()
f()
print('f took:', time() - t) # f took: 0.3001396656036377
t = time()
g()
print('g took:', time() - t) # g took: 0.5039339065551758

View File

@ -0,0 +1,42 @@
# decorators/two.decorators.py
from time import time
from functools import wraps
def measure(func):
@wraps(func)
def wrapper(*args, **kwargs):
t = time()
result = func(*args, **kwargs)
print(func.__name__, 'took:', time() - t)
return result
return wrapper
def max_result(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if result > 100:
print(
f'Result is too big ({result}). '
'Max allowed is 100.'
)
return result
return wrapper
@measure
@max_result
def cube(n):
return n ** 3
print(cube(2))
print(cube(5))
"""
$ python two.decorators.py
cube took: 3.0994415283203125e-06
8
Result is too big (125). Max allowed is 100.
cube took: 5.9604644775390625e-06
125
"""

View File

@ -0,0 +1,53 @@
# iterators/iterator.py
class OddEven:
def __init__(self, data):
self._data = data
self.indexes = (list(range(0, len(data), 2)) +
list(range(1, len(data), 2)))
def __iter__(self):
return self
def __next__(self):
if self.indexes:
return self._data[self.indexes.pop(0)]
raise StopIteration
oddeven = OddEven('ThIsIsCoOl!')
print(''.join(c for c in oddeven)) # TIICO!hssol
oddeven = OddEven('CiAo') # or manually...
it = iter(oddeven) # this calls oddeven.__iter__ internally
print(next(it)) # C
print(next(it)) # A
print(next(it)) # i
print(next(it)) # o
# make sure it works correctly with edge cases
oddeven = OddEven('')
print(' '.join(c for c in oddeven))
oddeven = OddEven('A')
print(' '.join(c for c in oddeven))
oddeven = OddEven('Ab')
print(' '.join(c for c in oddeven))
oddeven = OddEven('AbC')
print(' '.join(c for c in oddeven))
"""
$ python iterators/iterator.py
TIICO!hssol
C
A
i
o
A
A b
A C b
"""

View File

@ -0,0 +1,57 @@
# oop/cached.property.py
from functools import cached_property
class Client:
def __init__(self):
print("Setting up the client...")
def query(self, **kwargs):
print(f"Performing a query: {kwargs}")
class Manager:
@property
def client(self):
return Client()
def perform_query(self, **kwargs):
return self.client.query(**kwargs)
class ManualCacheManager:
@property
def client(self):
if not hasattr(self, '_client'):
self._client = Client()
return self._client
def perform_query(self, **kwargs):
return self.client.query(**kwargs)
class CachedPropertyManager:
@cached_property
def client(self):
return Client()
def perform_query(self, **kwargs):
return self.client.query(**kwargs)
manager = CachedPropertyManager()
manager.perform_query(object_id=42)
manager.perform_query(name_ilike='%Python%')
del manager.client # This causes a new Client on next call
manager.perform_query(age_gte=18)
"""
$ python cached.property.py
Setting up the client... # New Client
Performing a query: {'object_id': 42} # first query
Performing a query: {'name_ilike': '%Python%'} # second query
Setting up the client... # Another Client
Performing a query: {'age_gte': 18} # Third query
"""

View File

@ -0,0 +1,36 @@
# oop/class.attribute.shadowing.py
class Point:
x = 10
y = 7
p = Point()
print(p.x) # 10 (from class attribute)
print(p.y) # 7 (from class attribute)
p.x = 12 # p gets its own `x` attribute
print(p.x) # 12 (now found on the instance)
print(Point.x) # 10 (class attribute still the same)
del p.x # we delete instance attribute
print(p.x) # 10 (now search has to go again to find class attr)
p.z = 3 # let's make it a 3D point
print(p.z) # 3
print(Point.z)
# AttributeError: type object 'Point' has no attribute 'z'
"""
$ python class.attribute.shadowing.py
10
7
12
10
10
3
Traceback (most recent call last):
File "/Users/fab/srv/lpp3e/v3/ch06/oop/class.attribute.shadowing.py", line 20, in <module>
print(Point.z)
AttributeError: type object 'Point' has no attribute 'z'
"""

23
ch06/oop/class.init.py Normal file
View File

@ -0,0 +1,23 @@
# oop/class.init.py
class Rectangle:
def __init__(self, side_a, side_b):
self.side_a = side_a
self.side_b = side_b
def area(self):
return self.side_a * self.side_b
r1 = Rectangle(10, 4)
print(r1.side_a, r1.side_b) # 10 4
print(r1.area()) # 40
r2 = Rectangle(7, 3)
print(r2.area()) # 21
"""
$ python class.init.py
10 4
40
21
"""

View File

@ -0,0 +1,52 @@
# oop/class.issubclass.isinstance.py
from class_inheritance import Car, RaceCar, F1Car
car = Car()
racecar = RaceCar()
f1car = F1Car()
cars = [(car, 'car'), (racecar, 'racecar'), (f1car, 'f1car')]
car_classes = [Car, RaceCar, F1Car]
for car, car_name in cars:
for class_ in car_classes:
belongs = isinstance(car, class_)
msg = 'is a' if belongs else 'is not a'
print(car_name, msg, class_.__name__)
""" Prints:
Starting engine Engine for car Car... Wroom, wroom!
Starting engine V8Engine for car RaceCar... Wroom, wroom!
Starting engine ElectricEngine for car CityCar... Wroom, wroom!
Starting engine V8Engine for car F1Car... Wroom, wroom!
car is a Car
car is not a RaceCar
car is not a F1Car
racecar is a Car
racecar is a RaceCar
racecar is not a F1Car
f1car is a Car
f1car is a RaceCar
f1car is a F1Car
"""
print('-' * 60)
for class1 in car_classes:
for class2 in car_classes:
is_subclass = issubclass(class1, class2)
msg = '{0} a subclass of'.format(
'is' if is_subclass else 'is not')
print(class1.__name__, msg, class2.__name__)
""" Prints:
Car is a subclass of Car
Car is not a subclass of RaceCar
Car is not a subclass of F1Car
RaceCar is a subclass of Car
RaceCar is a subclass of RaceCar
RaceCar is not a subclass of F1Car
F1Car is a subclass of Car
F1Car is a subclass of RaceCar
F1Car is a subclass of F1Car
"""

View File

@ -0,0 +1,27 @@
# oop/class.methods.factory.py
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@classmethod
def from_tuple(cls, coords): # cls is Point
return cls(*coords)
@classmethod
def from_point(cls, point): # cls is Point
return cls(point.x, point.y)
p = Point.from_tuple((3, 7))
print(p.x, p.y) # 3 7
q = Point.from_point(p)
print(q.x, q.y) # 3 7
"""
$ python class.methods.factory.py
3 7
3 7
"""

View File

@ -0,0 +1,35 @@
# oop/class.methods.split.py
class StringUtil:
@classmethod
def is_palindrome(cls, s, case_insensitive=True):
s = cls._strip_string(s)
# For case insensitive comparison, we lower-case s
if case_insensitive:
s = s.lower()
return cls._is_palindrome(s)
@staticmethod
def _strip_string(s):
return ''.join(c for c in s if c.isalnum())
@staticmethod
def _is_palindrome(s):
for c in range(len(s) // 2):
if s[c] != s[-c -1]:
return False
return True
@staticmethod
def get_unique_words(sentence):
return set(sentence.split())
print(StringUtil.is_palindrome('A nut for a jar of tuna')) # True
print(StringUtil.is_palindrome('A nut for a jar of beans')) # False
"""
$ python class.methods.split.py
True
False
"""

View File

@ -0,0 +1,40 @@
# oop/class.namespaces.py
class Person:
species = 'Human'
print(Person.species) # Human
Person.alive = True # Added dynamically!
print(Person.alive) # True
man = Person()
print(man.species) # Human (inherited)
print(man.alive) # True (inherited)
Person.alive = False
print(man.alive) # False (inherited)
man.name = 'Darth'
man.surname = 'Vader'
print(man.name, man.surname) # Darth Vader
print(Person.name)
# This doesn't work. We try to access an instance attribute
# from a class. Doing the opposite works, but this will give
# the following error:
# AttributeError: type object 'Person' has no attribute 'name'
"""
$ python class.namespaces.py
Human
True
Human
True
False
Darth Vader
Traceback (most recent call last):
File "/Users/fab/srv/lpp3e/v3/ch06/oop/class.namespaces.py", line 21, in <module>
print(Person.name)
AttributeError: type object 'Person' has no attribute 'name'
"""

17
ch06/oop/class.price.py Normal file
View File

@ -0,0 +1,17 @@
# oop/class.price.py
class Price:
def final_price(self, vat, discount=0):
"""Returns price after applying vat and fixed discount."""
return (self.net_price * (100 + vat) / 100) - discount
p1 = Price()
p1.net_price = 100
print(Price.final_price(p1, 20, 10)) # 110 (100 * 1.2 - 10)
print(p1.final_price(20, 10)) # equivalent
"""
$ python class.price.py
110.0
110.0
"""

21
ch06/oop/class.self.py Normal file
View File

@ -0,0 +1,21 @@
# oop/class.self.py
class Square:
side = 8
def area(self): # self is a reference to an instance
return self.side ** 2
sq = Square()
print(sq.area()) # 64 (side is found on the class)
print(Square.area(sq)) # 64 (equivalent to sq.area())
sq.side = 10
print(sq.area()) # 100 (side is found on the instance)
"""
$ python class.self.py
64
64
100
"""

View File

@ -0,0 +1,57 @@
# oop/class_inheritance.py
class Engine:
def start(self):
pass
def stop(self):
pass
class ElectricEngine(Engine): # Is-A Engine
pass
class V8Engine(Engine): # Is-A Engine
pass
class Car:
engine_cls = Engine
def __init__(self):
self.engine = self.engine_cls() # Has-A Engine
def start(self):
print(
'Starting engine {0} for car {1}... Wroom, wroom!'
.format(
self.engine.__class__.__name__,
self.__class__.__name__)
)
self.engine.start()
def stop(self):
self.engine.stop()
class RaceCar(Car): # Is-A Car
engine_cls = V8Engine
class CityCar(Car): # Is-A Car
engine_cls = ElectricEngine
class F1Car(RaceCar): # Is-A RaceCar and also Is-A Car
pass # engine_cls same as parent
car = Car()
racecar = RaceCar()
citycar = CityCar()
f1car = F1Car()
cars = [car, racecar, citycar, f1car]
for car in cars:
car.start()
"""
$ python class_inheritance.py
Starting engine Engine for car Car... Wroom, wroom!
Starting engine V8Engine for car RaceCar... Wroom, wroom!
Starting engine ElectricEngine for car CityCar... Wroom, wroom!
Starting engine V8Engine for car F1Car... Wroom, wroom!
"""

25
ch06/oop/dataclass.py Normal file
View File

@ -0,0 +1,25 @@
# oop/dataclass.py
from dataclasses import dataclass
@dataclass
class Body:
'''Class to represent a physical body.'''
name: str
mass: float = 0. # Kg
speed: float = 1. # m/s
def kinetic_energy(self) -> float:
return (self.mass * self.speed ** 2) / 2
body = Body('Ball', 19, 3.1415)
print(body.kinetic_energy()) # 93.755711375 Joule
print(body) # Body(name='Ball', mass=19, speed=3.1415)
"""
$ python dataclass.py
93.755711375
Body(name='Ball', mass=19, speed=3.1415)
"""

31
ch06/oop/mro.py Normal file
View File

@ -0,0 +1,31 @@
# oop/mro.py
class A:
label = 'a'
class B(A):
pass # was: label = 'b'
class C(A):
label = 'c'
class D(B, C):
pass
d = D()
print(d.label) # 'c'
print(d.__class__.mro()) # notice another way to get the MRO
# prints:
# [<class '__main__.D'>, <class '__main__.B'>,
# <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
"""
$ python oop/mro.py
c
[
<class '__main__.D'>, <class '__main__.B'>,
<class '__main__.C'>, <class '__main__.A'>,
<class 'object'>
]
"""

30
ch06/oop/mro.simple.py Normal file
View File

@ -0,0 +1,30 @@
# oop/mro.simple.py
class A:
label = 'a'
class B(A):
label = 'b'
class C(A):
label = 'c'
class D(B, C):
pass
d = D()
print(d.label) # Hypothetically this could be either 'b' or 'c'
print(d.__class__.mro()) # notice another way to get the MRO
# prints:
# [<class '__main__.D'>, <class '__main__.B'>,
# <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
"""
$ python mro.simple.py
b
[
<class '__main__.D'>, <class '__main__.B'>,
<class '__main__.C'>, <class '__main__.A'>,
<class 'object'>
]
"""

View File

@ -0,0 +1,74 @@
# oop/multiple.inheritance.py
class Shape:
geometric_type = 'Generic Shape'
def area(self): # This acts as placeholder for the interface
raise NotImplementedError
def get_geometric_type(self):
return self.geometric_type
class Plotter:
def plot(self, ratio, topleft):
# Imagine some nice plotting logic here...
print('Plotting at {}, ratio {}.'.format(
topleft, ratio))
class Polygon(Shape, Plotter): # base class for polygons
geometric_type = 'Polygon'
class RegularPolygon(Polygon): # Is-A Polygon
geometric_type = 'Regular Polygon'
def __init__(self, side):
self.side = side
class RegularHexagon(RegularPolygon): # Is-A RegularPolygon
geometric_type = 'RegularHexagon'
def area(self):
return 1.5 * (3 ** .5 * self.side ** 2)
class Square(RegularPolygon): # Is-A RegularPolygon
geometric_type = 'Square'
def area(self):
return self.side * self.side
hexagon = RegularHexagon(10)
print(hexagon.area()) # 259.8076211353316
print(hexagon.get_geometric_type()) # RegularHexagon
hexagon.plot(0.8, (75, 77)) # Plotting at (75, 77), ratio 0.8.
square = Square(12)
print(square.area()) # 144
print(square.get_geometric_type()) # Square
square.plot(0.93, (74, 75)) # Plotting at (74, 75), ratio 0.93.
print(square.__class__.__mro__)
# prints:
# (<class '__main__.Square'>, <class '__main__.RegularPolygon'>,
# <class '__main__.Polygon'>, <class '__main__.Shape'>,
# <class '__main__.Plotter'>, <class 'object'>)
"""
$ python multiple.inheritance.py
259.8076211353316
RegularHexagon
Plotting at (75, 77), ratio 0.8.
144
Square
Plotting at (74, 75), ratio 0.93.
(
<class '__main__.Square'>, <class '__main__.RegularPolygon'>,
<class '__main__.Polygon'>, <class '__main__.Shape'>,
<class '__main__.Plotter'>, <class 'object'>
)
"""

View File

@ -0,0 +1,28 @@
# oop/operator.overloading.py
class Weird:
def __init__(self, s):
self._s = s
def __len__(self):
return len(self._s)
def __bool__(self):
return '42' in self._s
weird = Weird('Hello! I am 9 years old!')
print(len(weird)) # 24
print(bool(weird)) # False
weird2 = Weird('Hello! I am 42 years old!')
print(len(weird2)) # 25
print(bool(weird2)) # True
"""
$ python operator.overloading.py
24
False
25
True
"""

View File

@ -0,0 +1,30 @@
# oop/private.attrs.fixed.py
class A:
def __init__(self, factor):
self.__factor = factor
def op1(self):
print('Op1 with factor {}...'.format(self.__factor))
class B(A):
def op2(self, factor):
self.__factor = factor
print('Op2 with factor {}...'.format(self.__factor))
obj = B(100)
obj.op1() # Op1 with factor 100...
obj.op2(42) # Op2 with factor 42...
obj.op1() # Op1 with factor 100... <- Wohoo! Now it's GOOD!
print(obj.__dict__.keys())
# dict_keys(['_A__factor', '_B__factor'])
"""
$ python private.attrs.fixed.py
Op1 with factor 100...
Op2 with factor 42...
Op1 with factor 100...
dict_keys(['_A__factor', '_B__factor'])
"""

30
ch06/oop/private.attrs.py Normal file
View File

@ -0,0 +1,30 @@
# oop/private.attrs.py
class A:
def __init__(self, factor):
self._factor = factor
def op1(self):
print('Op1 with factor {}...'.format(self._factor))
class B(A):
def op2(self, factor):
self._factor = factor
print('Op2 with factor {}...'.format(self._factor))
obj = B(100)
obj.op1() # Op1 with factor 100...
obj.op2(42) # Op2 with factor 42...
obj.op1() # Op1 with factor 42... <- This is BAD
print(obj.__dict__.keys())
# dict_keys(['_factor'])
"""
$ python private.attrs.py
Op1 with factor 100...
Op2 with factor 42...
Op1 with factor 42...
dict_keys(['_factor'])
"""

51
ch06/oop/property.py Normal file
View File

@ -0,0 +1,51 @@
# oop/property.py
class Person:
def __init__(self, age):
self.age = age # anyone can modify this freely
class PersonWithAccessors:
def __init__(self, age):
self._age = age
def get_age(self):
return self._age
def set_age(self, age):
if 18 <= age <= 99:
self._age = age
else:
raise ValueError('Age must be within [18, 99]')
class PersonPythonic:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, age):
if 18 <= age <= 99:
self._age = age
else:
raise ValueError('Age must be within [18, 99]')
person = PersonPythonic(39)
print(person.age) # 39 - Notice we access as data attribute
person.age = 42 # Notice we access as data attribute
print(person.age) # 42
person.age = 100 # ValueError: Age must be within [18, 99]
"""
$ python property.py
39
42
Traceback (most recent call last):
File "/Users/fab/srv/lpp3e/v3/ch06/oop/property.py", line 38, in <module>
person.age = 100 # ValueError: Age must be within [18, 99]
File "/Users/fab/srv/lpp3e/v3/ch06/oop/property.py", line 32, in age
raise ValueError('Age must be within [18, 99]')
ValueError: Age must be within [18, 99]
"""

View File

@ -0,0 +1,18 @@
# oop/simplest.class.py
class Simplest(): # when empty, the braces are optional
pass
print(type(Simplest)) # what type is this object?
simp = Simplest() # we create an instance of Simplest: simp
print(type(simp)) # what type is simp?
# is simp an instance of Simplest?
print(type(simp) is Simplest) # There's a better way to do this
"""
$ python simplest.class.py
<class 'type'>
<class '__main__.Simplest'>
True
"""

View File

@ -0,0 +1,40 @@
# oop/static.methods.py
class StringUtil:
@staticmethod
def is_palindrome(s, case_insensitive=True):
# we allow only letters and numbers
s = ''.join(c for c in s if c.isalnum()) # Study this!
# For case insensitive comparison, we lower-case s
if case_insensitive:
s = s.lower()
for c in range(len(s) // 2):
if s[c] != s[-c -1]:
return False
return True
@staticmethod
def get_unique_words(sentence):
return set(sentence.split())
print(StringUtil.is_palindrome(
'Radar', case_insensitive=False)) # False: Case Sensitive
print(StringUtil.is_palindrome('A nut for a jar of tuna')) # True
print(StringUtil.is_palindrome('Never Odd, Or Even!')) # True
print(StringUtil.is_palindrome(
'In Girum Imus Nocte Et Consumimur Igni') # Latin! Show-off!
) # True
print(StringUtil.get_unique_words(
'I love palindromes. I really really love them!'))
# {'them!', 'palindromes.', 'I', 'really', 'love'}
"""
$ python static.methods.py
False
True
True
True
{'them!', 'palindromes.', 'I', 'really', 'love'}
"""

View File

@ -0,0 +1,16 @@
# oop/super.duplication.py
class Book:
def __init__(self, title, publisher, pages):
self.title = title
self.publisher = publisher
self.pages = pages
class Ebook(Book):
def __init__(self, title, publisher, pages, format_):
self.title = title
self.publisher = publisher
self.pages = pages
self.format_ = format_

View File

@ -0,0 +1,31 @@
# oop/super.explicit.py
class Book:
def __init__(self, title, publisher, pages):
self.title = title
self.publisher = publisher
self.pages = pages
class Ebook(Book):
def __init__(self, title, publisher, pages, format_):
Book.__init__(self, title, publisher, pages)
self.format_ = format_
ebook = Ebook(
'Learn Python Programming', 'Packt Publishing', 500, 'PDF')
print(ebook.title) # Learn Python Programming
print(ebook.publisher) # Packt Publishing
print(ebook.pages) # 500
print(ebook.format_) # PDF
"""
$ python super.explicit.py
Learn Python Programming
Packt Publishing
500
PDF
"""

View File

@ -0,0 +1,33 @@
# oop/super.implicit.py
class Book:
def __init__(self, title, publisher, pages):
self.title = title
self.publisher = publisher
self.pages = pages
class Ebook(Book):
def __init__(self, title, publisher, pages, format_):
super().__init__(title, publisher, pages)
# Another way to do the same thing is:
# super(Ebook, self).__init__(title, publisher, pages)
self.format_ = format_
ebook = Ebook(
'Learn Python Programming', 'Packt Publishing', 500, 'PDF')
print(ebook.title) # Learn Python Programming
print(ebook.publisher) # Packt Publishing
print(ebook.pages) # 500
print(ebook.format_) # PDF
"""
$ python super.implicit.py
Learn Python Programming
Packt Publishing
500
PDF
"""