9.3. Factory Method¶
EN: Factory Method
PL: Metoda wytwórcza
Type: class
9.3.1. Use Cases¶
9.3.2. Design¶
9.3.3. Implementation¶
class Setosa:
pass
class Versicolor:
pass
class Virginica:
pass
def iris_factory(species):
if species == 'setosa':
return Setosa
elif species == 'versicolor':
return Versicolor
elif species == 'virginica':
return Virginica
else:
raise NotImplementedError
if __name__ == '__main__':
iris = iris_factory('setosa')
print(iris)
# <class '__main__.Setosa'>
iris = iris_factory('virginica')
print(iris)
# <class '__main__.Virginica'>
iris = iris_factory('arctica')
print(iris)
# Traceback (most recent call last):
# NotImplementedError
class Setosa:
pass
class Versicolor:
pass
class Virginica:
pass
def iris_factory(species):
cls = {
'setosa': Setosa,
'versicolor': Versicolor,
'virginica': Virginica,
}.get(species, None)
if not cls:
raise NotImplementedError
else:
return cls
if __name__ == '__main__':
iris = iris_factory('setosa')
print(iris)
# <class '__main__.Setosa'>
iris = iris_factory('virginica')
print(iris)
# <class '__main__.Virginica'>
iris = iris_factory('arctica')
print(iris)
# Traceback (most recent call last):
# NotImplementedError
class Setosa:
pass
class Virginica:
pass
class Versicolor:
pass
def iris_factory(species):
try:
classname = species.capitalize()
return globals()[classname]
except KeyError:
raise NotImplementedError
if __name__ == '__main__':
iris = iris_factory('setosa')
print(iris)
# <class '__main__.Setosa'>
iris = iris_factory('virginica')
print(iris)
# <class '__main__.Virginica'>
iris = iris_factory('arctica')
print(iris)
# Traceback (most recent call last):
# NotImplementedError
class PDF:
pass
class TXT:
pass
class File:
def __new__(cls, *args, **kwargs):
filename, extension = args[0].split('.')
if extension == 'pdf':
return PDF()
elif extension == 'txt':
return TXT()
if __name__ == '__main__':
file = File('myfile.pdf')
print(file)
# <__main__.PDF object at 0x...>
file = File('myfile.txt')
print(file)
# <__main__.TXT object at 0x...>
from abc import ABCMeta, abstractproperty
class Document(metaclass=ABCMeta):
@abstractproperty
@property
def _extension(self):
return
def __new__(cls, filename, *args, **kwargs):
name, extension = filename.split('.')
for cls in Document.__subclasses__():
if cls._extension == extension:
return super().__new__(cls)
else:
raise NotImplementedError('File format unknown')
class PDF(Document):
_extension = 'pdf'
class Txt(Document):
_extension = 'txt'
class Word(Document):
_extension = 'docx'
if __name__ == '__main__':
file = Document('myfile.txt')
print(type(file))
# <class '__main__.Txt'>
file = Document('myfile.pdf')
print(type(file))
# <class '__main__.PDF'>
from abc import ABCMeta, abstractproperty, abstractmethod
from dataclasses import dataclass
@dataclass
class ConfigParser(metaclass=ABCMeta):
_filename: str
@abstractproperty
@property
def _extension(self):
pass
def show(self):
content = self.__read()
return self._parse(content)
@abstractmethod
def _parse(self, content: str) -> dict:
return NotImplementedError
def __read(self):
with open(self._filename) as file:
return file.read()
def __new__(cls, filename, *args, **kwargs):
_, extension = filename.split('.')
for parser in cls.__subclasses__():
if parser._extension == extension:
instance = super().__new__(parser)
instance.__init__(filename)
return instance
else:
raise NotImplementedError('Parser for given file type not found')
class ConfigParserINI(ConfigParser):
_extension = 'ini'
def _parse(self, content: str) -> dict:
print('Parsing INI file')
class ConfigParserCSV(ConfigParser):
_extension = 'csv'
def _parse(self, content: str) -> dict:
print('Parsing CSV file')
class ConfigParserYAML(ConfigParser):
_extension = 'yaml'
def _parse(self, content: str) -> dict:
print('Parsing YAML file')
class ConfigFileJSON(ConfigParser):
_extension = 'json'
def _parse(self, content: str) -> dict:
print('Parsing JSON file')
class ConfigFileXML(ConfigParser):
_extension = 'xml'
def _parse(self, content: str) -> dict:
print('Parsing XML file')
if __name__ == '__main__':
# iris.csv or *.csv, *.json *.yaml...
# filename = input('Type filename: ')
config = ConfigParser('/tmp/myfile.json')
config.show()
import os
class HttpClientInterface:
def GET(self):
raise NotImplementedError
def POST(self):
raise NotImplementedError
class GatewayLive(HttpClientInterface):
def GET(self):
print('Execute GET request over network')
return ...
def POST(self):
print('Execute POST request over network')
return ...
class GatewayStub(HttpClientInterface):
def GET(self):
print('Returning stub GET')
return {'firstname': 'Mark', 'lastname': 'Watney'}
def POST(self):
print('Returning stub POST')
return {'status': 200, 'reason': 'OK'}
class HttpGatewayFactory:
def __new__(cls, *args, **kwargs):
if os.getenv('ENVIRONMENT') == 'production':
return GatewayLive()
else:
return GatewayStub()
if __name__ == '__main__':
os.environ['ENVIRONMENT'] = 'testing'
client = HttpGatewayFactory()
result = client.GET()
# Returning stub GET
result = client.POST()
# Returning stub POST
os.environ['ENVIRONMENT'] = 'production'
client = HttpGatewayFactory()
result = client.GET()
# Execute GET request over network
result = client.POST()
# Execute POST request over network
from abc import ABCMeta, abstractmethod
class Path(metaclass=ABCMeta):
def __new__(cls, path, *args, **kwargs):
if path.startswith(r'C:\Users'):
instance = object.__new__(WindowsPath)
if path.startswith('/home'):
return object.__new__(LinuxPath)
if path.startswith('/Users'):
return object.__new__(macOSPath)
instance.__init__(path)
return instance
def __init__(self, filename):
self.filename = filename
@abstractmethod
def dir_create(self): pass
@abstractmethod
def dir_list(self): pass
@abstractmethod
def dir_remove(self): pass
class WindowsPath(Path):
def dir_create(self):
print('create directory on ')
def dir_list(self):
print('list directory on ')
def dir_remove(self):
print('remove directory on ')
class LinuxPath(Path):
def dir_create(self):
print('create directory on ')
def dir_list(self):
print('list directory on ')
def dir_remove(self):
print('remove directory on ')
class macOSPath(Path):
def dir_create(self):
print('create directory on ')
def dir_list(self):
print('list directory on ')
def dir_remove(self):
print('remove directory on ')
if __name__ == '__main__':
file = Path(r'C:\Users\MWatney\myfile.txt')
print(type(file))
# <class '__main__.WindowsPath'>
file = Path(r'/home/mwatney/myfile.txt')
print(type(file))
# <class '__main__.LinuxPath'>
file = Path(r'/Users/mwatney/myfile.txt')
print(type(file))
# <class '__main__.macOSPath'>
9.3.4. Assignments¶
"""
* Assignment: DesignPatterns Creational FactoryMethod
* Complexity: medium
* Lines of code: 8 lines
* Time: 8 min
English:
1. Create `result: list[Iris]`
2. Iterate over `DATA` skipping header
3. Separate `features` from `species` in each row
4. Append to `result`:
a. if `species` is "setosa" append instance of a class `Setosa`
b. if `species` is "versicolor" append instance of a class `Versicolor`
c. if `species` is "virginica" append instance of a class `Virginica`
5. Initialize instances with `features` using `*features` notation
6. Run doctests - all must succeed
Polish:
1. Stwórz `result: list[Iris]`
2. Iterując po `DATA` pomiń nagłówek
3. Odseparuj `features` od `species` w każdym wierszu
4. Dodaj do `result`:
a. jeżeli `species` to "setosa" to dodaj instancję klasy `Setosa`
b. jeżeli `species` to "versicolor" to dodaj instancję klasy `Versicolor`
c. jeżeli `species` to "virginica" to dodaj instancję klasy `Virginica`
5. Instancje inicjalizuj danymi z `features` używając notacji `*features`
6. Uruchom doctesty - wszystkie muszą się powieść
Hints:
* `globals()[classname]`
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> result # doctest: +NORMALIZE_WHITESPACE
[Virginica(5.8, 2.7, 5.1, 1.9),
Setosa(5.1, 3.5, 1.4, 0.2),
Versicolor(5.7, 2.8, 4.1, 1.3),
Virginica(6.3, 2.9, 5.6, 1.8),
Versicolor(6.4, 3.2, 4.5, 1.5),
Setosa(4.7, 3.2, 1.3, 0.2)]
"""
from dataclasses import dataclass
DATA = [
('Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'),
(5.8, 2.7, 5.1, 1.9, 'virginica'),
(5.1, 3.5, 1.4, 0.2, 'setosa'),
(5.7, 2.8, 4.1, 1.3, 'versicolor'),
(6.3, 2.9, 5.6, 1.8, 'virginica'),
(6.4, 3.2, 4.5, 1.5, 'versicolor'),
(4.7, 3.2, 1.3, 0.2, 'setosa')]
@dataclass(repr=False)
class Iris:
_sepal_length: float
_sepal_width: float
_petal_length: float
_petal_width: float
def __repr__(self):
name = self.__class__.__name__
args = tuple(self.__dict__.values())
return f'{name}{args}'
class Setosa(Iris):
pass
class Versicolor(Iris):
pass
class Virginica(Iris):
pass
result: list