跳转到主要内容

未提供项目描述

项目描述

包装纸

一组Python装饰器和实用工具,用于抽象常见的/繁琐的Python模式。

说明

这个包更侧重于提供有趣的抽象,并尝试拓展Python代码组织的可能性。我并不认为使用这些函数会提供“好”的代码,我也不会支持用它们来创建恶意的Python代码 ;)。

这里的一些函数可能会使代码更难以理解,但这是可以的。我想给它们留出空间存在,并希望我们可以进一步发展它们,使它们更容易理解,并提供更直观和熟悉的抽象。

这个包是关于实验,并试图在避开Python纯主义者(以及可能有更多理性的人)的审查的同时,创建基本的、有趣的、自然感觉的、方便的抽象。我想看看这个语言的其他有趣的结构。

所以,我想这个包的座右铭是:自由发展,但负责任地使用。<3

需要注意的是,其中一些功能可能与linters不兼容 😢

简单示例

import wrappingpaper as wp

@wp.contextdecorator
def doing_something(a, b):
    print(a)
    yield
    print(b)

# por que no los dos?

# you can do this
with doing_something(4, 5):
    print(1)
# prints 4 1 5

# as well as this
@doing_something(4, 5)
def something():
    print(1)
something()
# prints 4 1 5

包括

  • 辅助模块
    • 伪导入的实现,作为提供的导入机制类的案例研究
  • 日志记录/错误处理
    • 捕获函数抛出的错误,并将其重定向到记录器
  • 上下文管理器
    • 同时作为函数包装器的上下文管理器
  • 对象属性
    • 类和实例缓存
    • 动态属性对象 - 为属性提供嵌套属性和方法!!
  • 函数签名助手
    • 覆盖并更新函数签名
    • 过滤函数方案之外的函数参数
    • 实际更新包装器的partial
  • 导入机制
    • 创建伪模块并自定义模块的导入方式(感谢上帝,我处理了一些复杂的问题)
  • 可迭代对象
    • 包含我从其他项目中提取的一些基本可迭代函数,这样我就不必在各个地方重复它们。
  • 杂项
    • 我还没整理的东西。你知道的?
    • 异常时重试
    • 检查循环引用

安装

pip install wrappingpaper

用法

import wrappingpaper as wp

辅助模块

这些是模拟模块,利用 wrappingpaper导入机制 来修改从它们导入的模块。

lazyimport

这是使用定义的导入机制实现的简单懒加载导入。

from lazyimport import sklearn
import librosa # sklearn imports will be lazy

预置

这是对 bmcfee/presets 的重新实现,它包括导入机制,而不是在之后包装模块。我可能会给那个包添加一个 PR,但在这里实现它目前很简单,我觉得它并不重要到足以通过审查流程。

from presets import librosa
librosa.update(sr=44100) # now functions will default to sr=44100

日志记录

注意:我还没有投入精力来模拟日志对象进行测试,因此请注意,在当前形式下,它们未经测试,并且很可能存在 1 或 2 个错误。

我正在处理一个错误抑制和日志记录的项目。会有函数被 try-except 块包装,日志调用,以及大量冗余的框架。

所以我做了工作来提取这些内容,并在装饰器中执行许多常见模式。

这里的日志装饰器主要用于允许失败并返回默认/空值而不使整个程序崩溃的函数。

它还包括从跟踪信息中提取信息的实用程序。我还没有处理日志处理器和格式化器,所以这是一个待办事项。

import logging
log = logging.getLogger(__name__)

# handle and log error

@wp.log_error_as_warning(log, default=dict)
def get_stats(x=None):
    if x is True:
        raise ValueError() # some error happens
    return {'a': 5, 'b': 6}

assert get_stats() == {'a': 5, 'b': 6}
assert get_stats(True) == {}
大致相当于
def get_stats(x=None):
    try:
        if x is True:
            raise ValueError() # some error happens
        return {'a': 5, 'b': 6}
    except ValueError as e:
        log.warning('Exception in get_stats: %s', e)
        return {}

上下文管理器

Python 中的两种常见模式是上下文管理器和装饰器。它们通常具有相同的基本结构:进行一些初始化,运行一个函数,并进行一些清理。

在不同的上下文中,它们都可以用于提供干净的代码,但为了使用它们,我经常发现自己围绕上下文管理器编写一个额外的包装函数,然后你必须给它一个稍微不同的名称,这可能会让人困惑。

所以,出现了 contextdecorator,它与 contextlib.contextmanager 的工作方式相同,但也可以作为函数装饰器。当用作装饰器时,它将在上下文管理器内调用函数。

@wp.contextdecorator
def doing_something(a, b):
    print(a)
    yield
    print(b)

# por que no los dos?

# you can do this
with doing_something(4, 5):
    print(1)

# as well as this
@doing_something(4, 5)
def something():
    print(1)
something()

有时,你的装饰器并不那么简单,你需要在装饰器中进行一些不同的操作(例如,你需要包装函数的名称)。

@doing_something.caller # override default decorator
def doing_something(func, a, b): # wrapped function, decorator arguments
    # change arguments
    name = func.__name__
    a = 'calling {}: {}'.format(name, a)
    b = 'calling {}: {}'.format(name, b)

    # return the wrapped function
    @functools.wraps(func)
    def inner(*args, **kw):
        with doing_something(a, b):
            return func(*args, **kw)
    return inner
大致相当于
import functools
from contextlib import contextmanager

@contextmanager
def doing_something(a, b):
    print(a)
    yield
    print(b)

def doing_something2(a, b):
    def outer(func):
        @functools.wraps(func)
        def inner(*a, **kw):
            with doing_something(a, b):
                return func(*a, **kw)
        return inner
    return outer

# used like:
with doing_something(4, 5):
    print(1)

@doing_something2(4, 5)
def something():
    print(1)
something()

属性

Python 属性对象非常有用,因为它们允许你创建具有复杂功能的自然感觉对象,并且所有这些都捆绑在一个非常好的界面中。

但是使用它们时,我经常发现自己多次在实用文件中编写相同的类。

一个用例是缓存。你可以提供不同级别的缓存。

  • cachedproperty:在实例对象上缓存 - 每个实例运行一次
  • onceproperty:在类对象上缓存 - 每个类/基类运行一次
  • overridable_property:作为正常属性工作(调用包装函数),直到属性被分配。然后它返回分配的值。
  • overridable_method:作为正常方法工作(调用包装函数),直到函数作为装饰器被调用。然后它调用包装函数。在实例级别上工作。
import time

class SomeClass:
    @wp.cachedproperty
    def instance_prop(self):
        '''This is run once per object instance.'''
        return time.time()

    @wp.onceproperty
    def class_prop(self):
        '''This is run once. It is cached in the property
        object itself.'''
        return time.time()

    @wp.overridable_property
    def overridable(self):
        '''This property is run normally, until another value is assigned on top.'''
        return time.time()

    def __init__(self, overridable=None):
        if overridable: # override the property value
            # stores at self._overridable
            self.overridable = overridable
        # otherwise it just uses the property function like usual

a = SomeClass()
b = SomeClass()

assert a.instance_prop != b.instance_prop # prop runs once per object
assert a.class_prop == b.class_prop # prop runs only once
assert a.overridable != a.overridable # gets called twice, shouldn't be the same
a.overridable = 5
assert a.overridable == 5 # now the value is overridden

assert SomeClass(5).overridable == 5 # overriding inside class

函数签名

这是我一直在寻找的东西。

我个人喜欢将大量函数参数打包到文件中的配置文件的想法。

我也讨厌在 5 层嵌套函数调用中重复传递参数。

我喜欢将关键字参数(**kw)传递给下一个函数。

但是存在一些情况,您的关键词字典中有额外的配置值,而您只想传递您函数需要接收的值。

# dynamic function defaults

@wp.configfunction
def asdf(a=5, b=6, c=7):
    return a + b + c

assert asdf() == 5+6+7 # normal behavior
asdf.update(a=1)
assert asdf() == 1+6+7 # updated default
assert asdf(3) == 3+6+7 # automatically resolves kwargs and posargs
asdf.clear()
assert asdf() == 5+6+7 # back to normal behavior

# filter out kwargs not in the signature (if **kw, it's a no-op).

@wp.filterkw
def asdf(a=5, b=6, c=7):
    return a + b + c

assert asdf(b=10, d=1234) == 5+10+7

对象

Monkeypatching

class Blah:
    def asdf(self):
        return 10

b = Blah()

@wp.monkeypatch(b)
def asdf():
    return 11

assert asdf() == 11

asdf.reset() # remove patch
assert asdf() == 10

asdf.repatch() # re-place the patch
assert asdf() == 11

命名空间

class something(metaclass=wp.namespace):
    one_thing = 5
    other_thing = 6

    def blah(x):
        return one_thing + other_thing + x

assert something.blah(10) == 5+6+10

可迭代对象

#####################
# loop breaking
#####################

items = wp.until(x if x != 7 else wp.done for x in range(10))
assert list(items) == list(range(0, 6))


####################
# loop throttling
####################

# make sure that a for loop doesn't go too fast.
# limit the time one iteration takes.
t0 = time.time()
for x in wp.throttled(range(10), 1):
    print(x)
assert time.time() - t0 > 10

# limiting the number of iterations to 10.
# with no iterable passed, it loops infinitely and
# yields the total yield time and the time it had to sleep.
for dt, time_asleep in wp.limit(wp.throttled(secs=1), 10):
    print('Iteration took {}s. Had to sleep for {}s.'.format(dt, time_asleep))
    print('-'*10)

################################
# Use `while True:` in a loop
################################

for _ in wp.infinite():
    print('this is gonna be a while...')


#########################
# pre-check an iterable
#########################

# check the first n items in an iterable, without removing them.

it = iter(range(6))
items, it = wp.pre_check_iter(it, 3)
assert items == [0, 1, 2]
assert list(it) == [0, 1, 2, 3, 4, 5, 6]


###########################################
# repeat and chain a function infinitely
###########################################

import random

def get_numbers(): # function returns an iterable
    return [random.random() for _ in range(10)]



numbers = wp.run_iter_forever(get_numbers)
# repeat get_numbers() and chain iterable outputs together
all_numbers = list(wp.limit(numbers, 100))
assert all(isinstance(x, float) for x in all_numbers)

# If no items are returned by a call, instead of the iterable hanging
# indefinitely waiting for an item, return None.

def get_numbers():
    if random.random() > 0.8: # make random breaks
        return # returns empty
    return [random.random() for _ in range(10)]

numbers = wp.run_iter_forever(get_numbers, none_if_empty=True)
# this SHOULD contain sporadic None's at a multiple of 10
all_numbers = list(wp.limit(numbers, 5000))
assert None in all_numbers

导入机制

这可能是在这里玩耍最危险的事情。

Python公开了其许多内部机制,包括其导入系统。

因此,我们可以利用这一点来提供修改模块行为的导入包装器。

基本示例 - 懒加载

# lazyimport/__init__.py
import wrappingpaper as wp
wp.lazy_loader.activate(__name__)


# main.py
from lazyimport import sklearn.model_selection

# sklearn is not currently loaded

sklearn.model_selection.train_test_split() # now it's loaded.

修改已从您的伪模块导入的模块

import wrappingpaper as wp

@wp.PseudoImportFinder.modulemodifier
def my_loader(module):
    module.sneakything = '......hi'

my_loader.activate('somethingrandom')

# now somewhere else, you can do

from somethingrandom import numpy as np

assert np.sneakything == '......hi'

包装模块以修改模块内容

import importlib
import wrappingpaper as wp

# create the module wrapper that will traverse and modify the module when it is loaded.

class Module(wp.ModuleWrapper):
    # this is called for each item in the module
    def _wrapattr(self, attr, value):
        # do whatever you want with the value
        if callable(value) and getattr(value, '__doc__', None) is not None:
            value.__doc__ += '\nI was here.'
        elif self._is_submodule(value):
            value = Module(value)
        # always pass attr and modified value to be set,
        # otherwise it will be undefined.
        super()._wrapattr(attr, value)


# applies the module wrapper on load

@wp.PseudoImportFinder.moduleloader
def my_loader(spec):
    return Module(importlib.util.module_from_spec(spec))


# somewhere else (or in the same place. I'm not ur mom), actually use it

with my_loader.activated('somethingrandom'): # activated only inside context
    from somethingrandom import glob

print(glob.glob.__doc__)
assert glob.glob.__doc__.endswith('I was here.')

杂项

一些尚未整理的其他杂项内容。

import random

# retry a function if an exception is raised

@wp.retry_on_failure(10)
def asdf():
    x = random.random()
    if x < 0.5:
        raise ValueError
    return x

# will either return a number that is definitely > 0.5
# or every number in the first 10 tries were below 0.5
try:
    assert asdf() > 0.5
except ValueError:
    print("Couldn't get a number :/")


# ignore error

with wp.ignore():
    a, b = 5, 0
    c = a / b # throws divide by zero
    a = 10 # never run
assert a == 5

项目详情


下载文件

下载适合您平台的文件。如果您不确定该选择哪个,请了解更多关于 安装包 的信息。

源分布

wrappingpaper-0.0.6.tar.gz (29.9 kB 查看哈希值)

上传时间

支持者