diff --git a/docs/coding/python/pydantic.md b/docs/coding/python/pydantic.md index 3aae338fd3c..c22432389f5 100644 --- a/docs/coding/python/pydantic.md +++ b/docs/coding/python/pydantic.md @@ -495,6 +495,42 @@ print(m._secret_value) #> 5 ``` +### Define fields to exclude from exporting at config level + +This won't be necessary once they release the version 1.9 because you can [define +the fields to exclude in the `Config` of the +model](https://github.com/samuelcolvin/pydantic/issues/660) using something +like: + +```python +class User(BaseModel): + id: int + username: str + password: str + +class Transaction(BaseModel): + id: str + user: User + value: int + + class Config: + fields = { + 'value': { + 'alias': 'Amount', + 'exclude': ..., + }, + 'user': { + 'exclude': {'username', 'password'} + }, + 'id': { + 'dump_alias': 'external_id' + } + } +``` + +The release it's taking its time because [the developer's gremlin and salaried +work are sucking his time off](https://github.com/samuelcolvin/pydantic/discussions/3228). + ## [Update entity attributes with a dictionary](https://pydantic-docs.helpmanual.io/usage/exporting_models/#modelcopy) To update a model with the data of a dictionary you can create a new object with diff --git a/docs/coding/python/type_hints.md b/docs/coding/python/type_hints.md index f762607c77b..c80222b8ee4 100644 --- a/docs/coding/python/type_hints.md +++ b/docs/coding/python/type_hints.md @@ -328,6 +328,89 @@ def new_users(user_class: UserTypes[UserType]) -> UserType: # OK! pass ``` +## [Define a TypeVar with restrictions](https://mypy.readthedocs.io/en/stable/generics.html#type-variables-with-value-restriction) + +By default, a type variable can be replaced with any type. However, sometimes +it’s useful to have a type variable that can only have some specific types as +its value. A typical example is a type variable that can only have values `str` +and `bytes`: + +```python +from typing import TypeVar + +AnyStr = TypeVar('AnyStr', str, bytes) +``` +This is actually such a common type variable that `AnyStr` is defined in typing +and we don’t need to define it ourselves. + + +We can use `AnyStr` to define a function that can concatenate two strings or +bytes objects, but it can’t be called with other argument types: + +```python +from typing import AnyStr + +def concat(x: AnyStr, y: AnyStr) -> AnyStr: + return x + y + +concat('a', 'b') # Okay +concat(b'a', b'b') # Okay +concat(1, 2) # Error! +``` + +Note that this is different from a union type, since combinations of `str` and +`bytes` are not accepted: + +```python +concat('string', b'bytes') # Error! +``` + +In this case, this is exactly what we want, since it’s not possible to +concatenate a string and a bytes object! The type checker will reject this +function: + +```python +def union_concat(x: Union[str, bytes], y: Union[str, bytes]) -> Union[str, bytes]: + return x + y # Error: can't concatenate str and bytes +``` + +### Use a constrained TypeVar in the definition of a class attributes. + +If you try to use a `TypeVar` in the definition of a class attribute: + +```python +class File: + """Model a computer file.""" + + path: str + content: Optional[AnyStr] = None # mypy error! +``` + +[mypy](mypy.md) will complain with `Type variable AnyStr is unbound +[valid-type]`, to solve it, you need to make the class inherit from the +`Generic[AnyStr]`. + +```python +class File(Generic[AnyStr]): + """Model a computer file.""" + + path: str + content: Optional[AnyStr] = None +``` + +Why you ask? I have absolutely no clue. I've asked that question in the +[gitter python typing channel](https://gitter.im/python/typing#) but the kind +answer that @ktbarrett gave me sounded like Chinese. + +> You can't just use a type variable for attributes or variables, you have to +> create some generic context, whether that be a function or a class, so that +> you can instantiate the generic context (or the analyzer can infer it) (i.e. +> context[var]). That's not possible if you don't specify that the class is +> a generic context. It also ensure than all uses of that variable in the +> context resolve to the same type. + +If you don't mind helping me understand it, please [contact me](contact.md). + ## [Specify the type of the class in it's method and attributes](https://stackoverflow.com/questions/33533148/how-do-i-specify-that-the-return-type-of-a-method-is-the-same-as-the-class-itsel) If you are using Python 3.10 or later, it just works. diff --git a/docs/python_properties.md b/docs/python_properties.md new file mode 100644 index 00000000000..95eb98d3eb1 --- /dev/null +++ b/docs/python_properties.md @@ -0,0 +1,172 @@ +--- +title: Python Properties +date: 20211118 +author: Lyz +--- + +The `@property` is the pythonic way to use getters and setters in +object-oriented programming. It can be used to make methods look like attributes. + +The `property` decorator returns an object that proxies any request to set or +access the attribute value through the methods we have specified. + +```python +class Foo: + @property + def foo(self): + return 'bar' +``` + +We can specify a setter function for the new property + +```python +class Foo: + @property + def foo(self): + return self._foo + + @foo.setter + def foo(self, value): + self._foo = value +``` + +We first decorate the `foo` method a as getter. Then we decorate a second method +with exactly the same name by applying the `setter` attribute of the originally +decorated `foo` method. The `property` function returns an object; this object +always comes with its own `setter` attribute, which can then be applied as +a decorator to other functions. Using the same name for the get and set methods +is not required, but it does help group the multiple methods that access one +property together. + +We can also specify a deletion function with `@foo.deleter`. We cannot specify +a docstring using `property` decorators, so we need to rely on the property +copying the docstring from the initial getter method + +```python +class Silly: + @property + def silly(self): + "This is a silly property" + print("You are getting silly") + return self._silly + + @silly.setter + def silly(self, value): + print("You are making silly {}".format(value)) + self._silly = value + + @silly.deleter + def silly(self): + print("Whoah, you kicked silly!") + del self.silly +``` + +```python +>>> s = Silly() +>>> s.silly = "funny" +You are making silly funny +>>> s.silly +You are getting silly +'funny' +>>> del s.silly +Whoah, you kicked silly! +``` + +# When to use properties + +The most common use of a property is when we have some data on a class that we +later want to add behavior to. + +The fact that methods are just callable attributes, and properties are just +customizable attributes can help us make the decision. Methods should typically +represent actions; things that can be done to, or performed by, the object. When +you call a method, even with only one argument, it should *do* something. Method +names a generally verbs. + +Once confirming that an attribute is not an action, we need to decide between +standard data attributes and properties. In general, always use a standard +attribute until you need to control access to that property in some way. In +either case, your attribute is usually a noun . The only difference between an +attribute and a property is that we can invoke custom actions automatically when +a property is retrieved, set, or deleted + +## Cache expensive data + +A common need for custom behavior is caching a value that is difficult to +calculate or expensive to look up. + +We can do this with a custom getter on the property. The first time the value is +retrieved, we perform the lookup or calculation. Then we could locally cache the +value as a private attribute on our object, and the next time the value is +requested, we return the stored data. + +```python +from urlib.request import urlopen + +class Webpage: + def __init__(self, url): + self.url = url + self._content = None + + @property + def content(self): + if not self._content: + print("Retrieving New Page..") + self._content = urlopen(self.url).read() + return self._content +``` + +```python +>>> import time +>>> webpage = Webpage("http://ccphillips.net/") +>>> now = time.time() +>>> content1 = webpage.content +Retrieving new Page... +>>> time.time() - now +22.43316 +>>> now = time.time() +>>> content2 = webpage.content +>>> time.time() -now +1.926645 +>>> content1 == content2 +True +``` + +## Attributes calculated on the fly + +Custom getters are also useful for attributes that need to be calculated on the +fly, based on other object attributes. + +```python +clsas AverageList(list): + @property + def average(self): + return sum(self) / len(self) +``` +```python +>>> a = AverageList([1,2,3,4]) +>>> a.average +2.5 +``` + +Of course we could have made this a method instead, but then we should call it +`calculate_average()`, since methods represent actions. But a property called +`average` is more suitable, both easier to type, and easier to read. + +# [Abstract properties](https://stackoverflow.com/questions/5960337/how-to-create-abstract-properties-in-python-abstract-classes) + +Sometimes you want to define properties in your abstract classes, to do that, use: + +```python +from abc import ABC, abstractmethod + +class C(ABC): + @property + @abstractmethod + def my_abstract_property(self): + ... +``` + +If you want to use an abstract setter, you'll encounter the mypy `Decorated +property not supported` error, you'll need to add a `# type: ignore` until [this +issue is solved](https://github.com/python/mypy/issues/1362). diff --git a/docs/vim.md b/docs/vim.md index 1463d23c80f..b1ebecf9c5f 100644 --- a/docs/vim.md +++ b/docs/vim.md @@ -228,8 +228,8 @@ the following snippet to your vimrc file ```Vim augroup remember_folds autocmd! - autocmd BufWinLeave * mkview - autocmd BufWinEnter * silent! loadview + autocmd BufLeave * mkview + autocmd BufEnter * silent! loadview augroup END ``` diff --git a/mkdocs.yml b/mkdocs.yml index 42238ea43b4..3dbfcfd5612 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -125,6 +125,7 @@ nav: - Logging: python_logging.md - Code Styling: coding/python/python_code_styling.md - Docstrings: coding/python/docstrings.md + - Properties: python_properties.md - Lazy loading: lazy_loading.md - Plugin System: python_plugin_system.md - Profiling: python_profiling.md