<kaggle_start><code># # Lab 42
# - 7 Examples of Pythonisms:
# - Iterators and Generators
# - Data Model Methods
# - Decorators
# ## Iterators and Generators
# - Iterators and generators are both used to produce sequences of values.
# - Iterators are objects that implement the __iter__() and __next__() methods
# - Generators are functions that use the yield keyword to produce a sequence of values
def my_generator(start, end):
    current = start
    while current < end:
        yield current
        current += 1


def my_infinite_generator(start):
    current = start
    while True:
        yield current
        current += 1


# When you call this function, it will return a generator object that you can
# iterate over to get an infinite sequence of values:
gen = my_infinite_generator(10)
for i in range(5):
    print(next(gen))  # prints 10, 11, 12, 13, 14
# Note that you can't simply use a for loop to iterate over an infinite generator,
# since it would never terminate. You can, however, use the itertools module to
# extract a finite number of values:
import itertools

gen = my_infinite_generator(10)
values = itertools.islice(gen, 5)
for value in values:
    print(value)  # prints 10, 11, 12, 13, 14


class MyIterator:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < self.end:
            value = self.current
            self.current += 1
            return value
        else:
            raise StopIteration


# ## Data Model Methods
# - Data model methods are methods that allow you to define how your objects behave in various contexts,
# - such as when they are compared, printed, or used in mathematical operations.
# - Here are a few examples of data model methods:
class MyClass:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

    def __str__(self):
        return f"MyClass({self.value})"

    def __add__(self, other):
        return MyClass(self.value + other.value)


# ## Decorators
# - Decorators are functions that modify the behavior of other functions.
# - They are often used to add functionality such as logging, caching, or authentication to existing functions.
# - Here are a few examples of decorators:
import time


def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time} seconds")
        return result

    return wrapper


@timer
def slow_function():
    time.sleep(2)
    return "done"


def authenticated(func):
    def wrapper(*args, **kwargs):
        if check_authentication():
            return func(*args, **kwargs)
        else:
            raise Exception("Not authenticated")

    return wrapper


@authenticated
def secure_function():
    # this function will only run if the user is authenticated
    pass


def memoize(func):
    cache = {}

    def wrapper(*args):
        if args in cache:
            return cache[args]
        else:
            result = func(*args)
            cache[args] = result
            return result

    return wrapper


@memoize
def fibonacci(n):
    if n < 2:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)
<|endoftext|>
[{"Id": 119205695, "ScriptId": 35084581, "ParentScriptVersionId": NaN, "ScriptLanguageId": 9, "AuthorUserId": 13129125, "CreationDate": "02/15/2023 03:22:12", "VersionNumber": 1.0, "Title": "Lab 42", "EvaluationDate": "02/15/2023", "IsChange": true, "TotalLines": 129.0, "LinesInsertedFromPrevious": 129.0, "LinesChangedFromPrevious": 0.0, "LinesUnchangedFromPrevious": 0.0, "LinesInsertedFromFork": NaN, "LinesDeletedFromFork": NaN, "LinesChangedFromFork": NaN, "LinesUnchangedFromFork": NaN, "TotalVotes": 0}]
# # Lab 42
# - 7 Examples of Pythonisms:
# - Iterators and Generators
# - Data Model Methods
# - Decorators
# ## Iterators and Generators
# - Iterators and generators are both used to produce sequences of values.
# - Iterators are objects that implement the __iter__() and __next__() methods
# - Generators are functions that use the yield keyword to produce a sequence of values
def my_generator(start, end):
    current = start
    while current < end:
        yield current
        current += 1


def my_infinite_generator(start):
    current = start
    while True:
        yield current
        current += 1


# When you call this function, it will return a generator object that you can
# iterate over to get an infinite sequence of values:
gen = my_infinite_generator(10)
for i in range(5):
    print(next(gen))  # prints 10, 11, 12, 13, 14
# Note that you can't simply use a for loop to iterate over an infinite generator,
# since it would never terminate. You can, however, use the itertools module to
# extract a finite number of values:
import itertools

gen = my_infinite_generator(10)
values = itertools.islice(gen, 5)
for value in values:
    print(value)  # prints 10, 11, 12, 13, 14


class MyIterator:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < self.end:
            value = self.current
            self.current += 1
            return value
        else:
            raise StopIteration


# ## Data Model Methods
# - Data model methods are methods that allow you to define how your objects behave in various contexts,
# - such as when they are compared, printed, or used in mathematical operations.
# - Here are a few examples of data model methods:
class MyClass:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

    def __str__(self):
        return f"MyClass({self.value})"

    def __add__(self, other):
        return MyClass(self.value + other.value)


# ## Decorators
# - Decorators are functions that modify the behavior of other functions.
# - They are often used to add functionality such as logging, caching, or authentication to existing functions.
# - Here are a few examples of decorators:
import time


def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time} seconds")
        return result

    return wrapper


@timer
def slow_function():
    time.sleep(2)
    return "done"


def authenticated(func):
    def wrapper(*args, **kwargs):
        if check_authentication():
            return func(*args, **kwargs)
        else:
            raise Exception("Not authenticated")

    return wrapper


@authenticated
def secure_function():
    # this function will only run if the user is authenticated
    pass


def memoize(func):
    cache = {}

    def wrapper(*args):
        if args in cache:
            return cache[args]
        else:
            result = func(*args)
            cache[args] = result
            return result

    return wrapper


@memoize
def fibonacci(n):
    if n < 2:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)
