def original():
pass
original = modifier(original)
@modifier
def original():
pass
from functools import wraps
class ControlledException(Exception):
pass
def retry(operation):
@wraps(operation)
def wrapped(*args, **kwargs):
last_raised = None
RETRY_LIMIT = 3
for _ in RETRY_LIMIT:
try:
return operation(*args, **kwargs)
except ControlledException as e:
logger.info("retrying %s", operation.__qualname__)
last_raised = e
raise last_raised
return wrapped
@retry
def run_operation(task):
return task.run()
operation = retry(operation)
의 syntax sugar다.클래스 데코레이터는 인자로 클래스를 받는다.
클래스 데코레이터가 복잡하고 가독성이 떨어진다는 얘기도 많다.
우선은 클래스 데코레이터의 장점먼저 알아보자
아래 첫번쨰 예시 방법은 잘 동작하지만, 확장성에 문제가 있다.
import unittest
from datetime import datetime
class LoginEventSerializer:
def __init__(self, event):
self.event = event
def serialize(self) -> dict:
return {
"username": self.event.username,
"password": "**redacted**",
"ip": self.event.ip,
"timestamp": self.event.timestamp.strftime("%Y-%m-%d %H:%M"),
}
class LoginEvent:
SERIALIZER = LoginEventSerializer
def __init__(self, username, password, ip, timestamp):
self.username = username
self.password = password
self.ip = ip
self.timestamp = timestamp
def serialize(self) -> dict:
return self.SERIALIZER(self).serialize()
class TestLoginEventSerialized(unittest.TestCase):
def test_serializetion(self):
event = LoginEvent(
"username", "password", "127.0.0.1", datetime(2016, 7, 20, 15, 45)
)
expected = {
"username": "username",
"password": "**redacted**",
"ip": "127.0.0.1",
"timestamp": "2016-07-20 15:45",
}
self.assertEqual(event.serialize(), expected)
if __name__ == "__main__":
unittest.main()
import unittest
from datetime import datetime
def hide_field(field) -> str:
return "**redacted**"
def format_time(field_timestamp: datetime) -> str:
return field_timestamp.strftime("%Y-%m-%d %H:%M")
def show_original(event_field):
return event_field
class EventSerializer:
"""Apply the transformations to an Event object based on its properties and
the definition of the function to apply to each field.
"""
def __init__(self, serialization_fields: dict) -> None:
"""Created with a mapping of fields to functions.
Example::
>>> serialization_fields = {
... "username": str.upper,
... "name": str.title,
... }
Means that then this object is called with::
>>> from types import SimpleNamespace
>>> event = SimpleNamespace(username="usr", name="name")
>>> result = EventSerializer(serialization_fields).serialize(event)
Will return a dictionary where::
>>> result == {
... "username": event.username.upper(),
... "name": event.name.title(),
... }
True
"""
self.serialization_fields = serialization_fields
def serialize(self, event) -> dict:
"""Get all the attributes from ``event``, apply the transformations to
each attribute, and place it in a dictionary to be returned.
"""
return {
field: transformation(getattr(event, field))
for field, transformation in self.serialization_fields.items()
}
class Serialization:
"""A class decorator created with transformation functions to be applied
over the fields of the class instance.
"""
def __init__(self, **transformations):
"""The ``transformations`` dictionary contains the definition of how to
map the attributes of the instance of the class, at serialization time.
"""
self.serializer = EventSerializer(transformations)
def __call__(self, event_class):
"""Called when being applied to ``event_class``, will replace the
``serialize`` method of this one by a new version that uses the
serializer instance.
"""
def serialize_method(event_instance):
return self.serializer.serialize(event_instance)
event_class.serialize = serialize_method
return event_class
@Serialization(
username=str.lower,
password=hide_field,
ip=show_original,
timestamp=format_time,
)
class LoginEvent:
def __init__(self, username, password, ip, timestamp):
self.username = username
self.password = password
self.ip = ip
self.timestamp = timestamp
class TestLoginEventSerialized(unittest.TestCase):
def test_serialization(self):
event = LoginEvent(
"UserName", "password", "127.0.0.1", datetime(2016, 7, 20, 15, 45)
)
expected = {
"username": "username",
"password": "**redacted**",
"ip": "127.0.0.1",
"timestamp": "2016-07-20 15:45",
}
self.assertEqual(event.serialize(), expected)
if __name__ == "__main__":
unittest.main()
import sys
import unittest
from datetime import datetime
from decorator_class_2 import (
Serialization,
format_time,
hide_field,
show_original,
)
try:
from dataclasses import dataclass
except ImportError:
def dataclass(cls):
return cls
@Serialization(
username=show_original,
password=hide_field,
ip=show_original,
timestamp=format_time,
)
@dataclass
class LoginEvent:
username: str
password: str
ip: str
timestamp: datetime
class TestLoginEventSerialized(unittest.TestCase):
@unittest.skipIf(
sys.version_info[:3] < (3, 7, 0), reason="Requires Python 3.7+ to run"
)
def test_serializetion(self):
event = LoginEvent(
"username", "password", "127.0.0.1", datetime(2016, 7, 20, 15, 45)
)
expected = {
"username": "username",
"password": "**redacted**",
"ip": "127.0.0.1",
"timestamp": "2016-07-20 15:45",
}
self.assertEqual(event.serialize(), expected)
if __name__ == "__main__":
unittest.main()
from functools import wraps
from decorator_function_1 import ControlledException
from log import logger
RETRIES_LIMIT = 3
def with_retry(retries_limit=RETRIES_LIMIT, allowed_exceptions=None):
allowed_exceptions = allowed_exceptions or (ControlledException,)
def retry(operation):
@wraps(operation)
def wrapped(*args, **kwargs):
last_raised = None
for _ in range(retries_limit):
try:
return operation(*args, **kwargs)
except allowed_exceptions as e:
logger.warning(
"retrying %s due to %s", operation.__qualname__, e
)
last_raised = e
raise last_raised
return wrapped
return retry
@with_retry()
def run_operation(task):
return task.run()
@with_retry(retries_limit=5)
def run_with_custom_retries_limit(task):
return task.run()
@with_retry(allowed_exceptions=(AttributeError,))
def run_with_custom_exceptions(task):
return task.run()
@with_retry(
retries_limit=4, allowed_exceptions=(ZeroDivisionError, AttributeError)
)
def run_with_custom_parameters(task):
return task.run()
from functools import wraps
from unittest import TestCase, main, mock
from decorator_function_1 import (ControlledException, OperationObject,
RunWithFailure)
from log import logger
class Retry:
def __init__(self, operation):
self.operation = operation
wraps(operation)(self)
def __call__(self, *args, **kwargs):
last_raised = None
RETRIES_LIMIT = 3
for _ in range(RETRIES_LIMIT):
try:
return self.operation(*args, **kwargs)
except ControlledException as e:
logger.info("retrying %s", self.operation.__qualname__)
last_raised = e
raise last_raised
@Retry
def run_operation(task):
"""Run the operation in the task"""
return task.run()
class RetryDecoratorTest(TestCase):
def setUp(self):
self.info = mock.patch("log.logger.info").start()
def tearDown(self):
self.info.stop()
def test_fail_less_than_retry_limit(self):
"""Retry = 3, fail = 2 --> OK"""
task = OperationObject()
failing_task = RunWithFailure(task, fail_n_times=2)
times_run = run_operation(failing_task)
self.assertEqual(times_run, 3)
self.assertEqual(task._times_called, 3)
def test_fail_equal_retry_limit(self):
"""Retry = fail = 3, will fail"""
task = OperationObject()
failing_task = RunWithFailure(task, fail_n_times=3)
with self.assertRaises(ControlledException):
run_operation(failing_task)
def test_no_failures(self):
task = OperationObject()
failing_task = RunWithFailure(task, fail_n_times=0)
times_run = run_operation(failing_task)
self.assertEqual(times_run, 1)
self.assertEqual(task._times_called, 1)
def test_doc(self):
self.assertEqual(
run_operation.__doc__, "Run the operation in the task"
)
if __name__ == "__main__":
main()
def decorator(original_function):
@wraps(original_function):
def decorated_function(*args, **kwargs):
# do something
return original_function(*args, **kwargs)
return decorated_function
데코레이터를 만들 때는 항상 래핑된 함수 위에 functools.wraps를 사용한다.
def traced_function_wrong(function):
"""An example of a badly defined decorator."""
logger.debug("started execution of %s", function)
start_time = time.time()
@wraps(function)
def wrapped(*args, **kwargs):
result = function(*args, **kwargs)
logger.info(
"function %s took %.2fs", function, time.time() - start_time
)
return result
return wrapped
@traced_function_wrong
def process_with_delay(callback, delay=0):
logger.info("sleep(%d)", delay)
return callback
def traced_function(function):
@wraps(function)
def wrapped(*args, **kwargs):
logger.info("started execution of %s", function)
start_time = time.time()
result = function(*args, **kwargs)
logger.info(
"function %s took %.2fs", function, time.time() - start_time
)
return result
return wrapped
@traced_function
def call_with_delay(callback, delay=0):
logger.info("sleep(%d)", delay)
return callback
EVENTS_REGISTRY = {}
def register_event(event_cls):
"""Place the class for the event into the registry to make it accessible in
the module.
"""
EVENTS_REGISTRY[event_cls.__name__] = event_cls
return event_cls
class Event:
"""A base event object"""
class UserEvent:
TYPE = "user"
@register_event
class UserLoginEvent(UserEvent):
"""Represents the event of a user when it has just accessed the system."""
@register_event
class UserLogoutEvent(UserEvent):
"""Event triggered right after a user abandoned the system."""
def test():
"""
>>> sorted(EVENTS_REGISTRY.keys()) == sorted(('UserLoginEvent', 'UserLogoutEvent'))
True
from functools import wraps
from types import MethodType
class DBDriver:
def __init__(self, dbstring):
self.dbstring = dbstring
def execute(self, query):
return f"query {query} at {self.dbstring}"
class inject_db_driver:
"""Convert a string to a DBDriver instance and pass this to the wrapped
function.
"""
def __init__(self, function):
self.function = function
wraps(self.function)(self)
def __call__(self, dbstring):
return self.function(DBDriver(dbstring))
def __get__(self, instance, owner):
if instance is None:
return self
return self.__class__(MethodType(self.function, instance))
@inject_db_driver
def run_query(driver):
return driver.execute("test_function_2")
class DataHandler:
@inject_db_driver
def run_query(self, driver):
return driver.execute("test_method_2")
처음부터 데코레이터를 만들지 않는다.
패턴이 생기고 추상화가 명확해지면 그때 만든다.
적어도 3회 이상 필요한 경우에만 데코레이터를 만든다.
데코레이터 코드를 최소한으로 유지한다.
캡슐화와 관심사의 분리: 내부에서 어떻게 구현했는지 전혀 알 수 없는 블랙박스 모드로 동작해야 한다.
독립성: 데코레이팅되는 객체와 최대한 분리되어야 한다.
재사용성
celery의 @app.task
, flask의 `@route("/", method=["GET"])은 좋은 예시다.